読者です 読者をやめる 読者になる 読者になる

Yiiフレームワークでもっと理解したいMVCの話

2011年内に書ききれなかったトラックバックです。あけましておめでとうございました。

PHPアドベントカレンダーに Ruby on RailsCakePHP と Django と Symfony2(*1.x とは別物なので2と明記) の特長がうまくまとまってるいいエントリが書かれていました。
フレームワークで語るMVCの話 : PHP Advent Calendar #19 - basuke の日記

で、Yii をネタに加えて、勝手に追っかけたいと思います。Yii を題材にしますが、だからみんな Yii を使えという話ではなく、MVCフルスタックフレームワークは Yii から学ぶことがいっぱいあるという話です。

Yii の第一印象はよりオブジェクト指向的な CakePHP でした。config フォルダ以下のファイルに array で設定を書いて、models と controllers と views にそれぞれフレームワークの基底クラスから派生した php を書く。ビューはphpコードで書くのがもっとも書きやすい。アプリケーション全体に共通する独自の特性は、ベースクラスでいちど共通な振る舞いを実装して、それを実際のMやCに派生するようにする流儀。DBにテーブルありきでスキャフォルドもできる。

PHPの静的オブジェクト指向っぽさを尊重

ソースをざっと見て、最初に CakePHP と大きく違って見えるのは、

 var $belongsTo = array(...);

と書くところが

 public function relation() {
     return array(...);
 }

みたいな形になっているのが、大きく違って見えるところです。

なぜフィールドではなくメソッドなのか

PHPRubyよりまだもう少し、IDEでの静的解析ができる言語です。なので、オーバーライドであればコードの補完が効くかもしれません。自由なフィールドを定義してカスタマイズする方法だと、コード記述量そのものは減りますが、文法がミス防止の助けにならないですね。

またYiiでは、ActiveRecordを生成すると、フィールド名がクラスのdocコメントにずらずらと書かれます。どこがいいのか。__getや__setを使っていると、普通フィールド名を補完できません。が、なんと、EclipseのPDTはdocコメントに書かれたフィールド名も、コーディング中に候補として出してくれます。NetBeansとPHPStormも同じことをしてくれます。

/**
 * @property $hoge
 */
class Fuga extends CActiveRecord {
}

$fuga = new Fuga;
$fuga->h //ここでhogeが候補に出る!

気がきいてますよね、こういう配慮。

たぶんRailsは、RubyがIDEの補助をきわめて受けにくい言語だということに開き直って、その代わり、コード自体が極端に短くなるようにたんだと思います。

と、いうような言語の特徴から来るコンセプトだと、他の似てない言語に引き継ごうとしても、ちょっと歪んでしまう気がするんです。それより、「対象の言語の特性を逆手に取って、その言語独自の癖を活かす」というのが、良いライブラリAPIだと感じます。JavaRubyを参考にするのはいいけど、それだけじゃJavaRubyでやっておけばいい話で、PHPだからこその味付けが欲しい、みたいな。

Yii には PHP だからこそこうあるべき、という考えを正攻法で押し進めてる感じがあって、その独特な考え方を見るたびに、なぜそうなのか遡って考えると、いろいろ参考になります。まあ、どの文化圏出身のPHPerから見ても、第一印象はちょっと変な子に見えて損してそうですけどね。

さて触りはそんな感じで、一番言いたかったのはつぎ。

イベント駆動のモデル:より良いMVCへの誘導

フレームワークで語るMVCの話 : PHP Advent Calendar #19 - basuke の日記
Railsの罪」

コントローラが重要視されることとは関係ないはずですが、やたらコントローラにロジックを突っ込みがちなのも初心者に見受けられる特徴です。コントローラを太らすことは、一番やっちゃいけないこと。これの理由も、はじめにコントローラありきで始まってしまうことに起因してるのでは。置き場所に困ったコードをコントローラにおいてしまうということだと思います。

それから、モデル=ActiveRecordという誤解も広げたのもRailsです。MVCのモデルは、データベースアクセスで得られるエンティティの管理だけではありません。ビジネスロジックと呼ばれる、いわゆる処理などもモデルに属するものです。それらは、データベースのテーブル単位よりはるかに大きな枠組みで、複数のエンティティを扱う必要があるのが普通です。モデル=ActiveRecordという考えにとらわれているとどっかで破綻します。ロジックをもっとモデルの一部として認識してもらいたいなと思ってます。

