【PHP】spl_autoload_register()っていったい何してんの？

## 初めに

`/public/index.php`を起点にLaravelの処理を追っていたところ、`vendor/composer/autoload_real.php`の中に`spl_autoload_register()`という何やらよくわからない関数が登場したので、それの説明をする解説記事となります。

## 動作環境

* PHP `7.4.3`
* Composer `2.0.11`

## 該当の関数が登場したソースコード

`vendor/composer/autoload_real.php`の`getLoader()`メソッドの中にそれはありました。

```php:vendor/composer/autoload_real.php
class ComposerAutoloaderInitcde23787628405c61112bd11321f024e
{
    private static $loader;

    public static function loadClassLoader($class)
    {
        if ('Composer\Autoload\ClassLoader' === $class) {
            require __DIR__ . '/ClassLoader.php';
        }
    }

    public static function getLoader()
    {
        // ～～中略～～

        spl_autoload_register(array('ComposerAutoloaderInitcde23787628405c61112bd11321f024e', 'loadClassLoader'), true, true);
        self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(\dirname(__FILE__)));
        spl_autoload_unregister(array('ComposerAutoloaderInitcde23787628405c61112bd11321f024e', 'loadClassLoader'));

        // ～～後略～～
    }
}

```

では、この`spl_autoload_register()`がなにをしているのかという謎を解決してみましょう。


## 結論

先に結論を申し上げておきますと、`spl_autoload_register()`は、

**未定義のクラスやインターフェースが読み込まれたときに実行してほしい処理を登録しておくことができる関数**

と、私は解釈いたしました。

つまり、

**呼び出したクラス、インターフェースが、自ファイルや`require`されたものの中に定義されていなかった場合に、クラスのオートロード処理といったいわばセーフティネットのようなものを登録できる関数**

と認識すればわかりやすいのではないでしょうか。

以下、大変参考にさせていただきました記事になります。

