Ethna で綺麗な URL (Ethna_UrlHandler を使わない方法)

はいどうもー。 Ethna ってデフォルトのままだと URL がイケてないですよね。Ethna_UrlHandler を使う方法もありますがアクション毎に mapping を書く必要があって若干面倒です。

というわけで Ethna で URL を綺麗にする簡単な方法をご紹介します。フォーム値がそのままなのでちょっとダサいですが、一度設定してしまえばアクション毎にマッピングを書く必要もないのでとても楽です。

  • 使用前: http://example.org/?action_hogehoge_fugafuga=true&id=10
  • 使用後: http://example.org/hogehoge/fugagufa/?id=10

1. 以下の .htaccess ファイルをエントリポイントと同じ場所に設置する

<IfModule mod_rewrite.c>
	RewriteEngine On
	RewriteBase /

	RewriteCond %{REQUEST_FILENAME} !-f
	RewriteCond %{REQUEST_FILENAME} !-d
	RewriteRule . /index.php [L]
</IfModule>

mod_rewrite を使ってリクエストをエントリポイントに集約します。ちなみにこれは WordPress が自動生成するものを拝借しました :-p

2. Appid_Controller クラスの _getActionName_Form メソッドを以下のような感じでオーバーライドする

function _getActionName_Form()
{
	if (isset($_SERVER['REQUEST_METHOD']) == false) {
		return null;
	}
		if (strcasecmp($_SERVER['REQUEST_METHOD'], 'post') == 0) {
		$http_vars =& $_POST;
	} else {
		$http_vars =& $_GET;
	}

	foreach ($http_vars as $name => $value)
	{
		if ($value == "" || strncmp($name, 'action_', 7) != 0) {
			continue;
		}
		// オリジナル方式 http://hostname/?action_action_name
		return parent::_getActionName_Form();
	}

	// かっこいい http://hostname/action/name/ 方式
	if (!empty($_SERVER['REDIRECT_URL']))
	{
		$redirect_url = $_SERVER['REDIRECT_URL'];
		$action_name = str_replace('/', '_', $redirect_url);
		return trim($action_name, '_');
	}

	// まあ悪くはない http://hostname/?action=action_name 方式
	if (array_key_exists('action', $http_vars))
	{
		return $http_vars['action'];
	}
}

デフォルトのアクション指定方法 (18 行目) を潰してしまうとフォームヘルパが動作しなくなるので要注意です。ひとまずこれだけで簡単に URL を綺麗にすることができます。

Ethna を改造して Aether (えーてる) というものを作ってみた

はいどうも!ブログではご無沙汰のバックエンドネタですよー。

今回は業務でもよく使っている Ethna 2.5.0 をベースにして Aether (えーてる) というものを作ってみたのでソースコードを公開してみます。正確には、今までプロジェクト毎に作っていて散逸していたものを寄せ集めてみた、という感じです。

改造とか言ってみましたがそこまで大げさではなく Ethna のクラスを継承して便利メソッドを追加していったようなシロモノです。一個一個は小粒だけどそこそこ便利なメソッドが揃っているんじゃないかなーと勝手に思っています。後方互換性を重視して作っているので、 Ethna の挙動が大幅に変わったりとかはしないハズ。 (今までどおりの使い方をしていれば今までどおりの挙動を示します。)

github で公開していますので push とか fork 大歓迎でーす。

http://github.com/uniba-seiya/Aether

ある程度いい感じになってきたらバージョン番号をつけてリリースしていけたらなーと妄想しています。これからもどんどん改良を進めていきますので乞う御期待!

しばらくは Aether ネタで blog が書けそう。それでは!

Ethna の AppManager の getObjectPropList() で取得するカラムが異なるのに検索条件が同じだとキャッシュが返ってくるのを治す

どうもどうもこんにちは。今日は Ethna ネタですよ。

僕はどちらかというと AppObject を返してくれる getObjectList() 派なのですが、パフォーマンスが気になるときは AppObject を生成せず、カラムを制限して取得できる getObjectPropList() を使った方が良いケースもあったりします。その getObjectPropList() のキャッシュ周りでつまづいたのでそのメモでも。

サンプルとして Manager 側に id だけ取得する getIdList() と name だけ取得する getNameList() を実装してみます。

<?php
class Appid_HogeManager
{
    public function getIdList()
    {
        return $this->getObjectPropList('Hoge', array('id'), null, null, 0, 10);
    }
    public function getNameList()
    {
        return $this->getObjectPropList('Hoge', array('name'), null, null, 0, 10);
    }
}

