3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

LaravelAdvent Calendar 2022

Day 17

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

Posted at

これはQiita Advent Calendar 2022 Laravel 17日目の記事です。

この記事で扱うこと

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

autloadの仕組みについて

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

バージョン

ツール バージョン
PHP 8.1.12
Laravel 8.77.1
Composer 2.4.4

概観

autoloadの仕組みを見る前に全体的にどんな処理になっているかを載せておきます。
それぞれのクラスの処理を見る中で全体がどうなっているのか確認したい時などにぜひ見返してみてください~
autload image.png

autoload_real.php(autoloadを実行する部分)

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メソッドを解除する。

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

▾ /                 
  test.php
  testClass.php

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

class TestClass
{
    function output()
    {
        var_dump('This is TestClass.php !!');
    }
}

そんな時にspl_autoload_registerメソッドを使うと以下のような処理で実装できます。

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 !!"

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

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] => そのクラスのメソッド名]を入れることでそのクラス内のメソッドがオートローディングに登録されます。

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

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

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

autoload_static.php(autoloadのためのデータを保持する部分)

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クラスのインスタンス内の変数にこのファイルのクラス内の変数の値を代入します。

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

ClassLoader.php(autoloadを実装している部分)

    /**
     * @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に検索をかけ、合致するものがあった場合、そのパスと読み込むクラスのファイル名をつなげたものをファイルのパスとして返します。

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

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

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

終わりに

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

3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?