処理の流れも5〜9とだいたい同じ。
- プロジェクト作成
- アクション作成
- コントローラにてアクションの紐付け
- アクションにてフォーム値の定義
- アクションのprepare()にて検証の定義
- アクションのperform()にて表示ビューの指定
- コントローラーにて表示ビューおよびテンプレートファイルの紐付け
- ビューのpreforward()にてテンプレートで使用する変数をアクションフォームにセット(formもしくはapp, app_ns)
- テンプレートの定義(ヘルパーメソッド等でアクションフォームから取り出した値を利用)
|-- app (アプリケーションのスクリプト)
| |-- action (アクションスクリプト)
| |-- action_cli (CLI用アクションスクリプト)
| |-- action_xmlrpc (XMLRPC用アクションスクリプト)
| |-- plugin (フィルタスクリプト)
| |-- test (テストスクリプト)
| `-- view (ビュースクリプト)
|-- bin (コマンドラインスクリプト)
|-- etc (設定ファイル等)
|-- lib (アプリケーションのライブラリ)
|-- locale
| `-- ja_JP
|-- log (ログファイル)
|-- schema (DBスキーマ等)
|-- skel (アプリケーション用スケルトンファイル)
|-- template
| `-- ja_JP (テンプレートファイル)
|-- tmp (一時ファイル)
`-- www (ウェブ公開用ファイル)
|-- css (CSSファイル)
|-- js (JavaScriptファイル)
たくさんあるが、必ずしもこれら全てを使う必要はない。 よく使うのは下記のディレクトリ。
- app
- app/action
- app/view
- template/ja_JP
Controllerの$actionで呼び出すアクションを定義する。
アクションクラス(prepare()perform()メソッド)には最低限必要な処理のみ(必要なオブジェクトを生成してメソッドを実行+エラー処理程度)を記述し、実際のロジックは別途クラスを作成してそこに記述することを推奨する。
アクション内のロジックで表示させるViewを切り替えることが可能。 下記コマンドでスケルトンを生成可能。
## loginという名前のアクションを定義
# ethna add-action -b /var/www/html/sample login
file generated [/var/www/html/sample/skel/skel.action.php -> /var/www/html/sample/app/action/Login.php]
action script(s) successfully created [/var/www/html/sample/app/action/Login.php]
loginという名前のアクションを呼び出す場合。
# curl http://127.0.0.1/sample/www/?action_login=true
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
</head>
<body>
<h1>Sample/Login View</h1>
name: hoge
current time: 2016/10/22
</body>
アクションフォーム種類は全部で3つ。 アクションフォームへのアクセスはアクションクラス(+ビュー)にでのみ行うことを推奨する。
- {$form.フォーム名}: フォーム値
$this->af->set('フォーム名', 値)
でフォーム値へのデータ登録。get('フォーム名')
でフォーム値の取得。 - {$app.変数名}: アクションクラスがビューに渡したい値(HTML上での強制エスケープ)
$tihs->af->setApp('要素名', 値)
でアプリケーション設定値へのデータ登録。 - {$app.変数名}: アクションクラスがビューに渡したい値(HTML上でのエスケープなし)
参考サイト:フォーム値にアクセスする
<?php
class Sample_View_Login extends Ethna_ViewClass
{
function preforward()
{
$this->af->setApp('now', strftime('%Y/%m/%d'));
}
}
?>
Controllerの$forwardで呼び出すビューを定義する。 テンプレートはSmartyで実現。 下記コマンドでスケルトンを生成可能。
# ethna add-view -b /var/www/html/sample/ -t login
file generated [/var/www/html/sample/skel/skel.view.php -> /var/www/html/sample/app/view/Login.php]
view script(s) successfully created [/var/www/html/sample/app/view/Login.php]
file generated [/var/www/html/sample/skel/skel.template.tpl -> /var/www/html/sample/template/ja_JP/login.tpl]
template file(s) successfully created [/var/www/html/sample/template/ja_JP/login.tpl]
アクションと同じく$tihs->af->setApp('要素名', 値)
アクションフォームへのデータ登録が可能。
テンプレートからアクションフォームに格納された値は{$app.要素名}
と書くことで参照できる。
前章のloginアクションはログイン画面を表示させるものだったが、今回のアクション(login_do)はログイン画面で入力された認証情報を元にログイン処理を行う。
# ethna add-action -b /var/www/html/sample/ login_do
file generated [/var/www/html/sample/skel/skel.action.php -> /var/www/html/sample/app/action/Login/Do.php]
action script(s) successfully created [/var/www/html/sample/app/action/Login/Do.php]
アクションは、まずprepare()メソッドが呼ばれ、prepare()メソッドがnullを返した場合のみperform()メソッドが呼び出される。 つまり、入力値の検証はprepare()メソッドで行うことが好ましい。 このルールに従うことでperform()はフォーム値がサニタイズされた前提で処理を書けるためコードをシンプルにすることができる。(Strutsと同じらしい)
# /var/www/html/sample/app/action/Login/Do.php
// フォーム値を定義
var $form = array(
'mailaddress' => array(
'name' => 'メールアドレス',
'required' => true,
'type' => VAR_TYPE_STRING,
),
'password' => array(
'name' => 'パスワード',
'required' => true,
'type' => VAR_TYPE_STRING,
),
);
# function prepare()
{
// $formで定義した条件にマッチしているか検証する
// 検証エラーの場合は再びログイン画面を表示
if ($this->af->validate() > 0) {
return 'login';
}
// nullが返された場合はperform()呼び出し
return null;
}
function perform()
{
// 検証が成功した場合はindex Viewへ遷移
return 'index';
}
エラー時に前画面で入力されたフォーム値を入力状態にするには下記のように$form.要素名
を利用する。
$form.*の値は常にエスケープされているためサニタイズを考慮する必要がない。
# /var/www/html/sample/template/ja_JP/login.tpl
<td><input type="text" name="mailaddress" value="{$form.mailaddress}"></td>
テンプレートにてエラーメッセージを表示できる。
# vim /var/www/html/sample/template/ja_JP/login.tpl
# ループで全てのエラーメッセージをリスト表示
{if count($errors)}
<ul>
{foreach from=$errors item=error}
<li>{$error}</li>
{/foreach}
</ul>
{/if}
# フォームの真横に個別でエラーメッセージを表示させることもできる
<td><input type="password" name="password" value="">{messagename="password"}</td>
アプリケーションの実行中に生じるエラーは、次の2つにわけられる。
- System error システムが原因で起きるエラー。DBエラーなど。
- User error ユーザがフォームに入力した値が不正である場合のエラー。
Ethnaフレームワークでは、これらのエラーは全てEthna_Error(またはPEAR_Error)によって処理される。
Ethna_Errorには、エラーコードとエラーメッセージが格納されている。 EthnaクラスのisError()メソッドで、エラーの有無を確認できる。 ここではtest()メソッドからエラーオブジェクトが返ってきた場合に,エラー処理を行うようにしている。
function perform()
{
$sm =& new {アプリケーションID}_SampleManager();
$result = $sm->test();
if (Ethna::isError($result)) {
//エラーの場合の処理
.....
}
アプリケーションのManagerでエラーオブジェクトを返すには、次のようにする。
class {アプリケーションID}_SampleManager
{
function test($data)
{
// 実際には,まともなエラー処理を行う.
if (! $data) {
// 引数には,メッセージとエラーコードを与える
return Ethna::raiseNotice('データがありません', E_SAMPLE_TEST);
}
return 0;
}
}
エラーオブジェクトには、Notice,Warning,Errorの3つがある。エラーの内容に応じて,これらを使い分ける。 アプリケーション固有のエラーメッセージを渡したい場合は、EthnaクラスのraiseNotice,raiseWarning,raiseErrorメソッドを使って Ethna_Errorオブジェクトを生成する。この例ではraiseNoticeを用いてエラーオブジェクトを返している。
エラーコードの定義はProjectName_Error.phpにて行う。
# /var/www/html/sample/app/Sample_Error.php
<?php
/** エラーコード: ユーザ認証エラー */
define('E_SAMPLE_AUTH', -128);
?>
エラーの内容をユーザに提示したい場合、アクションクラスで受け取ったエラーオブジェクトをActionErrorに格納する。 具体的には、下記のようにae(actionError)のaddObjectメソッドを使う。
# アクションクラス
if (Ethna::isError($result)) {
$this->ae->addObject('testError', $result);
}
また、エラーメッセージとエラーコードからエラーオブジェクトを生成して、ActionErrorに追加するaddメソッドもある。 ActionErrorの内容を表示するには,ビューで次のように書く。
# テンプレート
<tr>
<td>エラーメッセージ</td>
<td>{message name="testError"}</td>
</tr>
基本的にはほぼ全ての処理はアプリケーションの核となるクラス(app/ディレクトリに置かれるスクリプト)に記述し、アクションクラスはそれらを単純に呼び出すのみ、というイメージ。例えば
perform()
{
// メールアドレスをキーにしてユーザオブジェクトを生成
$user =& new Sample_User($this->backend, $this->af->get('mailaddress'));
// 認証処理
$result = $user->auth($this->af->get('password');
// 以降結果によってビューを変更、等...
}
というようになる。これには、各アクションクラス間での処理の重複を防ぐ目的もあるが、主な目的は、アクションクラスはフロントエンドに徹することで、低コストで異なるクライアントに対応できる。
アクションクラスのperform()メソッドを記述する際の注意事項まとめる。
- アクションクラスにアプリケーションの核となる処理を記述しない
- アクションクラスはどんなに長くても100〜200行程度におさめる
- 他のアクションクラスと重複する処理を記述しない
- 重複する処理がある場合は、そのアクションクラスを継承するか、アプリケーションのマネージャ的処理に移行する
ロジック部分を記述したクラスを別に作成し、それをアクションクラスから呼び出すイメージ。 ここでは簡単に以下のようなスクリプトを作成してみる。
# /var/www/html/sample/app/Sample_UserManager.php
<?php
class Sample_UserManager
{
function auth($mailaddress, $password)
{
// 実際にはまともに認証処理を行う
if ($mailaddress != $password) {
return Ethna::raiseNotice('メールアドレスまたはパスワードが正しくありません', E_SAMPLE_AUTH);
}
return 0;
}
}
?>
先ほど作成したSample_UserManager.phpをControllerでインクルードする。appディレクトリとlibディレクトリは、プロジェクトスケルトンを生成した時点でinclude_pathに追加されているため、ファイル名を記述するだけでOK。
# /var/www/html/sample/app/Sample_Controller.php
require_once 'Sample_UserManager.php';
最後に、アクションクラスのperform()メソッドを記述します。ここでは、ユーザマネージャで認証処理を行う。
# /var/www/html/sample/app/action/Login/Do.php
function perform()
{
$um =& new Sample_UserManager();
# ここではauth()メソッドからエラーオブジェクトが返ってきた場合は再度ログイン画面を表示させ、認証が成功した場合はトップページを表示している
$result = $um->auth($this->af->get('mailaddress'), $this->af->ge
t('password'));
if (Ethna::isError($result)) {
$this->ae->addObject(null, $result);
return 'login';
}
//return 'login_do';
return 'index';
}
アクションクラスに多くのロジックを書いていくと、それなりの規模のWebアプリケーションでは必ず共通の業務ロジックというのが出てくる。 アプリケーションマネージャはそうした共通のロジックをアクションクラスから分離することが可能になる。
ethna add-app-manager -b /var/www/html/sample user
file generated [/usr/share/pear/Ethna/skel/skel.app_manager.php -> /var/www/html/sample/app/Sample_UserManager.php]
app-manager script(s) successfully created [/var/www/html/sample/app/Sample_UserManager.php]
呼び出し方は以下の通り。
$um = $this->backend->getManager('user');
class Sample_UserManager extends Ethna_AppManager
{
function auth($mailaddress, $password)
{
// 実際にはまともに認証処理を行う
if ($mailaddress != $password) {
}
if ( $password != $this->config->get('password')) {
return Ethna::raiseNotice('メールアドレスまたはパスワードが正しくあ>
りません', E_SAMPLE_AUTH);
}
$this->session->start();
return 0;
}
アクションの処理の前と後に呼び出されて任意の処理(入力や出力変換等)を行うというもの。 そして、この「フィルタ」はN重にネストさせることができるので「フィルタチェイン」と呼ばれている。
概念も簡単なら実装も簡単で、以下のようになる。
- コントローラの$filterメンバにフィルタクラス名を追加
- フィルタディレクトリ(デフォルトではapp/plugin/Filter)に"1.で追加したクラス名" + ".php"というファイル名でEthna_Filterを継承したクラスを記述
- prefilterメソッドとpostfilterメソッドを実装
# コントローラー
var $filter = array(
'ExecutionTime',
);
$filterにて定義した名前でファイルを作成後フィルタクラスを実装する。 prefilter()メソッドがアクション実行前に、postfilter()メソッドがアクション実行後にコントローラによって呼び出される。 あとはこの2つのメソッドに任意の処理を実装するだけ。
# /var/www/html/sample/app/plugin/Filter/Sample_Plugin_Filter_ExecutionTime.php
class Sample_Plugin_Filter_ExecutionTime extends Ethna_Plugin_Filter
{
function prefilter()
{
}
function preActionFilter($action_name)
{
}
function postActionFilter($action_name, $forward_name)
{
}
function postfilter()
{
}
}
なお、フィルタオブジェクトはprefilter()が呼ばれる前に生成され、postfilter()呼出し後に破棄される。 従って、prefilter()で設定したメンバ変数等はpostfilter()からも問題なくアクセスすることが出来る。
アクションクラスはメンバ変数にセッションオブジェクトを保持しているので簡単に参照できる。 例えば、セッションの値を取得する場合は
function perform()
{
var_dump($this->session->get('hoge'));
}
とすることでセッションhogeを取得できる。
ログインが必要なアクションクラスにauthenticateメソッド を追加。
function authenticate()
{
# セッションがなければloginアクションを実行する
if ( !$this->session->isStart() ) {
return 'login';
}
}
設定ファイルに設定されたpasswordとGETパラメータで渡ってきたpasswordが同じであればログイン成功とする。
$password = $this->config->get('password');
if ( $password == $this->af->get('password')) {
$this->session->start();
}
# vim /var/www/html/sample/etc/sample-ini.php
$config = array(
'password' => 'hogehoge',
EthnaではAppObjectと呼ぶ。 ethnaコマンドを使って簡単に作れます。あらかじめ'user'というテーブルをデータ ベースに作っておけば、
# ethna add-app-object user
とすると、app/Sample_User.phpが作られ、いっしょにAppManagerも作られる。 JOIN未対応なので対応付けられるテーブルはひとつだけ。