データベースにはあらかじめこんな感じのデータを入れておきます。

mysql> select * from hoge;
+----+----------------------------------------------+
| id | name                                         |
+----+----------------------------------------------+
|  1 | Akufen - My Way                              |
|  2 | Todd Edwards - Full On                       |
|  3 | Emerson, Lake & Palmer - Brain Salad Surgery |
+----+----------------------------------------------+
3 rows in set (0.01 sec)

Action から Manager のコードを呼び出します。しかしこのコードは期待どおりの動作をしません。

<?php
    ....
    function perform()
    {
        $manager = $this->backend->getManager('Hoge');
        $hoge_id_list = $manager->getIdList();
        $hoge_name_list = $manager->getNameList();

        $this->backend->log(LOG_DEBUG, var_export($hoge_id_list, true));
        $this->backend->log(LOG_DEBUG, var_export($hoge_name_list, true));

        ....
    }

アプリケーションのログを見るとクエリがいちどきりしか実行されていなくて二つのメソッドの戻り値が同じであることが分かります。

2010/04/07 17:44:48 Appid(DEBUG): SELECT COUNT(DISTINCT `hoge`.`id`) AS `id_count` FROM `hoge`
2010/04/07 17:44:48 Appid(DEBUG): SELECT `hoge`.`id` FROM `hoge`   LIMIT 10 OFFSET 0
2010/04/07 17:44:48 Appid(DEBUG): hoge_id_list = array (
  0 => '3',
  1 =>
  array (
    0 =>
    array (
      'id' => '1',
    ),
    1 =>
    array (
      'id' => '2',
    ),
    2 =>
    array (
      'id' => '3',
    ),
  ),
)
2010/04/07 17:44:48 Appid(DEBUG): hoge_name_list = array (
  0 => '3',
  1 =>
  array (
    0 =>
    array (
      'id' => '1',
    ),
    1 =>
    array (
      'id' => '2',
    ),
    2 =>
    array (
      'id' => '3',
    ),
  ),
)

Ethna_AppManager の実装を見てみます。

    function getObjectPropList($class, $keys = null, $filter = null,
                               $order = null, $offset = null, $count = null)
    {
        global $_ETHNA_APP_MANAGER_OPL_CACHE;

        $prop_list = array();
        $class_name = sprintf("%s_%s", $this->backend->getAppId(), $class);

        // キャッシュチェック
        $cache_class = strtolower($class_name);
        if (is_array($_ETHNA_APP_MANAGER_OPL_CACHE) == false
            || array_key_exists($cache_class, $_ETHNA_APP_MANAGER_OPL_CACHE) == false) {
            $_ETHNA_APP_MANAGER_OPL_CACHE[$cache_class] = array();
        }
        $cache_key = serialize(array($filter, $order, $offset, $count));
        if (array_key_exists($cache_key, $_ETHNA_APP_MANAGER_OPL_CACHE[$cache_class])) {
            // skip
        } else {
            // キャッシュ更新
            $tmp =& new $class_name($this->backend);
            $_ETHNA_APP_MANAGER_OPL_CACHE[$cache_class][$cache_key]
                = $tmp->searchProp($keys, $filter, $order, $offset, $count);
        }

        return $_ETHNA_APP_MANAGER_OPL_CACHE[$cache_class][$cache_key];
    }

キャッシュを返す条件が $filter, $order, $offset, $count の指定が同様な場合のようです。異なる $keys が指定された場合はキャッシュを使わないように getObjectPropList() メソッドをオーバーライドして挙動を変更してしまいます。オーバーライドしたら controller への require と Appid_HogeManager の extends 先の変更を忘れずに!

<?php
class Appid_AppManager extends Ethna_AppManager
{
    function getObjectPropList($class, $keys = null, $filter = null,
                               $order = null, $offset = null, $count = null)
    {
        global $_ETHNA_APP_MANAGER_OPL_CACHE;

        $prop_list = array();
        $class_name = sprintf("%s_%s", $this->backend->getAppId(), $class);

        // キャッシュチェック
        $cache_class = strtolower($class_name);
        if (is_array($_ETHNA_APP_MANAGER_OPL_CACHE) == false
            || array_key_exists($cache_class, $_ETHNA_APP_MANAGER_OPL_CACHE) == false) {
            $_ETHNA_APP_MANAGER_OPL_CACHE[$cache_class] = array();
        }
        $cache_key = serialize(array($keys, $filter, $order, $offset, $count));
        if (array_key_exists($cache_key, $_ETHNA_APP_MANAGER_OPL_CACHE[$cache_class])) {
            // skip
        } else {
            // キャッシュ更新
            $tmp =& new $class_name($this->backend);
            $_ETHNA_APP_MANAGER_OPL_CACHE[$cache_class][$cache_key]
                = $tmp->searchProp($keys, $filter, $order, $offset, $count);
        }

        return $_ETHNA_APP_MANAGER_OPL_CACHE[$cache_class][$cache_key];
    }
}

