LoginSignup
3
3

More than 5 years have passed since last update.

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

Last updated at Posted at 2015-05-19

この記事はオープンソースの Yii 2.0 Cookbook にある IDE autocompletion for custom components にヒントを得て書いています。ぜひ参考に。

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

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

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

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

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

/* @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 クラスを作り、それを使うという方法が取れます。

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

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

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

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

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

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

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(...);
        }
    }
}
// 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 に同じ設定を書く必要があるとき、それがプロジェクト内のデフォルトであったなら...

<?= $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 コンテナに「オブジェクト生成時のルール」を登録しておきます。

Yii::$container->set('yii\jui\DatePicker', [
    'dateFormat' => 'yyyy-MM-dd',
    'clientOption' => [
        ...
    ],
]);
<?= $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 を必要以上に大きくしないことがコツです。

3
3
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3