Ethna: 2009年6月アーカイブ

Ethnaの魅力を語る   はてなブックマークに登録  

自由度が高い

Ruby On Railsのように縛りがきつくないので、自由に自分の規約を作ったり、自作ライブラリと組み合わせて使うことができます。
また、そのような開発スタイルに向いています。

例えばADODBをラップして自作OR/Mapperを作るとか、lime.phpと組み合わせてPerl風のTDD開発をやるとか、そんな楽しみ方があります。
他のフレームワークではなかなか体験できないと思います。

ソースコードが読みやすい

フレームワークを開発している人がみな日本人で、コメントが日本語です。
私のような素人プログラマには、これはものすごく大きい。

また、ファイルやクラスの数が多くないので、印刷して電車の中で読めちゃいます。

あのGreeが使っている

上司や取引先を説得するときに、この一言は強いです。
「あのGree(時価総額1千億円)も使ってるんですよ!」と言えば、経営者層は安心します。
Ruby On RailsとかCakePHPとか言っても、プログラマじゃない人にはピンとこないですから。

覚えることが少ない

一つ目と関係しますが、学習コストが低いです。
個人的な体験ですが、プロジェクトの途中で新しいプログラマが参加してきたとき、1日ペアプログラミングで教えたら、2日目からはその人ひとりで開発できるようになっていました。
(その方がたまたま優秀だったという要因はありますが。)
_trigger_WWWは長いので、ばっさり要約してみます。
Ethna_Controller::_trigger_WWW (要約版)

function _trigger_WWW($default_action_name = "", $fallback_action_name = "")
{
    // アクション名の取得
    $action_name = $this->_getActionName(
            $default_action_name, $fallback_action_name
    );

    // アクション定義の取得
    $action_obj =& $this->_getAction($action_name);

    $this->action_name = $action_name;

    $backend =& $this->getBackend();

    // アクションフォームを呼び出し
    $form_name = $this->getActionFormName($action_name);
    $this->action_form =& new $form_name($this);
    $this->action_form->setFormVars();

    $backend->setActionForm($this->action_form);

    // セッションスタート
    $session =& $this->getSession();
    $session->restore();

    // アクション実行
    $forward_name = $backend->perform($action_name);

    // ビュー呼び出し・実行
    if ($forward_name != null) {
        $view_class_name = $this->getViewClassName($forward_name);

        $this->view =& new $view_class_name(
            $backend, $forward_name, $this->_getForwardPath($forward_name)
        );

        $this->view->preforward();
        $this->view->forward();
    }

    return 0;
}
実際には使用されていないコードを切り捨て、本質的なところだけ残すとこのようになります。
すごいシンプルになりましたね。
これがEthnaの「背骨」にあたる部分です。

要約版とは言っても、このコードの状態で十分りっぱに機能します。
実際私が携わっている商用Webサイトでは、この状態でも完全に動きます。

さて、_trigger_WWW内で行われていることが、より明確に見えてきましたね。

  • アクションフォームの呼び出し(インスタンス化)、初期化
  • アクションクラスの呼び出し、実行
  • ビュークラスの呼び出し(インスタンス化)、実行
  • プログラム全体の終了

Ehtna_Controllerはこのくらいにして、次回以降、個別のクラスの中をのぞいて見ましょう。
Windows上で、Ethna+PostgresqlでWebアプリを作っていて、コマンドライン用のアクションスクリプトを生成して、さあ実行しようとすると、こんなエラーが出ることがあります。

エラー

>php -f foo.php
Fatal error: Call to a member function execute() on a non-object in Ethna_DB_ADOdb.php on line 210

原因

Ethnaのせいではありません。

恐らく、コマンドラインから起動したPHPで、pg_connect()が使えないのが原因です。
それは、コマンドライン用のphp.ini(Web用のphp.iniとは別物)の中で"php_pgsql.dll"が読み込まれていないのが原因です。
解決法はこちら↓
Ethnaコマンドを使おうとすると、こんなエラーが出ることがあります。
私の場合は必ずこれが出ます。
これが出ると、add-actionとか何もできなくなって一瞬憂鬱になりますが、簡単に回避できるのでご安心を。

エラー

>ethna add-action ...なんとかかんとか
Fatal error: Cannot redeclare class Ethna in ... Ethna.php on line 424 

原因

Ethnaのライブラリ一式が、同じコンピュータ内に2重で存在している。
私の場合、
  • PEARチャンネルでインストールしたEthna (C:\php5\PEAR\Ethnaみたいな場所にある)
  • 既に作ったプロジェクトのlibフォルダ内のEthna (C:\www\myproject\lib\Ethna)
という形で、Ethnaのソースコードが複数の場所に存在していたのが原因のようです。

対処法

ethnaコマンドの実体であるethna.batを書き換えたら動きました。
環境によって違うので、あくまでも参考ということでお願いします。

