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 3 years have passed since last update.

【Composer】オートロードの仕組みを追う

Last updated at Posted at 2021-03-02

初めに

Laravelを使っている方ならこんな疑問を抱いたことがあるんじゃないでしょうか。

なんで\App\Userとか\Illuminate\Hogehogeって書くだけでApp/User.phpのUserクラスとかvendor/laravel/framework/src/Illuminate/Hogehoge.phpのHogeHogeクラスが使えるようになってんの?

通常、外部のクラスを使うときにはrequireなどを使って該当のファイルをインポートしなければならないはずです。

今回はそんな疑問と密接に関わるComposerのオートロードについて調べていき、提示した疑問を一緒に解決していきましょう。

実行環境

  • PHP 7.3.4
  • Composer 2.0.11
  • Laravel 6.20.16

そもそもオートロードとは

オートロードの仕組みについて追っていく前に、そもそもオートロードって何をしているのでしょうか。

結論を言いますと、単純にrequireincludeを使ってソースコードを1つ1つインポートしているだけです。

ただし、

require "/path/to/vendor/laravel/src/framework/Illuminate/Foo/Hogehoge1.php";
require "/path/to/vendor/laravel/src/framework/Illuminate/Foo/Hogehoge2.php";
// ...
require "/path/to/vendor/laravel/src/framework/Illuminate/Foo/Hogehoge10.php";

のように、べた書きで1つ1つソースコードをインポートしているわけではありません。

ちゃんとPHPの仕組みを利用して自動でソースコードを読み込む機能を構築しています。

PHPでは通常、外部のファイルに記述されているクラスなどを利用するときは、

require "path/to/Foo.php"
require "path/to/Bar.php"

というように、読み込みたいファイルを絶対パスもしくは自ファイルからの相対パスでrequireもしくはincludeしなければなりません。

もしもこれを行わないまま外部ファイルに書かれているクラスを呼びだそうとしても、

Uncaught Error: Call to undefined class ...

というようにエラーが出力され、クラスを呼び出すことができません。

このようにオートロードをせずとも、自身でrequireすることで外部ファイルを読み込むことはできます。

しかし、この方法では扱うファイル数が増えてくるにつれて以下のようなデメリットが生じてきます。

  • requireするためには呼び出したいクラスの定義されているソースコードへのパスをあらかじめ知っていなければならない
  • 必要なクラスが5個や6個となってくるとさすがに記述するのが煩わしいし、typoも発生しやすくなる。
  • パッケージの更新などによりディレクトリ構造が変更された日には、改めてrequireするソースコードのパスを正しいものに書き直さなければならない

想像しただけで萎えてきます。

人の手でソースコードの所在を管理することが如何に面倒で非効率的であるか分かるでしょうか。

こういった面倒なインポート作業を、Composerのオートロード機能が代わりに担当しています。

そのおかげで、私たちはクラスの所在などを一切気にせずビジネスロジックの開発に注力することができるのですね。

オートロード処理の起点:vendor/autoload.php

オートロードがなにをしているかが分かったところで、ではどのようにオートロードが行われているをソースコードを追いながら見てみましょう。

ここではLaravelを使っているものだと仮定します。

まずはLaravelアプリのエントリポイントとなるpublic/index.phpを見てみます。

public/index.php
<?php

define('LARAVEL_START', microtime(true));

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

$app = require_once __DIR__.'/../bootstrap/app.php';

$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);

$response = $kernel->handle(
    $request = Illuminate\Http\Request::capture()
);

$response->send();

$kernel->terminate($request, $response);

コメントなどを省くと、Laravelによる一連の処理はここに書かれていることがすべてです。Laravelの豊富な機能がこのかなり短いコードを起点に提供されているんですね。

この中の2番目の処理を見てください。

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

名前から見ていかにもオートロードしてそうなファイルをrequireしてますね。このファイルを見てみましょう。

vendor/autoload.php
<?php

require_once __DIR__ . '/composer/autoload_real.php';

return ComposerAutoloaderInitcde23787628405c61112bd11321f024e::getLoader();

ComposerAutoloaderInitcde23787628405c61112bd11321f024eの部分は人によって異なっているのだと思います。

public/index.phpに続きかなり短いコード量です。

ここではComposerAutoloaderInitcde23787628405c61112bd11321f024eという謎のクラスで定義されているgetLoader()とやらクラスメソッドが実行されているだけです。

vendor/autoload.phpの行っている処理がこれだけなので、おそらく::getLoader()メソッドの中にどうやってオートロード機能を提供しているかの答えがありそうです。

このクラスの実装は一つ手前の処理で読み込まれている、vendor/composer/autoload_real.phpにありそうです。

では、autoload_real.phpを見てみましょう。

オートロード機能の中核:vendor/composer/autoload_real.php

vendor/autoload.phprequire_onceされていたvendor/composer/autoload_real.phpを見てみると、
確かに先ほどの謎のクラスComposerAutoloaderInitcde23787628405c61112bd11321f024eが実装されていました。

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()
    {
        if (null !== self::$loader) {
            return self::$loader;
        }

        require __DIR__ . '/platform_check.php';

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

        $useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded());
        if ($useStaticLoader) {
            require __DIR__ . '/autoload_static.php';

            call_user_func(\Composer\Autoload\ComposerStaticInitcde23787628405c61112bd11321f024e::getInitializer($loader));
        } else {
            $map = require __DIR__ . '/autoload_namespaces.php';
            foreach ($map as $namespace => $path) {
                $loader->set($namespace, $path);
            }

            $map = require __DIR__ . '/autoload_psr4.php';
            foreach ($map as $namespace => $path) {
                $loader->setPsr4($namespace, $path);
            }

            $classMap = require __DIR__ . '/autoload_classmap.php';
            if ($classMap) {
                $loader->addClassMap($classMap);
            }
        }

        $loader->register(true);

        if ($useStaticLoader) {
            $includeFiles = Composer\Autoload\ComposerStaticInitcde23787628405c61112bd11321f024e::$files;
        } else {
            $includeFiles = require __DIR__ . '/autoload_files.php';
        }
        foreach ($includeFiles as $fileIdentifier => $file) {
            composerRequirecde23787628405c61112bd11321f024e($fileIdentifier, $file);
        }

        return $loader;
    }
}

