Zend Frameworkメモ

今年に入ってからPHPの仕事をやってます。最近ネット上ではフルボッコ状態だったPHPですが、たしかに言語だけ見ると同意する部分大ですが・・・、でもフレームワークの方はRailsより後発のFWが多い分、調べているとなかなか面白いです。Railsに比べて実行環境の面では安心して使える点も魅力です。
PHPJavaと同じくフレームワークが乱立しており混沌とした状況にありますが、その中で今回は、本命と言われるZend Frameworkを調べてみました。Railsに比べて各機能の独立性が高く、デフォルトの規約が少ない分個別機能だけを簡単に利用することが出来ます。反面、無設定の状態ではいちいち機能を呼び出してやらないと使えないことが多く、無駄な記述が増えてしまうのが欠点でしょうか。・・・ただ、拡張性が高いので、比較的簡単な設定で規約前提の挙動にすることも十分可能です。
というわけで、Zend Frameworkを調べつつ、出来るだけ規約前提で記述レスで動かせるような初期設定を目指してみました。まだ調べ始めたばかりでいろいろ間違ってるかもしれませんが・・・

初期化

まず、ZendFrameworkのフロントコントローラ「index.php」を、アプリの公開ディレクトリのルートに設置します。

<?php
$appRoot = '/Users/hidenoshin/Documents/workspace/zend';
$zendRoot = '/Users/hidenoshin/ZendFramework-1.5.0PR/library';
set_include_path(get_include_path() . PATH_SEPARATOR . $appRoot . '/lib' . PATH_SEPARATOR . $zendRoot);

$front->addModuleDirectory($appRoot . '/modules');

$front->dispatch();
?>

アプリのプロジェクトディレクトリは、Eclipseのプロジェクトとしてworkspace上に作っています。
Zend Frameworkは別のディレクトリにダウンロードし、libraryをinclude_pathに指定して使ってます。
また、プロジェクト/libディレクトリを自作クラス用に作成し、そこにもinclude_pathを指定しました。
プロジェクト/modulesディレクトリをモジュールパスとして指定します。これにより、modulesディレクトリ配下にディレクトリを追加していくだけで、モジュールを追加していくことが出来ます。モジュールを利用しない場合は、defaultディレクトリのみをモジュールとして作成しておきます、
最後に、frontオブジェクトのdispatch()メソッドを呼び出すことにより、URLに紐づいたコントローラが呼ばれます。
このindex.phpには結構定義を書くことになるので、最近のJavaScriptの記述法に習って、初期化クラスにまとめることにしました。そうすれば、よけいなグローバル変数を増やさずにすみます。

<?php

ZendInit::init();

class ZendInit {
	function init() {
		$appRoot = '/Users/hidenoshin/Documents/workspace/zend';
		$zendRoot = '/Users/hidenoshin/ZendFramework-1.5.0PR/library';
		set_include_path(get_include_path() . PATH_SEPARATOR . $appRoot . '/lib' . PATH_SEPARATOR . $zendRoot);
		require_once 'Zend/Controller/Front.php';

		$front = Zend_Controller_Front::getInstance();
		$front->setParam('appRoot', $appRoot);
		
		$front->addModuleDirectory($appRoot . '/modules');
						
		$front->dispatch();		
	}
}

?>

このindex.phpにRewiteする設定を、.htaccessに記述

RewriteEngine on
RewriteRule !\.(js|ico|gif|jpg|png|css|zip|gz|html|xml)$ index.php
RewriteBase /zend

これはドキュメント通り。

Zend_Config

次は環境設定ファイルの定義。ZFではZend_Configクラスを利用して設定ファイルを読み込みます。設定ファイルをどこに置くかは自由ですが、それだと利用するときにいちいちパスを指定しなければならなくなります。なので、パスを指定するindex.phpの記述の中で、このZend_Configオブジェクトも作成してしまい、frontオブジェクトの中に突っ込むことにしました。

require_once 'Zend/Config/Ini.php';

$config = new Zend_Config_Ini($front->getParam('appRoot') . '/config/config.ini', 'production');
$front->setParam('config', $config);

プロジェクト/config/config.iniに設定ファイルを置き、オブジェクトを作成しています。アプリ側でこのオブジェクトを使うときは

$front = Zend_Controller_Front::getInstance();
$config = $front->getParam('config');

と書けばOK

Zend_Db

config.iniの内容は以下の通り

[production]
database.adapter = Pdo_Mysql
database.params.host = localhost
database.params.port = 8889
database.params.username = root
database.params.password = root
database.params.dbname = zend

これはデータベースの接続定義です。今回はMAMPを利用しているので、そのデフォルトのMySQL設定を利用しています。Zend_Configオブジェクトを作成する際に「'production'」文字列を指定していますが、ここを変更すれば環境によって設定を変更することができます。Railsのdatabase.ymlのような環境変更がZFでも可能ってことですね。
この定義を使って、Zend_Dbオブジェクトを作成するときは

$db = Zend_Db::factory($config->database);

と書いてやればOK。「database」を別の文字列にすれば他のデータベース定義を利用することも出来ます。
Zend_Db_Tableを利用したO/Rマッピングを利用するときは、このdbオブジェクトを使った初期化定義をindex.phpに書いておくと便利