"rem"というのはコメントアウトのことです。(phpでの"//"に相当)

@echo off

rem
rem   ethna.bat
rem
rem   simple command line gateway
rem
rem   $Id: ethna.bat 457 2007-02-07 11:14:39Z ichii386 $
rem

if "%OS%"=="Windows_NT" @setlocal

if NOT "%PHP_PEAR_INSTALL_DIR%" == "" (
rem set DEFAULT_ETHNA_HOME= %PHP_PEAR_INSTALL_DIR%\Ethna
set DEFAULT_ETHNA_HOME=.\lib\Ethna
) ELSE (
set DEFAULT_ETHNA_HOME=%~dp0
)

goto init
goto cleanup

:init

if "%ETHNA_HOME%" == "" set ETHNA_HOME=%DEFAULT_ETHNA_HOME%
set DEFAULT_ETHNA_HOME=

if "%PHP_COMMAND%" == "" goto no_phpcommand
if "%PHP_CLASSPATH%" == "" goto set_classpath

goto run
goto cleanup

:run
rem IF EXIST "C:\php5\pear\Ethna" (
rem %PHP_COMMAND% -d html_errors=off -qC "C:\php5\pear\Ethna\bin\ethna_handle.php" %1 %2 %3 %4 %5 %6 %7 %8 %9
rem ) ELSE (
%PHP_COMMAND% -d html_errors=off -qC "%ETHNA_HOME%\bin\ethna_handle.php" %1 %2 %3 %4 %5 %6 %7 %8 %9
rem )
goto cleanup

:no_phpcommand
set PHP_COMMAND=php.exe
goto init

:set_classpath
set PHP_CLASSPATH=%ETHNA_HOME%\class
goto init

:cleanup
if "%OS%"=="Windows_NT" @endlocal
REM pause
ポイントは、"C:\php5\PEAR\Ethna"のような場所を見に行かないようにすること、"C:\www\myproject\lib\Ethna\bin\ethna_handle.php"を起動するように強制させることです。

なお、この書き方の場合は、必ずコマンドプロンプト上のカレントディレクトリを"C:\www\myproject\"にしてからEthnaコマンドを叩く必要があります。(ちょっと不便)
なぜこのようにしてるかというと、PC内にEthnaプロジェクトが3つも4つもあるからです。

どんだけEthna漬けやねん。。
Ethna_Controller::_trigger_WWW



function _trigger_WWW($default_action_name = "", $fallback_action_name = "")
{
    // アクション名の取得
    $action_name = $this->_getActionName($default_action_name, $fallback_action_name);

    // マネージャ実行チェック
    $this->_ethnaManagerEnabledCheck($action_name);

    // アクション定義の取得
    $action_obj =& $this->_getAction($action_name);
    if (is_null($action_obj)) {
        if ($fallback_action_name != "") {
            $this->logger->log(LOG_DEBUG, 'undefined action [%s] -> try fallback action [%s]', $action_name, $fallback_action_name);
            $action_obj =& $this->_getAction($fallback_action_name);
        }
        if (is_null($action_obj)) {
            return Ethna::raiseError("undefined action [%s]", E_APP_UNDEFINED_ACTION, $action_name);
        } else {
            $action_name = $fallback_action_name;
        }
    }

    // アクション実行前フィルタ
    for ($i = 0; $i < count($this->filter_chain); $i++) {
        $r = $this->filter_chain[$i]->preActionFilter($action_name);
        if ($r != null) {
            $this->logger->log(LOG_DEBUG, 'action [%s] -> [%s] by %s', $action_name, $r, get_class($this->filter_chain[$i]));
            $action_name = $r;
        }
    }
    $this->action_name = $action_name;

    // 言語設定
    $this->_setLanguage($this->language, $this->system_encoding, $this->client_encoding);

    // オブジェクト生成
    $backend =& $this->getBackend();

    $form_name = $this->getActionFormName($action_name);
    $this->action_form =& new $form_name($this);
    $this->action_form->setFormVars();

    // バックエンド処理実行
    $backend->setActionForm($this->action_form);

    $session =& $this->getSession();
    $session->restore();
    $forward_name = $backend->perform($action_name);

    // アクション実行後フィルタ
    for ($i = count($this->filter_chain) - 1; $i >= 0; $i--) {
        $r = $this->filter_chain[$i]->postActionFilter($action_name, $forward_name);
        if ($r != null) {
            $this->logger->log(LOG_DEBUG, 'forward [%s] -> [%s] by %s', $forward_name, $r, get_class($this->filter_chain[$i]));
            $forward_name = $r;
        }
    }

    // コントローラで遷移先を決定する(オプション)
    $forward_name = $this->_sortForward($action_name, $forward_name);

    if ($forward_name != null) {
        $view_class_name = $this->getViewClassName($forward_name);
        $this->view =& new $view_class_name($backend, $forward_name, $this->_getForwardPath($forward_name));
        $this->view->preforward();
        $this->view->forward();
    }

    return 0;
}


