Shin x Blog

PHPをメインにWebシステムを開発してます。Webシステム開発チームの技術サポートも行っています。

use 文は PHP ファイルを読み込まない

PHP の use 文では、クラス名や関数名、定数、名前空間などのエイリアスを設定できます。

<?php
  
use App\Foo;
use App\Bar as ABar;

$foo = new Foo();
$bar = new ABar();

https://www.php.net/manual/ja/language.namespaces.importing.php

この use 文は指定したシンボルにエイリアスを設定する、言い方を変えると名前空間をインポートするもので、オートロードでクラス定義 PHP ファイルを読み込むものではありません。*1

例えば、上記コードの場合、use 文の時点で App\Foo や App\Bar に対するオートロードは動作しません。

この動きを確認してみます。

use 文のみを実行

use 文でオートロードが動作するかは下記のようなコードで簡単に確かめられます。もし use 文でオートロードが動くのであれば、クラス名が var_dump() で出力されるはずです。

<?php

spl_autoload_register(function (string $class): void {
   var_dump($class);
});
  
use App\Foo;
use App\Bar as ABar;

このコードを実行しても何も出力されません。つまり、use 文だけではオートロードが動作しないことが分かります。

https://3v4l.org/ZMVmC

オペコードでの確認

PHP コードのオペコードを見ると、use 文の動きが良くわかります。下記の PHP コードがあるとします。

<?php

namespace App;

use Lib\Foo;
use Lib\Db\Bar as DbBar;

$foo = new Foo;
$bar = new DbBar;
$bar = new Bar;

このコードのオペコードは下記のようになります。

$_main:
0000 V2 = NEW 0 string("Lib\\Foo") // new Foo
0001 DO_FCALL
0002 ASSIGN CV0($foo) V2
0003 V5 = NEW 0 string("Lib\\Db\\Bar") // new DbBar
0004 DO_FCALL
0005 ASSIGN CV1($bar) V5
0006 V8 = NEW 0 string("App\\Bar") // new Bar
0007 DO_FCALL
0008 ASSIGN CV1($bar) V8
0009 RETURN int(1)

オペコードを見ると、まず use 文に相当するオペコードが無く、いきなり new 演算子のオペコードである NEW が先頭にあることことが分かります。つまり use 文は実行時ではなく、それまで(コンパイル時)に処理されていることが想像できます。

そして、注目すべきは NEW のオペランド(引数)です。オペランドにはインスタンス化するクラス名を指定するのですが、PHP コードでは use 文で指定したエイリアスを記述していたものが完全修飾名になっています。

  • new Foo => NEW 0 string("Lib\Foo")
  • new DbBar => NEW 0 string("Lib\Db\Bar")
  • new Bar => NEW 0 string("App\Bar")
    • use 文で指定していないが、現在の名前空間から完全修飾名となる。

つまり、use 文で指定したエイリアスはオペコード生成時には解決されており、完全修飾名で実行することになります。

コンパイル時の use 文による名前解決

PHP の内部実装を見て、コンパイル時の use 文による名前解決を確認してみます。

use 文を利用した名前解決には大きく分けて二つのフェーズがあります。それは、use 文によるエイリアス登録と、クラス名参照時のエイリアスを利用した名前解決です。

use 文によるエイリアス登録

use 文では指定した完全修飾名に対してエイリアスを登録する機能です。use 文は、コンパイル時の AST では ZEND_AST_USE ノードで表現されます。ZEND_AST_USE を処理する際は下記のようになっており、zend_compile_use() を呼んでいることが分かります。

https://github.com/php/php-src/blob/PHP-8.3.8/Zend/zend_compile.c#L10444-L10446

     case ZEND_AST_USE:
            zend_compile_use(ast);
            break;

zend_compile_use() では、エイリアスと完全修飾名のハッシュテーブルを生成します。このハッシュテーブルは compiler_globals.file_context.imports です。compiler_globals はグローバル変数で主にコンパイル処理で利用します。このハッシュテーブルは現在処理中の PHP ファイルでのみ有効となっており、異なる PHP ファイルをコンパイルする場合は新たなハッシュテーブルが利用されます。

use 文ではクラス名の他に関数名や定数名のエイリアスを定義できるため、下記のようにそれぞれのハッシュテーブルが用意されています。

  • compiler_globals
    • file_context
      • imports
        • クラス名エイリアスのハッシュテーブル
      • imports_function
        • 関数名エイリアスのハッシュテーブル(use function)
      • imports_const
        • 定数名エイリアスのハッシュテーブル(use const)

https://github.com/php/php-src/blob/PHP-8.3.8/Zend/zend_compile.c#L8373-L8443

zend_compile_use() の処理を簡略化したのが以下です。

