PHPのconst周りを再確認

2023-05-17
PHP
const
クラス定数

クラス定数について、何も知らなかったんだなと思ったので、色々まとめました。

発端

PHPerKaigi 2023の懇親会にて、AttributeでEnumのcasesを展開できない?みたいな質問をされたときに、自分はEnum関連だと最近RFCでてたなと思って、多分 PHP: rfc:dynamic_class_constant_fetch これですよという話をした。

Dynamic class constant fetch のおさらい

1
2
3
4
5
6
7
8
<?php
class Foo {
const Bar = 'bar';
}

$foo = new Foo();
$bar = 'Bar';
var_dump($foo::{$bar});

bar

と出力される

これは、8.3の新機能ですが、クラス定数を動的に参照できるだけであり、Enumのcasesのようなstaticメソッドの呼び出しに関する仕様ではない。
完全な勘違い、こんなことでは World Class PHPer にはなれない。

しかし、この件については、可能性のあるRFCが以前でていて…

Fetch property in const expressions

PHP: rfc:fetch_property_in_const_expressions

1
2
3
4
5
6
7
8
9
10
11
12
enum Status: int
{
case Open = 1;
case Close = 2;
case Invalid = 3;

const Values = [
self::Open->value => self::Open,
self::Close->value => self::Close,
self::Invalid->value => self::Invalid,
];
}

const で Backed Enum の value を参照できるようになった。

これを使えば、上記例Valuesのように、constにEnumとEnumの値を格納できる。このValuesはクラス定数なので、Attributeでも使える。

PHP: アトリビュートの文法 - Manual

アトリビュートの引数はリテラル値か、定数式のみが指定できます。

実装例

https://3v4l.org/cDmN8#v8.2.6

これなら、AttributeでもEnum::cases()相当の値を参照できる

ここまでのまとめ

dynamic class constant fetch

変数を使った動的な constant 呼び出しが出来る(RFC のタイトルままじゃん)
通常の constant 及び、 Enum の case が該当する

fetch property in const expressions

const において、プロパティの参照ができるようになった。
Enum::cases() のように、任意の式を const で展開することができない。

本当に必要だったもの

const において、staticメソッド呼び出しが出来れば、AttributeでもEnum::cases()がコールできる。

PHP: rfc:calls_in_constant_expressions

すでに似たようなコンセプトのRFCは一度出ていて、Withdrawn なので取り下げられています。
このRFCでは、ホワイトリストに入っているグローバル関数であれば、const 内でコール可能です。

しかし、ここで諦めていてはしょうがない。とりあえず仕様としては駄目なことは分かった。PHPerたるもの、PHPのソースを読まねば。

用意するもの

calls in constant expressions はPoCが出ていたので、そのまま利用できます。

https://github.com/php/php-src/pull/5139/files

これをビルドすると

1
2
3
4
5
6
7
<?php

class Foo {
const BAR=max(4, 5);
}

var_dump(Foo::BAR);

5

が出力される

組み込みのグローバル関数コールができる!

PoCの内容解説

Zend/zend_compile.c

  • zend_is_allowed_in_const_expr この分岐をひとまず超えないと行けない
  • ast の種別に応じてコンパイル方法を変えている

※現状は ZEND_AST_STATIC_CALL 用の分岐がない

Zend/zend_ast.c

  • zend_ast_evaluate
  • 実際に const の中身を評価している。ここにも ZEND_AST_STATIC_CALL 用の分岐を増やす必要がある

グローバル関数同様に、static関数も事前状態が必要なく実行可能なので、改造すれば static 関数をコールすることは比較的簡単だろうと思われる。

ところで

そもそも、PHP の const ってなんなの?

PHP: クラス定数 - Manual

クラス定数は、近年少しずつ機能追加されてきている

  • スカラー演算 php5.6
  • アクセス範囲 php7.1
  • クラス定数へのfinalの利用 php8.1
  • クラス定数でのbacked Enumプロパティ参照 php 8.2
  • trait でのクラス定数の利用 php 8.2
  • クラス定数の動的アクセス php 8.3
  • クラス定数の型宣言 php 8.3

なんとなくだけど、徐々に利便性が上がってきている。
sji さんもおっしゃっていたけど、php の const は存在がフワッとしている。

このフワフワ感にもう少し意味付けがされてこないと、PHPのconstは理解できないのかもしれない。フワフワだからこそ、Enum::cases() も別に呼べてもいいじゃん?とも思えるし、フワフワだからこそ、これ以上 const で演算は許さんという気持ちも分かる。

もしかしたら、ここは PHPのconst考古学が必要なのかもしれない。

その他の情報ソース

pangudashu/php7-internal: PHP7内核剖析

  • EG(zend_constants) ハッシュテーブルに値が格納されているので、参照自体は簡単。
  • zend_compile.cにおいて、zend_compile_const_decl(zend_ast *ast) でコンパイルされる。
  • ZEND_DECLARE_CONST命令でハッシュテーブルに格納される

現状では、定数はCONST型のみとなっているが、PHP8.3でクラス定数に型がつくはずなので、この辺は実装が変わってきていそう。

メモリ上の便宜とかあるのか?

C言語のように、メモリマップ上の静的領域に配置されて、プロセスまたいで使えるとかはない。名前に静的感があるが、メモリでの扱いは静的ではない。

まとめ

適当な受け答えをしていては、World Class PHPer にはなれないので、もっとちゃんとPHPの仕様とRFCを把握しような!