この関数こそがEthnaのメイン処理の部分です。
Ethnaのプログラムの流れがすべてここに凝縮されています。

ここでは、次のような重要なことが行われています。
  • アクションフォームの呼び出し(インスタンス化)
  • アクションクラスの呼び出し(インスタンス化)
  • ビュークラスの呼び出し(インスタンス化)
  • プログラム全体の終了

ここを読み解けば、Ethnaの全体像を把握することができます。
Ethnaで作ったアプリが動かないときは、ここでecho('hello')とか仕込んでおけば、プログラムがどこまで実行されたかわかります。

なお、この関数は最後に0を返しますが、この0はどこにも行きません。
呼び出し元のtrigger()関数のなかで空しく捨てられます。
そしてプログラム全体が終了します。

まとめると、

Ethnaのプログラム実行処理は、index.phpから始まって、trigger()で終わる。

と言えます。
そしてほとんどの処理は_trigger_WWW()の中で行われています。

次回以降、この_trigger_WWW()を詳しく見ていきましょう。
Ethna_Controller::trigger

    function trigger($default_action_name = "", $fallback_action_name = "", $enable_filter = true)
    {
        // フィルターの生成
        if ($enable_filter) {
            $this->_createFilterChain();
        }

        // 実行前フィルタ
        for ($i = 0; $i < count($this->filter_chain); $i++) {
            $r = $this->filter_chain[$i]->preFilter();
            if (Ethna::isError($r)) {
                return $r;
            }
        }

        // trigger
        switch ($this->getGateway()) {
        case GATEWAY_WWW:
            $this->_trigger_WWW($default_action_name, $fallback_action_name);
            break;
        case GATEWAY_CLI:
            $this->_trigger_CLI($default_action_name);
            break;
        case GATEWAY_XMLRPC:
            $this->_trigger_XMLRPC();
            break;
        case GATEWAY_SOAP:
            $this->_trigger_SOAP();
            break;
        }

        // 実行後フィルタ
        for ($i = count($this->filter_chain) - 1; $i >= 0; $i--) {
            $r = $this->filter_chain[$i]->postFilter();
            if (Ethna::isError($r)) {
                return $r;
            }
        }
    }
この関数、ちょっと長いので短くしてみましょう。

実はちょっとはしょりましたが、$this->getGateway() は "GATEWAY_WWW"という文字列を返します。
これでswitch文は簡単になります。


また連載1回目でも触れましたが、この連載では「フィルタ、CLI,XMLRPC, SOAPなどには触れない」ことにしています。
よって、「なんとかフィルタ」と書いてある部分はばっさり削除できます。
実際、Sampleプロジェクトではフィルタを定義してないので、この部分は不要です。

不要部分を削除するとこうなります。
    function trigger($default_action_name = "", $fallback_action_name = "", $enable_filter = true)
    {
            $this->_trigger_WWW($default_action_name, $fallback_action_name);
    }

実際の値を放り込むとこうなります。
    function trigger('index',  "",  true)
    {
            $this->_trigger_WWW('index',  "");
    }
シンプルですね。
_trigger_WWW()関数を実行しているだけのようです。

つづく
前回の続きより。
Ethna_Controller.php

    function main('Sample_Controller', 'index', "")
    {
        $c =& new Sample_Controller;
        $c->trigger('index',  "");
    }
$c には、Sample_Controllerクラスのオブジェクトが入ります。
(オブジェクトが何かわからない人は、「データ」だと思ってください。オブジェクト=「関数付きデータ」です。)

$c->trigger()で、Sample_Controllerのtrigger関数(メソッド)が実行されます。

では、Sample_Controller.phpを開いて、trigger関数を探しましょう。

ない。

ということは、Ethna_Controllerのtrigger関数が( $cのメソッドとして )呼ばれるはずです。

Ethna_Controller::triggerは・・・あった。
    function trigger($default_action_name = "", $fallback_action_name = "", $enable_filter = true)
    {
        // フィルターの生成
        if ($enable_filter) {
            $this->_createFilterChain();
        }

        // 実行前フィルタ
        for ($i = 0; $i < count($this->filter_chain); $i++) {
            $r = $this->filter_chain[$i]->preFilter();
            if (Ethna::isError($r)) {
                return $r;
            }
        }

        // trigger
        switch ($this->getGateway()) {
        case GATEWAY_WWW:
            $this->_trigger_WWW($default_action_name, $fallback_action_name);
            break;
        case GATEWAY_CLI:
            $this->_trigger_CLI($default_action_name);
            break;
        case GATEWAY_XMLRPC:
            $this->_trigger_XMLRPC();
            break;
        case GATEWAY_SOAP:
            $this->_trigger_SOAP();
            break;
        }

        // 実行後フィルタ
        for ($i = count($this->filter_chain) - 1; $i >= 0; $i--) {
            $r = $this->filter_chain[$i]->postFilter();
            if (Ethna::isError($r)) {
                return $r;
            }
        }
    }