これ、まったくそのとおりだと思いました。

Webアプリのフレームワークってだいたい、データベーステーブルのCRUDに割りきって、ブラウザで動く MS Access として使うなら綺麗にハマるんです。簡易ブログチュートリアルだけ比べると差がないように見えても、MとCのペアを崩す必要がある実際の業務で、ようやくそれぞれのフレームワークの腹の中が見えると思うんですよね。だいぶむかし、Railsが出始めの頃にどこかで読んだブログの意味がようやくわかってきました。

補足すると、コントローラにロジックを書くとまずいのには理由があって、それは、アプリケーションの実行環境に直接接しているという点ですね。どういうことかというと、HTTPの事情が影響するから、ロジックだけの単体テストがやりにくいということ。つまり、xUnitでコードだけを使って動作を保証しようとしたら、そう言い切れる部分を外の環境から切り離す必要があって、そうやって切り出してまとめた結果、もはやそこはコントローラではなくモデルになってしまう。だから、究極的にはモデルしかテスト駆動できない。だったら最初からコントローラにビジネスロジックを入れるべきじゃない。乱暴にいうとざっくりそんな感じ。

保守開発でよくあるのがモデル側の事情の変更。フィールド名が変わったなど。そのせいでコントローラもいっぱい書き換えないといけないってことが起こると、せっかくオブジェクト指向でやってるのに、直交性がすごく低いってことですよね。

テストを重視するなら、MVCのモデルをデータベーステーブルだと誤解させたり、コントローラにごりごり「書けてしまう」のは、実はうまい誘導じゃなかった。とまあ、そんな仮説を立てたとしましょう。

じゃあYiiはどうなんだというと...

まず目に付くのがYiiのモデルにはすべて getAttributeLabel なんてメソッドがあることです。コールすると、その属性の表示用の名称が出てきます。"wordPress"というフィールド名を入れると"WordPress"っていう表示用のフルスペルが出てくるという感じ。モデルの実装コードにフィールド名称の管理表が実装されます。変わってますね。

// モデル実装の一部
public function attributeLabels()
{
	return array(
		'id' => 'ID',
		'name' => 'Name',
		'seq' => 'Sequence',
	);
}

でもこれ考えてみれば合理的で、フィールド名を考えるのはモデルだから、そのとき、変数名と意味の対応についてすでに知識ありますよね。もしビューのほうでも、フィールド名と表示用の名称の両方を知らなければならないとしたら、コード上は疎結合でも、仕様意図が密結合になっちゃいます。表示するための名称がHTMLのあちこちに散らばると、DRYでもなくなります。表示だからビューっていう構造的なべき論ではなく、もっとも都合がいい責務分担を推奨するというのは、PHPが現実解バリバリな技術だというのと、相性がいい気がします。

RailsCakePHPと大きく違うのは「データベースを使うのがデフォルトなモデルであえて使わない」のではなく、データベースを使うモデルと使わないフォームモデルの両方があって、どっちを使うかを「選択」するようなAPIだというのが特徴です。model フォルダの中にCModel派生でないコンポーネントを置いていてもロード対象になってくれたりします。

他のフレームワークでも、知識があればできるんですが、Yii の面白いところは、アプリケーションのスキャフォルドがすでに、そういう非CRUDMVCの良いサンプルになっているということ。Yii のアプリケーションには最初から、

  • ログイン画面
  • お問い合わせフォーム
  • このサイトについてページ

が付いてます。ただ便利だからってだけではなく、これらの機能がデータベースと関係ないMVCのいいサンプルになっているんですね。最初からある models/LoginForm.php は、バリデーションできますがsaveできないフォーム入力用のモデルです。controllers/SiteController.php は特定のモデルとの関係なしで、いろいろ機能を提供しています。先にこういうのに目を通せると、MS Access の置き換えのような誤解をせずに済むんじゃないかと思います。

