PHPのフレームワーク『Ethna』徹底解説

初出:最新LLフレームワークエクスプローラ Ruby on Rails, Maple/Ethna(PHP),Catalyst(Perl),TurboGears(Python) 5大フレームワーク徹底攻略

1章 Ethna入門

グリー株式会社 藤本 真樹 <fujimoto@gree.co.jp>

Ethnaとは

Ethna(「えすな」と読みます)はPHPで記述されたウェブアプリケーションフレームワークです.2004年12月の初版リリース以降着々と,時には忙しさのあまり開発が止まりながらも皆様のご協力のもとにリリースを重ねて,2006年6月現在,最新版は2.1.2となっています.

動作実績やユーザの方もおかげさまでだんだん増えてきていて,筆者の勤めるグリー株式会社の提供するソーシャルネットワーキングサービスGREEを始めとして,幾つかの中規模以上のウェブサービスでも利用していただくまでになりました(具体名を挙げていいのかどうか微妙ですのでここでは伏せさせていただきます)(注1).

(注1)事例募集中です.事例を晒してもかまわない,という方はhttp://ethna.jp/ethna-about-cases.htmlにてお知らせください:)

特徴

Ethnaの特徴(あえて特長とは書きません...)はいくつかありますが,主なものは下記の3つです.

  • MVCモデル2とフロントコントローラパターンに基づいたスタンダードなウェブアプリケーションフレームワーク
  • ゲートウェイシステム(スタンダードなウェブアプリケーションと同じ感覚で,コマンドラインアプリケーションやXMLRPCをインターフェースとするアプリケーションを記述することができます)
  • 緩い制約(基本的に開発者のコードをなるべく縛らない構造になっています)

また,特徴的な機能としては以下のようなものがあります.

  • ethnaコマンドによるファイル,ディレクトリの自動生成(プロジェクトディレクトリや各種クラスなど)
  • アクションフォームクラスによるフォーム値の自動検証,テンプレート変数の自動サニタイズ
  • フィルタクラスによるリクエスト前後,モデル処理開始前後のフック処理

本章の第2節から,Ethnaのインストールから始めて基本的なアプリケーションができるまでを解説してありますので,詳細はそちらをご覧ください.

信念

実際には当初から一定の信念に基づいてEthnaを作っているわけではないのですが,最近は常に「絶妙に妥協」「柔軟に拡張」「高速に開発」という3つのことを意識しながら開発や設計をしています(Ethnaにどこまで反映できているやら,あまり自信はありませんが).

一定規模以上のソフトウェアを設計したことがある方でしたら,まずこの「妥協」というところになんとなく共感を覚えていただけるかな,と思います.特にEthnaのようなウェブアプリケーションフレームワークを考え続けていると,いろんな機能をつけたくなったり,抽象化のレベルをどのあたりで落とせばいいのか迷いだしてキリがなくなってしまいます.また,Ethnaは前述の通りPHPで記述されたフレームワークです.特にEthnaは,機能としてのOOPサポートが不十分なPHP 4もサポート対象としていますし,そもそもPHP自体が極めてプラクティカルな言語ですので,あまり設計にこだわるよりも,PHPなりの良さを生かしたフレームワークにしていく必要がありますので,そういった側面から適切な落としどころはどこか,と日々考えています.

また,もう1つ常に考えるのはフレームワークのせいで今まで出来ていた何かが出来なくなること,そしてフレームワークの振舞いをできるだけプラガブルにすることです.PHPの世界では自分用フレームワークが数多存在していたりして,それはそれでいいのですが,やはり長期的な発展を考えると幾つかのフレームワークに皆様のリソースが集中したほうがいいと考えているので(注2),Ethnaを利用してくださっている方が「あぁ,自分フレームワークならこんなこともできるのに」「自分フレームワークだったらこうやるのに」というストレスを感じないようにする,ということが非常に大事なことだと考えています.

最後に,当たり前ですがEthnaを使うことでウェブアプリケーションの開発を如何に効率化,高速化,高品質化できるかも当然大きなテーマです.この辺りは,Ruby on Railsやsymfonyといった他のフレームワークの機能を大いに参考にさせていただいています(笑).

(注2)究極的には,それがEthnaでなくてもよいと思っています(けれど,それがEthnaだったらいいな,とは思っています).

由来

Ethnaを利用してくださっている方とお話をすると,しばしば「Ethnaっていう名前の由来は何ですか?」というご質問をいただくので,せっかくなのでここに記述させていただきます.

Ethnaは前述の通り「えすな」と発音します.ここではあえてひらがなで記述していますが,実は本来は「エスナ」というのが正しい発音です.ここまでくるとお分かりの方はお分かりかとは思いますが,Ethnaの名前は2006年6月現在,既にシリーズ通算12(+1)作を数える某超有名ロールプレイングゲームの某魔法からいただいています(注3).

ちなみにEthnaという綴りは,筆者がeth.jpというドメインを持っていたので「eth」という文字を入れたい,と強く願っていたことに由来します.

ということで,ウェブにアプリケーションにもフレームワークにもPHPにも全く関係のない由来であり名前ですが,筆者は結構気にいっていますので,当面このままいこうと思います.なお,「どんなに死にそうなプロジェクトでも即座に元気になる『レイズ』」が出来ないものか,という話も筆者のグリー株式会社で出ていました(注4).

(注3)全く関係ありませんが,某超有名ロープレイングゲーム英語版での綴りはEsunaです. (注4)出来ないと思いますけど,僕は.

Ethnaでアプリケーション

Ethna入門,超入門ということで,Ethnaのアーキテクチャなどについて解説するのは後にしてまずは実際にアプリケーションを作成してみます.

インストール

まずはEthnaをインストールする必要があります.EthnaはPEARチャンネルサーバからのインストールに対応していますので,PEARのバージョンが1.4.x以降である場合には下記の手順でインストールが可能です.

(1) インストール済みのPEARパッケージをアップデートします

$ pear upgrade-all

(2) Ethnaのチャンネルサーバを登録します

$ pear channel-discover pear.ethna.jp
Adding Channel "pear.ethna.jp" succeeded
Discovery of channel "pear.ethna.jp" succeeded

(3) Ethnaをインストールします

$ pear install ethna/ethna

ちなみにこれは

$ pear install pear.ethna.jp/ethna

と同義です.このようにチャンネルサーバからEthnaをインストールしておくと,Ethnaのバージョンが上がった場合に

$ pear upgrade ethna/ethna

とするだけで,Ethnaをアップデートすることができます.

また,PEAR 1.3.xをご利用の場合(チャンネルサーバに対応していない場合)は下記のように直接パッケージを指定することでインストールすることも可能です.

$ pear install http://ethna.jp/pear/Ethna-2.1.2.tgz

