blog

フレームワークのMVC

2010-02-28 22:01:42 薄皮的 PHP Framework Comments: 0
いままでのまとめ。

設計を考えた結果、意外にもMVCの原点に立ち返ってしまった。
今まで自分はMVC否定派だと思っていたがそんな事は無かったぜ。

MVCを正しく成立させるためには、M-V-Cがそれぞれ対等でなければいけないのだ。
symfonyもcakeもMが強すぎる。Vが弱すぎる。
その結果Cが調整弁として多くの役目を果たさねばならず、肥大化する羽目になる。
アクションをシンプルにするのはフレームワークの命題だと言えるので、それではいけない。

対等なMVC。
  • コントローラ
    • ルーティングを行う。ルーティングはアクションに相当する処理を行う。
    • アクションに相当する処理にはすべて必ずルート定義が与えられる。アクション内で分岐が行われて2つの動作を1アクションに記述する事はありえない(禁止)。分岐が起きる場合、一方の処理は別アクションへの変更という形で処理する。
    • ルート定義には従来通りワイルドカードを用いる事が出来る。標準の規約では、URLに該当するビューを(アクションが定義されていなくても)呼び出す事が出来る。ビューのファイル名はルート定義により決定される。
    • よって、アクションはビューの面倒を見る必要性から解放される。Smartyのassignに相当する処理は暗黙に行われる。アクションによって明示的に変更しても良い。
    • アクションに相当する処理は、原則的に設定ファイルに記述する事が出来る。個別のロジックが必要な場合は、そのロジックを指定して呼び出す。(これはstruts.xmlに似ている)
  • ビュー
    • ビューはassignされた変数群とファイルのパスを受け取る。
    • ビューの設定はビュー専用の設定ファイルに記載される。
    • ビューは「受け取ったパスから実際にどのファイルを呼び出すか」を設定する権限を有する。これはアクションのsetTemplate()に相当する。
    • ビューはインクルード機能を有する。これを使い重複する実装をまとめる事が出来る。これはウィジェットの管理にも必須の概念である。
    • ビューはDOCTYPE宣言やヘッダ送出のために、自身の親となるファイルをインクルードする事が出来る。これは設定ファイルに記述する。
    • ビューはウィジェットを有する。たとえばPOSTリクエストを送信する方法はビューが決める。フォームなのかAjaxなのか、どんなインタフェースで送信させるのか、すべてビューが決める。
    • 以上の要件から、Webデザイナーは以下の事項を学習する事で開発に参加する事が出来る。
      • ビューの設定ファイルの記述方法
      • テンプレートエンジンに対応するコーディング方法
      • ウィジェットの定義と利用方法
      • そして、当該プロジェクトにおける個別のルート定義(先行開発の場合はワイルドカード)
  • モデル
    • 機能群とテーブルは一対一で結びつかないという経験から、機能群とは異なる。あくまでテーブルに関する操作しか実装されない。
  • ロジック
    • ロジックはMVCから分離されるが、MVCの要素をそれぞれ内包する。(これはcakeの考え方に近いのかも知れないが、真意はわからない)
    • モデルを操作するための機能群が含まれる。
    • コントローラに追加のコーディングが必要な場合はロジックが呼び出される。
    • ビューのウィジェットを実装するのに必要であればロジックが呼び出される。
  • ライブラリ
    • ロジックはプロジェクトに対して実装されるもので、ライブラリはフレームワークに対して実装されるものであると定義する。
    • ライブラリはロジックの代替になることが出来る。もちろんロジックからライブラリを呼び出すことも出来る。
これでだいたい、作りたいフレームワークの骨格は設計できた。
あとは具体的に実装をはじめていこうと思う。
オープンソースプロジェクトにしたいのだが、作法がわからない。

フレームワークのビュー #2

2010-02-28 15:27:23 薄皮的 PHP Framework Comments: 0
これは書き直しではなく続き。

