Shin x Blog

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

PHP のオートロードはいつ実行されるか

下記エントリを書いたところ、ではいつオートローダによって PHP ファイルが読み込まれるのかという質問をいただきました。

blog.shin1x1.com

このエントリでは、オートロードの仕組みを交えて、どのようなケースでオートローダが実行されるかをまとめました。

なお、このエントリではクラスの名称をクラス名としていますが、これはインターフェイス、トレイト、enumの名称も含んでいます。

オートローダが実行される条件

オートローダは以下の条件を満たした時に実行されます。

  • spl_autoload_register() でオートロード関数を登録している。
  • オートロード対象の PHP コードが実行される。
  • クラス定義が実行コンテクストに存在しない。

オートロード関数の登録

オートローダを実行するには、オートロード関数を登録しておく必要があります。オートロード関数は PHP の spl_autoload_register() で登録します。

https://www.php.net/spl_autoload_register

spl_autoload_register(?callable $callback = null, bool $throw = true, bool $prepend = false): bool

引数 $callback で指定するオートロード関数は、引数は string 型のクラス名を受け取ります。この関数はしかるべきタイミングでオートローダから呼ばれるだけです。この動きを見るために、下記では引数を出力するだけのオートロード関数を登録しています。このコードを実行すると、Lib\Foo クラスをインスタンス化しようとしたところ(1)で、クラス定義が無いのでオートロード関数が呼ばれます(2)。

<?php

namespace App;

use Lib\Foo;

// 引数を出力するだけのオートロード関数
spl_autoload_register(function (string $name): void {
    printf("autoload: %s\n", $name); // (2)
    // 通常はここでクラス定義が記述された PHP ファイルを include() などで読み込む。
});

$foo = new Foo();  // (1) クラス定義が無い

https://3v4l.org/uEIDb

autoload: Lib\Foo

Fatal error: Uncaught Error: Class "Lib\Foo" not found

このような動作を行うオートロード関数ですが、現在は Composer のオートロード関数を利用するのが主流です。PSR-4 に準拠したパス構成にして、composer.json に設定を行なっておけば、自前でオートロード関数を実装する必要は無いケースがほとんどです。

https://getcomposer.org/doc/01-basic-usage.md#autoloading

require __DIR__ . '/vendor/autoload.php';

さらに、Laravel や Symfony のようなフレームワークを利用している場合、フレームワークに同梱されているコードにオートロードを登録するコードが含まれているため、オートロード関数の登録すら不要です。例えば、Laravel ではエントリポイントの public/index.php に下記のような記述でオートロードを登録しています。

https://github.com/laravel/laravel/blob/3b3f9f13faab1753e7b7cad6a0e7098e54c8199f/public/index.php#L12-L13

// Register the Composer autoloader...
require __DIR__.'/../vendor/autoload.php';

オートロード関連のコードを目にしていなくても、アプリケーションでオートローダが実行されている(include() や require() を実行せずとも別ファイルの PHP コードが読み込まれている)ということは、どこかでオートロード関数が登録されているということなので、これは覚えておくと良いでしょう。

オートローダが実行される PHP コード

オートローダが実行される可能性があるのは下記のようなコードです。これらのコードで対象とするクラス定義が実行コンテクストで存在しなければオートローダが実行されます。

  • クラス定義
    • クラス継承(extends)
    • インターフェイス実装(implements)
    • トレイト利用(use)
  • new 演算子
  • クラス名による静的メソッド実行
  • クラス名による静的変数、定数参照
    • トレイト名による静的変数参照はオートロード実行後に Deprecated エラーになる
    • トレイト名による定数参照はオートロード実行後に Fatal エラーになる
  • enum 値参照
  • リフレクション
  • spl_autoload_call()
  • unserialize() でオブジェクトをデシリアライズする場合
    • オートローダが実行されるがクラス定義が見つからなくてもエラーにはならない。
<?php
declare(strict_types=1);