また、YiiのCRUD系スキャフォルドの結果は、良い感じのMとCの責務分担のサンプルになっています。生成直後のCRUDコントローラ(あえてCRUDコントローラと言うのは、非CRUDコントローラも生成できるから)には、ほとんどモデルの事情が現れません。たとえばこんな感じ。

public function actionDelete($id)
{
	if(Yii::app()->request->isPostRequest)
	{
		$this->loadModel($id)->delete();

		if(!isset($_GET['ajax']))
			$this->redirect(isset($_POST['returnUrl']) ? $_POST['returnUrl'] : array('admin'));
	}
	else
		throw new CHttpException(400,'Invalid request. Please do not repeat this request again.');
}

$this->loadModel($id) というのを経由させて、もう、何という名前のモデルクラスを使うのかすら隠してしまうという具合。これ、モデルの事情とコントローラがなすべきことが直行してますよね。アクションを受け付けてモデルにメッセージを送るだけで内情は知らない。特定のモデル名が現れないので、同じ挙動のコントローラは、モデルが違ってもコードがまったく同じ実装になりますね。わりとこんなふうに、モデルのクラス名すらも隠蔽してMとVCの間の密結合を避けるようにしてる箇所が多く現れます。ビューで表示するフィールドをわざと間接的に文字列で名前指定して拾ったりとか。

WebのMVCのコントローラで組みたいロジックってそもそも、HTTPに関する知識をもとにした制御ですね。ビジネスのドメインとは関係がない。ビジネスロジックの事情は知ったこっちゃない。このまま、コントローラはできるだけActiveRecordの標準APIだけに依存するようにできれば、コントローラはモデルのメソッドの仕様変更から影響を受けなくて済みます。

そこでイベント駆動なモデルなわけですよ。

たとえば、データが削除されるとき、もし添付ファイルがあったらそれも同時に削除したいですよね。削除だけはきっちりログに残しておきたい、なんてこともあるでしょう。そんなとき、コントローラの中で $model->delete(); のあと $model->attachments の全てにunlinkしますか。いや、それはHTTPまわりの責務じゃない。CakePHPにもありますが、Yiiでも、モデルに beforeDelete/afterDelete メソッドを定義するだけで、削除アクションの前後で起こって欲しいことを簡単に実装できます。

//Model実装の中で
public function beforeDelete()
{
    Yii::log('deleting ' . $this->id);
}
public function afterDelete()
{
    foreach($this->attachments as $a) {
        unlink($a);
    }
    Yii::log('deleted ' . $this->id);
}

他にもいろいろハンドラがあって、afterFindやbeforeSaveは利用頻度の高いハンドラです。
イベントハンドラがひとつなのが嫌だという人は attachEventHandler なんてのもあります。

Yiiのアプリケーションでは、なぜか、イベント駆動が普通の作り方だという感覚に陥ります。なぜなのかは自覚してなくて説明しにくいのですが、最初からできるだけこういうやり方で書きたくなるほうに誘導されちゃう。

で、それがうまい罠になっていて、イベントハンドラを書くのが自然にやれるようになると、findされたら必ずafterFindが呼ばれている状態が担保できるという安心感が得られる。そうすると、初期化メソッドの呼び忘れみたいなミスの心配がなくなる。CRUD操作とロジックが絶対にワンセットになり、afterFindが動いた後の状態をテストするだけでよくなる。ここまでのことが、コントローラからの呼び出しに頼らなくていい。

ほら、単純なORMだとfindをテストするなんて動いて当たり前でバカバカしいけど、afterFindイベントの可能性まで含んだら、find系のテストを書いておきたくなりますよね。ブラウザなしでも動かしてみたいですよね... はい、罠にかかりましたこれ。モデルってこういうものです、という概念への導線があったんですよね。

認証とセキュリティフィルタとOCP

自分が Yii を使っている最大の理由がコレです。

個人的にCakePHPACLがどうもダメで。アクセス制御ルールの割り当てはドメインモデルとは関係ないし、データの実体より先行するべきものだと思うのです。データベースが先にあって、それにソースコードが従属するという関係だと自分の仕事との相性がいまいち良くなかった。

Yiiのアクセス制御はコントローラにコードとして書きます。もっともシンプルなロールベースの制御はこんなコード。