「フォーム」はビューの管轄である。
(ただし「バリデータ」はモデルの管轄である)
symfony 1.4用語で言うと、errorsとhelpsとwidgetsはビューの管轄ということだ。

ただし、errorsはバリデータを取り扱うためにモデルを必要とする。
当然、保存処理を実行するためにもモデルが必要だ。
コントローラ≠モデルと定義してしまったので、「どのモデルを使うか」を記述しないといけない。

ビューはアクションと同数のビューを持つ。
newアクションとeditアクションで同じビューを使いたければ、そのようにビューが設定する。
ビューにはそれを設定する仕組みを提供する必要がある。

あるテンプレートファイルは別のテンプレートファイルをインクルードする事が出来る。
symfonyのlayout.phpも、親子関係は逆転しているが、インクルードの一環である。
どの親をインクルードするかも、設定ファイルに記述する。

インクルードされるファイルはアクション名と結びつかないため、
「テンプレートファイルがあれば必ず同名のアクションが存在する」とは言えない。
便宜上、たとえばファイル名の先頭に_をつけて区別すると良いかも知れない。

デバッグモードでは、アクションが存在しなくてもルーティングとテンプレートを結びつける事が出来る。
これはモックアップ作成のために必要である。
デザイナー視点だと、これを本番モードでも適用させれると良いのかも知れない。

ルート定義=アクション=ビューとするわけである・・・。

/ と /default と /default/index が同じビューであることを、ビューが定義する必要がある。
/hoge/fuga と /foo/bar が同じだとしても同様である。
ひょっとすると、そっちのほうがいいのかも知れない。
最も設定のボリュームは多くなるが、定義は透明になる。

この場合、バリデート失敗もひとつのアクションとして定義する必要がある。
システムエラー的なものも含めると、より数は増える。
そのかわりアクションはルート定義と完全に同化する。「コントローラ」になる。
なんてこった。一回りして完全なMVC構造になってしまった。

この仕組みにはもう一つメリットがある。
必ずすべてのビューにルートが割り当てられていると、ダミーデータを用いた表示テストが容易に出来るのだ。
もちろん、すべてのパラメタ(たとえば権限管理とか)を適用するのは難しいが。

フレームワークのアクション #2

2010-02-28 15:04:39 薄皮的 PHP Framework Comments: 0
これは書き直しではなく続き。

アクション定義のイメージ。まずはsfRouteCollectionにある7つの定義に相当するもの。
  • list 一覧を表示する
  • :id/show 1件を表示する
  • new レコードを追加するための入力画面を表示する
  • create newから入力された値を保存する
  • :id/edit レコードを編集するための入力画面を表示する
  • :id/update editから入力された値を保存する
  • :id/delete レコードを削除する
日本の習慣で実装しなければいけない事が多いもの。
  • confirm newもしくはeditの入力内容を確認する。ちなみに失敗時のビューはnewもしくはeditを代用する。
  • confirm_back 入力内容の確認をした結果修正を行いたいのでnewもしくはeditに戻りたい。もちろん入力内容は保持したい。ブラウザの戻るボタンで戻るとたまに入力内容が消えることがあるからね。ビューはnewもしくはeditを代用する。アクションでも代用は可能かも知れないが・・・。
  • confirm_delete 削除の確認をする画面を表示する。JavaScriptのconfirm関数などで代用される事も多い。
アクションとしては(ありがちな定型処理だとしても)10通りの処理を実装する必要がある。
ビューも基本的には同じ数だけ必要であるが、要件によって減る可能性がある。残念ながら手間はあまり減らない。
  • newとeditはビューを共有することがある。
  • create confirm(new)が完了した後に表示する画面。要件によってlistやshowを表示することもある。update後のビューと共有することもある。
  • update confirm(edit)が完了した後に表示する画面。要件によってlistやshowを表示することもある。create後のビューと共有することもある。
  • delete confirm_deleteが完了した後に表示する画面。要件によってlistを表示することもある。
  • confirm_backアクションに相当するビューは無い。
  • newとeditはバリデータの失敗時に別のビューを用意しなければならないケースもある。
