この記事はオープンソースの 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->user
を yii\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の型解析の問題を解消する以外に、もしかしたら 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*
の派生として作ってもよいことを示唆しています。
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 を必要以上に大きくしないことがコツです。