さらにまた,PEARを利用したインストールが出来ない,したくない場合には当然ですが通常のアーカイブからのインストールも可能です.こちらにつきましては本稿では割愛させていただきますので,Ethnaのサイトドキュメント(http://ethna.jp/ethna-document-tutorial-install_guide.html)をご覧ください.

(4) Smartyをインストールします

Ethnaはバージョン2.1.2現在,テンプレートエンジンとしてSmartyを利用していますので,合わせてこちらもインストールしておきます.簡単な手順は下記の通りです.

(4-1) アーカイブをダウンロードします

http://smarty.php.net/download.phpから最新版(2006年6月時点では2.6.14です)をダウンロードします.

(4-2) アーカイブを適当なディレクトリに展開します.

$ tar zxvf Smarty-2.6.14.tar.gz

(4-3) libs以下をPHPのライブラリディレクトリへSmartyという名前に変更して移動します

$ mv Smarty-2.6.14/libs /usr/local/lib/php/Smarty

以上で完了です.

また,Smartyのドキュメントを日本語化してくださっている方もいらっしゃいますので,詳細につきましてはそちらもご覧ください(http://sunset.freespace.jp/smarty/SmartyManual_2-6-6J_html/installing.smarty.basic.html).

アプリケーションスケルトンの作成

Ethnaのインストールが完了したら,いよいよアプリケーションの作成です.Ethnaでは,ethnaコマンドを利用することでアプリケーションのスケルトンを簡単に作成することができます.

まずご利用の環境でethnaコマンドを-vオプションを付加して実行してみます.

$ ethna -v

すると下記のようなヘルプが表示されると思います.

Ethna 2.1.2

Copyright (c) 2004-2006,
  Masaki Fujimoto <fujimoto@php.net>
  halt feits <halt.feits@gmail.com>
  Takuya Ookubo <sfio@sakura.ai.to>
  nozzzzz <nozzzzz@gmail.com>
  cocoitiban <cocoiti@comio.info>

http://ethna.jp/

なお,ethnaコマンドはpearコマンドと同じディレクトリにインストールされています.ethnaコマンドが見つからない場合は

$ pear config-get bin_dir

で表示されるディレクトリをご確認ください(筆者の環境では/usr/local/binが表示されます).

無事にEthnaのバージョンが表示されたら,今度は何も引数をつけずにethnaコマンドを実行してみます.

$ ethna

するとヘルプとして下記のように実行可能なコマンドの一覧が表示されます.

usage: ethna [option] [command] [args...]

available options are as follows:

  -v, --version    show version and exit

available commands are as follows:

  add-action -> add new action to project:
    add-action [action] ([project-base-dir])

  ...(省略)...

  add-project -> add new project:
    add-project [project-id] ([project-base-dir])

  ...(省略)...

  add-view-test -> add new view test to project:
    add-view-test [view] ([project-base-dir])

アプリケーションのスケルトンを作成するには,上記のadd-projectというコマンドを実行します.まず,アプリケーションを作成するディレクトリに移動してから(ここでは仮に/tmpで作業をします),下記のようにethnaコマンドを実行します.

$ cd /tmp
$ ethna add-project flare

add-projectの次に指定しているのはアプリケーションのIDです.ここにはアプリケーションに応じて適切なIDを指定してください.コマンドが無事実行されると下記のように確認の入力が表示されますので,yと入力すると下記のような出力と共にカレントディレクトリのflare(アプリケーションID)以下にアプリケーションスケルトンが生成されます.

creating directory (/tmp/flare) [y/n]: y
proejct sub directory created [/tmp/flare/app]
proejct sub directory created [/tmp/flare/app/action]
...(省略)...
proejct sub directory created [/tmp/flare/www/css]
proejct sub directory created [/tmp/flare/www/js]
file generated [www.index.php -> /tmp/flare/www/index.php]
file generated [www.info.php -> /tmp/flare/www/info.php]
...(省略)...
file generated [skel.view_test.php -> /tmp/flare/skel/skel.view_test.php]
file generated [template.index.tpl -> /tmp/flare/template/ja/index.tpl]

project skelton for [flare] is successfully generated at [/tmp]

Ethnaではアプリケーションディレクトリ直下に作成されるwwwディレクトリがウェブサーバを通じてアクセス可能である必要があります.ですので,ウェブサーバからアクセス可能なディレクトリにwwwディレクトリへのシンボリックリンクを張る,あるいは移動してしまうか,wwwディレクトリをウェブサーバのドキュメントルートに指定します.ここでは/home/masaki-f/public_html以下にシンボリックリンクを作成しておきます.

$ cd /home/masaki-f/public_html
$ ln -s /tmp/flare/www flare

これでhttp://[サーバ名]/[パス名]/flare/で,作成したアプリケーションスケルトンを実行することができます.実際にブラウザでアクセスしてみてfig.1のような画面が表示されればアプリケーションスケルトンの作成は完了です.

indexアクションを探検

Ethnaはユーザからのリクエストを全て「アクション」という単位で扱っています.例えば上記の初期ページは,デフォルトのアクション(リクエストで実行すべきアクションが指定されなかった場合に指定されるアクション)である「index」というアクションが実行された結果として表示されています.ちなみにデフォルトのアクションは,www/index.phpという実質2行のスクリプトの2行目の第2に引数で指定されいます.

Flare_Controller::main('Flare_Controller', 'index');

ここを書き換えることでデフォルトのアクションを変更することももちろん可能です.また,特定のアクションのみ実行可能に設定したり,リクエストで指定されたアクションが定義されていなかった場合に実行されるアクション(例:Not Foundのページを表示する,など)を指定することも可能です(詳細につきましては第3章をご覧ください).

そして,このindexアクションはapp/action/Index.phpに定義されていますので,早速中をのぞいてみましょう.コメント部分を除くと中身は非常にシンプルで,下記のようになっています.

<?php
class Flare_Form_Index extends Ethna_ActionForm {
    var $form = array();
}
class Flare_Action_Index extends Ethna_ActionClass {
    function prepare() {
        return null;
    }
    function perform() {
        return 'index';
    }
}
?>

つまり,実際に何かを行っているのはreturn 'index';という1行のみです.これは「indexというアクションの実行結果としてindexというビューを表示する」ということを示しています.

ここでは利用していませんが,Flare_Form_Indexクラスの$formメンバには「このアクションが受け取るべきフォームパラメータ」を,Flare_Action_Indexクラスののprepare()メソッドでは「このアクションが受け取ったフォームパラメータの検証処理」を,Flare_Action_Indexクラスのperform()メソッドでは「このアクションが行うべき処理」を記述します(つまりprepare()メソッドはperform()メソッドの前に実行されます).indexアクションは単純に画面を表示するだけのアクションなので全てが空になっているわけです.

アクションから'index'という文字列が返されると,app/view/Index.phpで定義されているFlare_View_Indexクラスのpreforward()メソッドが呼び出されます.このpreforward()メソッドでは,そのテンプレート固有の変数(例えばセレクトボックスの選択肢をデータベースから読み込んでくる,など)を設定するために準備されています.が,やはりindexアクションでは特に何も処理を行っていないので下記の通り空になっています.

<?php
class Flare_View_Index extends Ethna_ViewClass {
    function preforward() {
    }
}
?>

こういった場合にはクラスの定義自体を省略することも可能です.ですので,試しにview/Index.phpを削除してみても問題なく画面が表示されることを確認できるかと思います.

そして最後がテンプレートです.テンプレートファイルはtemplate/ja/index.tplに置かれています.これについては特にご説明の必要はないかと思いますが,下記のように普通にHTMLが記述されています.

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=EUC-JP" />
<link rel="stylesheet" href="css/ethna.css" type="text/css" />
</head>
<body>
...(省略)...
</body>
</html>

以上のように,Ethnaでは下記のような流れでリクエストが処理されています.

1. 全てのリクエストはアクション単位で実行される 2. 各アクションはapp/actionディレクトリ以下で定義されていて 2-1. まずprepare()メソッドが実行され(注5) 2-2. 次にperform()メソッドが実行される 3. 各ビューはapp/viewディレクトリ以下で定義されていて,preforward()メソッドが実行される 4. テンプレートファイルはtemplate/jaディレクトリ以下に置かれる

(注5)実際にはprepare()メソッドの前に,認証を行うためのauthenticate()メソッドが呼び出されますので,これは必要に応じて各アクションで定義してください.

indexアクションを変更

次に,既存のアクションに幾つかの変更を行ってみます.まず,テンプレートにダイナミックな値を表示させるためのテンプレート変数の設定です.

Ethnaでテンプレートに変数を設定するには,Ethna_ActionFormオブジェクトを経由します.具体的には,アクションクラス(app/action/Index.php)に以下のように記述します.

function perform() {
    $this->af->setApp('foo', sprintf('<s>%d</s>', time()));
    return 'index';
}

そして,テンプレートファイルで{$app.[キー]}のように記述すると,setApp()メソッド呼び出し時に第1引数で指定したキーで値を参照することができます.ここではtemplate/ja/index.tplに以下のように記述してみます.

...(省略)...
<div id="main">
 <h2>Index Page</h2>
 <p>hello, world!</p>
 <p>{$app.foo}</p>
</div>
...(省略)...

すると,fig.2のように,setApp()メソッドの第2引数で指定した値が表示されていると思います.

画面を見ていただくとお分かりになるかと思いますが,setApp()メソッドで指定した値は常にHTMLエスケープされた状態で表示されます.これはXSSを防止するためにデフォルトの振舞いとしてこのようになっているのですが,時にはHTMLタグをエスケープせずに表示したくなることもあるかと思います.こういう場合はsetAppNE()(注6)メソッドを利用します.インターフェースはsetApp()と同様で下記のようになります.

$this->af->setAppNE('foo', '<s>文字列</s>');

この場合,設定された値はテンプレートファイルで{$app_ne.[キー名]}で参照することができます.ですので,テンプレートファイルに

{$app_ne.foo}

と記述するとfig.3のような結果が得られます.このように,Ethnaではテンプレートに動的に表示する値を全てEthna_ActionFormオブジェクトを通じて表示させる仕組みになっています.こうしておくことで,前述のように全ての値を一括してHTMLエスケープしたり,第4章で試しているように値を全てJSONエンコードして出力する,といったことも可能になります.

(注6)お分かりかとは思いますが,setAppNE()のNEはNo Escapeの略です.

なお,同様の処理はビュークラスでも可能です.app/view/Index.phpで下記のように記述すると,やはりテンプレートで({$app.bar}のような形で)値を参照することができると思います.

function preforward() {
    $this->af->setApp('bar', 'ビューでも!');
}

これらの動的な値の設定をアクションクラスで行うかビュークラスで行うかはポリシーにもよりますが,例えばindexというビューは別のアクションからも参照される可能性があります(例えば'test'というアクションを追加して,そのperform()メソッドが'index'という文字列を返した場合などです).この場合もFlare_View_Index::preforward()メソッドは呼び出されますので,筆者は「ビュー(あるいはテンプレート)と密接に関連づいている値はビュークラスで表示」という緩やかなルールに基づいて値を設定する場所を使い分けています.

新規アクションを追加

それでは次に新しいアクションを追加してみます.新規にアクションを追加する場合にも,アプリケーションスケルトンを利用したときと同様にethnaコマンドを利用します.

まずアクションを追加したいアプリケーションのディレクトリ(ここでは/tmp/flare以下です.このディレクトリ以下ならばどのディレクトリがカレントディレクトリでもかまいません)に移動し,ethna add-actionコマンドを実行します.

$ cd /tmp/flare
$ ethna add-action index_test

add-actionに続けて追加したいアクション名を指定します.筆者はしばしばアンダースコア("_")でアクションの階層を分けています.特に問題が無い場合は

file generated [skel.action.php -> /tmp/flare/app/action/Index/Test.php]
action script(s) successfully created [/tmp/flare/app/action/Index/Test.php]

という表示がされてapp/action/Index/Test.phpというファイルが生成されます.なお,デフォルト状態の動作ではアンダースコアがそのままアクションのディレクトリ階層になります.このファイルの中身はクラス名以外はほぼ全て初期状態のIndex.phpと同じもので,それ以外の違いはperform()メソッドの戻り値が'index_test'になっているだけです.

それでは早速ブラウザからこのアクションを実行させて見ましょう.指定されたアクションを実行させるには,

http://[サーバ名]/[パス名]/flare/index.php?action_index_test=true

とします.つまり,「action_[アクション名]=[何らかの値]」というフォームパラメータを送信することで,指定されたアクションが実行される,という構造になっています.

上記のURLにアクセスすると,間違いなく

Flare[22757](WARNING): Smarty._fetch_resource_info(...): [PHP] E_USER_WARNING: Smarty error: unable to read resource: "index/test.tpl" in ...

というエラーメッセージが表示されたかと思います.これは読んで字のごとくテンプレートファイルが存在しないためです.そこで,引き続きethnaコマンドでビュークラスとテンプレートファイルを作成してみます.

$ ethna add-view -t index_test
file generated [skel.view.php -> /tmp/flare/app/view/Index/Test.php]
view script(s) successfully created [/tmp/flare/app/view/Index/Test.php]
file generated [skel.template.tpl -> /tmp/flare/template/ja/index/test.tpl]
template file(s) successfully created [/tmp/flare/template/ja/index/test.tpl]

これでビュークラスとテンプレートファイルが作成されましたので,今度は無事にアクセスできると思います.ビュークラスのみを作成したい場合は-tオプションなしでethna add-viewを実行し,逆にテンプレートのみを作成したい場合はethna add-templateコマンドを利用します.

なお,ethnaコマンドによって作成されるファイルを変更したい場合は,skelディレクトリ以下にある各ファイルを適宜変更することで対応することができます.

フォームパラメータの処理

入門編の最終節として,Ethnaでフォームパラメータを扱ってみます.一通りの動作を,ということで上記のindexアクションとindex_testアクションを利用して,ユーザからメールアドレスとパスワードを入力してもらって,それをデータベースに格納する,というアプリケーションをMySQLを利用して作成してみます.

まずindex_testアクションで受け取るフォームパラメータを定義します.app/action/Index/Test.phpに

class Flare_Form_IndexTest extends Ethna_ActionForm {
    var $form = array(
        'mailaddress' => array(
            'name'      => 'メールアドレス',
            'required'  => true,
            'max'       => 255,
            'filter'    => FILTER_HW,
            'custom'    => 'checkMailaddress',
            'form_type' => FORM_TYPE_TEXT,
            'type'      => VAR_TYPE_STRING,
        ),
        'password' => array(
            'name'      => 'パスワード',
            'required'  => true,
            'max'       => 255,
            'form_type' => FORM_TYPE_PASSWORD,
            'type'      => VAR_TYPE_STRING,
        ),
        'password_conf' => array(
            'name'      => 'パスワード(確認)',
            'required'  => true,
            'max'       => 255,
            'custom'    => 'checkPassword',
            'form_type' => FORM_TYPE_PASSWORD,
            'type'      => VAR_TYPE_STRING,
        ),
    );
}

という形で,受け取るフォームパラメータごとに,その表示名,必須かどうか,最大文字数,フィルタ(文字列変換),カスタムチェックメソッド,フォーム種別(テキスト,パスワード,チェックボックス,セレクトボックスなど),受け取る値の型を指定しています.なお,type属性以外は全て省略可能です.このように記述することで,index_testアクションが受け取るべき値を定義することができます.また,カスタムチェックメソッドとして,確認用パスワードが一致しているかどうかをチェックするcheckPassword()メソッドをFlare_Form_IndexTestクラスに追加しておきます(もう1つのカスタムチェックメソッドのcheckMailaddress()はEthna組み込みで提供されているため,定義する必要はありません).

function checkPassword($name) {
    if ($this->form_vars['password'] == "" || $this->form_vars[$name] == "") {
        return;
    }
    if ($this->form_vars['password'] == $this->form_vars[$name]) {
        return;
    }
    $this->ae->add($name, "{form}が一致しません");
}

引き続いて,index_testアクションのprepare()メソッドにユーザから送信されたフォームパラメータが上記の定義にマッチしているかどうかをチェックする処理を記述します.これは非常に簡単で,

class Flare_Action_IndexTest extends Ethna_ActionClass {
    function prepare() {
        if ($this->af->validate() > 0) {
            return 'index';
        }
        return null;
    }
    // ...(省略)...
}

という形で実質3行を追加するだけです.この3行は「validate()で,ユーザから送信されたフォームパラメータが定義にマッチしなかった場合(エラーが1つ以上あった場合)はperform()を行わずに'index'を表示させる」ということを意味しています.

今度はindexアクションで表示されるテンプレートにHTMLフォームを記述していきます.Ethnaには上記のフォームパラメータ定義から<input>タグを生成する機能が実験的に追加されていますのでこちらを利用してみます.

まずindexのビュークラス(app/view/Index.php)に,「index_testのアクションフォームクラスをヘルパーとして利用する」ということを下記の1行で教えてあげます(indexのアクションフォームクラスにフォームパラメータ定義が記述されている場合はこの記述は不要です).

class Flare_View_Index extends Ethna_ViewClass {
    var $helper_action_form = array(
        'index_test' => null,
    );
    // ...(省略)...
}

次に,tempalte/ja/index.tplに以下のように記述してみます.

<h2>Sign Up</h2>
<p>
 {form ethna_action="index_test"}
  <dl>
   <dt>メールアドレス</dt><dd>{form_input name="mailaddress"}</dd>
  </dl>
  <dl>
   <dt>パスワード</dt><dd>{form_input name="password"}</dd>
  </dl>
  <dl>
   <dt>パスワード(確認)</dt><dd>{form_input name="password_conf"}</dd>
  </dl>
  <input type="submit" value="送信" />
 {/form}
</p>

まず{form ethna_action="index_test"}...{/form}というのは,「送信時にindex_testをアクションとして実行する<form>タグを出力する」というタグをお手軽に出力するためのSmartyのプラグイン関数です.また,{form_input}は上記のフォームパラメータ定義に基づいて<input>タグを出力してくれるSmartyプラグイン関数です(注7).この段階で,http://[サーバ名]/[パス名]/flare/にアクセスするとfig.3のような画面が表示されると思います.

(注7)2.1.2現在では実験段階で,textやパスワードなど一部のタイプのフォームタグにのみ対応しています.

さらにここで「送信」ボタンを押すと,何も起こらずに再度同じ画面が表示されると思います.実際には内側で,index_testアクションのprepare()メソッドが実行され,必須指定であるメールアドレスがなどが入力されていないのでエラーとなり,再度indexのビューが表示される,という処理が行われています.ただ,これではなんのことかさっぱり分からないので,エラーメッセージを表示させてみます.

{if count($errors)}
<ul class="error">
 {foreach from=$errors item=error}
  <li>{$error}</li>
  {/foreach}
</ul>
{/if}

という内容をテンプレートファイル(template/ja/index.tpl)に追加してから再度「送信」ボタンを押すと「メールアドレスを入力してください」といったメッセージが表示されるはずです.

さて,以上で準備は完了ですので,いよいよロジック部分の実装を行います.まずデータベースにテーブルを作成しておきます.ここではlocalhostのtestというデータベースにuserという名前でテーブルを作成しておきます.テーブルスキーマは簡単に以下のようなものにします.

CREATE TABLE `user` (
    `user_id` int(11) NOT NULL auto_increment,
    `mailaddress` varchar(255) NOT NULL default '',
    `password` varchar(255) NOT NULL default '',
    PRIMARY KEY  (`user_id`),
    UNIQUE KEY `mailaddress` (`mailaddress`)
) TYPE=MyISAM;

データベースの準備が出来たら,etc/flare-ini.phpにDSN(Data Source Name)の定義を記述します.ここでは単純に以下のようなもので十分です.

'dsn' => 'mysql://[user]:[pass]@unix+localhost/test',

以上でデータベースと接続の準備は完了です.

次に,Ethna組み込みのORマッピングクラスを作成します.これも以下のようにethnaコマンドで作成できます.

$ ethna add-app-object user

add-app-objectに続けて,対応させたいテーブル名(ここでは「user」)を指定します.するとapp/Flare_User.phpというファイルが作成されると思います.ついでにコントローラクラス(app/Flare_Controller.php)でこのファイルをincludeさせておくために以下のような1行を追加します.

include_once('Flare_User.php');

最後にindex_testアクションクラスに,指定されたメールアドレスとパスワードでデータベースにレコードを追加する処理を記述します.これも単純で,以下のように3行を追加するだけです.

function perform() {
    $user =& new Flare_User($this->backend);
    $user->importForm();
    $user->add();
    return 'index_test';
}

ここまで書けたら,再度indexアクションを実行させて,メールアドレスとパスワードを正しく入力して「送信」ボタンを押します.すると下記のようにデータベースにレコードが追加されていると思います.

mysql> select * from user;
+---------+-----------------+----------+
| user_id | mailaddress     | password |
+---------+-----------------+----------+
|       1 | fujimoto@php.net | test     |
+---------+-----------------+----------+

最後に,プレインテキストで保存されてしまっているパスワードのエンコード処理と,既に登録されているメールアドレスが入力された場合にもエラーメッセージを表示させるように改善しておきます.この場合には,上記の$user->add();としている部分を下記のように書き換えます.

$user->set('password', md5($this->af->get('password')));
$r = $user->add();
if (Ethna::isError($r)) {
    if ($r->getCode() == E_APP_DUPENT) {
        $key = $r->getUserInfo(0);
        $this->ae->add($key, "入力された{form}は既に登録されています", E_APP_DUPENT);
    } else {
        $this->ae->add(null, "内部エラーです", E_SAMPLE_INTERNAL);
    }
    return 'index';
}

以上,極々簡単ではありますが,Ethnaの基本的な動作をご紹介できたかと思います.詳細については,付属CD-ROMに収録されておりますサンプルコードをご覧ください.

2章 Ethnaの内側

グリー株式会社 藤本 真樹 <fujimoto@gree.co.jp>

Ethnaの基本構造

第1章ではEthna入門として,Ethnaのとても簡単なアプリケーションを作成してみましたが,Ethnaにはその他にも多くの機能があります.ここでは,それらの機能のご紹介と,Ethnaの特徴の1つであるゲートウェイを利用したXMLRPCベースのアプリケーションを作成してみます.

Ethnaの周辺クラス

上記のEthnaコア部分以外にも,図に幾つか挙げているようにより楽にアプリケーションを作成するための周辺クラスが用意されています.ただ,これらはあくまで周辺クラスですので,純粋にEthnaをウェブアプリケーションのフレームワークとして利用する場合にはこれらのクラスを利用する必要は全くありません.ただ,うまく使えば便利なものも幾つかありますのでここで簡単にご紹介させていただきます.

Ethna_AppObject

Ethna_AppObjectは第1章にも登場したORマッピングオブジェクトです.現在MySQLにしか対応していませんが(そして幾つか足りない機能もありますが),データベースからのテーブル定義自動取得や,CRUDの一通りのメソッドをサポートしています.ですので,下記のような基本的な処理は問題なく行うことができます.

class Flare_User extends Ethna_AppObject {
}
$user =& new Flare_User($this->backend);
$user->set('mailaddress', 'fujimoto@php.net');
// Create
$user->add();
$id = $user->getId();
$user =& new Flare_User($this->backend, 'user_id', $id);
// Read
$mailaddress = $user->get('mailaddress');
$user->set('password', 'test');
// Update
$user->update();
// Remove
$user->remove();

Ethna_CacheManager

Ethna_CacheManagerはGREE(注1)で使われているコードを多少Ethna風にアレンジしてバックポートしてきたものです.同じインターフェースで,キャッシュストレージを入れ替えられるのが特徴です(Ethnaにはファイルシステム版しかありませんが,GREEでは他にmemcached版とmysql版が存在していますので,順次Ethnaにもバックポートしていきたいと思います.

使い方は至ってシンプルで基本的にはset()とget()するだけです.

$cache_manager =& Ethna_CacheManager::getInstance('localfile');
$cache_manager->setNamespace('flare');
$cache = $cache_manager->get('key', $lifetime_in_sec);
if (PEAR::isError($cache)) {
    $cache = $this->somelogic();
    $cache_manager->set($cache);
}

get()するときにキャッシュのライフタイムを指定するのがちょっと変な感じですが,ウェブアプリケーションだとこの方がマッチするかな,と思ってあえてこのようなインターフェースにしてみました.

(注1)http://gree.jp/

Ethna_Util

Ethna_Utilクラスには,筆者がアプリケーションを作成していて必要になった雑多なユーティリティ関数のいくつかが集められています.ここではそのうちの幾つかをご紹介させていただきます.

Ethna_Util::getRandom()はランダムなハッシュ値を生成します(多少いい加減で,そんなに速くないです).

var_dump(Ethna_Util::getRandom());
->
string(64) "5b73...232f"

Ethna_Util::get2dArray()は1次元の配列を2次元にして返します.

var_dump(Ethna_Util::get2dArray(range(1, 6), 3, 0));
->
array(2) {
  [0]=>
  array(3) {
    [0]=> int(1), [1]=> int(2), [2]=> int(3),
  }
  [1]=>
  array(3) {
    [0]=> int(4), [1]=> int(5), [2]=> int(6),
  }
}

Ethna_Util::explodeCSV()はCSV形式の文字列を配列に変換します.fgetcsv()がファイルポインタからの入力しか受け付けないので作ってみました.

というように十数個の関数が準備されています.

また,このほかにもログを出力するEthna_LoggerクラスとEthna_LogWriterクラス,Smartyプラグインなどが準備されています.

EthnaでXMLRPCアプリケーション

ゲートウェイシステム

ゲートウェイシステムとは,単純にいうとコントローラをゲートウェイとして利用して,通常のブラウザからのリクエスト,コマンドラインからの実行,XMLRPCなど(現バージョンでサポートされているのはこの3つです)のリクエストを全て「アクション」として処理するための機構です(fig.1).つまりEthnaでは,ブラウザからのリクエストもコマンドラインプログラムも第1章で記述したアクションと同様に記述することができます.

ここではこのゲートウェイシステムの例として,第1章で作成したアプリケーションを拡張してユーザ認証を行うXMLRPCのインターフェースを追加してみます.

XMLRPCアプリケーションの作成

Ethnaでは,XMLRPCのメソッド1つを1つのアクションに対応させていて,アクション名がそのままメソッド名になります.また,アクションフォームクラスに記述されたフォームパラメータ定義が受け取るべき引数のリストになります.ということで早速XMLRPCのメソッドを追加してみます.

XMLRPCのメソッドを追加する場合も同様にethnaコマンドを利用します.ここではloginというメソッドを追加します.

$ ethna add-action-xmlrpc login
file generated [skel.action_xmlrpc.php -> /tmp/flare/app/action_xmlrpc/Login.php]
action script(s) successfully created [/tmp/flare/app/action_xmlrpc/Login.php]

これでapp/action_xmlrpc/Login.phpというファイルが作成され,loginという名前の何も引数を取らないXMLRPCメソッドが追加されたことになります.試しにPEARのXML_RPCパッケージを利用してこのメソッドを利用してみます.クライアント側のコードは以下のようなものになります.

<?php
include_once('XML/RPC.php');
$client = new XML_RPC_Client('/~masaki-f/flare/xmlrpc.php', '[サーバ名]');
$client->setDebug(true);
$msg = new XML_RPC_Message('login', null);
$response = $client->send($msg);
if (!$response) {
    exit();
}
if (!$response->faultCode()) {
    $val = $response->value();
    $data = XML_RPC_decode($val);
    var_dump($data);
} else {
    die($response->faultString()."\n");
}
?>

このとき,呼び出すべきエントリポイントが第1章で利用したindex.phpではなく/~masaki-f/flare/xmlrpc.phpになっていることにご注意ください.この実行結果は下記のようなものになります.

pre>---GOT---

HTTP/1.1 200 OK
Date: Thu, 08 Jun 2006 13:42:47 GMT
...(省略)...
---END---</pre>
string(5) "login"

つまり,「login」という値が文字列として返されます.これは初期状態のLogin.phpのperform()メソッドが

function perform() {
    return 'login';
}

となっているためです.このようにXMLRPCを扱うアクションではprepare()あるいはperform()メソッドの戻り値がそのままメソッドとしての戻り値となります.これでアクションがXMLRPCメソッドとして呼び出される感じをつかめていただけると思います.

引き続いてアクションフォームクラスのパラメータを定義します.これも第1章で記述したものと全く同様で,下記のようなものになります.

var $form = array(
    'mailaddress' => array(
        'name'      => 'メールアドレス',
        ...(省略)...
        'type'      => VAR_TYPE_STRING,
    ),
    'password' => array(
        'name'      => 'パスワード',
        ...(省略)...
        'type'      => VAR_TYPE_STRING,
    ),
);

次に,アクションクラスのprepare()メソッドで引数のチェックを行います.

function prepare() {
    if ($this->af->validate() > 0) {
        return -1;
    }
    return null;
}

ここでは,validate()メソッドでエラーになった場合には問答無用で-1を戻り値として返しています.XMLRPCゲートウェイ特有の機能として,Ethnaのエラーオブジェクトを返すとXMLRPCのfaultオブジェクトとして返される,という機能を利用することもできます.この場合,Ethna_Error(あるいはPEAR_Error)オブジェクトのエラーメッセージとエラーコードがそれぞれfaultStringとfaultStringに対応します.

また,あらかじめ第1章で作成したFlare_Userクラスに認証を行うauth()メソッドを追加しておきます.これも下記のような非常に単純なもので問題ないと思います(とりあえずサンプルとしては:).

function auth($password) {
    if ($this->isValid() == false) {
        return Ethna::raiseNotice('認証エラー', E_GENERAL);
    }
    if (strcasecmp($this->get('password'), md5($password)) != 0) {
        return Ethna::raiseNotice('認証エラー', E_GENERAL);
    }
    return 0;
}

以上で準備は完了ですので,最後にアクションクラスのperform()メソッドにロジックを記述します.これも数行で済んでしまいます.

function perform() {
    $user =& new Flare_User($this->backend, 'mailaddress', $this->af->get('mailaddress'));
    $r = $user->auth($this->af->get('password'));
    if (Ethna::isError($r)) {
        return -1;
    }
    return 0;
}

以上でサーバ側の準備は完了です.最後にクライアントのコードを下記のように引数を取るように書き直します.

...(省略)...
$client = new XML_RPC_Client('/~masaki-f/flare/xmlrpc.php', '[サーバ名]');
$params = array(
    new XML_RPC_Value($_SERVER['argv'][1], 'string'),
    new XML_RPC_Value($_SERVER['argv'][2], 'string'),
);
$msg = new XML_RPC_Message('login', $params);
$response = $client->send($msg);
...(省略)...

そして

$ php xmlrpc_client.php [メールアドレス] [パスワード]

とすると,入力された内容によって0または-1が返されると思います.以上がXMLRPCでの例ですが,今後同様にSOAPなどにも対応していきたいと考えています(最近はRESTがメインになっているので需要は薄れているような気はしますが).

3章 Ethnaを拡張

個々一番 <cocoiti@comio.info>

はじめに

Ethnaは,各種フレームワークとしては当たり前のオブジェクト指向に基づいた実装が行われており,また,設計思想として,「フレームワークだからという理由でできないことはなくす」という思想があるため,非常に柔軟にEthna本体の動作のカスタマイズを行うことができます.

たとえば,すでにフロントコントローラに基づく設計が行われているアプリケーションからの移行を行いたい時に,「既存のプロジェクトと一部共存しながら進めていくために,アクションの引数は変えたくない」といった要件がある場合に,次項で紹介するような方法を取れば容易にその要件を満たすことができます.

また,当然ながら個別のカスタマイズを加えた場合も,Ethnaの親クラスを継承するだけなので,Ethnaのバージョンが変わっても本体のバージョンアップのみですむことがほとんどです.

PHPのような柔軟性のある言語,悪く言ってしまえばいい加減な言語とより親和性高く,芯はしっかりしていながらも,柔軟性の高いフレームワークというのが,筆者がEthnaに持っている印象です.

そこで本稿では,Ethnaの動作に変更を加えて,好みの動作を実現する方法を実例を交えてご紹介します.

アクション定義省略時の命名規則を変更する

Ethnaでは,各アクションとファイルの関係を定義することができますが,これを省略することもできます.筆者の周りのEthnaユーザを見た限りでは,アクションとファイルの関係を定義するユーザは少ないようです.(注1)

アクションの定義を省略した時には,以下の4種類のファイルパスとクラス名が自動的に決定されます.

  • アクションクラスが定義されたファイルのパス名
  • アクションクラス名
  • アクションフォームクラスが定義されたファイルのパス名
  • アクションフォームクラス名

デフォルトではアクションクラスと,アクションフォームクラスのファイルパスは共通で,

$r = preg_replace('/_(.)/e', "'/' . strtoupper('\$1')", ucfirst($action_name)) . '.' . $this->getExt('php');

つまり"foo_bar_hoge" -> "Foo/Bar/Hoge.php"となります.

アクションクラス名は,

$gateway_prefix = $this->_getGatewayPrefix($gateway);

$postfix = preg_replace('/_(.)/e', "strtoupper('\$1')", ucfirst($action_name));
$r = sprintf("%s_%sAction_%s", $this->getAppId(), $gateway_prefix ? $gateway_prefix . "_" : "", $postfix);
$this->logger->log(LOG_DEBUG, "default action class [%s]", $r);
(アクションフォームクラスは,上記"_Action_"の部分が"_Form_"になります)

つまり,プロジェクト名が"Sample"で,アクションが"foo_bar_hoge"の場合は,"Sample_Action_Foo_Bar_Hoge"と"Sample_Form_Foo_Bar_Hoge"となります.Gatewayが指定されている場合には,"Sample_{GateWayの名前}Action_Foo_Bar_Hoge"となります.

上記の設定を変更するには,それぞれEthna_Controller.phpにある次のメソッドをオーバライドすることで,変更が可能です.(注2)

アクションクラスが定義されたファイルのパス名

   Ethna_Controller::getDefaultActionPath($action_name)

アクションクラス名

   Ethna_Controller::getDefaultActionClass($action_name, $gateway)

アクションクラス名からアクション名を取得する(注3)

   actionClassToName($class_name)

アクションフォームクラスが定義されたファイルのパス名

   Ethna_Controller::getDefaultFormPath($action_name)

アクションフォームクラス名

   Ethna_Controller::getDefaultFormClass($action_name, $gateway)

アクションフォームクラス名からアクション名を取得する(注3)

   Ethna_Controller::actionFormToName($class_name)

例えば,"foo_bar_hoge"というアクションに対応するファイル名を"foo_bar_hoge.php"にして,アクションクラス名を "foo_bar_hoge_action",アクションフォームクラス名を"foo_bar_hoge_form"としたい場合でも,Ethna_Controller::getDefaultActionPath()など幾つかのメソッドをオーバーライドすることで対応することができます(詳細は付属CD-ROMのサンプルコード(Namingconvention\1.txt)をご覧下さい).

(注1) アクション追加するごとに,コントローラを書き換えるのも大変ですし. (注2) パスの変更はお勧めはしていません,基本的には現状で不自由なことはないと思います. (注3) 変更しなくても動作しますが,Ethna Infoの表示に影響が出る場合があります.

アクション名の決定方法を変更する

デフォルトでは,アクション名はフォーム値を利用して以下のように決定されます.

1. フォーム名のうち,名前が"action_"で始まり,かつフォーム値が空ではないものを探します 2. 1.に該当するフォーム名が見つかった場合,そこから先頭の"action_"を除いた部分をアクション名とします

アクション名の決定方法は,アプリケーションによっては"index.php?act=foo"等さまざまな方法があると思いますので,Ethnaではこの方法をアプリケーションで自由に決定することができるようになっています.

具体的には,Ethna_Controller::_getActionName_Form()をオーバーライドするだけです.例えばアクション名をindex.php?act=fooというようなリクエストで"foo"の部分をアクション名としたい場合は次のように記述します.

function _getActionName_Form()
{
    if (array_key_exists('act', $_REQUEST) == false) {
        return null;
    }
    return $_REQUEST['act'];
}

デフォルトの動作については,Ethnaのウェブページ(注4)をご覧ください.

デフォルトのaction_で始まるアクション名の指定のしかたは,あまりなじみのないものに思えるかもしれませんが,これは非常にメリットのある方法です.ひとつのFrom内で,Action名を指定する場合は,先ほどのような,変更を加えた場合には,

<input type="hidden" name="act" value="index">

という指定になりますが,ひとつのForm中でボタンによって,actionを変えたい場合には,ボタンに表示される文字が,Valueのため,Javascriptなどでhiddenの値などを書き換える方法が一般的ですが,デフォルトの"action_"から始まる,name属性でアクションが決まる方法ならば,以下のように記述ができます.

<input type="submit" name="action_write_do" value="書き込み">
<input type="submit" name="action_index" value="戻る">

(注4) http://ethna.jp/doc/__filesource/fsource_Ethna__classEthna_Controller.php.html#a839

複数のエントリポイントを作成する

Ethnaで作ったアプリケーションを公開するときには,index.phpを公開領域に,シンボリックリンクを張っていると思います.ですが,実際のアプリケーションは,たとえば,管理画面とユーザ画面など,一つのアプリケーションで複数の入り口が必要になることがある思います.この入り口をエントリポイントと呼びます.ここでは,そのエントリポイントを/index.phpと/admin/index.phpで2つ作成する例をあげます.

方法は単純で,単純にエントリポイントを2つ置くだけです.具体的にはまず/index.phpとして以下のようなスクリプトを作成します.

<?php
include_once('/tmp/sample/app/Sample_Controller.php');
Sample_Controller::main('Sample_Controller', 'index');
?>

次に/admin/index.phpに以下のようなスクリプトを作成します.

<?php
include_once('/tmp/sample/app/Sample_Controller.php');
Sample_Controller::main('Sample_Controller', 'admin_index');
?>

以上で完了です.非常に簡単です.

index.phpは,初期状態で,アクションindexを実行し,/admin/index.phpはアクションadmin_indexを実行します.

ただし,この状態では各エントリポイントは実行するアクションを制限していませんので,どちらのエントリポイントでも同じアクションを実行することが出来てしまいます.つまり

/index.php?action_login=true

/admin/index.php?action_login=true

も同じアクションと見なされる,ということになります.これは,想定外の動作を引き起こす原因となる可能性もあります.

したがって,Ethnaでは各エントリポイント毎に実行可能なアクションを制限することも可能です.詳細は次の項目の「複数のエントリポイントを作成する」をご参照ください.

[参考] Ethna_Controller::main()メソッド

Ethna_Controller::main($class_name, $action_name = "", $fallback_action_name = "")メソッドは最低1つ,最大で3つの引数をとります.

$class_name

   実行するコントローラのクラス名を指定します

$action_name(省略可)

   クライアントからアクション名の指定がなかった場合に実行するアクション名を指定します(デフォルトアクション)
   また,ここにアクション名の配列を指定すると,そこに指定されたアクション名以外は未定義として扱われます(実行するアクションを制限することができます)

$fallback_action_name(省略可)

   クライアントから指定されたアクション名が未定義であった場合に実行されるアクション名を指定します

エントリポイント毎に実行可能なアクションを制限する

<?php
include_once('/tmp/sample/app/Sample_Controller.php');
Sample_Controller::main('Sample_Controller', 'index');
?>

エントリポイントが1つの場合は特に問題はありませんが,エントリポイントが複数になってくると,この動きはあまり好ましいものではなくなってきます.

こうした場合,Ethna_Controller::main()メソッドの第2引数に配列を指定することで,実行するアクション名を限定させることができます.具体的には

Sample_Controller::main('Sample_Controller', array(
 'index',
 'user_login',
 'user_login_do',
));

のように記述します.この場合は,配列の最初の要素'index'がデフォルトのアクション名になり,また,'index','login','login_do'以外のアクションがリクエストされた場合でもそれらは未定義として扱われます.

ただ,実装したアクションが増えてくるとエントリポイントにアクションを記述するのも面倒になってきます.

// 非常に煩雑なことになっている例
Sample_Controller::main('Sample_Controller', array(
 'index',
 'user_login',
 'user_login_do',
 'user_add',
 'user_add_do',
 'user_modify',
 'user_modify_do',
 'user_remove',
 'user_remove_do',
 'user_logout',
 ...(省略)...
));

こんな場合は,アスタリスクを使用することで楽をすることができます.

// user_で始まるアクションは全て受け付ける
Sample_Controller::main('Sample_Controller', array(
 'index',
 'user_*',
));

アスタリスクはアクション名の末尾でのみ使用可能です.

なお,未定義のアクションが指定された場合に特定のアクションを実行させる方法については下記をご覧ください.

未定義のアクションが指定された場合に特定のアクションを実行する

想定外,あるいは許可されていないアクションがリクエストされた場合,通常はアクションが未定義としてエラーになります.しかしながら,未定義のアクションが指定された場合にも特定のアクションを実行したくなることもあるかと思います(エラー用アクションや,問答無用でトップページを表示,等).

こういった場合には,Ethna_Controller::main()メソッドの第3引数($fallback_action_name)に,アクション未定義の場合に実行したいアクション名を指定することで処理を実現することができます.具体的には,

Sample_Controller::main('Sample_Controller', array(
 'index',
 'login_*',
), 'undef');

のように記述します.この場合は,第2引数に指定したアクション以外が指定されると'undef'アクションが実行されます(undefアクションが定義されていない場合はエラーとなります).

アクションスクリプトの配置ディレクトリを変更する

デフォルトでは,アクションクラス,あるいはアクションフォームクラスが定義されたスクリプト(アクションスクリプト)はapp/actionに置かれます.このディレクトリはコントローラの$directoryメンバ変数を変更することで任意のディレクトリに設定することができるので好みに応じて変更してください.

例えば,アクションスクリプトのディレクトリをデフォルトのapp/actionからactionに変更する場合は以下のように記述します.

var $directory = array(
-    'action'        => 'app/action',
+    'action'        => 'action',
     'action_xmlrpc'  => 'app/action_xmlrpc',
     'app'            => 'app',
     'bin'            => 'bin',
     'etc'            => 'etc',
     'filter'         => 'app/filter',
     'locale'         => 'locale',
     'log'            => 'log',
     'plugins'        => array(),
     'template'       => 'template',
     'template_c'     => 'tmp',
     'tmp'            => 'tmp',
     'view'           => 'app/view',
);

なお,このメンバ変数を相対パスで記述した場合は「アプリケーションベースディレクトリからの相対パス」として扱われます.もちろん絶対パス('/'で始まるパス)で記述することも可能です.

使用するクラスを変更する

デフォルトで用意されているクラスでも十分アプリケーションを作成することはできますが,DBクラスをEthna_DBクラスを継承した独自のものに変更したい場合などがあります.この場合は,コントローラの$classメンバ変数を変更することで任意のクラスに変更できます.

require_once './Ethna/class/DB/Ethna_DB_ADOdb.php';
...(省略)...
var $class = array(
     'class'         => 'Ethna_ClassFactory',
     'backend'       => 'Ethna_Backend',
     'config'        => 'Ethna_Config',
-    'db'           => 'Ethna_DB_PEAR',
+    'db'           => 'Ethna_DB_ADOdb',
     'error'         => 'Ethna_ActionError',
     'form'          => 'Ethna_ActionForm',
     'i18n'          => 'Ethna_I18N',
     'logger'        => 'Ethna_Logger',
     'session'       => 'Ethna_Session',
     'sql'           => 'Ethna_AppSQL',
     'view'          => 'Ethna_ViewClass',
);

ethnaコマンドの追加

Ethna 2.1.0からは,ethnaコマンドが実装されました.ethnaコマンドは各種のコードジェネレータなどをサポートしています.機能のサポートは,有志の手によって日々追加されており,今後も非常に多くの機能が追加されていくことになる予定です.

有志の手によって追加されることが多いのは,ethnaコマンドが機能を各ファイルに分割されたハンドルを持っておりEthna_Handleクラスを継承し,ハンドルを追加することによりEthna本体にそれほど手を加えなくても実装が容易に行えるという理由があります.

開発作業をしていて,何度も同じような作業を行うときは,ハンドルを作成することによって省力化を図ることができます.得に,複数人で開発を行っている時は大きな力を発揮します.

ethnaコマンドの追加には,(Ethnaインストールディレクトリ)/Ethna/Class/Handle/にEthna_Handle_{コマンド名}.phpというEthna_Handleクラスを継承したクラスと同じ名前のPHPファイルを作成することにより特にEthna本体のスクリプトに手を加えることなく追加ができます.

Ethna_Handleクラスのメソッド

Ethna_Handleクラスの主なメソッドは次のようになっています.

getDescription

   戻り値としてコマンドの説明を返します.

perform

   実際の実行部分です.

usage

   戻り値としてethnaコマンドで利用する引数を返します.

コマンドの実装

基本的にコマンドの実装は,Ethna_Handleクラスを継承する以外は,通常のコンソールアプリケーションと同じようなコーディングとなります.簡単な例ということで,Ethna Infoと同じように現在のEthnaのバージョン情報を表示するコマンドを作成したいと思います(注5).

class Ethna_Handle_Version extends Ethna_Handle {
  function getDescription() {
     return "view ethna version info:\n    {$this->id}\n";
  }
  function perform() {
    $r = $this->_validateArgList();
    if (Ethna::isError($r)) {
      return $r;
    }
    printf("Ethna Version => %s\n", ETHNA_VERSION);
    printf("Ethna Install Directory => %s\n", ETHNA_BASE);
    printf("PHP Version => %s\n", PHP_VERSION );
    return true;
  }
  function usage() {
    printf("usage:\nethna %s\n", $this->id);
  }
  function _validateArgList() {
    $arg_list = array();
    if (count($this->arg_list) != 0) {
      return Ethna::raiseError('too many argments', 'usage');
    }
    return true;
  }
}
?>

ethnaコマンドは,Ethna本体を読み込んだ後に実行されるので,Ethna本体の情報などの情報を比較的簡単に取得できます. 各プロジェクトの,コントローラを取得したい場合には,下記のようなコードを記述することで取得が可能です.

$app_dir = (プロジェクトのディレクトリ名)
$controller_class = Ethna_SkeltonGenerator::_discoverController($app_dir);
if (Ethna::isError($controller_class)) {
    return $controller_class;
}
$c =& new $controller_class;

(注5) ethnaでは,-vオプションを実行すればバージョンの取得を行うオプションが存在します.

フィルタチェインを実装する

Ethnaでは,フィルタチェイン(注6)を利用することによって,さまざまな入力や出力の変換処理やロギングが可能となります.

フィルタチェインは,Ethna_Filterクラスを継承したクラスを作成して,Ethna自身に登録することでいくつでも同時に実行することができます.

Ethna_Filterのメソッド

Ethna_Filterクラスの主なメソッドは次のようになっています.

preFilter

   すべての処理の前に行われます.アプリケーション全体にかかわる処理の変更を行いたい場合にはこちらに記述します.

preActionFilter

   アクションの実行前に処理が行われます.戻り値にアクション名を返すことにより,アクションの変更を行うことができます.たとえば,動作しているサイトでアクション名を変更し,古いアクション名が来た場合にはこのメソッドで,新しいアクション名に変えることや,メンテナンス画面へのアクションに書き換えてしまうことが可能です.

postActionFilter

   アクションの実行後, Viewの実行前に処理が行われます.特定のViewの切り替えなどに利用します.

postFilter

   すべての処理の終わりに使用します.

フィルタチェインの実装

Ethnaのプロジェクトを作成することで,自動作成される{プロジェクト作成箇所}/app/filter/{project name}_Filter_ExecutionTime.phpに,スクリプトの実行時間を図るフィルタの実装がありますが,今回はこれを参考に,magic_quotes_gpcがONの環境でも,magic_quotes_gpcがONであるのと同じ入力値にするように,不要なバックスラッシュを削除するようなスクリプトを以下に記述します.

まず,雛形となるファイルですが,{project name}_Filter_ExecutionTime.phpを使います. これをコピーして,名前を変更します.今回は,Sample_Filter_StripSlashes.phpと名前を変更します. ファイルを開いて,コメントとクラス名を変更し,実際のコードを記述します.

class Sample_Filter_StripSlashes extends Ethna_Filter {
  function preFilter() {
    $_POST = $this->RStripSlashes($_POST);
    $_GET = $this->RStripSlashes($_GET);
    $_COOKIE = $this->RStripSlashes($_COOKIE);
    $_REQUEST = $this->RStripSlashes($_REQUEST);
  }
  function RStripSlashes($data) {
    if (is_array($data)) {
      return array_map(array($this, 'RStripSlashes'), $data);
    } else {
      if (get_magic_quotes_gpc()) {
        $result = stripslashes($data);
      } else {
        $result = $data;
      }
      return $result;
    }
  }
}

コードの簡単な解説をすると,magic_quotes_gpcのON,OFFを見て,必要があれば再帰的にstripslashesで不要なバックスラッシュを削除します.

これを以下のように記述してEthnaのアプリケーションに登録を行います.

var $filter = array(
     'Sample_Filter_StripSlashes',
);

(注6)フィルタチェインの概念についてはhttp://ethna.jp/ethna-document-dev_guide-app-filterchain.htmlも参照してください.

エラーメッセージをカスタマイズする

Ethnaは,自前でバリデートクラスを持っており標準で,入力に対してのエラーメッセージを表示します.といっても,サイトのポリシーによってメッセージの変更の必要が生じますので,Ethnaでは,エラーメッセージのカスタマイズもそれほど煩雑なコードを記述しないでも変更できるようになっています.エラーメッセージを変更する場合には,各アクションフォームクラスで定義されているフォーム入力の値に次のような項目を設定します.

'data' => array(
    'name'           => 'データ',        // 表示名
    'required'       => true,            // 必須オプション(true/false)
    'min'            => null,            // 最小値
    'max'            => null,            // 最大値
    'regexp'         => null,            // 文字種指定(正規表現)
    'custom'         => null,            // メソッドによるチェック
    'filter'         => null,            // 入力値変換フィルタオプション
    'form_type'      => FORM_TYPE_TEXT,  // フォーム型
    'type'           => VAR_TYPE_INT,    // 入力値型
+   'required_error' => 'データの項目は入力必須です'
+   'type_error'     => 'データの項目に不正な文字列があります'
),

入力がされていない場合には,requiredの制限で,required_errorのエラーが発生し,「データの項目は入力必須です」というエラーメッセージが設定されます.数字意外の物が入力されている時は,typeの制限でtype_errorで「データの項目に不正な文字列があります」というエラーメッセージが設定されます.

エラーの種類

エラータイプにはには以下の5種類が存在し,それぞれ次のエラータイプと連携しています.

required_error E_FORM_REQUIRED

type_error E_FORM_WRONGTYPE_SCALAR

                E_FORM_WRONGTYPE_ARRAY
                E_FORM_WRONGTYPE_INT    
                E_FORM_WRONGTYPE_FLOAT
                E_FORM_WRONGTYPE_DATETIME
                E_FORM_WRONGTYPE_BOOLEAN

min_error E_FORM_MIN_INT

                E_FORM_MIN_FLOAT
                E_FORM_MIN_DATETIME
                E_FORM_MIN_FILE
                E_FORM_MIN_STRING

max_error E_FORM_MAX_INT

                E_FORM_MAX_FLOAT
                E_FORM_MAX_DATETIME
                E_FORM_MAX_FILE
                E_FORM_MAX_STRING

regexp_error E_FORM_REGEXP

キャッシュ制限を標準のものに戻す

Ethnaでは,session_cache_limiterで,「private, must-revalidate」に設定されているため,ブラウザのキャッシュが通常のPHPでセッションを使った場合の動作とやや違う場面があります.Ethnaで設定されているもののほうが,ブラウザがコンテンツキャッシュを行ってくれるので便利なことが多いのですが,nocacheに指定したい場合などには次の手順で修正することによって処理を変更することができます.

Ethna_Sessionクラスを継承し,Sample_Sessionを作成します.

class Sample_Session {
    function Ethna_SessionSample($appid, $save_dir, $logger) {
        parent::Ethna_Session($appid, $save_dir, $logger)
        session_cache_limiter('nocache');
    }
}

次にコントローラクラスを以下のように書き換えます.

+require_once 'Sample_Session';
...(省略)...
var $class = array(
...(省略)...
-    'session'        => 'Ethna_Session',
+    'session'        => 'Sample_Session',
     'sql'            => 'Ethna_AppSQL',
     'view'           => 'Ethna_ViewClass',
);

セッションハンドラをmemcachedに対応させる

Webサーバをスケールする場合には,セッションを通常のファイル管理ではなくセッションハンドラを上書きしてDBなどに書き込む方法がよくとられます.これを参考に,memcached(注7)に対応したセッションハンドラの実装をしてみることも簡単です(実装に つきましては付属CD-ROMのサンプルコード(Session\1.txt)をご覧下さい).

memcachedについての詳細や,PECLのインストール方法については,よくとまった資料を手に入れやすいので今回は省略します.セッションの細かい制御は,Ethna_Sessionにすでに設定されているので,コンストラクタである,Ethna_Session_Memcacheで,接続に必要な情報を取得し,セッションハンドラを設定するだけクラスはできあがるので,あとはコントローラのクラス名を置き換えるだけで,簡単に動作を書き換えることができます.

require_once 'Ethna_Session_Memcache';
...(省略)...
var $class = array(
...(省略)...
-    'session'        => 'Ethna_Session',
+    'session'        => 'Ethna_Session_Memcache',
     'sql'            => 'Ethna_AppSQL',
     'view'           => 'Ethna_ViewClass',
);

(注7) http://www.danga.com/memcached/及びhttp://pecl.php.net/package/memcache参照

まとめ

ここで紹介した以外にも,さまざまなカスタマイズができるようになっています.このような機能によって,Webアプリケーションの分野でいえばPHPだからできない,Ethnaだからできないというということはないといってもいい状態になっていると思います.また,次のバージョンアップでは,バリデータクラスやフィルタチェインがプラグイン化されて,より機能のカスタマイズがより行いやすくなる予定です.PHPとEthnaを是非この機会に使ってみてください.

4章 Ethnaで実践

グリー株式会社 藤本 真樹 <fujimoto@gree.co.jp>

EthnaでJSON

最後に実践編として,第1章,第2章で作成したアプリケーションに,JSONとprototype.jsのAjaxクラスを利用したメールアドレス入力時の自動チェック機能を追加してみます.

JSONビュークラスの作成

非常に悲しいことに(笑),バージョン2.1.2現在,EthnaにネイティブでAJAX(的な)アプリケーションの構築をサポートする機能はありません.とはいえ,ちょっとした拡張でAJAXアプリケーションも非常に作りやすくなります.その一例として,ここではアクションクラスでアクションフォームクラスに設定された値をJSONエンコードして,X-JSONヘッダとして出力させてみます.

これを実現するには幾つかの方法がありますが,もっともシンプルなのはJSON用のビュークラスを1つつくることかと思います.具体的には,Ethna_ViewClassをオーバーライドしたFlare_View_Jsonクラスを作り,そのforward()メソッドをオーバーライドしてHTMLを出力する代わりにX-JSONヘッダとJSONエンコードされたオブジェクトを出力すればよいわけです.forward()メソッドはEthna_ViewClassで定義されていて,デフォルトの動作では,ビューに対応するテンプレートファイルをSmartyオブジェクトを利用して出力します.

というわけで実際に実際にJSONビュークラスを作成してみます.まずethna add-viewコマンドを-tオプション無し(テンプレート生成なし)で実行します.

$ ethna add-view json
file generated [skel.view.php -> /tmp/flare/app/view/Json.php]
view script(s) successfully created [/tmp/flare/app/view/Json.php]

次にforward()メソッドをオーバーライドしてテンプレートファイルの代わりにJSONエンコードされたオブジェクトを出力するのですが,その前にJSONのエンコーダを準備しておく必要があります.PHPで利用可能なJSONエンコーダにはPECLにも登録されているphp-json(注1)かPEARパッケージであるHTML_AJAXに含まれるHTML_AJAX_JSONなどがあります.ここではどちらかといえばインストールがお手軽なHTML_AJAXに含まれるJSONエンコーダを利用します.

HTML_AJAXのインストールも簡単で,

$ pear install HTML_AJAX-alpha

とするだけです.すると

downloading HTML_AJAX-0.4.0.tgz ...
Starting to download HTML_AJAX-0.4.0.tgz (129,440 bytes)
.............................done: 129,440 bytes
install ok: channel://pear.php.net/HTML_AJAX-0.4.0

というメッセージが出力されて,PEARのHTMLディレクトリ以下にHTML_AJAXパッケージがインストールされます.

(注1)http://aurore.net/projects/php-json/

以上でJSONエンコーダの準備は完了ですので,あとはFlare_View_Jsonクラスのforward()メソッドを記述するだけです.これも思いの他簡単で,下記のような数行で完了です.

function forward() {
    $json_object = $this->af->getAppArray();
    mb_convert_variables('UTF-8', 'EUC-JP', $json_object);
    $json =& new HTML_AJAX_JSON();
    $json_value = $json->encode($json_object);
    header(sprintf('X-JSON: (%s)', $json_value));
    print "dummy";
}

2006/12/05追記:safariではbodyが0 byteだとヘッダが取得できないというご連絡をいただきまして、ダミーを出力するように変更しました

もしソースコードをUTF-8で記述している場合は,2行目のmb_convert_variables()も当然ですが不要となります.これで,setApp()された値をJSONオブジェクトへ変換して出力するビュークラスが出来ましたので,あとはアクションクラスで各種処理を行ったりsetApp()をしてreturn 'json';とすればよいことになります.

アクションの作成

ここではindex_checkとして,与えられたメールアドレスが「登録可能か」「不正な値か」「既に登録可能か」をアクションフォームクラスにmessageというキーで設定するアクションを作成します.まずはいつもの通り

$ ethna add-action index_check

としてアクションクラスファイルを作成します.次にFlare_Form_IndexCheckクラスにメールアドレスのフォームパラメータ定義を記述します.内容は第1章で作成したindex_testアクションのフォームパラメータと同一です.

class Flare_Form_IndexCheck extends Ethna_ActionForm {
    var $form = array(
        'mailaddress' => array(
            'name'      => 'メールアドレス',
            'required'  => true,
            'max'       => 255,
            'filter'    => FILTER_HW,
            'custom'    => 'checkMailaddress',
            'form_type' => FORM_TYPE_TEXT,
            'type'      => VAR_TYPE_STRING,
        ),
    );
}

「同じ定義2回も書くのはかったるいなー」と思ったそこの貴方のために,Ethnaではフォームパラメータのテンプレート機能が用意されています.アクションフォームクラスの親クラスに$form_templateというメンバ変数でフォームパラメータを記述しておくと,

var $form = array(
    'mailaddress' => array(),
);

と記述するだけで,テンプレートとして指定された属性を引き継ぐことができます.また,Ethna_ActionFormクラスの_setFormDef()メソッドをオーバーライドすることで動的に値を設定することも可能です.

さて,アクションフォームクラスの定義が完了したらアクションクラスの定義です.まずメールアドレスの形式が正しくない場合のためにprepare()メソッドに下記のような処理を追加します.

function prepare() {
    if ($this->af->validate() > 0) {
        $this->af->setApp('message', $this->ae->getMessage('mailaddress'));
        return 'json';
    }
    return null;
}

これで,validate()メソッドでメールアドレスの形式や長さに問題があった場合は,messageメンバに適切なエラーメッセージが格納されたJSONオブジェクトを返すことができます.あとはロジック部分です.これも下記のように非常に単純です.

function perform() {
    $user =& new Flare_User($this->backend, 'mailaddress', $this->af->get('mailaddress'));
    if ($user->isValid() == false) {
        $this->af->setApp('message', '登録可能です');
    } else {
        $this->af->setApp('message', '入力されたメールアドレスは既に登録されています');
    }
    return 'json';
}

まずFlare_Userクラスを入力されたメールアドレスをキーとしてインスタンス化しています.このとき,正しくエントリが見つけられる(つまり,入力されたメールアドレスが既にデータベースに登録されている)とFlare_Userクラス(というかEthna_AppObjectクラス)のisValid()メソッドでtrueが返ってきますので,これを判断基準としてisValid()がtrueなら登録済み,falseなら登録可能というメッセージを設定しています.そして最後に前述のJSONビューを表示すべきビューとして設定すれば完了です.

テンプレートの作成

あとは第1章で作成したテンプレート(template/ja/index.tpl)にJavaScriptを記述するだけです.今回はコードをシンプルにする意味も含めてprototype.js(注2)を利用しています.ですので,prototype.jsをダウンロードしてwww/jsディレクトリにコピーしておきます.

あとはメールアドレスの入力フォームにonkeyup()属性とそのハンドラ,そしてxmlhttpリクエストのハンドラを記述することになります.まずはonkeyup属性です.ついでにメッセージを表示させるための<div>タグを追加しておきます.

{form_input name="mailaddress" id="mailaddress" onkeyup="onkeyup_mailaddress();"}
<div id="mailaddress_message" />

書くまでもありませんが,こんな感じです.実際にはonfocus()とsetinterval()とかを組み合わせたほうが軽そうですが,シンプルにするためにonkeyup()でフックしています.そしてJavaScriptの部分は,上記のprototype.jsの読み込みの部分含めると下記のようになっています.

<script src="js/prototype.js" type="text/javascript"></script>
<script language="JavaScript">
{literal}
function onkeyup_mailaddress() {
 ajax = new Ajax.Request(
  'index.php',
  {
   method: 'get',
   parameters: Form.Element.serialize('mailaddress') + "&action_index_check=true",
   onComplete: handler_onkeyup_mailaddress
  }
 );
}
function handler_onkeyup_mailaddress(ajax, json) {
 $('mailaddress_message').innerHTML = json.message;
}
{/literal}
</script>

(注2)http://prototype.conio.net/

以上で準備は完了です.アプリケーションのトップページにアクセスして,メールアドレスを入力していくと適切にメッセージが表示されていくと思います(fig.1).

Ethnaの今後

以上が2006年6月時点での最新バージョンであるEthna 2.1.1のご紹介です.初期バージョンのリリースから,1年半くらいの月日が経ちましたが,その間に技術の変遷もありましたし,個人的にもまだまだやりたいことが山積みです.ということで,最後に今後のEthnaの開発ロードマップをご紹介させていただきます.

フォームレンダリングサポート

ここまでお読みの方にはご説明は不要かもしれませんが,Ethnaの特長の一つに(Strutsの影響なのですが)フォーム値などを格納してアクションとビューの間をつなぐ,Ethna_ActionFormというクラスがあります.このクラスには受け取るべきフォーム値の定義が記述されますので,当然その定義を利用してHTMLフォームもレンダリングできるはずです.この考えに基づいてEthna 2.1.1では実験的にEthna_ActionFormに記述されたフォーム値定義に基づいてテキストなどのフォームをレンダリングを行う機能が実験的に追加されていますが(第1章のサンプルでも一部利用しています),チェックボックスは未サポートである等まだまだ不完全なので,これを実用レベルにもっていこうと思います.

ORマッピングオブジェクト改善

Ethna組み込みのORマッピングクラスであるEthna_AppObjectは,筆者が特定の環境,アプリケーションで個人的に利用するために記述したものを実験的に組み込んだことが始まりで,ソースコードのクオリティ,環境依存性などの面でまだまだ改善すべき点が多々あります.ですので,MySQLやEthnaコア部分への依存を解消し,汎用的な環境で動作させられるように改善してきます.

ORマッピングオブジェクトからのフォーム定義生成

単純にデータベースの値を操作するようなアプリケーションを考えてみると,Ethna_ActionFormのフォーム定義自体をデータベースのテーブル定義から取得できればそれに越したことはありません(Ruby on Railsでは当然のように行われていることですし...).ということで,次バージョンではEthna_AppObjectからEthna_ActionFormへフォーム定義をエクスポートするような機能を追加しようと思っています.

この機能を実装して上記のフォームレンダリングサポートの機能を組み合わせると,例えばシンプルなblogのような,データベースにレコードを書き込むアプリケーションを非常に簡単に書けるようになる気がします(有名な「15分でblog」みたいなデモはこの辺りの機能が前提になっていると思われます).

プラグイン対応

第2章以降に記述しましたとおり,Ethnaではアプリケーションごと,あるいはご利用の環境ごとに比較的自由に振舞いを変更したり拡張させることができます.とはいえ,この形ですとEthna本体に変更をうまくフィードバックできなかったり,見通しが悪くなったり,メンテナンスコストがかかったり,と良いことばかりではありません.

そこで次のメジャーバージョンアップ版であるEthna 2.3.xではEthnaに汎用プラグイン機構を追加しようと考えています.Ethnaプラグインは「ローカル」(アプリケーション固有),「マスタ」(全アプリケーション共通)という2つのプラグインと,それらをリモートからインストールやアップロードを可能にするネットワークリポジトリを想定しています(PEAR+CPANみたいなものになるといいなー,と思っています).

おそらく次期バージョンではEthna_Filterと,Ethna_ActionFormのバリデータがプラグイン化されると思われますので,こちらもご期待ください(実はコードベースも既に半分以上実装されています).

AJAXサポート

第4章では,Ethnaを拡張する形でAJAXアプリケーションを構築してみましたが,今後こういった需要は間違いなく増え続けていくと思いますので,Ethna自体でのAJAXやJSONのサポートを強化していきたいと思っています.ただ,これについては特定のJavaScriptライブラリ(prototype.js+scriptaculous,あるいはmochikitなど)との連携を前提とするのかしないのか,RJSのような機能をもたせるのか,などなどちょっと煮詰めきれていませんので,随時ディスカッションなどもしつつ進めていきたい,という状況です.

その他細かな改善

その他にも,SOAPゲートウェイと(出来れば)Flash Remotingゲートウェイのサポート,Ethna_ClassFactoryにもう少し汎用DIコンテナとして機能を持たせてみる,HTMLレンダリングエンジンをSmarty固定ではなくしてみる,ビューコンポーネントのサポート,なども要改善点として挙がっています.

バグフィックス

そして最後に,幸か不幸かEthna 2.1.1リリース後に幾つかの細かいバグが発見されておりますので,当然ですがこれらは全て修正します.

以上が現時点で確定している今後のロードマップです.なお,さらに長期的なところでは「フレームワークからプラットフォームへ」というところがキーワードになってくると考えています.時間のある限り実装を進めて行きたいと思っていますのでご期待ください,あるいはパッチ,もしくはグリー株式会社で一緒にお手伝いしてくださる方も募集しております(笑).最後に,この記事,そしてEthnaがみなさまのお役に立つことがあればこれ以上に嬉しいことはありません.今後ともEthnaをよろしくお願いします.