ビューを共有するかどうかはビューが決めることであって、アクションが決めることではない。
ビューはWebデザイナーに解放しなければならないので、この線引きはとても重要である。

アクションから見た時には、アクションと同じビューが必ず存在する前提で呼び出すことにする。
setTemplateメソッドに相当する実装はビューが行うべきである。

フレームワークのアクション

2010-02-28 14:19:23 薄皮的 PHP Framework Comments: 0
アクションは極力シンプルに実装されるべきである。
1つのアクションが2つの挙動をする時点で何かが狂っている。

しかし、アクション=ビューである以上、確認画面を出すために、
同じアクション内に分岐を書かなければいけない羽目になる。
どのアクションからどのビューを呼び出すかを変更出来る機能を利用することを前提にするなら、アクション≠ビューであることになる。
そうすると、ルーティングとビューの不一致が発生するので、ブラウザで /hoge にアクセスした場合に fuga.html が呼ばれるいう事態になる。ひいては開発(主にWeb製作)が混乱する原因になる。

本来であればconfirmでエラーになったからeditに戻ります、というケースでは、アドレスバーにはeditと記載されるべきだ。それを実現するにはRedirectを行わなければならず、POSTメソッドはRedirectに対応していない。
http://d.hatena.ne.jp/hakobe932/20090707/1246985195
対応させるためにはセッションにPOSTデータを置いてGETでリダイレクトさせればいいわけだが、壮絶に野暮ったい。
費用対効果に見合わないので、結局妥協してアドレスバーはconfirmのままになるケースが多いだろう。

ところで「新規にedit画面を開いたとき」と「confirmから差し戻されてエラー表示つきのedit画面を開いたとき」では、挙動は明らかに違う。現状symfony 1.4では、こんなコードを1つのアクションに書く。もちろんこれはまったく野暮である。
if ($this->form->isValid()) { /* 保存処理 */ } else { /* エラー処理 */ }
ビューにも別の問題があり、エラー表示をするhtmlと、普通に入力するhtmlを便宜上同じファイルにまとめる事がある。
PHPっぽく書くとこうなる。
<?php if ($mode_create): ?>
  <?php if ($is_confirm): ?>
    新規作成時の確認画面
  <?php else: ?>
     新規作成時の入力画面
  <?php endif; ?>
<?php else: ?>
   <?php if ($is_confirm): ?>
     既存編集時の確認画面
  <?php else: ?>
     既存編集時の入力画面
   <?php endif; ?>
<?php endif; ?>
この行以外使い回しがききます、のようなシチュエーションだと、こうまとめることは非常に合理的である。
フォームフィールドが1個追加になりました、という場合に4つのファイルを編集する手間を1つに出来るのだから。

しかし、ビューについては、インクルードを使えれば、別解はある。
タイトルだけを変えた4つのファイルを作り、共通部分はインクルードで対処するのだ。
この場合、インクルードされるファイルはアクション名とは関連性の無いものになる。

つまるところ、アクション≠ビューである、という仮定は覆りようがない。

それではルーティング≠アクションの仮定はどうか。
複数のルート定義が単一のアクションを指すケースは考えられる。
また、「confirmから差し戻されてエラー表示つきのedit画面を開いたとき」の挙動をeditアクション以外のアクションとして定義するとしたら、単一のルート定義に対して複数のアクションが存在することになる。
つまり、ルーティング≠アクションの仮定も覆らない。

処理の遷移は、こうなる。
  1. リクエストを受け取ったルータが、設定ファイルから呼び出し先のアクションを判別する。
  2. ルーティング≠アクションなので、アクションの設定ファイル(ex. actions.yml)が開かれる。
  3. 追加コードの必要無い処理であればメソッド呼び出しを介在させずに終わる。
  4. 追加コードの必要な処理であればメソッドを呼び出して処理する。
一時はアクション不要説を唱えたり、アクションは設定ファイルに出来ると思っていたが、最近少し考えがマイルドになった。
アクションは、「どこに も属さないが実装に必要なコード」を書くための最後の砦である。