// ②
function composerRequirecde23787628405c61112bd11321f024e($fileIdentifier, $file)
{
    if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
        require $file;

        $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
    }
}

このファイルには①で示したComposerAutoloaderInitcde23787628405c61112bd11321f024eクラスと、②で示したcomposerRequirecde23787628405c61112bd11321f024e()関数が定義されているようです。

クラスのほうにはさらに、静的プロパティである$loaderと静的メソッドであるloadClassLoader()getLoader()が実装されています。

vendor/autoload.phpで呼び出されているのは::getLoader()の方なのでそちらに注目してみましょう。

オートロード機能の実態を生成:getLoader()

以下、getLoader()部分のみの抜粋です。

vendor/composer/autoload_real.php
public static function getLoader()
{
    if (null !== self::$loader) {  // ①
        return self::$loader;
    }

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

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

    // ④
    $useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded());
    if ($useStaticLoader) {
        // ⑤
        require __DIR__ . '/autoload_static.php';

        call_user_func(\Composer\Autoload\ComposerStaticInitcde23787628405c61112bd11321f024e::getInitializer($loader));
    } else {

        // ⑤´
        $map = require __DIR__ . '/autoload_namespaces.php';
        foreach ($map as $namespace => $path) {
            $loader->set($namespace, $path);
        }

        $map = require __DIR__ . '/autoload_psr4.php';
        foreach ($map as $namespace => $path) {
            $loader->setPsr4($namespace, $path);
        }

        $classMap = require __DIR__ . '/autoload_classmap.php';
        if ($classMap) {
            $loader->addClassMap($classMap);
        }
    }

    // ⑥
    $loader->register(true);

    // ⑦
    if ($useStaticLoader) {
        $includeFiles = Composer\Autoload\ComposerStaticInitcde23787628405c61112bd11321f024e::$files;
    } else {
        $includeFiles = require __DIR__ . '/autoload_files.php';
    }
    foreach ($includeFiles as $fileIdentifier => $file) {
        composerRequirecde23787628405c61112bd11321f024e($fileIdentifier, $file);
    }

    return $loader;
}

ここから一つ一つ処理を追っているのでとても長くなります。耐えつつ読み進めていただければと思います。

①の処理:$loaderの有無を確認

①を見てください。

vendor/composer/autoload_real.php
if (null !== self::$loader) {  // ①
    return self::$loader;
}

クラスプロパティ、self::$loadernullでなければ$loaderを返すという分岐を行っています。

vendor/autoload.phpgetLoader()が実行されたときにはまだ$loaderには何も代入されていないので、この条件分岐ではなにもしません。

②の処理:動作環境のチェック

②を見てみましょう。

vendor/composer/autoload_real.php
require __DIR__ . '/platform_check.php';  // ②

名前から鑑みに、実行環境のチェックを行いそうなファイルがrequireされていますね。

このファイルの処理は本筋からそれるので、コードの記載とコメントによる簡単な説明にとどめておきます。

vendor/composer/platform_check.php.php
<?php

$issues = array(); // 問題があった時にその情報を保存する配列を保存する配列

if (!(PHP_VERSION_ID >= 70205)) { // PHPのバージョンが7.2.5より下だった場合
    $issues[] = 'Your Composer dependencies require a PHP version ">= 7.2.5". You are running ' . PHP_VERSION . '.';
}

// 上記の問題が保存されていた場合
if ($issues) {
    if (!headers_sent()) { // まだヘッダが送信されていない場合
        header('HTTP/1.1 500 Internal Server Error');  // ステータスコード500のヘッダを作成
    }
    if (!ini_get('display_errors')) { // php.iniにdisplay_errorsの項目がないか、またはあってもOffであるか
        if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') { // 使用しているPHPのインターフェースがcliもしくはphpdbgか
            fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL);  //STDERRに問題の情報を出力する
        } elseif (!headers_sent()) { // まだヘッダが送信されていない場合
            echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL;
            fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL);  // 自身の使っているPHPバージョンの情報を省いて出力する
        }
    }

    // エラーを引き起こして、エラーメッセージを通知する。
    trigger_error(
        'Composer detected issues in your platform: ' . implode(' ', $issues),
        E_USER_ERROR
    );
}

③:spl_autoload_register()で未定義クラス呼び出し時の処理を登録

③の処理は、Composerがオートロード機能を構築する上で重要な関数を使っている部分なので、重点的に解説していきます。

まずはコードの抜粋です

vendor/composer/autoload_real.php
spl_autoload_register(array('ComposerAutoloaderInitcde23787628405c61112bd11321f024e', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(\dirname(__FILE__)));
spl_autoload_unregister(array('ComposerAutoloaderInitcde23787628405c61112bd11321f024e', 'loadClassLoader'));

1行目

vendor/composer/autoload_real.php
spl_autoload_register(array('ComposerAutoloaderInitcde23787628405c61112bd11321f024e', 'loadClassLoader'), true, true);

このspl_autoload_register()という関数は何をしているのでしょうか。

以前私が投稿した記事でも解説したのですが、この関数は

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

と解釈すればおおむね問題ないと思います。

通常、

new HogeHoge();

のように呼び出さしたクラスが自ファイルやrequireしたファイルに定義されていないものだった場合、

PHP Error:  Class 'Hogehoge' not found in ...

とエラーが出力されてその場で処理が終了するのが普通です。

ここで、

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

new HogeHoge();

のような例で、spl_autoload_register()の第1引数に、未定義クラスが呼び出された時の処理を登録してみましょう。

このソースコードを実行すると、new HogeHoge()の部分でエラーが起きる前に、

Want to load <呼び出したクラス名>.\n

と出力されます。即座にエラーが出力されなかったので、確かに未定義クラス呼び出し時の処理が登録できているようです。

そして、echoの内容が出力された後、同一ディレクトリ内の$class.php$classには未定義クラスのクラス名が自動で代入されます)があればそれをrequireしようと試みます。

この場合ですと、同一ディレクトリ内にHogeHoge.phpがあればrequireします。

もしHogeHoge.phpHogeHogeクラスが存在すればそれを呼び出して、new HogeHoge()でインスタンスを生成します。

試しに同一ディレクトリ上にHogeHoge.phpという名前のファイルを作成し、その中でHogeHogeクラスを定義してください。

