読者です 読者をやめる 読者になる 読者になる

PHP のクラス定数のちょっと奇妙な話

PHP

このコードは Notice なく実行できる。よく考えると奇妙じゃない?

<?php
class Baz { const BAZ = FOO; };
define('FOO', 123);
var_dump(Baz::BAZ);
// int(123)

ちなみに次のコードだと Notice が出ます。

<?php
class Baz { const BAZ = FOO; };
var_dump(Baz::BAZ);
// Notice: Use of undefined constant FOO - assumed 'FOO'
// string(3) "FOO"
define('FOO', 123);

.

.

.

次のコードは実行できます。関数の定義より関数の呼び出しが先にありますが Fatal にはなりません。

<?php
f();
function f()
{
    var_dump(__FUNCTION__);
}

次のコードは Fatal エラーになります。

<?php
f();
if (true)
{
    function f()
    {
        var_dump(__FUNCTION__);
    }
}

この挙動はクラス定義でも同じです。下記のコードは実行可能ですが、

<?php
C::f();

class C
{
    public static function f()
    {
        var_dump(__METHOD__);
    }
}

下記のコードは Fatal エラーになります。

<?php
C::f();

if (true)
{
    class C
    {
        public static function f()
        {
            var_dump(__METHOD__);
        }
    }
}

制御文とかで囲われていない関数定義やクラス定義はコンパイル時に静的(PHP のコードの解釈時)に定義され、それ以外の(制御文とかで囲われている)関数やクラス定義は実行時に行われるからです(たぶん)。

つまり、前述の関数のケースでは PHP のソースファイルを PHP が読み込んだ時点で関数は定義されてます。2番目のケースでは、if の中のコードが実行されるまで関数は定義されません。この挙動はクラス定義でも同じです(たぶん)。

.

.

.

最初のクラス定数の例を見てみると・・・

<?php
class Baz { const BAZ = FOO; };
define('FOO', 123);
var_dump(Baz::BAZ);

このクラス定数はいつ定義されているのか?

なお、define による定数定義は明らかに実行時です。なぜなら define は関数だからです。define を call_user_func('define', ...) のように書き換えても動作します。

前述の説明だとクラス定義は静的(ソースファイルを解釈したとき)に行われているはずです。が、それだと実行時に定義されるはずの定数 FOO をクラス定義で使うことができる理由がわかりません。

.

.

.

これは、クラス定義そのものは静的に行われていて、ただし、クラス定数の値は最初に評価されるまで確定していないからです(たぶん)。

定義の時点では「クラス定数 BAZ は 定数 FOO である」という情報のみが保持されており、クラス定数が参照されたりクラスがインスタンス化されるなど、なんらかの形でクラス定数が評価されたときに実際の値を解決して「クラス定数 BAZ は 123 である」という定義というか値に差し替わっています(たぶん)。

PHP 5.6 の Constant scalar expressions も大体同じです。 下記のようなコードならクラスの定義の時点では「Baz::BAZ は FOO + 1 という式である」という情報が保持されていて、クラス定数が参照されたときに本当の値に解決されます(たぶん)。

<?php
class Baz
{
    const BAZ = FOO + 1;
}
define('FOO', 123);

が、それを実現するためには FOO + 1 という式を実行時に解釈できる必要があります。PHP 5.6 ではその情報を保持するために zval に ast メンバが追加されたようです。

PHP 5.5 だとあまり意味がないと思いますが、次のようにお互いに参照しあうような定義もできます。

<?php
class A
{
    const N = B::N + 1;
    const M = B::M + 1;
}

class B
{
    const N = A::M + 1;
    const M = 0;
}

var_dump(A::N); // int(3)
var_dump(A::M); // int(1)
var_dump(B::N); // int(2)
var_dump(B::M); // int(0)

もちろん循環参照はアウトです。

<?php
class A
{
    const N = B::N;
}

class B
{
    const N = A::N;
}

var_dump(A::N);
// Fatal error: Cannot declare self-referencing constant 'B::N'

.

.

.

制御文とかで囲われている関数定義やクラス定義は実行時に行われると書きましたが、それは正確ではありませんでした。

制御文とかで囲われている関数定義やクラス定義は、コンパイル時に匿名で定義され、実行時にシンボルテーブルに追加されます。これは vld などで opcode を見てみるとよく判ります。

このコードでは f1%00f2%2Fin%2F/in/mXXAH0x7fba1761903c という関数が定義されていて、実行時に ZEND_DECLARE_FUNCTION によって %00f2%2Fin%2F/in/mXXAH0x7fba1761903cf2 として定義されます。

f2 の呼び出しは ZEND_DECLARE_FUNCTION の前にあるため Fatal エラーになります。

.

.

.

なお、この記事は PHP のソースを詳しく見ずに書いているので妄想に溢れている可能性があります。