Yii Framework 1.1 の autoload を理解する

これは「[弁護士ドットコムアドベントカレンダー](https://qiita.com/advent-calendar/2021/bengo4com)」 の 19 日目の記事です。

18 日目の記事は @tttttt_621_s さんでした。

# はじめに

弁護士ドットコムには Yii Framework 1\.1（以下、Yii と表現します。） で作られたサービスがいくつかあります。
筆者は普段 Yii で作られたサービスの開発は行っていませんが、[TFD](https://qiita.com/mmorino/items/fd563703c22ccad9648f#tfd) という取り組みで開発に携わる機会があります。

本記事では TFD における [Dii](https://github.com/koriym/dii) の導入、 Yii 上に実装されたコードを解析する取り組みにおいてハマった、 Yii の autoload について解説します。

本記事ではまず Composer の autoload について簡単に紹介します。
その後、 Yii 独自の autoload の仕組みについて解説し、最後にハマった事例をどう解決するか（したか）について紹介します。

# Composer の autoload

https://getcomposer.org/doc/01-basic-usage.md#autoloading

上記は Composer の autoloading についてのドキュメントです。
Composer は依存パッケージマネージャとしての役割に加え、 autoload の仕組みも提供します。
　`vendor/autoload.php` を生成し、 `vendor/autoload.php` を利用することで、インストールしたパッケージを特別な手続きなしに利用することができます。

　`vendor/autoload.php` の先で何が行われているかについて詳細は割愛しますが、大きく以下のことをしています。

- `composer.json` の `autoload` の値からファイルパスを解決する PHP コードを生成
- `spl_autoload_register` を呼び出し、[loader](https://github.com/composer/composer/blob/main/src/Composer/Autoload/ClassLoader.php#L425) を登録

　`vendor/autoload.php` は Web アプリケーションであれば `public/index.php` のようなエントリースクリプトで読み込まれますが、現代フレームワークは各々スケルトンパッケージを公開しており、それをそのまま利用する開発者が `vendor/autoload.php` を読み込ませる機会は滅多にないでしょう。

https://github.com/bearsunday/BEAR.Skeleton

https://github.com/cakephp/app

https://github.com/laravel/laravel

https://github.com/symfony/skeleton

https://github.com/symfony/website-skeleton

https://github.com/yiisoft/yii2-app-basic


パッケージ開発においても、以下のようなスケルトンプロジェクトが公開されています。

https://github.com/koriym/Koriym.PhpSkeleton

https://github.com/ramsey/php-library-starter-kit

上記以外にも様々なスケルトンプロジェクトがあります。 Packagist で探してみましょう。

https://packagist.org

# Yii の autoload

Yii は 2008 年の 12 月に 1.0.0 がリリースされました。当時は、 autoload の仕組み自体はあるものの、 Composer のリリース前ということもあり、 Yii 独自の autoload が実装されています。以下が autoload の実装になります。

https://github.com/yiisoft/yii/blob/1.1.25/framework/YiiBase.php#L407

`spl_autoload_register` の呼び出し箇所は以下です。

https://github.com/yiisoft/yii/blob/1.1.25/framework/YiiBase.php#L910

autoload で読み込まれるファイルは以下になります。

- Yii のコアコンポーネントクラス
- `include_path` に登録されたパスに存在する名前空間を持たないクラス
-  パスエイリアスに登録された名前空間を持つクラス
- `classmap` に登録されたクラス

動作確認用の簡単な例を以下より引用します。

https://github.com/NaokiTsuchiya/advent-calendar-2021/blob/main/bin/importExample.php

```php
<?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 のコアコンポーネントクラスです。コアコンポーネントのパスはコード上に定義されており、定義されたパスから読み込みます。

https://github.com/yiisoft/yii/blob/master/framework/YiiBase.php#L436-L437

## `include_path` に登録されたパスに存在する名前空間を持たないクラス

`SiteController` と `UserIdentity` と `ContactForm` は `setPathOfAlias` で設定したパス内に存在しています。クラスを呼び出す前に `Yii::import('application.controllers.*');` のようにすることで、指定した path を include_path に動的に追加し、 autolode 可能にしています。

https://github.com/yiisoft/yii/blob/master/framework/YiiBase.php#L348

https://github.com/yiisoft/yii/blob/master/framework/YiiBase.php#L452

https://github.com/yiisoft/yii/blob/master/framework/YiiBase.php#L463

## パスエイリアスに登録された名前空間を持つクラス

`YiiNameSpacedController` は名前空間 `application\controllers` を持ったクラスです。
`application\controllers` を `application.controllers` に内部で変換し、 autolode 可能にしています。

https://github.com/yiisoft/yii/blob/master/framework/YiiBase.php#L465-L472

## `classmap` に登録されたクラス

`ClassMapController` は `classMap`　に `className => filePath` 形式で登録することで autoload 可能になります。

https://github.com/yiisoft/yii/blob/master/framework/YiiBase.php#L434

パスエイリアスやインポートなどの仕組みについての公式ドキュメントは以下になります。

https://www.yiiframework.com/doc/guide/1.1/ja/basics.namespace

# ハマった事例とどう解決したか

最後ハマった事例の紹介とそれをどう解決したかについて紹介します。

## ファイルの include に失敗し、Warning エラーが発生する

これは Dii の導入時にハマりました。

問題となったのは以下の部分です。

https://github.com/yiisoft/yii/blob/master/framework/YiiBase.php#L445-L463

```php
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 を作ったりしました。

https://github.com/koriym/dii/pull/25

上記の PR は結局マージせず、 直接的な原因となった存在しないアノテーションの warning を抑制する Silent annotation loader を実装して解決しました。

https://github.com/koriym/dii/pull/26

`spl_autoload_register` がどこで呼び出されているのかずっと見つからなかったのは今となってはいい思い出です :sweat_smile: 

# 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` 内に実装されたクラスになります。

https://github.com/yiisoft/yii/blob/master/framework/web/CUrlManager.php#L567

こちらは何らかの方法でクラスが実装されたファイルを読み込んでしまえば解決します。

- Composer の classmap に追加する
- bootstrap で `class_exists(CUrlManager::class)` を実行する等

`yii.php` の代わりに `yiilite.php` を require する方法にすると上記の設定が不要になりますが、 PHPDoc がない実装を読み込むようになり、 Psalm が MixedAssignment のエラーを大量に吐くようになるため避けるのが無難でしょう。

# まとめ

本記事では、 Composer の autolode と Yii の autolode について紹介し、実際にハマった Yii の autoload の事例をどう解決したか紹介しました。

これから Yii のツールを作る人や Yii 上に実装されたコードを解析したい人がツールの導入時に同じことに直面しても解決できるようにと考え、この記事を執筆しました。

明日は、 @blkclct さんです。