その後でnew HogeHoge()の処理があるソースコードを実行してみてください。

先ほどまではnew HogeHoge()でエラーが出力されたのに対して、spl_autoload_register()を追加した後ではエラーも何も起こらなくなったはずです。

これはつまり、HogeHoge()が正しくインスタンス化されたことを意味します。

sql_autoload_register()の動きが分かったところで改めてソースコードの1行目を見てみましょう。

vendor/composer/autoload_real.php
spl_autoload_register(array('ComposerAutoloaderInitcde23787628405c61112bd11321f024e', 'loadClassLoader'), true, true);

spl_autoload_register()の第1引数には、登録するオートロード関数を配列として指定することもできます。

今回の場合ですと、

ComposerAutoloaderInitcde23787628405c61112bd11321f024eクラス(つまり自身のクラスですね)のloadClassLoaderメソッドをオートロード関数として登録するという処理になります。

このloadClassLoader()メソッドは自クラスに実装されていた2つのクラスメソッドのうちのもう1つですね。(1つはgetLoader()

第2、第3引数の意味はそれぞれ

第2引数
第1引数のオートロード関数が登録できなかった場合に例外をthrowするか
第3引数
すでに別のオートロード関数が登録されている場合、それらより優先して第1引数のオートロード関数を実行させるか

となっています。

以上のことから、1行目の処理を説明しますと、

ComposerAutoloaderInitcde23787628405c61112bd11321f024e::loadClassLoaderをオートロード関数として最優先に登録する。もし登録できなかったら例外をthrowする。

といった処理になります。

ではこのloadClassLoader()メソッドについて見る、その前に次の行の説明をしておいたほうがloadClassLoader()メソッドについても理解しやすいと思います。

2行目

vendor/composer/autoload_real.php
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(\dirname(__FILE__)));

self::$loaderとローカルスコープの$loader\Composer\Autoload\ClassLoaderクラスのインスタンスを代入しています。

ですが\Composer\Autoload\ClassLoaderはこの時点では未定義です。

なので、通常ではここでエラーが出力され処理が止まるのですが、先ほどspl_autoload_register()でオートロード関数としてloadClassLoader()を登録しておきましたね。

なので、エラーが出力される前にそのメソッドが実行されます。

では改めて、loadClassLoader()メソッドを見てみましょう。

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

中身を見てわかるように、やはり\Composer\Autoload\ClassLoaderの実装ファイルであろうvendor/composer/ClassLoader.phprequireしてましたね。

詳しく処理を見てみると、呼び出された未定義クラスが\Composer\Autoload\ClassLoaderであれば、同一ディレクトリ上のClassLoader.phprequireする。という処理になっています。

vendor/composer/ClassLoader.phpについて、現在必要な部分のみを見てみましょう。

vendor/composer/ClassLoader.php
namespace Composer\Autoload;

class ClassLoader
{
    private $vendorDir;

    // ~~中略~~

    public function __construct($vendorDir = null)
    {
        $this->vendorDir = $vendorDir;
    }

    // ~~後略~~
}

やはり、\Composer\Autoload\ClassLoaderクラスが実装されていましたね。

__construct()でパラメータに受け取ったパスをprivate $vendorDirに代入しているようです。

\Composer\Autoload\ClassLoaderクラスについては今はこの程度の理解で十分です。

改めて2行目の処理を見てみましょう。

(以下、該当部分を再掲)

vendor/composer/autoload_real.php
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(\dirname(__FILE__)));

\dirname(\dirname(__FILE__))は内側から

__FILE__ /path/to/vendor/composer/autoload_real.php
\dirname(__FILE__) /path/to/vendor/composer
\dirname(\dirname(__FILE__)) /path/to/vendor

と、解決されます。

ですので、new \Composer\Autoload\ClassLoader(\dirname(\dirname(__FILE__)));によって、
$vendorDirプロパティに'/path/to/vendor'が代入されたClassLoaderインスタンスをself::$loader$loaderに代入する。

というのが2行目で行っている処理になります。

ここで代入された\Composer\Autoload\ClassLoaderインスタンスが、Composerのオートロードを実現する上での核となるインスタンスになります。

詳細については後ほど(⑥あたりで)説明します。

3行目

vendor/composer/autoload_real.php
spl_autoload_unregister(array('ComposerAutoloaderInitcde23787628405c61112bd11321f024e', 'loadClassLoader'));

これは関数名からなんとなく察せるんじゃないでしょうか。

ここでは、spl_autoload_register()の反対、オートロード関数の登録解除を行っています。

具体的には、先ほどのloadClassLoader()メソッドをオートロード関数から削除しています。

③のまとめ

つまるところ③では、

  1. spl_autoload_register()を使い、loadClassLoader()をオートロード関数として登録。
  2. \Composer\Autoload\ClassLoaderクラスがloadClassLoader()により読み込まれ、vendorディレクトリへの絶対パスをパラメータとして\Composer\Autoload\ClassLoaderクラスのインスタンスを作成、self::$loader$loaderに代入。
  3. spl_autoload_unregister()を使い、loadClassLoader()をオートロード関数から削除

という処理が行われていることが分かりました。

④の処理:マッピング情報の読み込み方法を決定

vendor/composer/autoload_real.php
$useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded());

④では論理演算式によって、どのような読み込み方法を取るのかを決定しています。

この値によって、この後の⑤が⑤´に分岐するが決まります。

ここも②と同様本筋からそれますので軽い説明程度にとどめておきます。

条件式 説明
PHP_VERSION_ID >= 50600 PHPのバージョンが5.6.0以上であればtrue
!defined('HHVM_VERSION') HHVMというvirtual machineを使用していなければtrue
(!function_exists('zend_loader_file_encoded') zend guard loaderというランタイムを使っていなければtrue
!zend_loader_file_encoded() zend guard loaderを使っていてもzend guardによりエンコードされていなければtrue

HHVM_VERSIONzend_loader_file_encodedについてはかなりあいまいな説明です。有識者の方がいらっしゃっいましたらコメントしていただけると助かります)

正直ほとんどの方が、PHP_VERSION_ID >= 50600くらいにしか結果を左右されないと思います。

