コードから読み解くautoloadの仕組み　～より深くautoloadの仕組みを理解しよう～

これは[Qiita Advent Calendar 2022 Laravel 17日目](https://qiita.com/advent-calendar/2022/present-calendar)の記事です。

# この記事で扱うこと
この記事は、Laravelで使用されているComposerのautoloadの仕組みについて実際のコードを見ていく中で理解することを目的とした記事です。なので、この記事においてautoloadをどう使用するかについては一切触れていません。

# autloadの仕組みについて

:::note warn
コードを簡略化して解説しやすくするために、この記事で紹介するコードの中で比較的重要ではない（と自分が感じる）部分は省略しています。
:::

## バージョン
| ツール | バージョン |
|:-:|:-:|
| PHP  | 8.1.12  |
| Laravel  | 8.77.1  |
|Composer|2.4.4|

## 概観
autoloadの仕組みを見る前に全体的にどんな処理になっているかを載せておきます。
それぞれのクラスの処理を見る中で全体がどうなっているのか確認したい時などにぜひ見返してみてください～
![autload image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478883/5c911cfc-9b8a-e4d0-f7c4-ce80357ed8e0.png)

## autoload_real.php（autoloadを実行する部分）

```php
class ComposerAutoloaderInit0a0278aba4e6854e7b6b378ee91b41bb
{
    private static $loader;

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

    /**
     * @return \Composer\Autoload\ClassLoader
     */
    public static function getLoader() 
    {
        if (null !== self::$loader) {
            return self::$loader;
        }

        require __DIR__ . '/platform_check.php';
// ........ 1

        spl_autoload_register(array('ComposerAutoloaderInit0a0278aba4e6854e7b6b378ee91b41bb', 'loadClassLoader'), true, true);
        self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__));
        spl_autoload_unregister(array('ComposerAutoloaderInit0a0278aba4e6854e7b6b378ee91b41bb', 'loadClassLoader'));
// ........ 2

        require __DIR__ . '/autoload_static.php';
        call_user_func(\Composer\Autoload\ComposerStaticInit0a0278aba4e6854e7b6b378ee91b41bb::getInitializer($loader));
// ........ 3

        $loader->register(true);
// ........ 4

        $includeFiles = \Composer\Autoload\ComposerStaticInit0a0278aba4e6854e7b6b378ee91b41bb::$files;
        foreach ($includeFiles as $fileIdentifier => $file) {
            composerRequire0a0278aba4e6854e7b6b378ee91b41bb($fileIdentifier, $file);
        }
// ........ 5

        return $loader;
    }
}

/**
 * @param string $fileIdentifier
 * @param string $file
 * @return void
 */
function composerRequire0a0278aba4e6854e7b6b378ee91b41bb($fileIdentifier, $file) 
{
    if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
        $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;

        require $file;
    }
}
```
autoloadの実行を行う`getLoader`メソッドの処理の内容を説明していきます。
`getLoader`メソッドでは以下の5つを行っています。
1. このクラスの静的変数`$loader`（`$loader`に何が入るのかは後で述べる）に何も入っていない時にその後の処理を行い、入っているときは$loaderを返して終了。その後、`vendor/composer/platform_checku.php`でComposerの扱えるPHPのバージョンチェックなどを行い、問題があればHTTPの`header`でエラーを表示し、問題がなければその後の処理へ移る。
2. このクラスの`loadClassLoader`メソッドを実行して`vendor/composer/ClassLoader.php`を読み込み、そのファイル内の`ClassLoader`クラスのインスタンスをこのクラスの静的変数`$loader`に入れる。そして`spl_autoload_unregister`メソッドでオートローディングから`loadClassLoader`メソッドを解除する。

:::note info
**`spl_autoload_register`メソッドについて**
このメソッドはPHPに標準であるSPL関数の一つであり、PHP8で削除されたクラスのオートローディングを行う`__autoload`メソッドの代わりとなるメソッドなようです。**PHPのオートローディングとは何かというと、簡単に言えば処理の中でクラスの呼び出し時に実行する処理のこと**を指します。ちなみにオートローディングは自動でファイルを読み込んでくれる機能ではないので、クラス呼び出し時にそのファイルを読み込む処理は自分で実装しなければいけません。
例えばこんな構造のファイルがあったとして

```
▾ /　　　　　　　　　　　　　　　　　
  test.php
  testClass.php
```

`testClass.php`の`output`メソッドを`test.php`内で実行するとします。

```php
class TestClass
{
    function output()
    {
        var_dump('This is TestClass.php !!');
    }
}
```
そんな時に`spl_autoload_register`メソッドを使うと以下のような処理で実装できます。
```php
function my_autoload(string $class_name) // 引数はクラス名の文字列データ
{
        include __DIR__ . '/' . $class_name . '.php';
} // クラスが登場した際に同じ階層内のクラス名に合致するファイル名を持ったファイルを読み込む

spl_autoload_register('my_autoload'); // オートロードで行うメソッドの登録

$obj = new TestClass();
$obj->output(); // -> string(24) "This is TestClass.php !!"
```

また、こんなスクリプトでも同じことができます。

```php
class Test
{
        function my_autoload(string $class_name)
    {
            include __DIR__ . '/' . $class_name . '.php';
    }

    function spl() // Testクラス内のmy_autoloadメソッドの実行
    {
        spl_autoload_register(array('Test', 'my_autoload')); // arrayメソッドの第一引数は'test'、$thisでも可
    }

}
$test = new Test();
$test->spl(); // spl_autoload_registerメソッドの実行
$obj = new TestClass();
$obj->output(); // -> string(24) "This is TestClass.php !!"
```
`spl_autoload_register`メソッドの第一引数に`array`メソッドで`[[0] => クラス名, [1] => そのクラスのメソッド名]`を入れることでそのクラス内のメソッドがオートローディングに登録されます。
:::

https://www.php.net/manual/ja/function.spl-autoload-register.php

https://www.php.net/manual/ja/function.autoload.php

3\. `vendor/composer/autoload_static.php`を読み込み、そのファイルにあるクラス内の`getInitializer`メソッドを実行し（正確には`getInitializer`メソッドの返り値であるクロージャを実行）、`ClassLoader`クラスのインスタンス内の変数にクラス読み込み用のデータを格納。（`getInitializer`メソッドの処理は後述）`call_user_func`メソッドの内容は以下のリンクをチェック！

https://www.php.net/manual/ja/function.call-user-func.php

4\. `ClassLoader`クラスのインスタンス内の`register`メソッドを実行してクラスのオートロードを実行。これ以降クラスの呼び出しがされた際、そのクラスのファイルが読み込まれる。（`register`メソッドの処理は後述）

5\. グローバル変数配列`__composer_autoload_files`のキーに`ClassLoader`クラスの`$files`配列のキー、値に`true`を代入し、`$files`配列の値のファイルのパスからファイルを読み込む


## autoload_static.php（autoloadのためのデータを保持する部分）

```php
namespace Composer\Autoload;

class ComposerStaticInit0a0278aba4e6854e7b6b378ee91b41bb
{
    public static function getInitializer(ClassLoader $loader)
    {
        return \Closure::bind(function () use ($loader) {
            $loader->prefixLengthsPsr4 = ComposerStaticInit0a0278aba4e6854e7b6b378ee91b41bb::$prefixLengthsPsr4;
            $loader->prefixDirsPsr4 = ComposerStaticInit0a0278aba4e6854e7b6b378ee91b41bb::$prefixDirsPsr4;
            $loader->prefixesPsr0 = ComposerStaticInit0a0278aba4e6854e7b6b378ee91b41bb::$prefixesPsr0;
            $loader->classMap = ComposerStaticInit0a0278aba4e6854e7b6b378ee91b41bb::$classMap;

        }, null, ClassLoader::class);
    }
}
```
`autoload_static.php`は読み込むファイルのパスやクラスの名前空間などたくさんの情報がある（なんと6774行！）のですが、今回は処理の部分だけ紹介します。
`getInitializer`メソッドでは`ClassLoader`クラスのインスタンス内の変数にこのファイルのクラス内の変数の値を代入します。

:::note info
**`Closure::bind`メソッドについて**
第三引数で指定したクラスのに属する静的変数、第二引数の動的変数変数を使用して第一引数のメソッドを実行します。
:::

https://www.php.net/manual/ja/closure.bind.php

## ClassLoader.php（autoloadを実装している部分）

```php
    /**
     * @param ?string $vendorDir
     */
    public function __construct($vendorDir = null)
    {
        $this->vendorDir = $vendorDir;
    }

    /**
     * Registers this instance as an autoloader.
     *
     * @param bool $prepend Whether to prepend the autoloader or not
     *
     * @return void
     */
    public function register($prepend = false)
    {
        spl_autoload_register(array($this, 'loadClass'), true, $prepend);

        if (null === $this->vendorDir) {
            return;
        }
    }

    /**
     * Loads the given class or interface.
     *
     * @param  string    $class The name of the class
     * @return true|null True if loaded, null otherwise
     */
    public function loadClass($class)
    {
        if ($file = $this->findFile($class)) {
            includeFile($file);

            return true;
        }

        return null;
    }

    /**
     * Finds the path to the file where the class is defined.
     *
     * @param string $class The name of the class
     *
     * @return string|false The path if found, false otherwise
     */
    public function findFile($class)
    {
        // class map lookup
        if (isset($this->classMap[$class])) {
            return $this->classMap[$class];
        }

        $file = $this->findFileWithExtension($class, '.php');

        return $file;
    }

    /**
     * @param  string       $class
     * @param  string       $ext
     * @return string|false
     */
    private function findFileWithExtension($class, $ext)
    {
        // PSR-4 lookup
        $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;

        $first = $class[0];
        if (isset($this->prefixLengthsPsr4[$first])) {
            $subPath = $class;
            while (false !== $lastPos = strrpos($subPath, '\\')) {
                $subPath = substr($subPath, 0, $lastPos); // 読み込むクラスのファイルの親ディレクトリのパスを取得
                $search = $subPath . '\\';
                if (isset($this->prefixDirsPsr4[$search])) { // 親ディレクトリのパスが適切なものかチェック
                    $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1); // ファイル名を取得
                    foreach ($this->prefixDirsPsr4[$search] as $dir) {
                        if (file_exists($file = $dir . $pathEnd)) { // ディレクトリのパスとファイル名を結合
                            return $file;
                        }
                    }
                }
            }
        }
     }

/**
 * Scope isolated include.
 *
 * Prevents access to $this/self from included files.
 *
 * @param  string $file
 * @return void
 * @private                                                                                                                                                                                                       */
function includeFile($file)
{
    include $file;
}
```

### constructメソッド
`composer`ディレクトリの親である`vendor`ディレクトリのパスを変数加えます。

### registerメソッド
同じクラス内の`loadClass`メソッドを実行します。これ以降クラスが呼び出された際に自動で`loadClass`メソッドが実行されます。

### loadClassメソッド
同じクラス内の`findFile`メソッドによって存在が確認されたファイルをクラス外の`includeFile`メソッドによって読み込みます。

### findFileメソッド
`autoload_static.php`のクラス内の`$classMap`で登録されたクラスであれば、そのクラスのファイルのパスを返します。もしそうでなければ同じクラス内の`findFileWithExtension`メソッドを実行してファイルのパスを返します。

### findFileWithExtensionメソッド
このメソッドでは`autoload_static.php`から渡された変数の情報をもとにファイルのパスを特定します。読み込むクラスのファイルのパスを取得し、そこからファイル名を除いたもの（`$search`）を`autoload_static.php`にある`$prefixDirsPsr4`に検索をかけ、合致するものがあった場合、そのパスと読み込むクラスのファイル名をつなげたものをファイルのパスとして返します。

::: note info
**`strtr`メソッドについて**
文字列データ内の文字を他の文字へ変換します。今回の場合はクラスの名前空間の区切りを表す`'\\'`をファイルのパスの区切りを表す`/`に変換します。
:::

https://www.php.net/manual/ja/function.strtr

::: note info
**`strrpos`メソッドについて**
文字列データ内に特定のキーワードが最後に存在する場所を表します。今回はクラス名の場所をこのメソッドで取得して`substr`メソッドを使用したことで、クラス名のみの取得やクラス名以外の取得ができるようになりました。
:::

https://www.php.net/manual/ja/function.strrpos

::: note info
**`substr`メソッドについて**
文字列データの一部分を切り取ったデータを生成します。その際、元のデータに影響はありません。第二引数に切り取る先頭、第三引数にどれくらいの長さのデータを生成するかを入力します。
:::

https://www.php.net/manual/ja/function.substr

# 終わりに
見ていただきありがとうございました！
今回の記事を書いていて、PHPに備わっている機能の新たな使い方を知れて楽しかったです。
しかし、その中で`autoload_static.php`の`$classMap`に自分が作成したファイルのパスがいつの間にか入っていたのですが、いつ入ったのだろうか？という疑問が解決できませんでした。知っている方がいればぜひ教えていただきたいです。