static void zend_compile_use(zend_ast *ast) /* {{{ */
{
    // (snip)
    // ハッシュテーブルの決定
    HashTable *current_import = zend_get_import_ht(type);
    // (snip)

    for (i = 0; i < list->children; ++i) {
        zend_ast *use_ast = list->child[i];
        zend_ast *old_name_ast = use_ast->child[0];
        zend_ast *new_name_ast = use_ast->child[1];
        zend_string *old_name = zend_ast_get_str(old_name_ast);
        zend_string *new_name, *lookup_name;

        // new_name(エイリアス) の決定
        if (new_name_ast) {
            new_name = zend_string_copy(zend_ast_get_str(new_name_ast));
        } else {
            const char *unqualified_name;
            size_t unqualified_name_len;
            if (zend_get_unqualified_name(old_name, &unqualified_name, &unqualified_name_len)) {
                /* The form "use A\B" is equivalent to "use A\B as B" */
                new_name = zend_string_init(unqualified_name, unqualified_name_len, 0);
            } else {
                new_name = zend_string_copy(old_name);

                if (!current_ns) {
                    zend_error(E_WARNING, "The use statement with non-compound name '%s' "
                                          "has no effect", ZSTR_VAL(new_name));
                }
            }
        }

        // エイリアスから lookup_name(キー)を生成
        if (case_sensitive) {
            lookup_name = zend_string_copy(new_name);
        } else {
            lookup_name = zend_string_tolower(new_name);
        }
        // (snip)

        // current_import に lookup_name(キー)と old_name(完全修飾名)を追加
        if (!zend_hash_add_ptr(current_import, lookup_name, old_name)) {
            zend_error_noreturn(E_COMPILE_ERROR, "Cannot use%s %s as %s because the name "
                                                 "is already in use", zend_get_use_type_str(type), ZSTR_VAL(old_name), ZSTR_VAL(new_name));
        }
        // (snip)
    }
}

use 文実行後の compiler_globals.file_context.imports をダンプすると下記のようにエイリアスと完全修飾名が格納されています。キーがエイリアスで、値が完全修飾名です。キーは小文字になっています。

>>> print_htzstr compiler_globals.file_context.imports
Hash(2)[0xfffff4e5b2a0]: {
  [0] "foo" => string(7) "Lib\Foo"
  [1] "dbbar" => string(10) "Lib\Db\Bar"
}

エイリアス利用によるクラス名解決

use 文で生成したハッシュテーブルは、new 演算子やクラスメソッド実行、instanceof 演算子などクラス名を参照する際の名前解決に利用されます。この処理はコンパイル時に行われます。

コンパイル時のクラス名解決は zend_resolve_class_name() で行います。

https://github.com/php/php-src/blob/PHP-8.3.8/Zend/zend_compile.c#L1069-L1129

この関数では下記の流れでクラス名を決定し、オペコードに利用されます。

  • オペランドのクラス名が完全修飾名であれば先頭のバックスラッシュを省いて利用。
  • compiler_globals->file_context->importsハッシュテーブルからオペランドに指定されたクラス名に合致するキーがあるか確認し、合致すれば対応する完全修飾名を利用。
  • 上記を満たさない場合は、現在の名前空間をオペランドのクラス名に連結したものを利用。
    • 現在の名前空間が App でオペランドのクラス名が Barなら App\Barとなる。

実行時のクラス参照には利用されない

use 文によるエイリアスはコンパイル時に利用されるので、変数にクラス名を入れてインスタンス化する場合など実行時にクラス名を参照する場合はエイリアスは利用できません。

<?php

namespace App;

use Lib\Foo;

$className = 'Foo';
$o = new $className(); // 実行時に解決されるので \Foo クラスとなる。

gdb-php による挙動確認

gdb-php を利用して、zend_compile_use() や zend_resolve_class_name() にブレークポイントをセットすることでコンパイル時の挙動を確認することができます。

$ make gdb-php

>>> b zend_compile_use
Breakpoint 1 at 0x6e0530: file /php-src/Zend/zend_compile.c, line 8375.
>>> b zend_resolve_class_name
Breakpoint 2 at 0x6cb378: file /php-src/Zend/zend_compile.c, line 1073.

>>> r /app/use.php

blog.shin1x1.com

さいごに

ときおり use 文によって PHP ファイルが読み込まれるという意見を見聞きすることがあるので、その挙動をまとめてみました。冒頭にも触れたとおり、あくまで名前にエイリアスを設定するだけの機能であり、コンパイル時のみ利用されるものということを覚えておくと良いでしょう。

*1:本エントリの use 文はクラス定義でトレイトを指定する use では無いので注意してください。