これは「弁護士ドットコムアドベントカレンダー」 の 19 日目の記事です。
18 日目の記事は @tttttt_621_s さんでした。
はじめに
弁護士ドットコムには Yii Framework 1.1(以下、Yii と表現します。) で作られたサービスがいくつかあります。
筆者は普段 Yii で作られたサービスの開発は行っていませんが、TFD という取り組みで開発に携わる機会があります。
本記事では TFD における Dii の導入、 Yii 上に実装されたコードを解析する取り組みにおいてハマった、 Yii の autoload について解説します。
本記事ではまず Composer の autoload について簡単に紹介します。
その後、 Yii 独自の autoload の仕組みについて解説し、最後にハマった事例をどう解決するか(したか)について紹介します。
Composer の autoload
上記は Composer の autoloading についてのドキュメントです。
Composer は依存パッケージマネージャとしての役割に加え、 autoload の仕組みも提供します。
vendor/autoload.php
を生成し、 vendor/autoload.php
を利用することで、インストールしたパッケージを特別な手続きなしに利用することができます。
vendor/autoload.php
の先で何が行われているかについて詳細は割愛しますが、大きく以下のことをしています。
-
composer.json
のautoload
の値からファイルパスを解決する PHP コードを生成 -
spl_autoload_register
を呼び出し、loader を登録
vendor/autoload.php
は Web アプリケーションであれば public/index.php
のようなエントリースクリプトで読み込まれますが、現代フレームワークは各々スケルトンパッケージを公開しており、それをそのまま利用する開発者が vendor/autoload.php
を読み込ませる機会は滅多にないでしょう。
パッケージ開発においても、以下のようなスケルトンプロジェクトが公開されています。
上記以外にも様々なスケルトンプロジェクトがあります。 Packagist で探してみましょう。
Yii の autoload
Yii は 2008 年の 12 月に 1.0.0 がリリースされました。当時は、 autoload の仕組み自体はあるものの、 Composer のリリース前ということもあり、 Yii 独自の autoload が実装されています。以下が autoload の実装になります。
spl_autoload_register
の呼び出し箇所は以下です。
autoload で読み込まれるファイルは以下になります。
- Yii のコアコンポーネントクラス
-
include_path
に登録されたパスに存在する名前空間を持たないクラス - パスエイリアスに登録された名前空間を持つクラス
-
classmap
に登録されたクラス
動作確認用の簡単な例を以下より引用します。
<?php
use application\controllers\YiiNameSpacedController;
use NaokiTsuchiya\AdventCalendar2021\PSR4NameSpacedController;
require __DIR__ . '/../vendor/autoload.php';
require __DIR__ . '/../vendor/yiisoft/yii/framework/yii.php';
echo class_exists(CController::class) . PHP_EOL; // yii core component
echo get_include_path() . PHP_EOL;
Yii::setPathOfAlias('application', __DIR__ . '/../public/protected');
Yii::import('application.controllers.*');
Yii::import('application.components.*');
Yii::import('application.models.*');
Yii::$classMap['ClassMapController'] = __DIR__ . '/../public/classmaps/ClassMapController.php';
echo get_include_path() . PHP_EOL;
echo class_exists(SiteController::class) . PHP_EOL; // include from include_path
echo class_exists(UserIdentity::class) . PHP_EOL; // include from include_path
echo class_exists(ContactForm::class) . PHP_EOL; // include from include_path
echo class_exists(YiiNameSpacedController::class) . PHP_EOL; // include from alias path
echo class_exists(PSR4NameSpacedController::class) . PHP_EOL; // include from composer autoloader
echo class_exists(ClassMapController::class) . PHP_EOL; // include from classmap
上記では、 PSR4NameSpacedController
を除くクラスが Yii の autolode で解決されます。
Yii のコアコンポーネントクラス
CController
は Yii のコアコンポーネントクラスです。コアコンポーネントのパスはコード上に定義されており、定義されたパスから読み込みます。
include_path
に登録されたパスに存在する名前空間を持たないクラス
SiteController
と UserIdentity
と ContactForm
は setPathOfAlias
で設定したパス内に存在しています。クラスを呼び出す前に Yii::import('application.controllers.*');
のようにすることで、指定した path を include_path に動的に追加し、 autolode 可能にしています。
パスエイリアスに登録された名前空間を持つクラス
YiiNameSpacedController
は名前空間 application\controllers
を持ったクラスです。
application\controllers
を application.controllers
に内部で変換し、 autolode 可能にしています。
classmap
に登録されたクラス
ClassMapController
は classMap
に className => filePath
形式で登録することで autoload 可能になります。
パスエイリアスやインポートなどの仕組みについての公式ドキュメントは以下になります。
ハマった事例とどう解決したか
最後ハマった事例の紹介とそれをどう解決したかについて紹介します。
ファイルの include に失敗し、Warning エラーが発生する
これは Dii の導入時にハマりました。
問題となったのは以下の部分です。
if(self::$enableIncludePath===false)
{
foreach(self::$_includePaths as $path)
{
$classFile=$path.DIRECTORY_SEPARATOR.$className.'.php';
if(is_file($classFile))
{
include($classFile);
if(YII_DEBUG && basename(realpath($classFile))!==$className.'.php')
throw new CException(Yii::t('yii','Class name "{class}" does not match class file "{file}".', array(
'{class}'=>$className,
'{file}'=>$classFile,
)));
break;
}
}
}
else
include($className.'.php'); // ファイルが存在しなくても include を実行する
上記のコメントにある通り、存在しないファイルをincludeする実装があります。
これは、class_exists(NotFoundClass::class)
とするだけで発生してしまうので、非常に厄介でした。
この記事を執筆するにあたりいろいろ調べた結果からいうと、 Yii::enableIncludePath = false
とするだけでよかったのではないかと思いますが、当時はこれを解決するために Yii の autoload を廃止する PR を作ったりしました。
上記の PR は結局マージせず、 直接的な原因となった存在しないアノテーションの warning を抑制する Silent annotation loader を実装して解決しました。
spl_autoload_register
がどこで呼び出されているのかずっと見つからなかったのは今となってはいい思い出です
Could not read file: T.php
このエラーは、 PHPStan でテンプレートを利用した際に発生しました。こちらのエラーも発生原因は上記の存在しないファイルを include する実装によるものです。
解決方法としては以下の2つの方法があります。
-
Yii::enableIncludePath = false
を設定する -
spl_autoload_unregister([YiiBase::class, 'autolod'])
を実行する
後者は Yii の autoload 自体を利用しなくする方法になります。こちらの場合、 composer.json
の classmap
に Yii のコアコンポーネントを読み込むようにするなどの設定が別途必要になります。
Could not read file: CUrlRule.php
こちらも静的解析実行時に発生したものです。CUrlRule
は独立したファイルに定義されたクラスではなく、 CUrlManager.php
内に実装されたクラスになります。
こちらは何らかの方法でクラスが実装されたファイルを読み込んでしまえば解決します。
- Composer の classmap に追加する
- bootstrap で
class_exists(CUrlManager::class)
を実行する等
yii.php
の代わりに yiilite.php
を require する方法にすると上記の設定が不要になりますが、 PHPDoc がない実装を読み込むようになり、 Psalm が MixedAssignment のエラーを大量に吐くようになるため避けるのが無難でしょう。
まとめ
本記事では、 Composer の autolode と Yii の autolode について紹介し、実際にハマった Yii の autoload の事例をどう解決したか紹介しました。
これから Yii のツールを作る人や Yii 上に実装されたコードを解析したい人がツールの導入時に同じことに直面しても解決できるようにと考え、この記事を執筆しました。
明日は、 @blkclct さんです。