public function accessRules()
{
	return array(
		array('allow',
			'roles'=>array('admin'),
		),
		array('deny',
			'users'=>array('*'),
		),
	);
}

で、認証はどうするかというと、CUserIdentity を派生させたクラスを作って authenticate をオーバーライドする。そのときデータベースのユーザテーブルを使ってもいいし、ファイルを読んで照合してもいい。何ならパスワードをハードコーディングしてもいい。なので、先に完全な認証が作れないとアプリケーションにログインできないのではなく、認証をずっと仮実装のままにしておいて、先に機能を作ってしまうことだってできる。

class UserIdentity extends CUserIdentity
{
	public function authenticate()
	{
		// 仮
		return 0;
	}
}

ロールとユーザの割り当ては? これもオーバーライドでどうにかなります。自分の場合は、ロールの数が限られているのでこんな感じのを実際に使っています。

class AuthManager extends CPhpAuthManager {
    public function checkAccess($itemName, $userId, $params=array())
    {
        $user = User::model()->findByAttributes(array('name'=>$userId));
        if($itemName == 'admin') {
            return !empty($user) && $user->admin;
        }
        else {
            return TRUE;
        }
    }
}

といった具合で、まずコードありきが基本です。データベースを使うかどうかの判断は、コードの実装に権限がある。ホント、正攻法ですよね。この正攻法のなにが嬉しいかというと、より上の権限を持つのが「ソースコード」のほうで、そいつは「バージョン管理対象だ」ってことです。

データベースはソースコードのようにプログラマが管理できるわけではない、いわば「相手の環境」の一部です。都合のいいようにならない場合もあるでしょう。それでも、ユースケースに沿ってユーザの種類ごとに役割を正しく割り振るという作業は、コード実装者の責務なんですね。だったらそのとおりにしたほうがいい。

こういう、オブジェクト指向の正攻法で考えればできないことはない、と期待できる設計だということが、とても大事なことだと思うのです。あと一歩でやりたいことができる、って状況で、コアライブラリにパッチを当てなきゃいけないのは、OCP(Open Close Principal)に反してますよね。OOPってのはOCPのためにあると言ってもいいぐらい、この期待できる感はOO的に大事だと思います。

でもいやなところも

まるでYii褒めちぎりみたいですが、そんなYiiにもちょっとこれはどうなんだろうって点があります。

Yii はビューまわりにいわゆるHTMLコーダーの入る余地がほとんどありません。ああざんねん。Yii のビューはそのほとんどがYiiのAPIをコールするPHPタグで埋まります。たとえば...

<?php
    Yii::app()->clientScript->registerCoreScript('jquery');
    Yii::app()->clientScript->registerScriptFile(Yii::app()->request->baseUrl . '/js/jquery.json.js');
?>

jQueryのプラグインをリンクしたいだけでこれが標準の方法だとかまじかと。

まあでも考えてみてください。MVCにおけるVは「ビューロジック」です。かつてであれば、手続き言語だけでやっていたわけで、それを考えれば、まだHTMLレンダラが利用できるだけマシというものかもしれません。Yiiで作るものはアプリケーションであってコンテンツではない。そういうスタンスなんでしょうね、きっと。

あと... あえて言うとすれば「かわいくない」こと。あのロゴのMSNみたいな感じはどうにかならないものかとw それに名前っ!


と、思ったことをいっぱい書いたら読むのもたいへんになってしまいました。
とくにイベントなんかの機能は、CakePHPにそっくりで、改めて言うことじゃないかもしれないかも。けれど僕が気にしたいのはその「同じようなもの」の「最初の見せ方」と「学習の流れづくり」がすごく上手だということです。YiiはRailsCakePHPの設計をすごく参考にしていると思います。それを、内部の設計も使われ方の想定も、もっとオブジェクト指向的に突き詰めて考えてみた結果、って感じでした。
その一方で、所詮PHPなところはあっさり妥協してやりすぎないようにしてるようにも見えます。

まだ自分もユーザとして経験は浅いけど、その中からわかってきたことがこんなにあったので。PHPフレームワーク話をするとき、これ外して考えるのはちょっと損してるんじゃないかなと思います。すでに別のフレームワークで技術を確立してる人でも、YiiのAPIからはいろいろなことが学べるんじゃないでしょうか。