ですので、PHPバージョンが5.6.0以上であれば$useStaticLoaderにはtrueが入ると思って差し支えないでしょう。(差し支えありましたらすいません)

⑤の処理:マッピング情報を読み込み

ここから先、$useStaticLoaderの値によって⑤と⑤´に処理が分かれますが、行っていることとしてはどちらとも、クラスとソースコードのマッピング情報を$loaderにセットしている処理になりますので、そのことを念頭に置いておくと良いかもしれません。

$useStaticLoadertrueの場合はこちらになります。

vendor/composer/autoload_real.php
require __DIR__ . '/autoload_static.php';

call_user_func(\Composer\Autoload\ComposerStaticInitcde23787628405c61112bd11321f024e::getInitializer($loader));

ここではまず、vendor/composer/autoload_static.phprequireしています。

その後に、\Composer\Autoload\ComposerStaticInitcde23787628405c61112bd11321f024eというまた長ったらしい名前をしているクラスのクラスメソッド、getInitializer()を実行しているようです。

最後にgetInitializer()から返されるクロージャ(でしょうか?)をcall_user_func()を使って実行しています。

おそらく\Composer\Autoload\ComposerStaticInitcde23787628405c61112bd11321f024eクラスの定義もvendor/composer/autoload_static.phpで行われているでしょうから、そちらを見てみましょう。

このファイルを開くと、人によっては数千行にも及ぶソースコードが表示されると思います。

ですが記述のほとんどが、[クラス名 => 定義されているソースコードへのパス]という連想配列で構成されているはずです。

なので\Composer\Autoload\ComposerStaticInitcde23787628405c61112bd11321f024e
の実態は、以下のようにpublicなクラスプロパティが5つとpublicなクラスメソッドが1つだけのクラスです。

<?php

namespace Composer\Autoload;

class ComposerStaticInitcde23787628405c61112bd11321f024e
{
    public static $files = array (
        // ...
    );

    public static $prefixLengthsPsr4 = array (
        // ...
    );

    public static $prefixDirsPsr4 = array (
        // ...
    );

    public static $prefixesPsr0 = array (
        // ...
    );

    public static $classMap = array (
        // ...
    );

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

        }, null, ClassLoader::class);
    }
}

今回vendor/composer/autoload_real.phpで呼び出されたのは::getInitializer()ですので、そちらを詳しく見てみましょう。

以下、getInitializer()の抜粋です。

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

    }, null, ClassLoader::class);
}

やはり、\Closure::bind()によってクロージャが返されていました。

ところで、この\Closure::bind()は一体何をしているのでしょうか。

(ここでは\Closure::bind()について簡単な説明のみにとどめておきます。

\Closure::bind()の詳しい説明は公式マニュアルに記載されていますのでご参照ください)

\Closure::bind()について

かいつまんで説明すると、\Closure::bind()

第1引数に指定した関数に対して、第2引数に指定したオブジェクトと第3引数に指定したクラスのスコープを関連付けてクロージャを複製する

メソッドです。

もう少しわかりやすい例でいうと

第3引数にクラス名を指定することで、複製されたクロージャはまるでそのクラスに実装されたメソッドであるかのように、そのクラスに定義されているプロパティに対して(privateであっても)アクセスが可能となります

また今回の例では関係ないのですが、

第2引数にインスタンスを渡すことで、クロージャ内で$thisと記述した部分にそのインスタンスが割り当てられます

`\Closure::bind()`について具体的な説明

以下のようなFooクラスがあります。

class Foo
{
    private static $sFoo = "private static foo\n";
    private $foo = "private foo\n"; 
}

このクラスはプロパティを2つ持っていますが両方ともprivateなプロパティなので

echo Foo::$sFoo;  // PHP Fatal error:  Uncaught Error: Cannot access private property Foo::$sFoo in ...
echo (new Foo())->foo;  // PHP Fatal error:  Uncaught Error: Cannot access private property Foo::$foo in ...

のように外部からアクセスしようとしても当然エラーが起きます。

そこで、\Closure::bind()を使いprivateなプロパティへアクセスできるクロージャを作成してみましょう。

class Foo
{
    private static $sFoo = 'private static foo\n';
    private $foo = 'private foo\n';
}

$cls = \Closure::bind(function (): void {
    echo Foo::$sFoo;
    (new Foo())->foo;
}, null, Foo::class);

$cls();

すると、

private static foo
private foo

privateなプロパティを出力することができました。

このクロージャはさながら、

class Foo
{
    private static $sFoo = 'private static foo\n';

    private $foo = 'private foo\n';

    public function cls()
    {
        echo Foo::$sFoo;
        (new Foo())->foo;   
    }
}

のようなクラスを定義した時の、cls()メソッドと同等の振る舞いをしているものだとみなせます。

ただし違いが1つあり、それはメソッドなら再利用可能だが、\Closure::bind()によって複製されたクロージャは他の変数に保存しないと再利用不可になるという点です。

これは、初期化処理のように最初の1回だけしかその処理が必要じゃない、しかしprivateprotectedされたプロパティに対して処理を施したい、という場面でこそ有効な特徴になると思います。

では本筋に戻ります。

以上の説明を踏まえて、\Closure::bind()が何をやっているのかを見てみましょう。

以下、::getInitializer()の再掲です。

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

    }, null, ClassLoader::class);
}

\Closure::bind()の第1引数に複製したい無名関数を渡すことは先ほど説明しました。

第2引数はnull、つまりどのインスタンスともバインドしない静的メソッドとして複製することを示しています。

第3引数では、classキーワードを使ってClassLoaderクラスの完全修飾名を渡しています。

つまりここで複製されるクロージャは、まるで\Composer\Autoload\ClassLoaderクラスに実装されたメソッドであるかのように\Composer\Autoload\ClassLoaderクラスのプロパティに対してアクセスが可能となっているクロージャです。

そのことを確認する方法として、試しに第3引数をClassLoader::classからnullに変更した後

php vendor/autoload.php

をコンソール上で実行してみてください。

PHP Fatal error:  Uncaught Error: Cannot access private property Composer\Autoload\ClassLoader::$prefixLengthsPsr4

というようなエラーが出力されるはずです。どうやら、

$loader->prefixLengthsPsr4 = ComposerStaticInitcde23787628405c61112bd11321f024e::$prefixLengthsPsr4;