spl_autoload_register(function (string $name): void {
    printf("autoload: %s\n", $name);
    switch ($name) {
        case 'C1':
            class C1 {}
            break;
        case 'C2':
            class C2 {}
            break;
        case 'C3':
            class C3
            {
                public static function method() {}
            }
            break;
        case 'C4':
            class C4
            {
                public const V = 1;
            }
            break;
        case 'C5':
            class C5
            {
                public static $v;
            }
            break;
        case 'C6':
            class C6 {}
            break;
        case 'C7':
            class C7 {}
            break;
        case 'I1':
            interface I1 {}
            break;
        case 'I2':
            interface I2
            {
                public const V = 1;
            }
            break;
        case 'I3':
            interface I3 {}
            break;
        case 'T1':
            trait T1 {}
            break;
        case 'T2':
            trait T2 {}
            break;
        case 'E1':
            enum E1: int
            {
                case V = 10;
            }
            break;
        case 'A1':
            #[Attribute]
            class A1 {}
            break;
    }
});

final class Foo extends C1 implements I1
{
    use T1;
}

new C2;
C3::method();
C4::V;
C5::$v;
I2::V;
E1::V;
// T9::V; トレイト名による定数参照でオートロードは動作するが、その後 Cannot access trait constant エラーになる

new ReflectionClass(C6::class);
(new ReflectionClass(Foo::class))->getAttributes('A1', ReflectionAttribute::IS_INSTANCEOF);

spl_autoload_call(C7::class);

unserialize('O:2:"C8":0:{}'); // クラス定義が無くてもエラーにはならない
/*
object(__PHP_Incomplete_Class)#2 (1) {
  ["__PHP_Incomplete_Class_Name"]=>
  string(2) "C6"
}
*/
autoload: C1
autoload: T1
autoload: I1
autoload: C2
autoload: C3
autoload: C4
autoload: C5
autoload: I2
autoload: E1
autoload: C6
autoload: A1
autoload: C7
autoload: C8

https://3v4l.org/mqWuo

余談ですが、クラス定義、インターフェイス定数、enum 値の参照は同じ構文です。

C4::V;
I2::V;
E1::V;

このコードをコンパイルすると、同じオペコードにコンパイルされます。これらの違いはオペコードを実行して、クラス情報を取得した時に必要に応じてそれぞれの処理を行います。

0000 T0 = FETCH_CLASS_CONSTANT string("C4") string("V")
0001 FREE T0
0002 T1 = FETCH_CLASS_CONSTANT string("I2") string("V")
0003 FREE T1
0004 T2 = FETCH_CLASS_CONSTANT string("E1") string("V")
0005 FREE T2

https://github.com/php/php-src/blob/PHP-8.3.8/Zend/zend_vm_def.h#L5912-L6036

オートローダが実行されない PHP コード

下記コードでは、クラス名を指定しますがオートローダは実行されません。なお、use 文や ::class 以外は実行コンテクストにすでにクラス情報が存在すればその情報を利用します。

  • 型宣言、型検査
    • メソッド(関数)の引数、戻り値
    • クラス(トレイト)プロパティ
  • use 文(名前空間のエイリアス宣言。クラス定義のトレイト利用では無いので注意。)
  • ::class
    • これはクラス名を指定するが、クラス定義は不要。単に名前空間から完全修飾名を引いてくるだけ。
  • instanceof 演算子
  • アトリビュート
<?php
declare(strict_types=1);

namespace App;

spl_autoload_register(function (string $name): void {
    printf("autoload: %s\n", $name);
});

use stdClass;
use Lib\Bar;

#[C10]
final class Foo
{
    public C1 $c1;

    public function method(C2 $c2) {}
}

$foo = new Foo();
try {
    $foo->c1 = new stdClass();
} catch (\Throwable $e) {
    // TypeError にはなるがオートローダは実行されない
    var_dump($e->getMessage());
}
try {
    $foo->method(new stdClass());
} catch (\Throwable $e) {
    // TypeError にはなるがオートローダは実行されない
    var_dump($e->getMessage());
}

var_dump(C3::class);
var_dump($foo instanceOf C4);
string(62) "Cannot assign stdClass to property App\Foo::$c1 of type App\C1"
string(107) "App\Foo::method(): Argument #1 ($c2) must be of type App\C2, stdClass given, called in /in/biFQq on line 28"
string(6) "App\C3"
bool(false)

https://3v4l.org/MtoWM

引数によってオートローダが実行される関数

