今まで mojavi 、 symfony と php のフレームワークを使ってきたわけだけど、今回 Zend Frameworkを使ってみて明らかに一番使い易かった(入りやすかった)ので、ちょっとそれについてまとめておこうかなと。
# Cake はソースコードを見た時点で使う気うせた。。
まあ zend がやってるってことで、少くてもやっておいて損は無いかと。とりあえず QuickForm と Smarty は手放せないので、その当たりを交えてうまいこと C-MVC を構築する方法のメモ。
環境
CentOS-5
php-5.1.6
Zend Framework-1.0.1
QuickForm2-1.4
Smarty-2.6.18
apache やら php のインストールは各自 RPM パッケージ行う。
QuickFrom2 は pear パッケージなので、以下のコマンドでインストールを行う。
# pear install QuickFrom2
Smarty と Zend は以下のサイトからそれぞれアーカイブをダウンロードして /usr/share/pear 配下辺りのパスが通った所に置く。
Zend Framework-1.0.1
Smarty-2.6.18
Zend Framework は解凍すると library っていうディレクトリがでてくるので、その中の Zend ディレクトリをそのままパスディレクトリに配置してやればいい。
Zend FW は何故か apache の rewrite module が無いと動かないようになっている。
このせいで色々と不便もあるんだけど、まあ利点も多いんだろうな。
とにかく apache の設定ファイルをいじって mod_rewrite を有効にする。
# vi /etc/httpd/conf/http.conf
... LoadModule rewrite_module modules/mod_rewrite.so ...
Zend FW のディレクトリ構成はいたって単純。 Front Controller としての役割を果たす index.php が mod_rewrite と連携して全てのリクエストをインターセプトして、それぞれの Controller の Action を呼び出す仕組み。
実際に想定されるディレクトリ構成は以下のとおり。
.../index.php .../application/ /controllers/ /My1Controller.php /My2Controller.php /... /models/ /MyAModel.php /MyBModel.php /... /views/ /filters/ /helpers/ /scripts/ /My1/ /Smarty1.tpl /Smarty2.tpl /... /My2/ /... /...
実際は module 分けをしたりしてもっと色々できるんだけど、とりあえずシンプルな例で。
前述したとおり index.php は全てのクライアントからのリクエストをインターセプトする Front Controller 。
同時に、セッションのスタートやタイムゾーンの設定、 php.ini の設定、場合によっては DB への接続など、全てのアプリケーションで共通して行われる処理を行うことも可能だと思う。
index.php がどのようにリクエストを解析するかというと、例えば、
http://localhost/my1/edit/id/10
という URL へのリクエストが来た場合、 index.php はこのパスを解析して、id パラメータに 10 をセットして my1 コントローラの edit アクションをディスパッチする。
始めのパスがコントローラ名、二番目がアクション名、その後はパラメータ名、パラメータ値、パラメータ名、パラメータ値、…と続く。
ではこのコントローラとアクションはどこでインプリメントされるかというと、次で説明する application/controllers/ ディレクトリになる。
この application/controllers ディレクトリは、 index.php がコントローラーを探す場所になる。
ファイル名は以下のルールに従っている必要がある。
[ControllerName]Controller.php
例えばコントローラ名が user なら、 UserController.php になる。
このコントローラはユーザが自由に決めることができ、特定の機能ごとに作成すると良い。
例えば、ユーザ情報閲覧・変更で1コントローラ、ブログ機能で1コントローラ、みたいに。
そして一つのコントローラファイルは一つのコントローラクラスが定義されていて、
そのコントローラクラスは複数のアクションをファンクションとして定義することになる。
このコントローラクラスは Zend FW に実装されている Zend_Controller_Action のサブクラスである必要がある。
これが Zend FW のコアの仕組み。
次に application/models だが、これは別にあってもなくてもいいし、
ただたんに Zend がここを Model のためのファイルに推奨しているだけみたい。
さらに Zend はモデル層のためのスケルトンクラスなどは一切提供していない、っぽい。自分で好き勝手に実装できる。
Zend FW に存在する多くのクラスは、モデルを実装する上で非常に役立つ。
最後に application/views ディレクトリだが、ここは View となるテンプレートやフィルタリングルールなどを、コントローラ、アクション毎に用意するのが Zend の推奨する使いかたになる。
要するに、コントローラはそれぞれのアクションに応じて自動的にこのディレクトリにあるテンプレートをディスプレイするように設計されている。
ただし、この自動という機能は Smarty を使う上では非常に実装が面倒で複雑になるので、ここでは無効化し、アクションがこのディレクトリに配置された Smarty テンプレートをマニュアルで呼び出すことにする。
こういうものはいちいち説明するより、コードを見て書いた方が早いので、ここから一気に実装を説明する。
例として、ユーザ情報をデータベースから取得して編集画面と編集完了画面を表示するアプリケーションを考える。
まずは Zend Framework を動作させるために mod_rewrite 用の .htaccess を作成する。
これは Zend のドキュメント通り以下のようなファイルを作る。
$ vi ./.htaccess
RewriteEngine on RewriteRule !\.(js|ico|gif|jpg|png|css|jpeg|html)$ index.php
要は特定の拡張子を持ったファイル以外は全て index.php にスルーするという設定。
もし、 AVI やフラッシュなどのメディアファイルがコンテンツとして存在するなら、それらも追加しておく必要がある。
RewriteRule !\.(js|ico|gif|jpg|png|css|jpeg|html|avi|swf)$ index.php
次に index.php を作成する。まずは実装から。
$ vi ./index.php
<?php require_once("Zend/Controller/Front.php"); require_once("Zend/Session.php"); // Start session Zend_Session::start(); // Get front controller instance $front = Zend_Controller_Front::getInstance(); // Set the default controller directory: $front->setControllerDirectory('./application/controllers'); // run front controller $front->run('./application/controllers'); ?>
やっている事は、セッションをスタートして、
Front Controller のインスタンスを取得し、コントローラのディレクトリを指定した後に、
コントローラディスパッチャーをを実行している。
ここで分かるように、実はコントローラの場所もどこでも良くて、指定可能なんです。
次にコントローラとアクションを作成する。
とりあえずここでは UserController とする。
$ vi ./application/controller/UserController.php
<?php require_once("Zend/Controller/Action.php"); require_once("Smarty.class.php"); require_once("application/models/UserModel.php"); class UserController extends Zend_Controller_Action { private $_smarty; private $_template; private $_db; private $_userform; public function init() { $this->_helper->viewRenderer->setNoRender(true); $this->_smarty = new Smarty(); $params = array( "host" => "localhost", "username" => "db_name", "password" => "db_pass", "dbname" => "db_name", "port" => 3306 ); $this->_db = Zend_Db::factory("Pdo_Mysql", $params); try { $this->_db->getConnection(); } catch(Zend_Db_Adapter_Exception $e) { return false; } catch(Zend_Exception $e) { return false; } } public function preDispatch() { $this->_userform = new UserModel( $this->_db, intval($this->_request->getParam('user_id')) ); } public function postDispatch() { $this->_smarty->display($this->_template); } public function EditAction() { $this->_smarty->assign("form_user", $this->_userform->getDefaultFormArray($this->_smarty) ); $this->_template = "user/edit.tpl"; } public function UpdateAction() { if( $this->_userform->update() ) { $this->_smarty->assign( "form_user", $this->_userform->getFrozenFormArray($this->_smarty) ); } else { $this->_smarty->assign(" form_user", $this->_userform->getFormArray($this->_smarty) ); } $this->_template = "user/edit.tpl"; } } ?>
ここで出てくるファンクションは全部で5つ。内 EditAction と UpdateAction の二つがアクション部分の実装。
それ以外の init と preDispatch 、 postDispatch の役割はそれぞれ以下のとおり。
コントローラ共通で初めにディスパッチされるファンクション。
コントローラの設定や、オブジェクトの初期化を行うと良い、と思う。
実際の実装では ACL の処理なんかもここで書いていたりする。
そもそも、 Zend_Controller_Action クラスのサブクラスを作成して、色々共通処理は書いてしまうけど。
ここでは、 DB への接続、 Smarty オブジェクトの初期化、自動レンダラの無効化を行っている。
アクションがディスパッチされる前にコントローラ共通で呼び出されるファンクション。
アクション共通の設定やオブジェクトの初期化を行う。
この例ではアクション共通で利用される UserModel の初期化を行っている。
アクションがディスパッチされた後にコントローラ共通で呼び出されるファンクション。
サンプルコードのように最終的な描画処理何かを行うといいと思う。
この例では Smarty テンプレートの Display をコールしている。
アクション Edit の実装。 http://localhost/user/edit/uid/x に対する処理。
ここでは、ユーザ情報を編集するフォームのための Smarty 用配列を UserModel から取得してアサインし、このアクションで使用されるテンプレートを指定している。
アクション Update の実装。 http://localhost/user/update に対する処理。
UserModel の update ファンクションをコールして、問題が無ければフリーズしたフォームの Smarty 用配列を、問題があればそのままのフォーム用配列をアサインする。
この辺の実装は、どこまでをコントローラがやってどこまでをモデルがやるかで悩む。俺の中では、フリーズさせたフォームを表示するか、ノーマルなフォームを表示させるかはコントローラの仕事。
で、実際のアクションで用いられているモデルクラスの実装を次に書く。
モデル。
$ vi application/models/UserModel.php
<?php require_once("Zend/Db.php"); require_once("HTML/QuickForm/Renderer/ArraySmarty.php"); class UserModel { private $_qform = null; private $_db; private $_user_id; public function __construct($db, $user_id) { $this->_db = $db; $this->_user_id = $user_id; $this->initializeForm(); } public function getFormArray($smarty) { $ret = array(); if( $smarty != null ) { $renderer =& new HTML_QuickForm_Renderer_ArraySmarty($smarty); $this->_qform->accept($renderer); $ret = $renderer->toArray(); } return $ret; } public function getDefaultFormArray($smarty) { $this->setDefaultForm(); return $this->getFormArray($smarty); } public function getFrozenFormArray($smarty) { $this->_qform->freeze(); return $this->getFormArray($smarty); } public function update() { if( $_this->_qform->validate() ) { return false; } $data = array( 'name' => $this->_qform->getElementValue('name'), 'email' => $this->_qform->getElementValue('email'), 'gender' => $this->_qform->getElementValue('gender') ); $n = $this->_db->update( 'users', $data, 'uid = ?', intval($this->_user_id) ); return true; } private function setDefaultForm() { $this->_db->setFetchMode(Zend_Db::FETCH_OBJ); $result = $this->_db->fetchRow( 'SELECT * FROM users WHERE uid = ?', intval($this->_user_id) ); $this->_qform->setDefaults (array ( "uid" => $result->uid, "name" => $result->name, "email" => $result->email, "gender" => $result->gender) ); } private function initializeForm() { $this->_qform = new HTML_QuickForm( "form", "post", "user/update"); $this->_qform->addElement("hidden", "uid"); $this->_qform->addElement("text", "name", ""); $this->_qform->addElement("text", "email", ""); $this->_qform->addElement( "select", "gender", "", array("Male" => 0, "Female" => 1) ); $this->_qform->addElement("reset", "reset", "reset"); $this->_qform->addElement("submit", "submit", "submit"); $this->_qform->addRule( "text", "Please enter Name", "required", null, "client" ); $this->_qform->addRule( "email", "Please enter Email", "required", null, "client" ); } } ?>
これがモデル実装。 QuickForm のオブジェクトをコンストラクタで初期化し、コントローラからの呼び出しに応じて QuickForm オブジェクトを初期化したり、freeze したりして Smarty 用の配列を返す。
モデルのコンストラクタ。
ユーザ ID を受け取って QuickForm オブジェクトの初期化を行う。
DB オブジェクトも受け取ってるけど、これはとりあえずここだけの例。多分実際は index.php か何かで初期化して Zend_Registry か何かにレジスターしておく。
そうすれば、いちいちコントローラがそれぞれのモデルに渡さなくても、モデルが自分自身で DB オブジェクトを取得できる。
QuickForm オブジェクトを Smarty 用 array に変換して返す。
QuickForm オブジェクトに DB から取得した値をディフォルト値としてセットし、 Smarty 用 array に変換して返す。
QuickForm オブジェクトをフリーズして Smarty 用 array に変換して返す。
QuickForm でバリデーションを実施し、問題が無ければ DB をアップデートする。
QuickForm オブジェクトに DB から取得した値をディフォルト値をセットする。
QuickForm オブジェクトを初期化する。
ここまでで分かるように、コントローラはどんなフォームが作成されているのか中身は全く分からない。
フォームの初期化、データベースへのアクセス、入力値のチェックなど、ビジネスロジックは全てモデルが行うのが俺のコンセプト。
最後に Smarty テンプレートを作成する。
$ vi application/views/scripts/user/edit.tpl
<html> <head> <title>user edit</title> </head> <body> {$form_user.javascript} <form {$form_user.attributes}> {$form_user.hidden} Name: {$form_user.name.html} {$form_user.name.error}<br /> Email: {$form_user.email.html} {$form_user.email.error}<br /> Gender: {$form_user.gender.html} {$form_user.gender.error}<br /> {if $form_user.frozen EQ false} {$form_user.reset.html} {$form_user.submit.html} {/if} </form> </body> </html>
これは普通の Smarty テンプレート。
こんな感じで、割ときれいに MVC でまとまる。しかも QuickForm も Smarty も違和感無く使える。
実際 Zend Framework は他にも色々詰まってて、アクセスコントロールや認証もシンプルに実装できるし、ルールに従えばセキュアな実装が可能。
いくらかテンプレート用意しておけば、かなりラピッドディベロップメントが可能だと思う。
っていう事で、結構お奨めです。