* [PHP: クラスのオートローディング - Manual](https://www.php.net/manual/ja/language.oop5.autoload.php)
* [【PHP】spl_autoload_registerとオートロード | とものブログ](https://se-tomo.com/2018/12/19/%E3%80%90php%E3%80%91spl_autoload_register%E3%81%A8%E3%82%AA%E3%83%BC%E3%83%88%E3%83%AD%E3%83%BC%E3%83%89/)
* [public/index.php の最初の三行(vendor/autoload.php の挙動)｜gallu｜note](https://note.com/gallu/n/ne1bcb25c412a)

## 具体例

サンプルとして同一ディレクトリ上に、`Foo.php`と`execute.php`があったとします。


```php:./Foo.php
class Foo
{
   public function test()
   {
       echo 'test in Foo class';
   }
}
```

```php:./execute.php
$foo = new Foo()  // PHP Error:  Class 'Foo' not found in ...

$foo->test();
```

`execute.php`での`Foo`クラスの呼び出しは`require`も何もされていないので、インスタンス化の時点で当然例外が出力されます。

では`spl_autoload_register()`を使って`Foo`クラスをオートロードしてみたいと思います。


`execute.php`に`spl_autoload_register()`を追加しました。

```php:execute.php

<?php

spl_autoload_register(function($class) {  // オートロード処理を登録
        echo "Want to load $class.\n";
        require __DIR__ . "/${class}.php";
});

$foo = new Foo();

$foo->test();
```

`spl_autoload_register()`の`execute.php`を実行してみるとどうでしょう。

```bash
Want to load Foo.  
test in Foo class  
```

と`require ./Foo.php`という記述をしていないのにも関わらず、`execute.php`で`Foo`クラスオブジェクトによる`test()`メソッドが実行されていることが分かります。

以下、解説です。

## spl_autoload_register()の使い方

[マニュアル](https://www.php.net/manual/ja/function.spl-autoload-register.php)によると`spl_autoload_register()`には3つ引数を指定できるようです。

> `spl_autoload_register ( callable $autoload_function = ? , bool $throw = true , bool $prepend = false ) : bool`


> **autoload_function**
登録したい autoload 関数。 パラメータが指定されなかった場合は、デフォルト実装である spl_autoload() が登録されます。
**throw**
このパラメータは、 spl_autoload_register() が autoload_function を登録できなかったときに例外をスローするかどうかを指定します。
**prepend**
true の場合、spl_autoload_register() はキューの最後に追加するのではなく先頭に追加します。

> 引用：[PHP: spl_autoload_register - Manual](https://www.php.net/manual/ja/function.spl-autoload-register.php)

かいつまんで説明すると、

* `autoload_function`には登録したいオートローディング処理を`callable`型で指定
* `throw`は引用した文章の通り
* `prepend`には登録したコールバックを最優先に実行するかを`bool`型で指定

といったように理解すればいいでしょうか。（`callable`型については[こちら](https://www.php.net/manual/ja/language.types.callable.php)を参照してください）

この`autoload_function`に指定したコールバック関数には、引数として呼び出そうとした未定義のクラス名やインターフェース名が文字列として渡されます。

この渡される文字列を用いて、動的に`require`などのファイル読み込み処理を記述しておける、というのが`autoload_function`の意義でしょう。

`spl_autoload_register()`の呼び出し方法が分かったところで、`execute.php`の処理を1つ1つ見てみましょう。


## サンプルコードの解説

以下、再掲です。

```php:execute.php
<?php

spl_autoload_register(function($class) {  
        echo "Want to load $class.\n";
        require __DIR__ . "/${class}.php";
});

$foo = new Foo();

$foo->test();
```

まず`spl_autoload_register()`に渡した無名関数には、

1. `"Want to load $class.\n"`という文字列を出力
2. `$class`に渡されたクラス名をもとに、同一ディレクトリ内の`${class}.php`を読み込む

という処理が記述されています。

なので、`"Want to load $class.\n"`という文字列が出力されれば`spl_autoload_register()`によるオートロードが働いているということが分かりますね。

`spl_autoload_register()`によりオートロード処理の登録が完了したところで、次の処理を見てみましょう。

```php:./execute.php
$foo = new Foo();
```

`./execute.php`では未定義の`Foo`クラスが呼び出されました。


通常でしたらそのまま例外が出力されるのですが、未定義クラスが呼び出されたことをトリガーとして、`spl_autoload_register()`に登録した処理が実行されます。

今回の例でいうと、無名関数の引数`$class`には`"Foo"`が渡され、

1. `"Want to load $class.\n"`が出力され
2. 同一ディレクトリ内の`./Foo.php`を読み込む

という処理が実行されます。

このオートローディング処理を経て、無事`./Foo.php`を読み込むことができました。

なので、`$foo = new Foo()`で例外が出力されることはなく、そして`$foo->test()`も例外を出すことなく正しい出力がなされるのです。


## vendor/composer/autoload_real.php内のgetLoader()の解説

`spl_autoload_register()`の動作が分かったところで、本記事の趣旨とは外れますが`vendor/composer/autoload_real.php`の`getLoader()`のうち`spl_autoload_register()`が絡んでくる部分を解説してみたいと思います。

以下、再掲です。

```php:vendor/composer/autoload_real.php
class ComposerAutoloaderInitcde23787628405c61112bd11321f024e
{
    private static $loader;

    public static function loadClassLoader($class)
    {
        if ('Composer\Autoload\ClassLoader' === $class) {
            require __DIR__ . '/ClassLoader.php';
        }
    }

    public static function getLoader()
    {
        // ～～中略～～

        spl_autoload_register(array('ComposerAutoloaderInitcde23787628405c61112bd11321f024e', 'loadClassLoader'), true, true);
        self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(\dirname(__FILE__)));
        spl_autoload_unregister(array('ComposerAutoloaderInitcde23787628405c61112bd11321f024e', 'loadClassLoader'));

        // ～～後略～～
    }
}

```

`getLoader()`メソッドを見てください。こちらで`spl_autoload_register()`が呼び出されています。

第1引数には

```php
array('ComposerAutoloaderInitcde23787628405c61112bd11321f024e', 'loadClassLoader')
```
という配列が渡されています。

先ほど`autoload_function`には登録したいオートローディング処理を`callable`型で指定すると述べましたが、`callable`型として配列を渡すことも可能になっております。

`callable`型の引数として配列を渡した場合、

* 0番目にはクラスオブジェクト、またはクラス名の文字列
* 1番目にはそのクラスで定義されているメソッド名の文字列

と指定することで、0番目のクラスに定義されている1番目のメソッドをコールバックとして指定するといったことができます。

参考：[PHP: コールバック / Callable - Manual](https://www.php.net/manual/ja/language.types.callable.php)

これを踏まえて該当の`spl_autoload_register()`を見てみましょう。

```php
spl_autoload_register(array('ComposerAutoloaderInitcde23787628405c61112bd11321f024e', 'loadClassLoader'), true, true);
```

配列の0番目に`"ComposerAutoloaderInitcde23787628405c61112bd11321f024e"`（つまり自身のクラスですね）が、1番目に`"loadClassLoader"`が渡されています。


ですので、この呼び出しで`ComposerAutoloaderInitcde23787628405c61112bd11321f024e::loadClassLoader`メソッドがオートローディング処理として登録されることになります。

次の行を見てみると、さっそく未定義のクラス`\Composer\Autoload\ClassLoader`が呼び出されています。

```php
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(\dirname(__FILE__)));
```

なので、先ほど指定したばかりのオートロードが実行されます。つまり、自クラスの`loadClassLoader()`メソッドが実行されます。

```php
public static function loadClassLoader($class)
{
    if ('Composer\Autoload\ClassLoader' === $class) {
        require __DIR__ . '/ClassLoader.php';
    }
}
```

先ほどの`execute.php`と似たような処理ですね。言葉で説明すると、

**呼び出しクラス名が`'Composer\Autoload\ClassLoader'`と一致するなら、`vendor/composer/autoload_real.php`と同一ディレクトリにある`ClassLoader.php`を`require`する**

となります。

そして今回、`new`するクラスがまさに`\Composer\Autoload\ClassLoader`クラスであるので、`vendor/composer/ClassLoader.php`がオートロードされて、
無事に`self::$loader`とローカルスコープ上の`$loader`に`\Composer\Autoload\ClassLoader`クラスのインスタンスが代入されます。

最後に後処理として、先ほど登録したオートロード処理を

```php
spl_autoload_unregister(array('ComposerAutoloaderInitcde23787628405c61112bd11321f024e', 'loadClassLoader'))
```
によって解除したというところで、今回の解説は以上になります。


## 終わりに

`composer`を使っている方ならオートロードという仕組みが備わっていることを一度は耳にしたことがあると思います。

ですが、その仕組みがどのように提供されているのか詳しく知っている人はあまり多くないと思います。

そういった、普段であれば考えなくていい、ツールが裏側で担っている処理がどのように構築されているかを追ってみるのは私の中でいい経験になりました。


皆さんも一度、いつも使っているツールがどのような処理の上に提供されているのか、1から紐解いてみてはいかがでしょうか。そのツールに対して普段とはまた違った知見を得られるかもしれません。