PHP 関数やメソッドには、引数によってオートローダが実行されるものがあります。下記はその一部ですが、オートローダが実行されるケースを挙げています。いずれの場合も実行コンテクストにクラス定義が存在しないケースに実行されます。

  • is_a() / is_subclass_of() の引数 $object_or_class の値が文字列で、$allow_string の値が true の場合
  • class_alias() / class_exists() / enum_exists() / interface_exists() / trait_exists() の引数 $autoload の値が true の場合
  • get_class_methods() / get_class_vars() / get_parent_class() の第一引数の値が文字列の場合
  • class_implements()、class_parents()、class_uses() の引数 $autoload の値を true の場合
  • ReflectionClass::getAttributes() などのアトリビュートを取得するリフレクションメソッドで、引数 $name を指定し、$flags に ReflectionAttribute::IS_INSTANCEOF を指定した場合

オートローダ実行の仕組み

PHP 内部実装(php-src)でオートローダがどのように実行されているかを見てみましょう。

オートローダ実行の流れを見るために重要な関数が二つあります。それが、zend_lookup_class_ex() と spl_perform_autoload() です。前者は、クラス情報を取得する関数で、クラス情報が存在しない場合にオートローダを実行します。後者は、オートローダの実体でこの関数から PHP の spl_autoload_register() で登録したオートロード関数を実行します。

zend_lookup_class_ex()

zend_lookup_class_ex() は、実行コンテクストからクラスエントリ(クラス定義情報)を取得する関数です。引数に、名称(クラス名、インターフェイス名等)を示す name、EG(class_table) という実行コンテクスト内のクラスエントリを保持しているハッシュテーブルのキーを示す key、そして動作を指定する flags を取り、クラスエントリを返します。

https://github.com/php/php-src/blob/PHP-8.3.8/Zend/zend_execute_API.c#L1118-L1239

ZEND_API zend_class_entry *zend_lookup_class_ex(zend_string *name, zend_string *key, uint32_t flags)

まず、EG(class_table) から指定されたキー(キーが未指定なら name からキーを生成)に対応するクラスエントリがあるかチェックします。存在すればクラスエントリを返して*1終了します。PHP コードレベルではすでにクラス定義が存在する状態です。

クラスエントリが無い、つまりクラス定義が無い場合は処理を継続します。ここからはオートローダを実行するか否かの判定がいくつか続きます。オートローダが実行されるには下記の条件を満たす必要があります。

  • 引数 flags & ZEND_FETCH_CLASS_NO_AUTOLOAD が偽である
  • zend_is_compiling() が偽である(コンパイル中ではない)*2
  • zend_autoload に値がある
  • 引数 key に値がある、もしくは name が有効な値である
  • 再帰的なオートローダ実行で、引数で指定されたクラスをオートロード中ではない(EG(in_autoload) に lc_name キーが存在しない)

これらが満たされた場合にオートローダである zend_autoload() (実体は spl_perform_autoload() )が実行されます。満たされない場合は NULL を返します。これは、クラス定義が存在しないことを意味し、呼び出し元で Class not found などのエラーとします。

 ce = zend_autoload(autoload_name, lc_name);

spl_perform_autoload()

上記で見たように spl_perform_autoload() はオートローダを示す関数ポインタ zend_autoload の実体です。引数に、クラス名を示す class_name とキーを示す lc_name を取り、クラスエントリを返します。

https://github.com/php/php-src/blob/PHP-8.3.8/ext/spl/php_spl.c#L423-L465

static zend_class_entry *spl_perform_autoload(zend_string *class_name, zend_string *lc_name)

最初に spl_autoload_functions が空かどうかチェックします。この変数はオートロード関数を保持しているハッシュテーブルで、PHP の spl_autoload_register() を実行するとこのハッシュテーブルにオートロード関数が登録されます。リクエスト初期状態は spl_autoload_functions は NULL になっているので、空であればそのまま処理を終了します。

 if (!spl_autoload_functions) {
        return NULL;
    }

下記がメインループです。この中で spl_autoload_functions の要素であるオートロード関数を順に実行していきます。オートロード関数を一つ実行して、クラスエントリの存在チェックを行います。ここで存在していればクラスエントリを返して終了します。存在しない場合は次の要素のオートロード関数を実行します。オートロード関数は複数登録することができるのでそれに対応した実装となっています。