治ったよー。

2010/04/07 17:49:10 Appid(DEBUG): SELECT COUNT(DISTINCT `hoge`.`id`) AS `id_count` FROM `hoge`
2010/04/07 17:49:10 Appid(DEBUG): SELECT `hoge`.`id` FROM `hoge`   LIMIT 10 OFFSET 0
2010/04/07 17:49:10 Appid(DEBUG): SELECT COUNT(DISTINCT `hoge`.`id`) AS `id_count` FROM `hoge`
2010/04/07 17:49:10 Appid(DEBUG): SELECT `hoge`.`name` FROM `hoge`   LIMIT 10 OFFSET 0
2010/04/07 17:49:10 Appid(DEBUG): hoge_id_list = array (
  0 => '3',
  1 =>
  array (
    0 =>
    array (
      'id' => '1',
    ),
    1 =>
    array (
      'id' => '2',
    ),
    2 =>
    array (
      'id' => '3',
    ),
  ),
)
2010/04/07 17:49:10 Appid(DEBUG): hoge_name_list = array (
  0 => '3',
  1 =>
  array (
    0 =>
    array (
      'name' => 'Akufen - My Way',
    ),
    1 =>
    array (
      'name' => 'Todd Edwards - Full On',
    ),
    2 =>
    array (
      'name' => 'Emerson, Lake & Palmer - Brain Salad Surgery',
    ),
  ),
)

そもそも同様の条件のクエリを複数回発生するコードを書く事自体が問題な気もしますが、複数のメソッドにまたがって getObjectPropList() を使っていると結構ハマりそうな仕様かなと思いました。まる。

Ethna で MySQL の予約語とかぶっているテーブル名を AppObject で扱う場合エラーするのを治すたった一つだけの方法

はじめまして。エンジニアの今野です。社内の圧力に屈したため Blog を書くことになりました。よろしくお願いします。

Ethna 2.5.0 を案件で使用していて release など MySQL の予約語になっているテーブルを AppObject で扱う場合不正なクエリが発行され動かないケースがありました。以下は debug log からの抜粋。

2010/03/09 16:46:17 Appid(DEBUG): SELECT COUNT(DISTINCT `release`.``) AS `id_count` FROM `release`
2010/03/09 16:46:17 Appid(ERR): Query Error SQL[SELECT COUNT(DISTINCT `release`.``) AS `id_count` FROM `release` ] CODE[1054] MESSAGE[SELECT COUNT(DISTINCT `release`.``) AS `id_count` FROM `release`  [nativecode=1054 ** Unknown column 'release.' in 'field list']] [ERROR CODE(4)]

MySQL の query log はこんな感じ。 release が予約語なので不正なクエリ。

13 Query       SELECT * FROM release LIMIT 0

Ethna の AppObject は prop_def を書かなくてもテーブル構造から勝手に prop_def を作ってくれる機能があるのですが、そこでテーブル名をエスケープしていない為に不正なクエリが実行されてしまい $prop_def の定義がおかしくなってしまうようですね。

$prop_def を手書きしてしまうか、テーブル名を変えればしのげるのですが、それでは若干悔しいので Ethna_DB_PEAR クラスを継承してサクッと治してしまいます。Ethna が include 出来る場所に Appid_DB_PEAR.php と言うファイル名で以下の内容で保存しましょう。 (Appid はご利用のプロジェクト名に置き換えてね!)

<?php
require_once 'Ethna/class/DB/Ethna_DB_PEAR.php';

class Appid_DB_PEAR extends Ethna_DB_PEAR
{
    function getMetaData($table)
    {
        return parent::getMetaData($this->db->quoteIdentifier($table));
    }
}

Controller クラスに教え込みます。