の部分でComposer\Autoload\ClassLoaderprivateなプロパティにアクセスしてしまっているようですね。

Composer\Autoload\ClassLoaderクラスのプロパティを確認してみましょう。

<?php

namespace Composer\Autoload;

class ClassLoader
{
    private $vendorDir;

    // PSR-4
    private $prefixLengthsPsr4 = array();
    private $prefixDirsPsr4 = array();
    private $fallbackDirsPsr4 = array();

    // PSR-0
    private $prefixesPsr0 = array();
    private $fallbackDirsPsr0 = array();

    private $useIncludePath = false;
    private $classMap = array();
    private $classMapAuthoritative = false;
    private $missingClasses = array();
    private $apcuPrefix;

    private static $registeredLoaders = array();

    public function __construct($vendorDir = null)
    {
        $this->vendorDir = $vendorDir;
    }

    // 後略
}

このようにすべてprivateで定義されています。

ですので先ほどのクロージャからではクラス外からのアクセスになってしまうので、Cannot access private propertyと怒られてしまったんですね。

以上で、\Closure::bind()自体の説明は終わりです。

vendor/composer/autoload_static.phpの変更した部分をClassLoader::classに戻して、

php vendor/autoload.php

がエラーを出力することなく完了したことを確認したら次に進みましょう。

\Closure:bind()に渡した無名関数

では、\Closure:bind()が複製する対象の無名関数を見てみましょう。

以下、無名関数の部分のみの抜粋です。

function () use ($loader) {
    $loader->prefixLengthsPsr4 = ComposerStaticInitcde23787628405c61112bd11321f024e::$prefixLengthsPsr4;
    $loader->prefixDirsPsr4 = ComposerStaticInitcde23787628405c61112bd11321f024e::$prefixDirsPsr4;
    $loader->prefixesPsr0 = ComposerStaticInitcde23787628405c61112bd11321f024e::$prefixesPsr0;
    $loader->classMap = ComposerStaticInitcde23787628405c61112bd11321f024e::$classMap;
}

無名関数の中で外部の変数を使う場合は、予めuseを使って内部で使う変数を渡してあげる必要があります。

今回では、$loaderが外部の変数ですのでuseに含めておきます。

先ほど示した、\Composer\Autoload\ClassLoaderクラスのプロパティにこの4つのプロパティもあります。

これらのプロパティに対してそれぞれ、ComposerStaticInitcde23787628405c61112bd11321f024eクラス(つまり自クラスですね)で定義されている同名のクラスプロパティを代入しています。

つまりこの無名関数の中で、オートローディングに必要なマッピング情報を$loaderに渡しているのです。

以上でこの無名関数に関する説明は終了です。

まとめ

さて、⑤の処理の流れを総括すると

  1. \Composer\Autoload\ComposerStaticInitcde23787628405c61112bd11321f024eが呼び出せるように、requirevendor/composer/autoload_static.phpを読み込み

  2. ComposerStaticInitcde23787628405c61112bd11321f024e::getInitializer($loader)の戻り値をcall_user_func()の引数に指定

  3. ComposerStaticInitcde23787628405c61112bd11321f024e::getInitializer()では、渡ってきた$loaderを無名関数の処理に組み込み、適切なクラススコープを付与したクロージャを返す。

  4. call_user_func()は返ってきた無名関数を実行して、$loaderのプライベートなプロパティにマッピング情報を渡して読み込ませる。

といったことが行われていることが分かりました。

⑤´の処理:⑤と異なる方法でマッピング情報を読み込み

こちらは$useStaticLoaderfalseの場合の処理になります。

vendor/composer/autoload_real.php
$map = require __DIR__ . '/autoload_namespaces.php';
foreach ($map as $namespace => $path) {
    $loader->set($namespace, $path);
}

$map = require __DIR__ . '/autoload_psr4.php';
foreach ($map as $namespace => $path) {
    $loader->setPsr4($namespace, $path);
}

$classMap = require __DIR__ . '/autoload_classmap.php';
if ($classMap) {
    $loader->addClassMap($classMap);
}

大きく分けて3つの処理に分けることができますが、どれも流れは同じです。

まずはvendor/composer/autoload_namespaces.phpを見てみましょう。

vendor/composer/autoload_namespaces.php
<?php

$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);

return array(
    'Mockery' => array($vendorDir . '/mockery/mockery/library'),
    'Highlight\\' => array($vendorDir . '/scrivo/highlight.php'),
    'HighlightUtilities\\' => array($vendorDir . '/scrivo/highlight.php'),
);

このように、[名前空間 => 対応させるファイルパスの入った配列]という連想配列を$mapに返しています。

これを、foreachを使って$namespace$pathに切り分けながら1つずつ$loader->set()にかけているようです。

名前から想像できると思いますが一応、中身を見てみましょう。

vendor/composer/ClassLoader.php
public function set($prefix, $paths)
{
    if (!$prefix) {
        $this->fallbackDirsPsr0 = (array) $paths;
    } else {
        $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
    }
}

このメソッドでは、

  • $prefixつまり$mapのキーが空文字だったらfallbackDirsPsr0プロパティにパスを追加

  • $prefixがあれば、prefixesPsr0プロパティ内の$prefix[0]つまり頭文字に対応する配列>prefixesPsr0[$prefix[0]][$prefix => array($path)]の形で追加

している処理を行っていると思われます。

同様にvendor/comoser/autoload_psr4.phpを見てみると、

vendor/comoser/autoload_psr4.php
<?php

$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);

return array(
    'phpDocumentor\\Reflection\\' => array($vendorDir . '/phpdocumentor/reflection-common/src', $vendorDir . '/phpdocumentor/reflection-docblock/src', $vendorDir . '/phpdocumentor/type-resolver/src'),
    // ...
);

と、autoload_namespaces.phpと同様の構造でマッピング情報を$mapに返します。

そしてやはり先ほどと同様、foreachを使って$namespace$pathに切り分けながら1つずつ$loader->setPsr4()にかけているようです。

こちらも実装を見てみましょう。

vendor/composer/ClassLoader.php
public function setPsr4($prefix, $paths)
{
    if (!$prefix) {
        $this->fallbackDirsPsr4 = (array) $paths;
    } else {
        $length = strlen($prefix);
        if ('\\' !== $prefix[$length - 1]) {
            throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
        }
        $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
        $this->prefixDirsPsr4[$prefix] = (array) $paths;
    }
}

