IDEとの親和性を保ちつつYiiを拡張する

この記事はオープンソースの [Yii 2.0 Cookbook](https://github.com/samdark/yii2-cookbook) にある [IDE autocompletion for custom components](https://github.com/samdark/yii2-cookbook/blob/master/book/ide-autocompletion.md#ide-autocompletion-for-custom-components) にヒントを得て書いています。ぜひ参考に。

Yiiの拡張ポイントは大きく分けて3つの箇所にあります。

## 拡張したい箇所がアプリケーションコンポーネントの場合

Yii はサービスロケーターにデフォルトでない拡張クラスを登録することで、簡単にフレームワークのデフォルト実装をプロジェクトに合わせて特殊化できます。

```php
$config = [
    'components' => [
        'user' => [
            'class' => 'app\components\User', // yii\web\User の派生
            'identityClass' => 'app\models\User', // userテーブルを指すAR
        ],
```

ところが、これは動的なカスタマイズのため、IDEではまだ `Yii::$app->user` を `yii\web\User` であるとみなしてしまいます。たしかにクラスの型は間違っていないので問題というわけではないのですが、 `Yii::$app->user->identity` で自分のモデルではなく `IdentityInterface` 型であると言われると、いいかげん抽象的すぎてアッパーキャストが面倒になってきます。

```php
/* @var $webUser \app\components\User */
$webUser = Yii::$app->user;
if (!$webUser->isGuest /* もしくは何かカスタムな $webUser->... コール */) {
    /* @var $dbUser \app\models\User */
    $dbUser = $webUser->identity;
    $dbUser->... // ここでようやくARのフィールドをIDEが認識してくれる
}
```

この問題を解決するには、独自の `Yii` クラスと `Application` クラスを作り、それを使うという方法が取れます。

```php
namespace {
    class Yii extends \yii\BaseYii
    {
        /**
         * @var \app\WebApplication|\app\ConsoleApplication
         */
        public static $app;
    }
    spl_autoload_register(['Yii', 'autoload'], true, true);
    Yii::$classMap = require(__DIR__ . '/classes.php');
    Yii::$container = new yii\di\Container();
}

namespace app {
    /**
     * @property \app\components\User $user
     */
    class WebApplication extends yii\web\Application
    {
        /**
         * @return \app\components\User
         */
        public function getUser()
        {
            return parent::getUser();
        }
    }

    class ConsoleApplication extends yii\console\Application
    {
    }
}
```

```php
// require(__DIR__ . '/../vendor/yiisoft/yii2/Yii.php');
require(__DIR__ . '/../MyCustomYii.php');

...

// $application = new yii\web\Application($config);
$application = new \app\WebApplication($config);
$application->run();
```

PhpStorm など、個別にファイル/フォルダを無視できるIDEでは、`vendor\yiisoft\yii2\Yii.php` を無視するようにします。

チュートリアルのようなプロジェクトでは、プロジェクトの都合に合わせた特殊化が必要なく、あまり活用されないように見えますが、おそらくは、ほとんどの実務プロジェクトに有効です。

なぜなら、IDEの型解析の問題を解消する以外に、もしかしたら `Yii` や `Yii::$app` に独自のメソッド/プロパティを追加したい場合 (`Yii::$app->get('db2')` を `Yii::$app->db2` と書きたいなど) が出てきても、すぐに対応できるからです。

## 拡張したい箇所がスタティックコールの場合

Yii では、交換可能なフレームワークコンポーネントだけでなく、 `Html::encode($string)` のように、静的メソッドの呼び出しも多用します。(Laravel と異なり) Yii は静的コールを何かのコンテナの実装にディスパッチすることなく、ありのまま呼び出します。そのため、高速ですが、コンテナの実装を交換する方法でカスタマイズすることができません。

前節の `Yii` クラスの継承で、アプリケーション独自の `Yii` は `yii\BaseYii` を継承していました。実は、`Yii` クラスは何一つメソッドを実装しておらず、`BaseYii` がすべてのメソッド実装を持っています。このようなパターンのクラスは他に、`yii\helpers\Html` と `BaseHtml`、 `yii\helpers\Url` と `BaseUrl` など、`yii\helpers` 名前空間に多く見られます。

※ Yiiでヘルパーというと、静的メソッドを(時には DI コンテナを使うという荒業もありで)使って問題を解決するユーティリティ、という意味合いがあります。

これらのパターンに当てはまるクラスは、Yii 標準のものと同じ名前のクラスを自分のアプリケーションの名前空間に、`Base*` の派生として作ってもよいことを示唆しています。

```php
namespace app\helpers

class Url extends yii\helpers\BaseUrl
{
    public static function someSpecialRouteTo(...)
    {
    }

    public static function to(...)
    {
        if (...special pattern detected...) {
            return static::someSpecialRouteTo(...);
        } else {
            return parent::to(...);
        }
    }
}
```

```php
// use yii\helpers\Url
use app\helpers\Url

Url::to(...)
```

このように `use` を置き換えるだけで、拡張バージョンを使うことができます。

IDEでは、`vendor\yiisoft` 以下の拡張前のクラス (上の例なら `yii\helper\Url`) を無視するようにします。それにより以下のメリットが得られます:

- コード編集中に `Url::` としたとき、まだ `use` していない段階でも、どのクラスであるか決定できます
- 誤って拡張前のものを `use` しているコードが残っていれば、コミット前に警告が出る

もし上のコードを `class Url extends yii\helpers\Url` としてしまうと、拡張前の `Url` を無視することができなくなってしまい、IDEの静的解析の恩恵を受けることができなくなってしまいます。

## 拡張したい箇所が任意クラスインスタンスのデフォルト設定の場合

DI コンテナを使います。

アプリケーションコンポーネントであれば、ひとつのコンフィグを書き換えてプロパティをカスタマイズするのは簡単です。なぜなら、アプリケーションコンポーネントはシングルトンだからです。しかしひとつのコンフィグだけでは、`ActiveField::widget($className, $config)` のように、アプリケーションコンポーネントに含まれないコンポーネントのカスタマイズはできません。

そこで、サービスロケーター = `Yii::$app` とは別の DI コンテナ = `Yii::$cotainer` の出番です。

次のように、毎回 `widget()` の `$config` に同じ設定を書く必要があるとき、それがプロジェクト内のデフォルトであったなら...

```php
<?= $form->field($model, 'date1')->widget(
    \yii\jui\DatePicker::className(),
    [
        'dateFormat' => 'yyyy-MM-dd',
        'clientOption' => [
            ...
        ],
    ]
) ?>

<?= $form->field($model, 'date2')->widget(
    \yii\jui\DatePicker::className(),
    [
        'dateFormat' => 'yyyy-MM-dd',
        'clientOption' => [
            ...
        ],
    ]
) ?>
```

という場合は、コンフィグと同じタイミングで DI コンテナに「オブジェクト生成時のルール」を登録しておきます。

```php
Yii::$container->set('yii\jui\DatePicker', [
    'dateFormat' => 'yyyy-MM-dd',
    'clientOption' => [
        ...
    ],
]);
```

```php
<?= $form->field($model, 'date1')
    ->widget(\yii\jui\DatePicker::className()) ?>

<?= $form->field($model, 'date2')
    ->widget(\yii\jui\DatePicker::className(), [/* 個別に必要な設定のみ書く */]) ?>
```

Yii 内部ではオブジェクト生成にかならず `Yii::createObject()` を使い、DI コンテナからオブジェクトを得ます。もし DI コンテナにシングルトンとして登録されていればシングルトンを返しますが、通常は動的な生成となります。他のコンポーネントや設定値に依存するなら注入されます。

Yii の `Object` および `Component` は、コンストラクタのコンフィグ規約と、セッタ/ゲッタとパブリックフィールドの統合により、DI コンテナにとって非常に構成しやすい形になってます。なので、Yii の習慣にしたがっていれば無理なく DI コンテナを導入できます。アプリケーション独自のコンポーネントでも、つねに `Yii::createObject()` を使って生成するように習慣化すれば、後で依存関係や初期値を一括して構成することが可能になります。

また、任意 Array の記述は IDE の助けを借りるのが難しい箇所です。IDE を使う場合は、コード内の Array を必要以上に大きくしないことがコツです。