前回はデフォルトの「コントローラ名」「アクション名」を自分仕様に変えてトップページを作成しました。
そして存在しないページがリクエストされても、トップページを表示させるトリックを施しました。
今回は各種ファイル置き場を定数定義して設定を集約したり、クラスファイル読み出し記述の煩わしさから解放される魔法をかけたいと思います。
うまく魔法がかかればソースコードがスッキリとシンプルなものとなります。
Zend Frameworkの設計思想として「使いたいように使ってくれ」というのがあります。
その一端として、ファイル配置にも自由度の高さがあります。
つまり各種ファイルは柔軟な配置ができるわけです。このことは裏を返せばどのファイルをどこに置くか自分で決め把握できていないと混乱するということでもあります。
なのでMVCアプリケーション開発では、フロントコントローラに処理がある間に次に処理を渡す先であるアクションコントローラの置き場所を必ず教えてやる必要があります。
これにはrun()メソッドやsetControllerDirectory()メソッドを使います。
Zend_Controller_Front::run()
or
Zend_Controller_Front::setControllerDirectory()
run()メソッドを使って渡したパスも、内部ではsetControllerDirectory()メソッドに渡しているのでどちらを使っても同じことになります。
ここまででMVCのうちコントローラ(C)についての配置確定要素がはっきりしました。
ではそれ以外の配置はどう決まってくるのでしょう?
実は「C」配置を確定させることで、そこから相対的な位置をデフォルトのビュー(V)配置として見るようになります。
デフォルトというのは、特に指定が無ければ「V」置き場として認識します程度のゆるいものです。
このゆるさが自由度の高さなわけです。
できるだけ独自指定しないで済むファイル配置で開発していくのが理想的だという観点で考えると、デフォルトで決まるその「V」配置を採用するのが無難でしょう。
これでMVCのうち「C」「V」の置き場所を確定する要素が判明しました。
さて、あとはモデル(M)です。
「M」に関しては特に専用のクラスも無いので「C」置き場を確定させたところで相対的にこの場所だとか認識することはないです。
つまりどこに置いてもいいんでしょう。
なので総合するとこれまで配置してきたようにapplication直下に「M」「V」「C」が並ぶフラット3形式(造語)で作っていくのがシンプルでわかり易いと思います。
application/
models/
views/
controllers/
実にわかり易い配置ですね。
わかり易い名称や配置にしておくことは、その後使っていく上で理解を深める助けになると思います。
今しがた「M」配置に関してはなにも決まりがないということを書きました。
このことはつまり「M」置き場専用のディレクトリを作って1箇所にまとめようがまとめまいが自由だということを意味しています。
全く決まりがないからこそ自分のスタイルを決めておく必要があります。そして私の場合は「M」置き場専用ディレクトリを作るフラット3形式なわけです。
「M」と同じように、専用ディレクトリを設けてそこに同じような機能の(クラス)ファイルをまとめておきたいという場合があります。
例えば使い慣れた自作の「ユーティリティ」クラスファイル専用置き場としてutilディレクトリを、「フォーム」専用にformディレクトリを、「広告」専用にadディレクトリを設けたいなどあるかも知れません。
「M」専用にディレクトリを用意することはこれらとまったく同じテンションなので、ZFではMVC設計のアプリケーションと言えどもガチガチに決め打ちされた配置なんてものは無く、自由度の高さゆるさがハンパないです。
このゆるさを味方に付け、武器にすることで魔法使いになりましょう。
専用に設けたディレクトリに対してdirname(__FILE__)を使ってクラスファイル読み出し記述する方法もありますが、今回は魔法を使う為の下準備をしておきます。
1.クラスファイル置き場のパスを通しておいて
2.「ファイル名」と中身の「クラス名」を一致させておく
という2点が下準備となります。
まずは各種クラスファイル置き場を定数定義することを考えます。
これはフロントコントローラに定義定義を並べて書けばよさそうですね。
絶対必要な定義はまず「Zend Framework本体」置き場でしょう。
あと「アクションコントローラ」置き場を必ず教える必要がありました。
「ビュー」置き場はアクションコントローラ置き場を教えることでゆるく決まってくるので必要ない感じがしますが、一応定義しておいてもよさそうです。
他は「モデル」置き場を定義する必要があります。
雑多な自作のクラスファイル置き場としてutilディレクトリを設けようと思うので、これも定義しておくことにします。
その他、システム寄りのクラス置き場としてsysディレクトリも作っておきたいと思います。
フラット3置き場をそれぞれ定義することになるので、これらを統括するapplicationの位置も定義しておくことにします。
// 環境のディレクトリ区切り文字をコンパクトに定義
define("DS", DIRECTORY_SEPARATOR);
// パス定義
define("PATH_ZEND_LIBRARY", realpath("/xampp"));
define("PATH_APPLICATION" , realpath("../application"));
;
define("PATH_CONTROLLERS", PATH_APPLICATION . DS . "controllers");
define("PATH_VIEWS" , PATH_APPLICATION . DS . "views");
define("PATH_MODELS" , PATH_APPLICATION . DS . "models");
;
define("PATH_UTIL" , PATH_APPLICATION . DS . "util");
define("PATH_SYS" , PATH_APPLICATION . DS . "sys");
以上がとりあえずはアプリケーションで使用するすべてのパスとなります。
パスを定義したら、次はそのパスを通す設定を行います。
パスを通す必要があるのは上記のうちPATH_ZEND_LIBRARY, PATH_MODELS, PATH_UTIL, PATH_SYSの4つでよさそうです。
// クラス置き場のパスを通す
$path = implode(PATH_SEPARATOR, array(
PATH_ZEND_LIBRARY,
PATH_MODELS,
PATH_UTIL,
PATH_SYS,
));
set_include_path($path);
どうしてもZF本体を数多く呼ぶことになるので、PATH_ZEND_LIBRARYを先頭指定しました。
で、あとは自動読み出しの部分を定義して魔法を完成させます。
要するに__autoload()関数を定義します。
function __autoload($class) {
include str_replace('_', '/', $class) . '.php';
}
はいできました。
以上を脳内シミュレートしてみます。
まずソースのどこかに以下の記述があったとします。
$obj = new Zend_View();
ところが以下のようなクラス定義読み込み記述が前もってなされてないわけですから
require_once 'Zend/View.php';
Zend_Viewクラスをインスタンス化できないって話になり、多少PHPサイドがザワつきます。
このままではクラス定義がありませんよエラーになってしまいます。
でもPHPサイドはそんな時、エラーにする前に念のため__autoload()関数が定義されているかチラ見します。
「お! __autoload定義されてるやん」って事になるとエラーにする前にこの関数を呼び出します。
その際、引数に必要となるクラス名を渡して呼び出します。
__autoload('Zend_View');
実装コードを展開してみると、以下が実行されたことになります。
include 'Zend/View.php';
クラス定義が見つからなかったのでエラーにしようとしてたけど、これでクラス定義が読み出せたのでエラーは回避され、
しかもrequire_onceではなくincludeを使っているので多少の負荷減になるかも知れません。
これまでrequire_onceを使って書いてたのは、一度読み出ししたファイルをまた読み出すように書いたコードがソースのどこかにあっても、二度目からは読み出さないようにしてくれるからで、結果的にクラスの二重定義エラーを回避していました。
それとrequire系だと読み出し失敗なら処理をストップしてくれる保険に加入しています。
今度から強気のincludeで攻められるのは、未定義のクラスを使おうとした場合に__autoload()が呼ばれるという挙動はすなわちクラスファイル定義がまだメモリ上に取得できていないので読み出していいという逆説的な発想になります。
__autoload()関数を定義しておけばファイル読み出しを個別に書く必要が一切無くなります。
【ケース1】クラスのインスタンス化
$obj = new Child();
クラスをインスタンス化しようとした時にそのクラス定義が読み出されてなかったというケースにおいて、__autoload()関数を定義しておけばこちらに捜索依頼が来ることになります。
【ケース2】staticメソッド呼び出し
Child::foo();
staticメソッドを使おうとした時にそのクラス定義が読み出されてなかったというケースにおいて、__autoload()関数を定義しておけばこちらに捜索依頼が来ることになります。
【ケース3】クラス継承
class Child extends Parents
{
}
Childクラス定義を読み込んだ時にその継承元であるParentsクラス定義が読み出されてないケースにおいて、__autoload()関数を定義しておけばこちらに捜索依頼が来ることになります。
【ケース4】関数使用
$methods = get_class_methods('Child');
クラスを扱う系のPHP関数呼び出し時にそのクラス定義が読み出されてなかったというケースにおいて、__autoload()関数を定義しておけばこちらに捜索依頼が来ることになります。
一方、__autoload()関数には捜索依頼が来たときに備えて捜索手順を実装しておく必要があります。
今回はinclude_path設定のあるパスを捜索位置とし、そこからクラスファイル読み出しを試みるように定義しています。
見つかった場合は処理は何事もなかったように遂行され、見つからなかった時初めてエラーとなります。
では実際にこれまで作成してきたMVCファイル群について修正していきます。
パス関連を定数定義し、
クラスファイル読み出し記述部分を取り払います。
そして、チョットしたスパイスを加えて書いてみましょ
それでは「Hello world!」の挨拶を表示させるアプリケーション第2弾です。
■ファイル配置
application/
models/
GreetModel.php
views/
scripts/
error/
error.phtml
greet/
hello.phtml
controllers/
AppliController.php
ErrorController.php
GreetController.php
util/
FigletUtil.php
ValidUtil.php
sys/
MsgSys.php
htdocs/
.htaccess
index.php
今回も前回同様、デフォルトのコントローラ名を「appli」、デフォルトのアクション名を「top」に設定して作成します。
トップページ用のアクションに来たらそこからすべて挨拶ページに飛ばす処理を今回は施します。
なので挨拶ページ用に別アクションコントローラGreetControllerを設けたいと思います。
となるとトップページ描画が発生しない為、ビュースクリプトviews/scripts/appli/top.phtmlは省くことにします。
●リライト設定 - .htaccess
これまでと同じもので大丈夫です。
●フロントコントローラ - index.php
<?php
// 環境のディレクトリ区切り文字をコンパクトに定義
define("DS", DIRECTORY_SEPARATOR);
// パス定数定義
define("PATH_ZEND_LIBRARY", realpath("/xampp"));
define("PATH_APPLICATION" , realpath("../application"));
;
define("PATH_CONTROLLERS", PATH_APPLICATION . DS . "controllers");
define("PATH_VIEWS" , PATH_APPLICATION . DS . "views");
define("PATH_MODELS" , PATH_APPLICATION . DS . "models");
;
define("PATH_UTIL" , PATH_APPLICATION . DS . "util");
define("PATH_SYS" , PATH_APPLICATION . DS . "sys");
// 自動呼び出しクラス置き場のパスを通す
$path = implode(PATH_SEPARATOR, array(
PATH_ZEND_LIBRARY,
PATH_MODELS,
PATH_UTIL,
PATH_SYS,
));
set_include_path($path);
// 基本はコレ
$front = Zend_Controller_Front::getInstance();
$front->setControllerDirectory(PATH_CONTROLLERS);
$front->setDefaultControllerName('appli');
$front->setDefaultAction('top');
$front->setParam('useDefaultControllerAlways', true);
$front->dispatch();
// クラス呼び出し自動化関数定義
function __autoload($class) {
include str_replace('_', DS, $class) . '.php';
}
パス定義を定数化しました。
また、require_once読み出し記述がなくなりました。
これくらいのボリュームならフロントコントローラの可読性を落とさずに済みそうです。
●アクションコントローラ
AppliController.php
<?php
class AppliController extends Zend_Controller_Action
{
public function topAction() {
// 挨拶ページへ飛ばす
return $this->_forward('message', 'greet');
}
public function __call($name, $args) {
if (substr($name, -6) === 'Action') {
return $this->_redirect('/');
}
throw new Exception('Method not found: ' . $name);
}
}
前回はデフォルトのアクションtopAction()メソッドで処理をし、appli/top.phtmlを描画したページをトップページとして表示させました。
今回はここに来たらすべて「greet」コントローラの「message」アクションに飛ばすよう指示しています。
さて、_forward()と_redirect()似たようなメソッドがあります。
これらの違いは_forward()が別のアクションに実行を移す為のもので、
_redirect()はブラウザにアクセス先の指示を与えます。
つまり_forward()はサーバ側だけで処理を移しているのに対して、_redirect()はサーバ側からブラウザに行き再びサーバ側へ処理が移ることになるのでリクエスト時の送信データは_redirect()以降では渡って来ないので使えないということになります。
GreetController.php
<?php
class GreetController extends Zend_Controller_Action
{
public function init() {
$this->view->setEncoding("sjis");
}
public function messageAction() {
$message = FigletUtil::render(GreetModel::message());
//$message = ''; // メッセージ取得できない場合のテスト用
if (!ValidUtil::notEmpty($message)) {
$this->view->errmsg = MsgSys::ERR_001;
}
$this->view->message = $message;
}
}
エラーメッセージに日本語(SJISコード)を使っているので、一応sjis指定しておきます。
アクションコントローラにinit()メソッドを定義すると、そこに書いたコードが真っ先に実行されることになります。
メッセージを「モデル」から取得してFigletというある種の描画を施しています。
取得データが空文字だった場合はエラーメッセージをセットするようにしています。
MVCとは役割分担です。
この「コントローラ」では「モデル」にデータをくれという指示を与えています。
ユーティリティにはデータを変換してくれと指示を与えています。
別のユーティリティにはデータが取得できてるか検査してくれと指示を与えています。
このように「コントローラ」の役割りは指示を与えることで、コントローラ以外は指示待ち人間です。
私は根っからの我流なのでホントのMVC記述はどうすべきかよくわかりませんが、この記述で大きく外してはいないと思っています。
いろいろなクラスのメソッドを使っているにもかかわらず、今回はrequire_once読み出し記述が無いのが特徴的です。
●エラーコントローラ
これまで使ってきたものでOKです。
これまで使ってきたものには以下の呼び出しを記述していましたが、残しておいても問題ないと思います。
require_once 'Zend/Controller/Action.php';
しかしZend_Controller_Frontクラスが読み出されたら内部でZend_Controller_Actionクラスも間接的に読み出しているので実は自動呼出しを定義してない場合でも記述不要でした。
残しても消しても問題ない記述なのでここは柔軟に考えてどっちでもいいということで・・
エラー時にしか呼ばれないものなので通常はページ表示の負荷に関係ないと考えることもできます。
●モデル
models/GreetModel.php
<?php
class GreetModel
{
public static function message() {
return 'Hello world!';
}
}
「モデル」の仕事はデータの出し入れになります。
ファイルへ出し入れするのかデータベースを使うのか、「コントローラ」からの指示(メソッド呼び出し)どおりに出し入れするだけです。
●自作ライブラリ系
util/FigletUtil.php
<?php
class FigletUtil
{
public static function render($s) {
$figlet = new Zend_Text_Figlet();
$figlet->setOutputWidth(1000);
return $figlet->render($s);
}
}
Zend_Text_Figletクラスを使ってアスキーアート文字みたいな変換を施します。
ブログのコメントなどでスパム投稿を防ぐ為、画像に描かれた文字を入力してくださいみたいな処理があります。
兄弟クラスのZend_Captcha_Figletを使えば画像の代わりにアスキーアートでそれを実現できます。
util/ValidUtil.php
<?php
class ValidUtil
{
public static function notEmpty($s) {
return (string)$s !== "";
}
}
いわゆるバリデーションで使う処理を集めたクラスを想定して用意しました。
sys/MsgSys.php
<?php
class MsgSys
{
const ERR_001 = 'データの取得に失敗しました。';
const ERR_002 = 'MPが足りません。';
const INFO_001 = 'へんじがない ただのしかばねのようだ。';
}
システム寄りのデータということでutilではなくsysに分けて定義しました。
●ビュースクリプト
greet/message.phtml
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=Shift_JIS" />
<title>ZendFrameworkの練習</title>
</head>
<body>
<h1>世界へあいさつ</h1>
<!-- エラー表示 -->
<?php if (ValidUtil::notEmpty($this->errmsg)) { ?>
<div style="background-color:Red;color:White;font-weight:bold;padding:10px;">
<?php echo $this->escape($this->errmsg);?>
</div>
<?php } ?>
<!-- メッセージ表示 -->
<pre style="font-size:6px;font-family:'Courier New';">
<?php echo $this->escape($this->message);?>
</pre>
</body>
</html>
◎実行結果1
◎実行結果2
GreetController::messageAction()において、メッセージ取得できなかった場合のエラー表示画面になります。
どうでしょうか?
無事実行できたので、目的のrequire_once読み出し記述を書かずに済む魔法がうまくかけられたということになります。
これまでは各ファイルのあちこちに読み出し記述を書く必要があった為、可読性も悪かったように思います。
これからはシンプルで読みやすいソースコードに保つことで、管理しやすくバグの少ないアプリケーション開発にまい進できそうです。
今のところクラスファイル名で唯一、命名規則があるのがアクションコントローラです。
controllers/FooController.php
これに習い、
sys/MsgSys.php
util/ValidUtil.php
util/FigletUtil.php
models/GreetModel.php
のようにパターン化した作りにしてみました。
これでValidUtil::notEmpty($str)のように使った時に、ファイル置き場がはっきり分かります。
この自己ルールをきっちり守れば、クラス名がバッティングする悲劇も起こらないでしょう。
ファイル名・クラス名が冗長になってしまうのが難ですが。。
ZFライブラリ方式だと、
PATH_APPLICATIONパスを通しておいて、
Sys/Msg.php にファイル配置し、クラス名をSys_Msgとするとか・・・
うぅn。。 どうもファイル名とクラス名の乖離が気になりますね。
発想転換して__autoload()定義を変更して
FooBarクラスが無い
↓
__autoload($class='FooBar')呼び出し
↓
preg_match('/(.+)([A-Z][a-z\d]+)$/', $class, $match);
include strtolower($match[2]) . DS . $match[1] . '.php';
こんな風に呼び出しを定義しておけば、
bar/Foo.php という風に短いファイル名で済む。
しかしクラス名はFooBarのままなわけで、コンパクト化にはならない。
class FooBar{}をダミー定義してついでに必ず呼ばれるように
class Foo {}を同じファイルに書くとか
いやいやこんないびつな構造にしてしまっては、せっかくのZendFramework様が悲しまはりますやん。
極力シンプルに保つ意味でも、最初のフォーメーション1でいくことにします。
と、ここまでやってきて必須だけどまだ扱ってない部分がそれなりに残ってることに気づきました。
送信パラメータの受け取りや
アクションコントローラで処理させたいタイミングに応じて定義できるメソッドの説明
ビューに描画させるスクリプトファイル指定方法や描画を手助けするビューヘルパー作成方法など・・
今まで極小規模ではありますがZFによるMVCアプリケーションを作成してきて、ビューが裏でどんな挙動してるかとか一切気にせずにやってこれたわけですから、Zend Framework恐るべしですね。