流れを追うと、

  • $prefixつまり$mapが空文字だったらfallbackDirsPsr0プロパティにパスを追加

  • $prefixの最後の文字が'\\'でなければ例外を投げる。

  • そのどちらでもなければ

    • 先ほどと同様の指定方法でprefixLengthsPsr4[$prefix[0]][$prefix => $length]を保存

    • prefixDirsPsr4プロパティに[$prefix => array($paths)]の形で保存

という処理が行われているようです。

これまた同様に、vendor/composer/autoload_classMap.phpを見てみましょう。

vendor/composer/autoload_classMap.php
<?php

$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);

return array(
    'App\\Console\\Kernel' => $baseDir . '/app/Console/Kernel.php',
    'App\\Exceptions\\Handler' => $baseDir . '/app/Exceptions/Handler.php',
    // ...
);

先ほど2つと同様に連想配列の形式です。ただし、値には配列ではなくて文字列を指定しているようです。

このマップ情報を$classMapに返し、$classMapが空ではなければ$loader->addClassMap()に渡しているようです。

以下、addClassMap()の実装です。


public function addClassMap(array $classMap)
{
    if ($this->classMap) {
        $this->classMap = array_merge($this->classMap, $classMap);
    } else {
        $this->classMap = $classMap;
    }
}

これは見たまんまの処理ですね。

  • 既にclassMapプロパティに何かあれば、渡された$classMapclassMapプロパティに統合

  • classMapプロパティが空なら、$classMapをそのままclassMapプロパティに代入

となります。

以上3つの読み込み処理を経て、⑤´でもマッピング情報の読み込みが行われています。

⑥の処理:オートローダーの登録

⑥の処理を見てみましょう。

vendor/composer/autoload_real.php
$loader->register(true);

おそらく、メソッド名から鑑みてオートロードを行う上で重要な処理を担っているものだと思われます。

それではregister()の定義を見てみましょう。

vendor/composer/ClassLoader.phpを探してみますと確かにClassLoaderクラスのメソッドとして定義されていました。

vendor/composer/ClassLoader.php
public function register($prepend = false)
{
    spl_autoload_register(array($this, 'loadClass'), true, $prepend);

    if (null === $this->vendorDir) {
        //no-op
    } elseif ($prepend) {
        self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders;
    } else {
        unset(self::$registeredLoaders[$this->vendorDir]);
        self::$registeredLoaders[$this->vendorDir] = $this;
    }
}

処理を追ってみます。

復習になりますが、spl_autoload_register()は未定義のクラス、インターフェースが呼び出されたときに実行されるオートロード関数を登録しておけるPHPの関数でしたね。

ここでは、オートロード関数として自インスタンスのloadClassメソッドをオートロード関数として実行順を最優先で登録しています。

登録したオートロード関数が何をするのかは後で詳しく見るとして、続けてif文の処理を見てみましょう。

記述されている条件分岐は以下のようになります。

  • $this->vendorDirnull、つまりインスタンス生成時にパスを渡さしていなかった場合何もしない

  • $this->vendorDirが設定されていて、かつregister()の引数$prependtrueを渡していた場合、[$this->vendorDir => $this]という配列を順番的に最初にしてself::$registeredLoadersに追加。

  • $prependfalseなら、self::$registeredLoadersから$this->vendorDirをキーとする要素を削除した後、順番的に一番後ろに[$this->vendorDir => $this]という配列を追加。

どうやら、if文ではself::$registeredLoadersというクラスプロパティに、

「このvendorDirのパスの時は、このローダを使う」という情報を[$this->vendorDir => $this]という形式の配列で蓄積しているようです。

register()メソッドの行っていることはこれで以上です。

では先ほどオートロード関数として登録したloadClassメソッドの実装を見てみましょう。

vendor/composer/ClassLoader.php
public function loadClass($class)
{
    if ($file = $this->findFile($class)) {
        includeFile($file);

        return true;
    }
}

オートロード関数には実行時に、引数へ呼び出したパラメータが渡されることは既に説明致しました。

処理を予測してみると、

$this->findFile($class)によってクラスが定義されているファイルの所在を探し出し、見つかればincludeFile($file)でそれをincludeする。

といったところでしょうか。

実際に、includeFile()関数はvendor/composer/ClassLoader.phpの一番下で定義されています。

vendor/composer/ClassLoader.php
function includeFile($file)
{
    include $file;
}

純粋に渡されたファイルパスをincludeするだけの関数のようです。

ですのでloadClass()メソッドの肝はどうやら$this->findFile($class)にありそうです。

ではその実装を見てみましょう。

findFile():マッピング情報の検索

vendor/composer/ClassLoader.php
 public function findFile($class)
{
    // class map lookup
    if (isset($this->classMap[$class])) {  // ①
        return $this->classMap[$class];
    }
    if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {  // ②
        return false;
    }
    if (null !== $this->apcuPrefix) { // ③
        $file = apcu_fetch($this->apcuPrefix.$class, $hit);
        if ($hit) {
            return $file;
        }
    }

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

    // Search for Hack files if we are running on HHVM
    if (false === $file && defined('HHVM_VERSION')) { // ⑤
        $file = $this->findFileWithExtension($class, '.hh');
    }

    if (null !== $this->apcuPrefix) { // ⑥
        apcu_add($this->apcuPrefix.$class, $file);
    }

    if (false === $file) { // ⑦
        // Remember that this class does not exist.
        $this->missingClasses[$class] = true;
    }

    return $file;
}

少々長いです。前提として、$classには先ほどのloadClassメソッドと同じものが渡されています。

では割り振った番号ごとに処理を追ってみます。

vendor/composer/ClassLoader.php
if (isset($this->classMap[$class])) {  // ①
    return $this->classMap[$class];
}

$this->classMap$classのキーが見つかればその値を返します。

vendor/composer/ClassLoader.php
if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {  // ②
    return false;
}

$this->classMapAuthoritativetrueもしくは$this->missingClasses$classのキーが見つかれば、見つからなかったという意味のfalseを返す。