while (1) {
        autoload_func_info *alfi =
            zend_hash_get_current_data_ptr_ex(spl_autoload_functions, &pos);
        if (!alfi) {
            break;
        }

        zend_function *func = alfi->func_ptr;
        if (UNEXPECTED(func->common.fn_flags & ZEND_ACC_CALL_VIA_TRAMPOLINE)) {
            func = emalloc(sizeof(zend_op_array));
            memcpy(func, alfi->func_ptr, sizeof(zend_op_array));
            zend_string_addref(func->op_array.function_name);
        }

        zval param;
        ZVAL_STR(&param, class_name);
        zend_call_known_function(func, alfi->obj, alfi->ce, NULL, 1, &param, NULL);
        if (EG(exception)) {
            break;
        }

        if (ZSTR_HAS_CE_CACHE(class_name) &&  ZSTR_GET_CE_CACHE(class_name)) {
            return (zend_class_entry*)ZSTR_GET_CE_CACHE(class_name);
        } else {
            zend_class_entry *ce = zend_hash_find_ptr(EG(class_table), lc_name);
            if (ce) {
                return ce;
            }
        }

        zend_hash_move_forward_ex(spl_autoload_functions, &pos);
    }

ちなみに new 演算子で存在しないクラスをインスタンス化しようとすると、下記のようなスタックでオートロード関数が呼ばれます。

[0] from 0x0000aaaaab18a1ac in zend_call_known_function+36 at /php-src/Zend/zend_execute_API.c:1037
[1] from 0x0000aaaaaaf21ff8 in spl_perform_autoload+340 at /php-src/ext/spl/php_spl.c:448
[2] from 0x0000aaaaab18aa24 in zend_lookup_class_ex+1232 at /php-src/Zend/zend_execute_API.c:1221
[3] from 0x0000aaaaab18b7cc in zend_fetch_class_by_name+36 at /php-src/Zend/zend_execute_API.c:1738
[4] from 0x0000aaaaab209e54 in ZEND_NEW_SPEC_CONST_UNUSED_HANDLER+148 at /php-src/Zend/zend_vm_execute.h:10519
[5] from 0x0000aaaaab27b954 in execute_ex+6572 at /php-src/Zend/zend_vm_execute.h:58125
[6] from 0x0000aaaaab27f07c in zend_execute+296 at /php-src/Zend/zend_vm_execute.h:61604
[7] from 0x0000aaaaab1a6da0 in zend_execute_scripts+332 at /php-src/Zend/zend.c:1891
[8] from 0x0000aaaaab0e7fb0 in php_execute_script+640 at /php-src/main/main.c:2515
[9] from 0x0000aaaaab347828 in do_cli+3076 at /php-src/sapi/cli/php_cli.c:966

型検査

PHP では、ユーザ定義型はクラス、インターフェイス、enum のみ定義できます。型検査の場合は、値が持つクラスエントリと型名から取得したクラスエントリを比較します(値の継承ツリー、実装インターフェイス含む)。この型名からクラスエントリを取得する際は zend_lookup_class_ex() を利用するのですが、引数 flags に ZEND_FETCH_CLASS_NO_AUTOLOAD を指定しているのでオートロードは動作しません。

動作をイメージしてみると、型宣言でユーザ定義型が指定されている場合、値が型検査を通過するには宣言された型名に対応するクラスエントリが存在する必要があります。型検索時に型名に対応するクラスエントリが存在しないのであれば、値が型検査を通過することはないので、オートロードする必要はありません。(失敗することが分かっている型検査のためにオートロードを実行する必要は無い)

関連する関数は以下です。

  • ユーザ定義型検査
    • instanceof_function()
    • instanceof_function_slow()
  • メソッド、関数での型検査
    • zend_check_type()
    • zend_check_type_slow()
    • zend_fetch_ce_from_cache_slot()
  • クラスプロパティ、定数の型検査
    • zend_check_and_resolve_property_or_class_constant_class_type()
    • zend_ce_from_type()

さいごに

PHP コードの中でオートローダが実行されるのかを見てきました。こうして見直すと、クラス名を記述していてもオートローダが動かないケースや PHP 関数で意外とオートローダが絡むものがあったりで発見がありました。

オートローダは、フレームワークを利用するのが当たり前になった昨今では使っていながらあまり意識されない機能の一つかもしれません。面白い機能なので、コードを追ってみたり、オートロード関数を自作してみるなどしてみてください。

*1:リンク関連の処理は本エントリでは無視します

*2:PHP は実行時でも include() や eval() でコンパイル処理が実行される