$db = Zend_Db::factory($config->database);
Zend_Db_Table_Abstract::setDefaultAdapter($db);

こうすれば、dbオブジェクト呼び出し定義を各アプリ側で書く必要はほとんどなくなります。

routes定義

ZFのURL定義のデフォルトは、
/プロジェクトパス/controller名/action名/key名/value/key名/value
となってますが、このデフォルト定義だといまいちパラメータ渡しが面倒に感じます。当然フォームパラメータを使えばいいのですが、ここはRailsのREST対応コントローラのように、/controller/action/value みたいな記述にしてみたいところです。
この場合、Zend_Configの設定ファイルにroute定義を書いて読み込ませればOKです。

routes.messages.route = "messages/:id"
routes.messages.defaults.controller = messages
routes.messages.defaults.action = edit
routes.messages.reqs.id = "\d+"

そしてindex.phpに以下の記述を追加

$router = $front->getRouter();
$router->addConfig($config, 'routes');

こう書けば、messagesコントローラとIDの指定だけでeditActionを呼び出すことが出来ます。本当はRailsのようにREST APIも絡ませてroute定義をやってみたかったのですが、デフォルトではHTTP のメソッドによるroute定義には対応していないようです。
Zend_Configの設定ファイルに書くということは、当然定義を後から追加していくことが出来ます。

Plugin

ZFは様々な拡張ポイントを用意していますが、一番汎用的に使えるのはZend_Controller_Pluginでしょうか。このプラグインを利用することによって、各Controllerの呼び出し前後に処理を追加することが出来ます。
ここでは、このPluginを使って、Zend_Viewの設定追加処理を記述してみます。Zend_Viewはviewテンプレートで使うescape関数を提供していますが、この関数のデフォルト設定には問題があって、PHPのhtmlspecialcars関数と同様、デフォルトではシングルクォーテーションをエスケープしてくれません。なので、この関数の挙動を変更する設定を追加します。

<?php
require_once 'Zend/Controller/Plugin/Abstract.php';
require_once 'Zend/Controller/Action/HelperBroker.php';
require_once 'Zend/View.php';

class My_Plugin extends Zend_Controller_Plugin_Abstract
{
	public function preDispatch(Zend_Controller_Request_Abstract $request)
    {
    	$helper = Zend_Controller_Action_HelperBroker::getStaticHelper('viewRenderer');
    	if (!isset($helper->view)) {
    		$helper->setView(new Zend_View());
    	}
    	$helper->view->setEncoding(mb_internal_encoding());
	$helper->view->setEscape(array('My_Plugin', 'quotesEscape'));
		
    }
    
    public static function quotesEscape($var)
	{
		return htmlspecialchars($var, ENT_QUOTES, mb_internal_encoding());
	}	
    
}
?>

他と同様、このクラスをindex.php上で定義します。

$front->registerPlugin(new My_Plugin());

これで、escape関数を安全に利用できるようになりました。

Zend_Layout

ZF1.5からレイアウト機能が追加されています。レイアウトはWebアプリ開発には必須の機能なので、この定義も追加してみます。

Zend_Layout::startMvc(array('layoutPath' => $appRoot . '/layout'));

アプリケーション/layout/layout.phtml にレイアウトテンプレートを置く前提です。Zend_Configに書くことも出来るのですが、アプリのパス指定が二重定義になってしまうので、index.php上に書くことにしました。


以上が、とりあえず自分が調べてみた初期化の定義です。全部まとめたindex.phpは以下のようになりました。

<?php

ZendInit::init();

class ZendInit {
	function init() {
		$appRoot = '/Users/hidenoshin/Documents/workspace/zend';
		$zendRoot = '/Users/hidenoshin/ZendFramework-1.5.0PR/library';
		set_include_path(get_include_path() . PATH_SEPARATOR . $appRoot . '/lib' . PATH_SEPARATOR . $zendRoot);
		require_once 'Zend/Controller/Front.php';
		require_once 'Zend/Config/Ini.php';
		require_once 'Zend/Db/Table/Abstract.php';
		require_once 'Zend/Layout.php';
		require_once 'My/Plugin.php';

		$front = Zend_Controller_Front::getInstance();
		$front->setParam('appRoot', $appRoot);
		
		$front->addModuleDirectory($appRoot . '/modules');
				
		$config = new Zend_Config_Ini($front->getParam('appRoot') . '/config/config.ini', 'production');
		$front->setParam('config', $config);
		
		$router = $front->getRouter();
		$router->addConfig($config, 'routes');
		
		$front->registerPlugin(new My_Plugin());
		
		$db = Zend_Db::factory($config->database);
		Zend_Db_Table_Abstract::setDefaultAdapter($db);
		
		Zend_Layout::startMvc(array('layoutPath' => $appRoot . '/layout'));
				
		$front->dispatch();		
	}
}

?>

ちなみに、このindex.php上でrequire_onceを使ったクラスについては、Controllerなどの各クラス上ではrequire_once記述無しで利用することが出来ます。デフォルトの状態よりもかなり記述が無くなって、冗長な設定記述も消すことができました。設定次第でいろいろな挙動をさせることができるのが、ZFの面白いところかなと感じています。