$this->classMapAuthoritativeprefixfallbackのディレクトリ検索を禁止するかのプロパティでしょうか、デフォルトではfalseなので禁止しません)

vendor/composer/ClassLoader.php
if (null !== $this->apcuPrefix) { // ③
    $file = apcu_fetch($this->apcuPrefix.$class, $hit);
    if ($hit) {
        return $file;
    }
}

$this->apcuPrefixnullでなければ、APCuというPHPの拡張機能(?)が提供するapcu_fetch()という関数を使ってキャッシュに$this->apcuPrefix.$classというキーがないか検索、ヒットしたらその検索結果をリターンします。

デフォルトではnullなのでここの部分はスキップされます。

(この部分に関してはかなりあいまいな説明となっています、見識のある方がおられましたらご指摘していただけると助かります)

参考:PHP: apcu_fetch - Manual

vendor/composer/ClassLoader.php
$file = $this->findFileWithExtension($class, '.php'); // ④

$this->findFileWithExtension()というメソッドを使ってファイルを探しているようです。

かいつまんで説明すると、findFileWithExtensiton()メソッドは$this->prefixDirsPsr4$this->prefixesPsr0から、$classに対応するファイルパスを検索し、見つかればそれを返すメソッドになります。

このメソッドの処理はコード量が長いので折りたたんで解説します。

findFileWithExtension()の解説

このメソッドでは、渡された文字列を地道に解析して$this->prefixDirsPsr4$this->prefixesPsr0からファイルパスを取り出しています。

1つずつ解説するととても量が多くなるのでコメントに付記する形で説明を行います。

vendor/composer/ClassLoader.php

private function findFileWithExtension($class, $ext)  
{
    // 以下$class = Illuminate\\Support\\Facades\\Auth として実行
    
    $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
    // $logicalPathPsr4 = Illuminate/Support/Facades/Auth.php

    $first = $class[0];  // $first = I
    if (isset($this->prefixLengthsPsr4[$first])) {
        $subPath = $class;  // $subPath = Illuminate\\Support\\Facades\\Auth

        
        // $subPathから"\\"のうち最後のものの位置を$lastPosに代入。無ければfalseを返す
        while (false !== $lastPos = strrpos($subPath, '\\')) {
            /*
            $subPathがIlluminate\\Support\\Facades\\Authなら
            Illuminate\\Support\\Facadesに置き換え
            */
            $subPath = substr($subPath, 0, $lastPos);
            $search = $subPath . '\\';

            /*
            $searchが$this->prefixDirsPsr4にあればif文以下を実行
            この場合であれば$subPath = Illuminate, $search = Illuminate\\, $lastPos = 10 の時にtrueとなる
            */
            if (isset($this->prefixDirsPsr4[$search])) {  

                /*
                substr()の結果がSupport/Facades/Authなので
                $pathEnd = /Support/Facades/Auth.php
                */
                $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);

                /*
                $searchに対応するファイルパスを$dirに取得。
                この場合であれば $dir = /path/to/vendor/composer/../laravel/framework/src/Illuminate
                */
                foreach ($this->prefixDirsPsr4[$search] as $dir) {
                    
                    // $file =  /path/to/vendor/composer/../laravel/framework/src/Illuminate/Support/Facades/Auth.php
                    if (file_exists($file = $dir . $pathEnd)) { // $fileの位置にファイルがあればtrue
                        return $file; // $fileのパスを返す
                    }
                }
            }
        }
    }

    // $this->fallbackDirsPsr4があればループを実行
    foreach ($this->fallbackDirsPsr4 as $dir) {
        if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
            return $file;
        }
    }

    // 以下、$class = Mockery, $logicalPathPsr4 = Mockery.php, $first = M として実行 

    // $classに"\\"があれば$posに最後のそれの位置を代入。無ければfalseを返す
    if (false !== $pos = strrpos($class, '\\')) { 

        // ここでは$class =  Hightlight\\Hogehoge, $$logicalPathPsr4 = Hightlight/Hogehoge.php  として実行

        // $logicalPathPsr0 = Hightlight/Hogehoge.php
        $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
            . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
    } else {
        // $logicalPathPsr0 = Mockery.php
        $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
    }

    if (isset($this->prefixesPsr0[$first])) { // $this->prefixesPsr0["M"] があればif文以下を実行

        // $this->prefixesPsr0["M"]を$prefixと$dirsに切り分けてループ
        foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {

            // ここでは$prefix = Mockery として実行
            
            // $classに"Mockery"があり、その最初の位置が0番目ならif文以下を実行
            if (0 === strpos($class, $prefix)) {

                // $dirsを1つずつループ
                foreach ($dirs as $dir) {

                    // ここでは$dir = /path/to/vendor/composer/../mockery/mockery/library として実行
                    
                    /*
                    $file = /path/to/vendor/composer/../mockery/mockery/library/Mockery.php
                    $fileがあれば$fileを返す
                    */
                    if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
                        return $file;
                    }
                }
            }
        }
    }
    
    // $this->fallbackDirsPsr0があればループを実行
    foreach ($this->fallbackDirsPsr0 as $dir) {
        if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
            return $file;
        }
    }

    // $this->useIncludePathがtrueで、$logicalPathPsr0のファイルパスが存在すれば$fileを返す
    if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
        return $file;
    }

    // ここまで来たら何も見つからなかったということでfalseを返す
    return false;
}

vendor/composer/ClassLoader.php
// Search for Hack files if we are running on HHVM
if (false === $file && defined('HHVM_VERSION')) { // ⑤
    $file = $this->findFileWithExtension($class, '.hh');
}

コメントアウトされた文章にもあるようにHHVMが起動している場合に限り、$filefindFileWithExtension()によって見つからなかったときに、拡張子を.hhに変えて再びfindFileWithExtension()を実行し、ファイルパスを取得しようと試みています。

この時点でファイルパスの検索結果は$fileに保存されています。
ですので後の処理は次回以降の処理を省くためのものがほとんどです。

vendor/composer/ClassLoader.php
if (null !== $this->apcuPrefix) { // ⑥
    apcu_add($this->apcuPrefix.$class, $file);
}

$this->apcuPrefixnullでなければ、APCuというPHPの拡張機能(?)が提供するapcu_add()という関数を使ってキャッシュに、$this->apcuPrefix.$classというキーで検索結果をキャッシュに追加しています。