もちろん、こういうどうでもいいコードは、コーディングしないで済むにこ したことはない。
$this->page = $request->getParameter('page', 0);
$this->form = new MemberForm();
規模感としては、DefaultActions、AdminActions、SetupActionsの3ファイルで済む程度だと思っているが(あとはlibで書け)そうじゃないケースのために、任意ディレクトリ任意ファイル任意メソッドを設定ファイルに記載できるようにしておくのはやぶさかではない。

アクションの引数については、そもそもHTTPRequestであるのはおかしい。
当然、HTTPRequest以外からもコール出来るべきであるし、
別 のアクションを呼び出すのにforwardメソッドを呼ぶ事もいびつである。
なので$paramsを引数にする。
起動したフレームワークのインスタンスやHTTPRequestはActionsクラスのメンバ変数が持つべきである。
よってこれもsymfony 1.0の実装のほうが、どちらかというと適切だと感じてしまう。
とにかく「任意の引数」とHTTPRequestは分けられるべきなのだ。

フレームワークのリクエスト

2010-02-28 13:05:44 薄皮的 PHP Framework Comments: 0
最難関。

現状、symfonyでは次のものがごっちゃになって定義されている。
  • リクエストパラメタ
  • ルータから渡された変数
  • アクションコントローラ
  • フォーム
  • ウィジェット
  • バリデータ
  • モデル
  • ビュー
  • ルーティングへのリンク
ORMを尊重するなら、フォーム=モデルである。
そして、コントローラ=モデルでもある。この場合MVCにおけるCはルータになる。苦しい言い訳だが。
リクエストパラメタをフォームオブジェクトに引き渡すだけのアクションなんてものは不要なのだ。
MVCを尊重するなら、コントローラ≠モデルになる。
MVCを尊重しつつORMを利用すると、アクションの存在はやや不毛であるが、
理論の整合性を取るために効率を犠牲にするという思想の産物(≒趣味)なのだから、まあ仕方がない。

機能を尊重する折衷案としては、フォーム=モデルでかつコントローラ≠モデルとなる状態にする。
  • ルータの管轄
    • リクエストパラメタ
    • ルーティング
    • 変数の整理
    • ルー ティングへのリンクを判別
  • アクションの管轄
    • アクション
  • モデルの管轄
    • モデル
    • フォーム変数
    • フォームバリデータ
    • フォーム以外のバリデータ
  • ビューの管轄
    • ビュー
    • ウィジェット
    • フォームレイアウト
  • ライブラリの管轄
    • ビュー以外のアクションの振る舞い
「モデルの管轄」の中にある「フォーム」は、実はフォームでもなんでもない。
POSTメソッドによる情報の送信を行うのはformタグがしている事であって、
formタグはhtmlに書かれるものである。htmlを書くということは、それはviewだ。

仮にPOSTメソッドによる情報の送信を行うのがAjaxなら、そのバリデータを「フォーム」が行うのは不適切になる。
単なるデータを扱う存在である。つまりモデルである。

しかし、ORMのように、1テーブル=1モデルであるとは言い切れない。
「このフォームにPOSTした時にAテーブルとBテーブルにレコードを追加する」という要件を満たすに、
AクラスにBを追加するためのメソッドを実装するのは必ずしも適切だとは言えないし、
「このフォームにPOSTした時にはどのテーブルも取り扱わない」とか、
「このテーブルはあっちのフォームでもこっちのフォームでも取り扱う」となる事もある。
だから機能単位でクラスを作らなければいけない。

バリデータは1モデルに複数存在する。
追加時、編集時、削除時でバリデーション処理が異なるからだ。
同時に、データの書き込みを実際に行うメソッドも3つ必要になる。

実はsymfony 1.0のバリデータのほうがsfFormよりずっと理にかなってたんじゃなかろうかと思った。
1 2 3 次へ 14 件中 1 ~ 5 件目