初めに
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
そもそもオートロードとは
オートロードの仕組みについて追っていく前に、そもそもオートロードって何をしているのでしょうか。
結論を言いますと、単純にrequire
やinclude
を使ってソースコードを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
を見てみます。
<?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番目の処理を見てください。
require __DIR__.'/../vendor/autoload.php';
名前から見ていかにもオートロードしてそうなファイルをrequire
してますね。このファイルを見てみましょう。
<?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.php
でrequire_once
されていたvendor/composer/autoload_real.php
を見てみると、
確かに先ほどの謎のクラスComposerAutoloaderInitcde23787628405c61112bd11321f024e
が実装されていました。
// ①
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()
部分のみの抜粋です。
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の有無を確認
①を見てください。
if (null !== self::$loader) { // ①
return self::$loader;
}
クラスプロパティ、self::$loader
がnull
でなければ$loader
を返すという分岐を行っています。
vendor/autoload.php
でgetLoader()
が実行されたときにはまだ$loader
には何も代入されていないので、この条件分岐ではなにもしません。
②の処理:動作環境のチェック
②を見てみましょう。
require __DIR__ . '/platform_check.php'; // ②
名前から鑑みに、実行環境のチェックを行いそうなファイルがrequire
されていますね。
このファイルの処理は本筋からそれるので、コードの記載とコメントによる簡単な説明にとどめておきます。
<?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がオートロード機能を構築する上で重要な関数を使っている部分なので、重点的に解説していきます。
まずはコードの抜粋です
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行目
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.php
にHogeHoge
クラスが存在すればそれを呼び出して、new HogeHoge()
でインスタンスを生成します。
試しに同一ディレクトリ上にHogeHoge.phpという名前のファイルを作成し、その中でHogeHoge
クラスを定義してください。
その後でnew HogeHoge()の処理があるソースコードを実行してみてください。
先ほどまではnew HogeHoge()
でエラーが出力されたのに対して、spl_autoload_register()
を追加した後ではエラーも何も起こらなくなったはずです。
これはつまり、HogeHoge()
が正しくインスタンス化されたことを意味します。
sql_autoload_register()
の動きが分かったところで改めてソースコードの1行目を見てみましょう。
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行目
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(\dirname(__FILE__)));
self::$loader
とローカルスコープの$loader
に\Composer\Autoload\ClassLoader
クラスのインスタンスを代入しています。
ですが\Composer\Autoload\ClassLoader
はこの時点では未定義です。
なので、通常ではここでエラーが出力され処理が止まるのですが、先ほどspl_autoload_register()
でオートロード関数としてloadClassLoader()
を登録しておきましたね。
なので、エラーが出力される前にそのメソッドが実行されます。
では改めて、loadClassLoader()
メソッドを見てみましょう。
public static function loadClassLoader($class)
{
if ('Composer\Autoload\ClassLoader' === $class) {
require __DIR__ . '/ClassLoader.php';
}
}
中身を見てわかるように、やはり\Composer\Autoload\ClassLoader
の実装ファイルであろうvendor/composer/ClassLoader.php
をrequire
してましたね。
詳しく処理を見てみると、呼び出された未定義クラスが\Composer\Autoload\ClassLoader
であれば、同一ディレクトリ上のClassLoader.php
をrequire
する。という処理になっています。
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行目の処理を見てみましょう。
(以下、該当部分を再掲)
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行目
spl_autoload_unregister(array('ComposerAutoloaderInitcde23787628405c61112bd11321f024e', 'loadClassLoader'));
これは関数名からなんとなく察せるんじゃないでしょうか。
ここでは、spl_autoload_register()
の反対、オートロード関数の登録解除を行っています。
具体的には、先ほどのloadClassLoader()
メソッドをオートロード関数から削除しています。
③のまとめ
つまるところ③では、
-
spl_autoload_register()
を使い、loadClassLoader()
をオートロード関数として登録。 -
\Composer\Autoload\ClassLoader
クラスがloadClassLoader()
により読み込まれ、vendor
ディレクトリへの絶対パスをパラメータとして\Composer\Autoload\ClassLoader
クラスのインスタンスを作成、self::$loader
と$loader
に代入。 -
spl_autoload_unregister()
を使い、loadClassLoader()
をオートロード関数から削除
という処理が行われていることが分かりました。
④の処理:マッピング情報の読み込み方法を決定
$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_VERSION
とzend_loader_file_encoded
についてはかなりあいまいな説明です。有識者の方がいらっしゃっいましたらコメントしていただけると助かります)
正直ほとんどの方が、PHP_VERSION_ID >= 50600
くらいにしか結果を左右されないと思います。
ですので、PHPバージョンが5.6.0以上であれば$useStaticLoader
にはtrue
が入ると思って差し支えないでしょう。(差し支えありましたらすいません)
⑤の処理:マッピング情報を読み込み
ここから先、$useStaticLoader
の値によって⑤と⑤´に処理が分かれますが、行っていることとしてはどちらとも、クラスとソースコードのマッピング情報を$loader
にセットしている処理になりますので、そのことを念頭に置いておくと良いかもしれません。
$useStaticLoader
がtrue
の場合はこちらになります。
require __DIR__ . '/autoload_static.php';
call_user_func(\Composer\Autoload\ComposerStaticInitcde23787628405c61112bd11321f024e::getInitializer($loader));
ここではまず、vendor/composer/autoload_static.php
をrequire
しています。
その後に、\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回だけしかその処理が必要じゃない、しかしprivate
やprotected
されたプロパティに対して処理を施したい、という場面でこそ有効な特徴になると思います。
では本筋に戻ります。
以上の説明を踏まえて、\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\ClassLoader
のprivate
なプロパティにアクセスしてしまっているようですね。
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
に渡しているのです。
以上でこの無名関数に関する説明は終了です。
まとめ
さて、⑤の処理の流れを総括すると
-
\Composer\Autoload\ComposerStaticInitcde23787628405c61112bd11321f024e
が呼び出せるように、require
でvendor/composer/autoload_static.php
を読み込み -
ComposerStaticInitcde23787628405c61112bd11321f024e::getInitializer($loader)
の戻り値をcall_user_func()
の引数に指定 -
ComposerStaticInitcde23787628405c61112bd11321f024e::getInitializer()
では、渡ってきた$loader
を無名関数の処理に組み込み、適切なクラススコープを付与したクロージャを返す。 -
call_user_func()
は返ってきた無名関数を実行して、$loader
のプライベートなプロパティにマッピング情報を渡して読み込ませる。
といったことが行われていることが分かりました。
⑤´の処理:⑤と異なる方法でマッピング情報を読み込み
こちらは$useStaticLoader
がfalse
の場合の処理になります。
$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
を見てみましょう。
<?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()
にかけているようです。
名前から想像できると思いますが一応、中身を見てみましょう。
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
を見てみると、
<?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()
にかけているようです。
こちらも実装を見てみましょう。
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
を見てみましょう。
<?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
プロパティに何かあれば、渡された$classMap
をclassMap
プロパティに統合 -
classMap
プロパティが空なら、$classMap
をそのままclassMap
プロパティに代入
となります。
以上3つの読み込み処理を経て、⑤´でもマッピング情報の読み込みが行われています。
⑥の処理:オートローダーの登録
⑥の処理を見てみましょう。
$loader->register(true);
おそらく、メソッド名から鑑みてオートロードを行う上で重要な処理を担っているものだと思われます。
それではregister()
の定義を見てみましょう。
vendor/composer/ClassLoader.php
を探してみますと確かにClassLoader
クラスのメソッドとして定義されていました。
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->vendorDir
がnull
、つまりインスタンス生成時にパスを渡さしていなかった場合何もしない -
$this->vendorDir
が設定されていて、かつregister()
の引数$prepend
にtrue
を渡していた場合、[$this->vendorDir => $this]
という配列を順番的に最初にしてself::$registeredLoaders
に追加。 -
$prepend
がfalse
なら、self::$registeredLoaders
から$this->vendorDir
をキーとする要素を削除した後、順番的に一番後ろに[$this->vendorDir => $this]
という配列を追加。
どうやら、if
文ではself::$registeredLoaders
というクラスプロパティに、
「このvendorDir
のパスの時は、このローダを使う」という情報を[$this->vendorDir => $this]
という形式の配列で蓄積しているようです。
register()
メソッドの行っていることはこれで以上です。
では先ほどオートロード関数として登録したloadClass
メソッドの実装を見てみましょう。
public function loadClass($class)
{
if ($file = $this->findFile($class)) {
includeFile($file);
return true;
}
}
オートロード関数には実行時に、引数へ呼び出したパラメータが渡されることは既に説明致しました。
処理を予測してみると、
$this->findFile($class)
によってクラスが定義されているファイルの所在を探し出し、見つかればincludeFile($file)
でそれをinclude
する。
といったところでしょうか。
実際に、includeFile()
関数はvendor/composer/ClassLoader.php
の一番下で定義されています。
function includeFile($file)
{
include $file;
}
純粋に渡されたファイルパスをinclude
するだけの関数のようです。
ですのでloadClass()
メソッドの肝はどうやら$this->findFile($class)
にありそうです。
ではその実装を見てみましょう。
findFile():マッピング情報の検索
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
メソッドと同じものが渡されています。
では割り振った番号ごとに処理を追ってみます。
①
if (isset($this->classMap[$class])) { // ①
return $this->classMap[$class];
}
$this->classMap
に$class
のキーが見つかればその値を返します。
②
if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) { // ②
return false;
}
$this->classMapAuthoritative
がtrue
もしくは$this->missingClasses
に$class
のキーが見つかれば、見つからなかったという意味のfalse
を返す。
($this->classMapAuthoritative
はprefix
やfallback
のディレクトリ検索を禁止するかのプロパティでしょうか、デフォルトではfalse
なので禁止しません)
③
if (null !== $this->apcuPrefix) { // ③
$file = apcu_fetch($this->apcuPrefix.$class, $hit);
if ($hit) {
return $file;
}
}
$this->apcuPrefix
がnull
でなければ、APCuというPHPの拡張機能(?)が提供するapcu_fetch()
という関数を使ってキャッシュに$this->apcuPrefix.$class
というキーがないか検索、ヒットしたらその検索結果をリターンします。
デフォルトではnull
なのでここの部分はスキップされます。
(この部分に関してはかなりあいまいな説明となっています、見識のある方がおられましたらご指摘していただけると助かります)
④
$file = $this->findFileWithExtension($class, '.php'); // ④
$this->findFileWithExtension()
というメソッドを使ってファイルを探しているようです。
かいつまんで説明すると、findFileWithExtensiton()
メソッドは$this->prefixDirsPsr4
、$this->prefixesPsr0
から、$class
に対応するファイルパスを検索し、見つかればそれを返すメソッドになります。
このメソッドの処理はコード量が長いので折りたたんで解説します。
findFileWithExtension()の解説
このメソッドでは、渡された文字列を地道に解析して$this->prefixDirsPsr4
、$this->prefixesPsr0
からファイルパスを取り出しています。
1つずつ解説するととても量が多くなるのでコメントに付記する形で説明を行います。
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;
}
⑤
// Search for Hack files if we are running on HHVM
if (false === $file && defined('HHVM_VERSION')) { // ⑤
$file = $this->findFileWithExtension($class, '.hh');
}
コメントアウトされた文章にもあるようにHHVM
が起動している場合に限り、$file
がfindFileWithExtension()
によって見つからなかったときに、拡張子を.hh
に変えて再びfindFileWithExtension()
を実行し、ファイルパスを取得しようと試みています。
⑥
この時点でファイルパスの検索結果は$file
に保存されています。
ですので後の処理は次回以降の処理を省くためのものがほとんどです。
if (null !== $this->apcuPrefix) { // ⑥
apcu_add($this->apcuPrefix.$class, $file);
}
$this->apcuPrefix
がnull
でなければ、APCuというPHPの拡張機能(?)が提供するapcu_add()
という関数を使ってキャッシュに、$this->apcuPrefix.$class
というキーで検索結果をキャッシュに追加しています。
デフォルトではnull
なのでここの部分はスキップされます。
(この部分に関してもかなりあいまいな説明となっています、見識のある方がおられましたらご指摘していただけると助かります)
⑦
if (false === $file) { // ⑦
// Remember that this class does not exist.
$this->missingClasses[$class] = true;
}
return $file;
検索の結果何も見つからなかった場合、次回以降の検索処理を省略するために$this->missinglasses
に[$class => true]
という形式の配列を追加します。
これによって、次回以降同じクラス名を呼び出した際に①から⑥まで処理を経なくとも②の時点で該当ファイルがないという結果を返すことができます。
以上の処理を経てfindFile()
メソッドは呼び出しクラスに対するファイルパス情報を検索しているのですね。
⑦の処理:クラスファイル以外のソースコードを読み込む
いよいよ最後の処理です。ここでは、クラスファイル以外のもの、例えば起動処理をまとめただけのファイルでしたり、ヘルパ関数をまとめたファイルなどをハッシュ値と紐づけて読み込ませる処理を行っています。
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
の一番下、クラスの外に定義されています。
実装を見てみましょう。
function composerRequirecde23787628405c61112bd11321f024e($fileIdentifier, $file)
{
if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
require $file;
$GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
}
}
この関数によって、クラスファイル以外のソースコードが読み込まれているようです。
処理を追うと、
-
$GLOBALS['__composer_autoload_files'][$fileIdentifier]
が空でないか確認 -
空であればそのハッシュ値に対応するファイルがまだ読み込まれていないので
require
する -
$fileIdentifier
に対応するファイルを読み込んだことを記憶しておくために、
$GLOBALS['__composer_autoload_files'][$fileIdentifier]
にtrue
を代入。
といった流れになっています。
この処理が終わると、getLoader()
は$loader
をリターンして処理を終了します。
getLoader()の処理まとめ
とても長くなってしまいましたが以上でgetLoader()
の説明は終わりになります。
最後に各番号の処理を簡単に説明します。
-
self::loader
がすでにあればそれを返す。 -
実行環境を確認。
-
コンストラクタの引数に
/path/to/vendor
を指定して\Composer\Autoload\ClassLoader
クラスをインスタンス化、それをself::$loader
と$loader
に代入。 -
useStaticLoader
をするか決定。 -
マッピング情報を
$loader
に読み込ませる。 -
実際にオートロード処理を登録。これ以降の未定義クラスの呼び出しはマッピング情報をもとに解決される。
-
クラスファイル以外のソースコードをハッシュ値と対応させて読み込む。その後
$loader
をリターンする
このような処理の流れで、オートロード機能の構築が行われているということがわかりましたね。
この::getLoader()
によって生成された$loader
は呼び出し元であるvendor/autoload.php
に返ってきます。
return ComposerAutoloaderInitcde23787628405c61112bd11321f024e::getLoader();
返ってきた$loader
をvendor/autoload.php
は更にリターンしています。なのでもし外部でローダーを使う処理を実装したい場合は
$loader = require "/path/to/vendor/autoload.php";
とすれば::getLoader()
によって生成されたローダーを使うことができます。
今回起点としたLaravelのindex.php
では
<?php
// ...
require __DIR__.'/../vendor/autoload.php';
// ...
のようにローダーの保存は行わず、::getLoader()
を介してオートロード関数の登録だけを行っています。
保存を行わなかった場合でもクラスプロパティとしてローダーは保存されているので、未定義のクラスが呼び出された場合でもそちらに保存されているローダーのloadClass
メソッドを通してオートロード処理が行われている、といったところでしょうか。
処理の流れまとめ
では、最後に今まで説明してきたことを順序だてて簡潔に追ってみましょう。
-
public/index.php
でvendor/autoload.php
がrequire
される。 -
vendor/autoload.php
でvendor/composer/autoload_real.php
がrequire
され、そこに定義されているComposerAutoloaderInitcde23787628405c61112bd11321f024e::getLoader()
を実行。 -
::getLoader()
の処理の中でオートロード機能の実体となる\Composer\Autoload\ClassLoader
インスタンスを生成し、自クラスの静的プロパティとして保存。 -
そのインスタンスに対してマッピング情報の読み込み、オートロード処理の登録、そしてクラス以外のソースコードに関しても読み込み、といった処理を行う。
このような流れでComposerによるファイルのオートロード処理が行われているのだということが分かりました。
終わりに
非常に長い説明になってしまったことをお詫びしたいと思います。
ですが、このように処理を1つ1つ丁寧に追っていったことでComposerのオートロード機能についてはほとんどぬけのない理解が出来たのではないでしょうか。
以前私のほうでも投稿しました記事でも述べたことがあるのですが、普段何気なく使っている機能について一度は深くまで追ってみるという経験をするのは、非常に力がつきます。
現に、この機能を調べるまで知りもしなかった情報や仕様について理解する機会を得られました。
月並みな締めになってしまいますが、皆さんも一度はこのように外部パッケージが提供している仕組みについて、1から深堀りして追ってみる経験をしてみてはいかがでしょうか。