これはQiita Advent Calendar 2022 Laravel 17日目の記事です。
この記事で扱うこと
この記事は、Laravelで使用されているComposerのautoloadの仕組みについて実際のコードを見ていく中で理解することを目的とした記事です。なので、この記事においてautoloadをどう使用するかについては一切触れていません。
autloadの仕組みについて
コードを簡略化して解説しやすくするために、この記事で紹介するコードの中で比較的重要ではない(と自分が感じる)部分は省略しています。
バージョン
ツール | バージョン |
---|---|
PHP | 8.1.12 |
Laravel | 8.77.1 |
Composer | 2.4.4 |
概観
autoloadの仕組みを見る前に全体的にどんな処理になっているかを載せておきます。
それぞれのクラスの処理を見る中で全体がどうなっているのか確認したい時などにぜひ見返してみてください~
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つを行っています。
- このクラスの静的変数
$loader
($loader
に何が入るのかは後で述べる)に何も入っていない時にその後の処理を行い、入っているときは$loaderを返して終了。その後、vendor/composer/platform_checku.php
でComposerの扱えるPHPのバージョンチェックなどを行い、問題があればHTTPのheader
でエラーを表示し、問題がなければその後の処理へ移る。 - このクラスの
loadClassLoader
メソッドを実行してvendor/composer/ClassLoader.php
を読み込み、そのファイル内のClassLoader
クラスのインスタンスをこのクラスの静的変数$loader
に入れる。そしてspl_autoload_unregister
メソッドでオートローディングからloadClassLoader
メソッドを解除する。
spl_autoload_register
メソッドについて
このメソッドはPHPに標準であるSPL関数の一つであり、PHP8で削除されたクラスのオートローディングを行う__autoload
メソッドの代わりとなるメソッドなようです。PHPのオートローディングとは何かというと、簡単に言えば処理の中でクラスの呼び出し時に実行する処理のことを指します。ちなみにオートローディングは自動でファイルを読み込んでくれる機能ではないので、クラス呼び出し時にそのファイルを読み込む処理は自分で実装しなければいけません。
例えばこんな構造のファイルがあったとして
▾ /
test.php
testClass.php
testClass.php
のoutput
メソッドを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
に自分が作成したファイルのパスがいつの間にか入っていたのですが、いつ入ったのだろうか?という疑問が解決できませんでした。知っている方がいればぜひ教えていただきたいです。