デフォルトではnullなのでここの部分はスキップされます。

(この部分に関してもかなりあいまいな説明となっています、見識のある方がおられましたらご指摘していただけると助かります)

vendor/composer/ClassLoader.php
if (false === $file) { // ⑦
    // Remember that this class does not exist.
    $this->missingClasses[$class] = true;
}

return $file;

検索の結果何も見つからなかった場合、次回以降の検索処理を省略するために$this->missinglasses[$class => true]という形式の配列を追加します。

これによって、次回以降同じクラス名を呼び出した際に①から⑥まで処理を経なくとも②の時点で該当ファイルがないという結果を返すことができます。

以上の処理を経てfindFile()メソッドは呼び出しクラスに対するファイルパス情報を検索しているのですね。

⑦の処理:クラスファイル以外のソースコードを読み込む

いよいよ最後の処理です。ここでは、クラスファイル以外のもの、例えば起動処理をまとめただけのファイルでしたり、ヘルパ関数をまとめたファイルなどをハッシュ値と紐づけて読み込ませる処理を行っています。

vendor/composer/autoload_real.php
if ($useStaticLoader) {
    $includeFiles = Composer\Autoload\ComposerStaticInitcde23787628405c61112bd11321f024e::$files;
} else {
    $includeFiles = require __DIR__ . '/autoload_files.php';
}
foreach ($includeFiles as $fileIdentifier => $file) {
    composerRequirecde23787628405c61112bd11321f024e($fileIdentifier, $file);
}

return $loader;

では処理を追ってみましょう。

まず、$useStaticLoaderの値によって処理が分岐していますがどちらとも$includeFiles[ハッシュ値 => ファイルパス]の構造をした配列を代入しています。

Composer\Autoload\ComposerStaticInitcde23787628405c61112bd11321f024e::$filesプロパティにも、vendor/composer/autoload_files.phpの戻り値にも同様の配列が記載されていると思います。

この配列の要素をハッシュ値$fileIdentifierとファイルパス$fileに切り分けて1つずつ、composerRequirecde23787628405c61112bd11321f024e()という関数にかけているようです。

このcomposerRequirecde23787628405c61112bd11321f024e()関数はvendor/composer/autoload_real.phpの一番下、クラスの外に定義されています。

実装を見てみましょう。

vendor/composer/autoload_real.php

function composerRequirecde23787628405c61112bd11321f024e($fileIdentifier, $file)
{
    if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
        require $file;

        $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
    }
}

この関数によって、クラスファイル以外のソースコードが読み込まれているようです。

処理を追うと、

  1. $GLOBALS['__composer_autoload_files'][$fileIdentifier]が空でないか確認

  2. 空であればそのハッシュ値に対応するファイルがまだ読み込まれていないのでrequireする

  3. $fileIdentifierに対応するファイルを読み込んだことを記憶しておくために、
    $GLOBALS['__composer_autoload_files'][$fileIdentifier]trueを代入。

といった流れになっています。

この処理が終わると、getLoader()$loaderをリターンして処理を終了します。

getLoader()の処理まとめ

とても長くなってしまいましたが以上でgetLoader()の説明は終わりになります。

最後に各番号の処理を簡単に説明します。

  1. self::loaderがすでにあればそれを返す。

  2. 実行環境を確認。

  3. コンストラクタの引数に/path/to/vendorを指定して\Composer\Autoload\ClassLoaderクラスをインスタンス化、それをself::$loader$loaderに代入。

  4. useStaticLoaderをするか決定。

  5. マッピング情報を$loaderに読み込ませる。

  6. 実際にオートロード処理を登録。これ以降の未定義クラスの呼び出しはマッピング情報をもとに解決される。

  7. クラスファイル以外のソースコードをハッシュ値と対応させて読み込む。その後$loaderをリターンする

このような処理の流れで、オートロード機能の構築が行われているということがわかりましたね。

この::getLoader()によって生成された$loaderは呼び出し元であるvendor/autoload.phpに返ってきます。

return ComposerAutoloaderInitcde23787628405c61112bd11321f024e::getLoader();

返ってきた$loadervendor/autoload.phpは更にリターンしています。なのでもし外部でローダーを使う処理を実装したい場合は

$loader = require "/path/to/vendor/autoload.php";

とすれば::getLoader()によって生成されたローダーを使うことができます。

今回起点としたLaravelのindex.phpでは

public/index.php
<?php
// ...

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

// ...

のようにローダーの保存は行わず、::getLoader()を介してオートロード関数の登録だけを行っています。

保存を行わなかった場合でもクラスプロパティとしてローダーは保存されているので、未定義のクラスが呼び出された場合でもそちらに保存されているローダーのloadClassメソッドを通してオートロード処理が行われている、といったところでしょうか。

処理の流れまとめ

では、最後に今まで説明してきたことを順序だてて簡潔に追ってみましょう。

  1. public/index.phpvendor/autoload.phprequireされる。

  2. vendor/autoload.phpvendor/composer/autoload_real.phprequireされ、そこに定義されているComposerAutoloaderInitcde23787628405c61112bd11321f024e::getLoader()を実行。

  3. ::getLoader()の処理の中でオートロード機能の実体となる\Composer\Autoload\ClassLoaderインスタンスを生成し、自クラスの静的プロパティとして保存。

  4. そのインスタンスに対してマッピング情報の読み込み、オートロード処理の登録、そしてクラス以外のソースコードに関しても読み込み、といった処理を行う。

このような流れでComposerによるファイルのオートロード処理が行われているのだということが分かりました。

終わりに

非常に長い説明になってしまったことをお詫びしたいと思います。

ですが、このように処理を1つ1つ丁寧に追っていったことでComposerのオートロード機能についてはほとんどぬけのない理解が出来たのではないでしょうか。

以前私のほうでも投稿しました記事でも述べたことがあるのですが、普段何気なく使っている機能について一度は深くまで追ってみるという経験をするのは、非常に力がつきます。

現に、この機能を調べるまで知りもしなかった情報や仕様について理解する機会を得られました。

月並みな締めになってしまいますが、皆さんも一度はこのように外部パッケージが提供している仕組みについて、1から深堀りして追ってみる経験をしてみてはいかがでしょうか。

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?