var $class = array(
    /*
    *  TODO: When you override Configuration class, Logger class,
    *        SQL class, don't forget to change definition as follows!
    */
    'class'         => 'Ethna_ClassFactory',
    'backend'       => 'Ethna_Backend',
    'config'        => 'Ethna_Config',
    'db'            => 'Appid_DB_PEAR',    // ← ここを変更
    'error'         => 'Ethna_ActionError',
    'form'          => 'Appid_ActionForm',
    'i18n'          => 'Ethna_I18N',
    'logger'        => 'Ethna_Logger',
    'plugin'        => 'Ethna_Plugin',
    'session'       => 'Ethna_Session',
    'sql'           => 'Ethna_AppSQL',
    'view'          => 'Appid_ViewClass',
    'renderer'      => 'Ethna_Renderer_Smarty',
    'url_handler'   => 'Appid_UrlHandler',
);

これで治りました。 Ethna_DB_PEAR::getMetaData() 自体がエラーするわけでは無いので最初ハマりましたが、同じような現象で困っている方の参考になればと思います。 (と言うか予約語とかぶるようなテーブルを作るなって話ですよね……。)

しばらくは Ethna ネタで記事の投稿を行っていく予定です。では、お粗末さまでした!

Ethna でフォーム定義の値を取ってくる

Ethna で下記のようなフォーム定義を使えば

'pref' => array(
       'type' => VAR_TYPE_INT,
       'form_type' => FORM_TYPE_SELECT,
       'name'  => '出身地',
       'required' => 'true',
       'option' => array(
            '01' => '北海道', '02' => '青森県', '03' => '岩手県', '04' => '宮城県', '05' => '秋田県',
            '06' => '山形県', '07' => '福島県', '08' => '茨城県', '09' => '栃木県', '10' => '群馬県',
            '11' => '埼玉県', '12' => '千葉県', '13' => '東京都', '14' => '神奈川県', '15' => '新潟県',
            '16' => '富山県', '17' => '石川県', '18' => '福井県', '19' => '山梨県', '20' => '長野県',
            '21' => '岐阜県', '22' => '静岡県', '23' => '愛知県', '24' => '三重県', '25' => '滋賀県',
            '26' => '京都府', '27' => '大阪府', '28' => '兵庫県', '29' => '奈良県', '30' => '和歌山県',
            '31' => '鳥取県', '32' => '島根県', '33' => '岡山県', '34' => '広島県', '35' => '山口県',
            '36' => '徳島県', '37' => '香川県', '38' => '愛媛県', '39' => '高知県', '40' => '福岡県',
            '41' => '佐賀県', '42' => '長崎県', '43' => '熊本県', '44' => '大分県', '45' => '宮崎県',
            '46' => '鹿児島県', '47' => '沖縄県',
       ),
)

テンプレートではフォームヘルパを利用して

{form_input name=”pref”}

と書くだけで都道府県を選択する確認ダイアログが作れる。
ただ、この次の確認画面で

{$form.pref}

とだけ書いた場合に、意味の無い値が表示されてしまう。

こういう場合は、View でフォーム定義を取得して setApp しておく。

$pref_def = $this->af->getDef(‘pref’);
$this->af->setApp(‘pref’, $pref_def['option']);

すると、template では

{$app.pref[$form.pref]}

として、都道府県名のラベルと値を紐付ける事ができる。やったね!


普通にフォーム定義で上記定義使ってる画面はこれでいいけど、フォーム定義なしで、単にDBから取ってきただけの値から都道府県名を表示したい時はどうするんだろう。

プロジェクトの ActionForm ベースクラスで form_template に代入する形で冒頭のコードようなフォーム定義が行なわれていたら

$this->af->form_template['pref'];

とする事で、上記の getDef で取ってきたもの相当は取れるからこれを使えばいいのかな。
プロパティだから、2行に分けなくても

$this->af->setApp($this->af->form_template['pref']['option']);

みたいに書けるし。

Ethna のフォームヘルパ

Ethna のフォームヘルパ を使っていた人に聞かれて、
思い出せずに30分程コードを読む羽目になったのでメモ。

{form_input} が上手くHTMLのタグに展開されないという事で、
ターゲットは、/class/Ethna_SmartyPlugin.php
705〜707行目がキーになる。

// action
$action = null;
if (isset($params['action'])) {
    $action = $params['action'];
    unset($params['action']);
} else if (isset($block_params['ethna_action'])) {
    $action = $block_params['ethna_action'];
}
if ($action !== null) {
    $view->addActionFormHelper($action);
}

この指定のため、{form_input} を囲む {form} ブロックタグに、
ethna_action パラメータの指定がある場合は、
そこで指定された Action を定義しているファイルに
ActionForm の定義を読みに行くようになっている。
つまりそっちで定義するのが正解。

とはいえ、エラーで戻ってきたりした時のために
手元のファイルにも持ってた方がいいのかな?未調査

preload preload preload