むむむ。。ちょっと長いですね。
私のようなへっぽこプログラマは、長い関数を見ると尻込みしてしまいます。

つづく

Ethna_Controller.php

いよいよ、Ethnaの本体とも言うべきEthna_Controllerです。
前回、index.phpを見るとこう書いてありました。

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

これは、こう書くのと同じです。
 (Sample_Controllerクラスにはmain関数が存在しないので、親クラスのmain関数が実行される。)

Ethna_Controller::main('Sample_Controller', 'index');
では、Ethna_Controllerを覗いてみましょう。

さすがにEthnaの司令塔(コントローラ)というだけあって、ボリュームがあります。

しかし今は気にしない。main()関数を探します。

お、あった。


    function main($class_name, $action_name = "", $fallback_action_name = "")
    {
        $c =& new $class_name;
        $c->trigger($action_name, $fallback_action_name);
    }

これに、先ほどの値が放り込まれてこうなります。


    function main('Sample_Controller', 'index', "")
    {
        $c =& new Sample_Controller;
        $c->trigger('index',  "");
    }

spp/Sample_Controller.php

再びindex.phpを見てみましょう。
<?php
require_once 'C:¥xampp¥htdocs¥sample¥app/Sample_Controller.php';

Sample_Controller::main('Sample_Controller', 'index');
?>
Sample_Controllerのmain関数を呼び出しています。

では、Sample_Controller.phpを開いて、Sample_Controllerクラスを探しましょう。

おっ、ありました。

class Sample_Controller extends Ethna_Controller
{
...
よしよし。
では、main関数はと・・・

あれ、ない。

Sample_Controllerにmain関数はないようです。

この場合、Sample_Controllerの親クラスであるEthna_Controllerのmain関数が呼び出されることになっています。

次回は、Ethna_Controllerの中を覗いてみましょう。

はじめに

この連載では、Ethna2.3.6のソースコードとその読み方を解説します。

Ethnaコマンドを使って"Sample"という名前のプロジェクトを作ったという前提で話を進めます。

手元にEthnaがない方は、公式サイトを見てインストールしてみてください。
Ethnaインストールガイド

Windowsユーザの方はコレ↓で一発です。
5分でまっさらなWindowsにEthnaをインストールしてHello Worldを表示する方法

話を単純化するために、CLI,XML-RPC,SOAPなどには一切触れませんのでご了承ください。


Ethnaのプログラム実行はどこから始まるのか


EthnaでWebアプリを作るとき、通常はアクション、ビュー、テンプレートファイルの3セット(+DBクラスや自作ライブラリ)をバリバリ記述することになると思います。

では、問題です。

Ethnaで作ったWebアプリの実行はどこから始まるのでしょうか?



こたえ:index.php



全てはindex.phpから始まる

これは重要です。

そのアプリが、たとえ"hello world"と表示するだけの簡単なアプリであろうと、
あるいは、何百人のユーザを抱える大規模なソーシャルネットワーキングSNSサービスであろうと、

ユーザがindex.phpにアクセスしてきたなら、プログラムの実行はindex.phpから始まります。

つまりここから始まります。

<?php
require_once 'C:¥xampp¥htdocs¥sample¥app/Sample_Controller.php';

Sample_Controller::main('Sample_Controller', 'index');
?>
これがEthnaの第一歩です。

解説

1行目では、Sampleプロジェクトのコントローラーファイルを読み込んでいます。
コントローラというのは、メインのプログラムのことです。
require_once 'C:¥xampp¥htdocs¥sample¥app/Sample_Controller.php';

2行目では、読み込んだファイルの中の、Sample_Controllerクラスのmain関数を実行しています。
Sample_Controller::main('Sample_Controller', 'index');

さあここからが、Ethna内部への旅の始まりです。

つづく

余談:MVCなど忘れてしまえ

公式サイトやEthna本によると、
Ethnaとは、
MVCモデル2とフロントコントローラパターンに基づいたスタンダードなウェブアプリケーションフレームワーク
であると書かれています。

んがっ、そんな話は忘れてしまいましょう。

Ethnaとは、PHPで書かれたプログラムである。
これで十分です。

このアーカイブについて

このページには、2009年6月以降に書かれたブログ記事のうちEthnaカテゴリに属しているものが含まれています。

前のアーカイブはEthna: 2009年5月です。

次のアーカイブはEthna: 2009年9月です。

Ethna: 2009年6月: 月別アーカイブ