トップ 最新 追記

いしなお!

2002|01|02|03|04|05|06|07|08|11|12|
2003|01|02|03|04|05|06|07|08|09|10|11|12|
2004|01|02|03|04|05|06|07|08|09|10|11|12|
2005|01|02|03|04|05|06|07|08|09|10|11|12|
2006|01|02|03|04|05|06|07|08|09|10|11|12|
2007|01|02|03|04|05|06|07|08|09|10|11|12|
2008|02|03|04|07|
2009|03|

2006-03-27 [長年日記]

_ [PHP][PHP5][Zend Framework] Zend Frameworkをどう使うか その18

Zend_InputFilterは、Zend_Filterの機能を使って特定の入力(=連想配列)に対するフィルタリングを行うためのクラス。コンストラクタで入力値を渡し、以降はその入力値のキー名に対して、Zend_Filterと同名のメソッドをコールすると、フィルタリングやバリデーションを行うことができる。

具体的には、

$filteredPost = new Zend_InputFilter($_POST);
if (!$filteredPost->isAlpha('name')) {
    // 不正な入力値によるエラー処理
} else {
    $name = $filteredPost->getAlpha('name'); // if文でalphaであることは確定しているんで、実際にはgetRawでいいけど
}

なんて感じになるんだけど、内部的にZend_Filterを使っているんで、そっちの紹介の追記で解説したように、それらをまとめて、

$filteredPost = new Zend_InputFilter($_POST);
if (!$name = $filteredPost->isAlpha('name')) {
    // 不正な入力値によるエラー処理
} else {
    // $nameを使った処理
}

といった形で書くこともできる。慣れるまでは可読性が落ちる(is〜で実体が返るのかよ)んで、俺はあんまり好きじゃないけど。

ちなみにZend_InputFilterのコンストラクタの引数には、通常$_POSTみたいなスーパーグローバル変数にセットされた入力値を渡すわけだけど、恐ろしいことにこの引数に対して、デフォルトでは破壊的な操作が行われる。

$filteredPost = new Zend_InputFilter($_POST);

とやったら、ここで$_POSTはNULLにセットされてしまい、以降$_POST相当のデータには$filteredPostを通してしかアクセスできないようになる。

俺は、これってものすごく大きなお世話って気がするんだけど、どうなんだろう? ちなみに第2引数に、

$filteredPost = new Zend_InputFilter($_POST, false);

なんて感じでFALSEを指定してやると、元のスーパーグローバル変数(には限らないけど、第1引数として渡された変数)のリセットは行われない。デフォルトはこっちにしておいた方が良くないか?

ちなみに現状ではこのZend_InputFilterって、フレームワークの一部としてフレームワークの他のコードから利用されたりはしていないんだけど、Zend_Controller_Routerには、$_SERVERから直接REQUEST_URIを取得しているところに、

@todo Replace with Zend_Request object

なんてことが書かれている。ということは、もしかしたら、

class Zend_Request
{
    var $_get = null;
    /* snip */

    public function __construct()
    {
        $this->_get = new Zend_InputFilter($_GET);
        /* snip */
    }

    function GET()
    {
        return $this->_get;
    }

    /* snip */
}

なんてクラスが登場して、Controller配下からそのインスタンスにアクセスできるようになるんじゃなかろうか? ちなみに使い方としては、

$req = new Zend_Request();
$name = $req->GET()->getAlpha('name'); // $_GET['name']のアルファベット要素のみを取得

みたいなイメージね。

ただこうやっちゃうと、

  • デフォルトですべての入力値系スーパーグローバル変数をリセットしていいのか
  • 上記のようなオブジェクトは、さまざまな場所で利用することになるけど、そのアクセスインターフェースはどうする?(シングルトン?)

あたりが微妙なんで、まだ公開されていないとか?

_ [日常][花粉][花見] クスリだけもらってきた

桜ヶ丘の桜そろそろ花粉症のクスリが切れる頃なので、処方箋だけもらって3週間分クスリを追加。それにしても、この2、3日は花粉がひどいことになっているな。ちょっとクスリを飲む間隔が空くと、目鼻がひどいことになる。今週末は会社の花見なんだけど、そんなのにいっている場合なんだろうか?

そういや渋谷の桜はもうほとんど満開だね。通りすがりに、桜ヶ丘の坂の写真を撮ってきたけど、なんかひどい写りだ。これじゃ全然桜がきれいに見えないね。木の下から上に向かって撮れば、もうちょっときれいに取れただろうに。携帯の画面だと逆光かどうかの判断がいまいちわからないんだよな。


2006-03-26 [長年日記]

_ [PHP][PHP5][Zend Framework][Zend_Filter] Zend Frameworkをどう使うか その17

そろそろ本格的に、いわゆるフレームワークとは関係ない、ただのライブラリしか残っていないなー。あとはせいぜいフィルター周りが、ちょっとはフレームワークの一部っぽいか?

入力フィルターの機能を受け持っているのは、汎用フィルター関数群のZend_Filterと、それを使って実際の入力値をフィルタリングするZend_InputFilterの二つ。

Zend_Filterはクラスとして宣言されているけど、public static functionしかないんで、実質は名前空間の代わりだね。で、持っている機能としては、

  • Zend_Filter::getAlpha($value) - アルファベット以外の文字を除去して返す。01ab!#23cd → abcd。
  • Zend_Filter::getAlnum($value) - アルファベットと数字以外の文字を除去して返す。01ab!#23cd → 01ab23cd。
  • Zend_Filter::getDigit($value) - 数字以外の文字を除去して返す。01ab!#23cd → 0123。

みたいな感じで、元となる文字列から、指定した文字要素以外を除去した文字列を取得するパターンのものがいくつか。

  • Zend_Filter::getDir($value) - パスからディレクトリ部分のみを抽出する。っつーか単にbasedir関数。
  • Zend_Filter::getInt($value) - intにキャストして返す。
  • Zend_Filter::getLength($value, $length = NULL) - 最初のn文字を返す。
  • Zend_Filter::getPath($value) - 相対パスを絶対パスに変換する。っつーか単にrealpath関数。
  • Zend_Filter::noTags($value) - HTMLタグ除去。striptags。
  • Zend_Filter::noPath($value) - パスからディレクトリ部分を除去。basename。

みたいな文字列変換系のもの。

  • Zend_Filter::isAlpha($value) - アルファベットのみで構成されているかどうかを返す。

みたいなある表現形式に合致しているかどうかを返すもの(このパターンはたくさんあるんでいちいち列挙しない)がある。

うーん、このクラス(ライブラリ)はなんかビミョーな出来だなー。

AlphaやAlphaNum、Digitあたりに関する関数は、まあ汎用性があるからこういう形で持っていてもいいだろう。でもパス操作なんて、別にPHPの関数そのまま使えばいいじゃん。Zend_Filterって殻にかぶせた方が使いやすくなっているとは思えない。

ましてや、isDateとかisPhoneとかisZipとか中途半端に特定の(地域の)パターンに対応した関数をフレームワークの標準機能の一つとして持っていられても、いまいち使えない気がする。たとえばisPhoneなんて、アメリカの地域コード一覧とか直値で持っていたりして、その辺のチェックまで行っているけど、これってどうよ?

一応アメリカ以外の国のデータも、持とうと思えばもてるようになっているから、将来的には少なくとも数カ国分のパターンは入れるつもりなんだろうけど、でもこういうアプローチじゃ完全な国際化対応はできないよなー。こういう微妙な機能はもっと根本的にプラガブルに設計するか、じゃなかったら標準では取り込まない方がいい気がするなー*1

と、長くなってきたのでここでいったん終了。Zend_InputFilterについては後で。

訂正

Zend_Filter::isAlpha($value) - アルファベットのみで構成されているかどうかを返す。

みたいなある表現形式に合致しているかどうかを返すもの(このパターンはたくさんあるんでいちいち列挙しない)がある。

と書いたけど、この手のis〜系の関数はbool値を返す一般的なValidate関数ではなかった。Validateに失敗した場合はfalseを返すけど、Validateに成功した場合は、引数として渡された$valueの内容をそのまま返す。

つまり、

if ($value = Zend_Filter::isAlpha($value)) {
    // $valueには元の$valueが入っているので
    // そのまま$valueを使った処理を書ける
} else {
    // $valueにはFALSEが返されている
}

なんて書き方ができるっつーのが、このZend_Filter(およびZend_InputFilter)の工夫らしい。

Zend_Filterだとこの工夫にはあまり意味がないけど、Zend_InputFilterでこの表現を使うとコードが1行省略できる(Validate行とFilterされた値を取得する行を2行に分ける必要がない)んで便利でしょ、ってことらしい。

でも俺的には、この手の基本的なロジックは、変に工夫して行数を削減するよりも、読んでわかりやすい方がいい気がするんだけどね。

*1 ってのは、HTML_QuickFormのDate周りとかを見たときにも思ったし、あれを汎用的に日本語対応するのは結構苦労した


2006-03-25 [長年日記]

_ [PHP][PHP5][Zend Framework][Zend_Db] こっちの方がましか?

Zend Frameworkをどう使うか その14」の最後の方で、Zend_Dbを使うよりもPEAR DB_DataObjectを使った方がきれいに書けたなーといっていたサンプルの、Zend_Dbを使ったもうちょっとましな書き方を思いついた。

class Stock extends Zend_Db_Table_Row {}
class Product extends Zend_Db_Table_Row {}

$select = $db->select();
$select->from('stock', 'stock.id as stockid');
$select->join('product', 'stock.product = product.id', 'product.id as productid');
$select->where('product.kind = ?', 'book');
$sql = $select->__toString();
$rows = $db->fetchAll($sql);

$stockTable = new Stock();
$productTable = new Product();
foreach ($rows as $row) {
    $stock = $stockTable->find($row['stockid']);
    $product = $productTable->find($row['productid']);
    if ($stock->amount == 0) {
        // 在庫がない本の場合の処理
    }
}

毎ループで必ず2回ずつ1行レコードを取ってくるクエリーが投げられてうざいけど、primary keyに対する検索だから検索自体は高速だし、末端でのレコードレベルに対するPHPコードからの操作は、それぞれZend_Db_Table_Rowに対する操作になっているんで、連想配列に対する操作と比べると安全だ。O/Rマッパーを使うんなら、内部的なDBとのやりとりが多少冗長でも、PHPコード上の冗長さが少ない方がましだろう。

Zend_Db_TableによるO/Rマッパーを使った操作は、基本的にこういった感じのアプローチで書くようになるのかな?

_ [PHP][PHP5][Zend Framework][Zend_Log] Zend Frameworkをどう使うか その16

あとふつうに使いそうなクラスといったらどれだろうなー、とつらつらながめてみて、俺的に重要度が高そうだったZend_Logクラスを見てみた。

まず、基本的にZend_Logクラスでは、すべてのログ出力は、

Zend_Log::log($message, $level, $logName_or_fields, $logName);

というpublic staticなクラスメソッドを経由して行う。ログ出力がしたければどこでも

Zend_Log::log('log message', Zend_Log::LEVEL_INFO);

なんて書くだけでいい。ただし、これだけではログを記録する実体がないんで、何も起こらないけど。

ログ記録の実体は、Zend_Log_Adapter_*というクラスが受け持つ。これはZend_Log_Adapter_Interfaceを持つクラス群で、open、write、close、setOptionという4つのメソッドが実装されていればいい。

標準では

  • Zend_Log_Adapter_Console - コンソール出力用
  • Zend_Log_Adapter_Db - DB出力用
  • Zend_Log_File - ファイル出力用
  • Zend_Log_Null - 何もしない。これって何のためにあるんだっけ? Null Logの意味が説明されているテキストをどこかで読んだ気がするんだけど。

が用意されている。ファイルログを出力したければ、

Zend_Log::registerLogger(new Zend_Log_Adapter_File('/path/to/log');

なんてあらかじめログ出力用Adapterを登録しておけばいい。マルチログにも対応していて、

Zend_Log::registerLogger(new Zend_Log_Adapter_File('/path/to/log'), 'file');
Zend_Log::registerLogger(new Zend_Log_Adapter_Db($db /* Zend_Db_Adapter */, 'tablename'), 'db');

なんて感じで、第2引数にログ名をつけて複数のAdapterを登録しておく。特定のAdapterに対して操作したい場合は、ここでつけたログ名を指定した、Zend_Logのクラスメソッドを呼ぶことになる。ログ名が省略された場合はデフォルトのログ名('LOG')が使われる。

出力先ごとに出力を行うLogLevelを設定したい場合は、

Zend_Log::setLevel(Zend_Log::LEVEL_DEBUG, 'debuglog'); // debuglogはログ名
Zend_Log::setMask(Zend_Log::LEVEL_ALL ~ Zend_Log::LEVEL_DEBUG); // ログ名を省略した場合は標準のログ

なんて感じでセットすればいい。

んだけど、試してみたらうまく動かなかった。ソースを見てみたら、Zend/Log.phpの382行目が

        if ($level | $logger->_levelMask) {

になってたけど、これって

        if ($level & $logger->_levelMask) {

の間違いだよね。一応こっちもMLに投げておいた

ちなみに内部的に「ZF」というログ名は、Zend Framework内部のログ出力用に予約されている模様。Zend Frameworkレベルの動作が怪しい場合なんかには、ZFという名前でログ出力先を登録しておけば、フレームワークレベルでのログが保存されるようになるんだろう。ただ現時点ではZend Frameworkを構成するファイルで、その機能を使っているものはないみたいだけど。

とまあ一通りのことはできるんだけど、マルチログの機能をデフォルトで取り込んだんだったら、すべてのログ出力先に(LogLevelの設定に応じて)ログを出力する機能もつけて欲しいよなー。PEAR LogのComposite Loggerみたいなやつ。まあZend_Log_Adapter_Compositeとかを作って、それをデフォルトロガーにすれば、現在の仕様でもやりたいことはできるんだけど。

っつーか、Zend_Log::logは$logNameが指定されていない場合は、登録されているすべての出力先に(LogLevelに応じて)ログを出力する、という仕様の方がいいんじゃないだろうか。それがダメなら、Zend_Log::logAllとか別にメソッドを追加するか、かな。

ちなみに前者の方法を実現する場合は、Zend/Log.php 353行目あたりに、次のようなパッチを当てればいい。

        if (is_null($logName)) {
-             $logName = self::$_defaultLogName;
+             $logName = array_keys(Zend_Log::$_instances);
+             if (isset($logName['ZF'])) {unset($logName['ZF']);}

ひとまず現状の設計に合わせて、Zend_Log_Adapter_Compositeを作って試してみて、それがいまいちならこっちもMLに投げてみようかなー。でも俺の英語表現がつたないせいか、微妙にスルーされている気もしないでもない。

_ [PHP][PHP5][Zend Framework][Zend_Log] Zend_Log_Adapter_Compositeを作ってみた

Zend_Log_Adapter_Compositeを作ってみたら、それで十分って感じだったんで、Zend_Log本体の実装は現状のままで特に問題はないか。

<?php
/**
 * @license LGPL
 * @author ishinao <ishinao@ishinao.net>
 */

require_once 'Zend/Log/Adapter/Interface.php';

require_once 'Zend/Log/Adapter/Exception.php';

/**
 *
 */
class Zend_Log_Adapter_Composite implements Zend_Log_Adapter_Interface
{

    private $_options = array('logNames' => array());

    /**
     * new Zend_Log_Adapter($logName1, $logName2, ....);
     *
     */
    public function __construct()
    {
        $logNames = func_get_args();
        foreach ((array)$logNames as $logName) {
            $this->add($logName);
        }
    }

    /**
     *
     * @params string $logName
     */
    public function add($logName)
    {
        if (in_array($logName, array_keys($this->_options['logNames']))) {
            throw new Zend_Log_Exception('logName already exists: ' . $logName);
        }
        if (!strlen($logName) || !is_string($logName)) {
            throw new Zend_Log_Exception('invalid logName: ' . $logName);
        }
        $this->_options['logNames'][$logName] = true;
        return true;
    }

    /**
     *
     * @params string $logName
     */
    public function remove($logName)
    {
        if (!in_array($logName, $this->_options['logNames'])) {
            throw new Zend_Log_Exception('logName not exists: ' . $logName);
        }
        unset($this->_options['logNames'][$logName]);
        return true;
    }

    public function setOption($optionKey, $optionValue)
    {
        $this->_options[$optionKey] = $optionValue;
        return true;
    }

    public function open()
    {
        return true;
    }

    public function close()
    {
        return true;
    }

    public function write($fields)
    {
        $message = $fields['message'];
        unset($fields['message']);
        $level = $fields['level'];
        unset($fields['level']);
        foreach (array_keys($this->_options['logNames']) as $logName) {
            Zend_Log::log($message, $level, $fields, $logName);
        }
    }

}

?>

使い方は、

Zend_Log::registerLogger(new Zend_Log_Adapter_File('/path/to/app.log'), 'file);
Zend_Log::registerLogger(new Zend_Log_Adapter_Db($db, 'logtable'), 'db');
Zend_Log::registerLogger(new Zend_Log_Adapter_Composite('file', 'db'));

なんてしておくと、ふつうに、

Zend_Log::log('message');

としただけで、ファイルログにもDBログにも同じログが出力される。ってだけだとあんまり使い道がないけど、たとえば、

Zend_Log::registerLogger(new Zend_Log_Adapter_File('/path/to/app.log'), 'app);
Zend_Log::registerLogger(new Zend_Log_Adapter_File('/path/to/error.log'), 'error');
Zend_Log::setLevel(Zend_Log::LEVEL_ERROR, 'error');
Zend_Log::registerLogger(new Zend_Log_Adapter_Composite('app', 'error'));

なんてやると、/path/to/app.logにはすべてのログが出力されつつ、LEVEL_ERRORなログだけは/path/to/error.logにも出力される。みたいな感じでログ出力側のコードは変えずに、必要に応じてログ出力先を切り替えるイメージね。

本日のツッコミ(全1件) [ツッコミを入れる]

_ .cat [こんにちわ、Zend_Logについて探していてたどり着きました。Zend_Log_Adapter_Composite..]


2006-03-24 [長年日記]

_ [PHP][PHP5][Zend Framework][Zend_Db] Zend Frameworkをどう使うか その10だと思う

さて、Zend_Db周りを見てみよう。「確かO/Rマッパーとして、Zend_Db_DataObjectが作られると発表されていたはずだけど、0.1.2にはそんなの入ってないなー。まだできてないのかなー」と思いつつマニュアルを読んだら、Zend_Db_Table、Zend_Db_Table_Row、Zend_Db_Table_RowsetあたりがO/Rマッパーな機能を持っているのね。名前が変わったのか。

で、Zend_DbのコアであるZend_Db_Adapterが、DB操作全般の基本機能をすべて持っている、PEAR DB相当のもの。PEAR DBと比べるとメソッド名とかずいぶんきれいに整理されつつ、便利そうな類似メソッドもいろいろ増えていて、非常に良さそう(ちなみに俺はMDBとかMDB2とかは使ってないんで、そっちとは比べられない)。

と思いつつよく見たら、Zend_Db_Adapterの設計(インターフェース)って、PDOの機能を元にしているのね。PDOが使える場合はZend_Db_AdapterはPDOのラッパー的に動作し、PDO以外のDBドライバを使う場合は、Zend_Db_Adapter側(の各ドライバエンジン。Zend_Db_Adapter_*とかZend_Db_Statement_*とか)でPDO相当の機能を実装して互換性を保とうってアプローチか。というわけで、Zend_Db周りを理解するためには、PDOに関する基礎知識が必要そう。まあでも面倒くさいからそっちのお勉強は省略。多分Zend_Db_Adapterの機能を関数ベースで実装したものなんだろう。

ところでZend_DB::factory(≒PDO)って、PEAR DBな頃のようなDSN文字列は受け取ってもらえず、配列形式のDBコネクション設定を渡さなきゃいけないみたいだね。たとえばマニュアルには、

$params = array ('host'     => '127.0.0.1',
                 'username' => 'malory',
                 'password' => '******',
                 'dbname'   => 'camelot');

$db = Zend_Db::factory('pdo_mysql', $params);

なんて例が書かれているけど、これって、

$dsn = 'pdo_mysql://malory:****@127.0.0.1/camelot';
$db = Zend_DB::factory($dsn);

を受けつけてくれても良さそうな気がするんだけど、なんでそうなってないんだろう? 従来のURI形式の文字列だと表現しきれないような細かい設定に対応するためかなー。でも過去(PEAR)互換性はキープしてくれてもいいと思うんだけどなー。DBコネクション設定なんて設定ファイルに一行で書いておいてDBライブラリにそれを渡すだけ、って感じにしたいのに。

そういえばZend_Db_Adapter(≒PDO)では、エスケープ関連の機能が充実していて、単純エスケープメソッドだけでなく、

$where = $db->quoteInto('foo = ? and bar = ?', 1, 'BAR'); // foo = 1 and bar = 'BAR'

みたいにprepareもどきな書き方ができる。あと、

$result = $db->query(
    'select * from tbl where foo = :foo and bar = :bar',
    array('foo' => 1, 'bar' => 'BAR')
); // select * from tbl where foo = 1 and bar = 'BAR'

みたいにprepareを使うこともできる模様。この辺の記法が充実していると、SQL文を書くときにいろいろきれいに書けるようになって便利。O/Rマッパーを使ったところで、いざとなったらSQL文(whereだけとかでも)は書かなきゃいけなくなるわけだし、そういう時の利便性を考えていろいろ用意しているっぽい。

って書いているうちにずいぶん長くなってきたからいったん休憩。

_ [PHP][PHP5][Zend Framework][Zend_Db] Zend Frameworkをどう使うか その11

Zend_Dbの続き。

Zend_Db_Adapterは、エスケープが書きやすくなったところがPEAR DBと比べての一番大きな違いかなー。メソッド名がわかりやすくなって、PEAR DBでは

$db->autoCommit(false);

だったのが、

$db->beginTransaction();

とふつうに書けるようになってたりするのも、可読性が上がっていいと思うけど。

ちなみにqueryメソッドで直接SQL文を渡す方法以外に、insertやupdateメソッドでは、

$table = 'tbl';
$row = array('foo' => 1, 'bar' => 'BAR');
$db->insert($table, $row); // insert into tbl(foo, bar) values(1, 'BAR');

みたいな書き方ができるみたいだけど、この書き方はうれしいかどうかちょっと微妙。PHPコードだけを見たら、それなりの利点(コードの見やすさ)は感じるんだけど、このPHPコードがDBに対してどういう操作をするのか、いまいちわかりにくい。

O/Rマッパーくらい突き抜けちゃえば、その利点の大きさは認めるけど、こういう中途半端な形でSQL文の隠蔽を行うくらいならば、ふつうにplaceholderとかを使ったSQL文へのパラメータ埋め込みを書いた方が、トータルでの可読性は高いんじゃなかろうか。

っつーかまあ、どうせZend_Dbには標準でO/Rマッパーもあるから、この中途半端な記法は、本来はO/Rマッパー機能の中の人(メソッド)が使うために用意されているんだろうけどね。

ちなみにZend_Db_Adapter周りの機能は、Zend_Db_Adapter+Zend_Db_Statement(PDOを利用する場合は、PDOStatementがそのまま使われる)の組み合わせで実現されているんで、Zend_Db_Adapter周りを一通り知りたければ、本当はZend_Db_Statementの方も追う必要がある。PDOがPDOクラスとPDOStatementクラスの二つから構成されているのと、相似関係にあるわけだね(ってことで、前に書いた多分Zend_Db_Adapterの機能を関数ベースで実装したものなんだろうって予想は間違いだった。PDOは関数ベースではなく、PHPの組み込みクラスとして実装されていた)。まあでもこの辺は深追いしても大して面白くなさそうだから追わない。

_ [PHP][PHP5][Zend Framework][ML] オランダ語?

Zend FrameworkのMLにZend_View_Smartyの件でメールを投げてみたら、すぐにレスポンスメールが返ってきたんだけど、なにやら読めない言語で書いてある。.nlドメインってオランダ? ドイツ語とちょっと似ているけど、やたらと母音を連続する単語が多いな。文中に含まれる日付らしき要素(これは英語と似ているからだいたいわかる)から察するに、多分自動応答の不在通知メールだろう。まあMLでは良くあることだけど(文字化けではなく)読めない言語で返ってくるとなかなか焦る。

_ [PHP][PHP5][Zend Framework][Zend_Db] Zend Frameworkをどう使うか その12

もちろんこんなことになるなら、連番ではなくちゃんとしたセクションタイトルを付けるべきだったと後悔しているわけですよ。またZend_Dbネタの続きで、今度はselectクエリー生成ユーティリティクラスであるZend_Db_Selectクラスについて。

第一印象としては、Zend_Db_Selectに関しても、Zend_Db_Adapter::insertとかZend_Db_Adapter::updateとかと同様に、微妙に中途半端な感じがする。ちなみにZend_Db_Selectクラスの具体的な使い方としては、

// $db - Zend_Db_Adapterオブジェクト
$select = $db->select();
$select->from('tbl', '*'); // select * from tbl
$select->where('foo = ?', 1); // where foo = 1
$select->order('bar'); // order by bar
$select->limit(10, 5); // MySQLなら limit 10, 5
$sql = $select->__toString(); // select * from tbl where foo = 1 order by bar limit 10,5

って感じ。ちなみに上記サンプルには使ってないけど、他にもgroupとかhavingとかSQL文法要素のパターン一通り*1のメソッドがあって、メソッド名から想像したとおりに動く。

なんかもう「うわー」って感じがしない? こういうユーティリティクラスはそれなりに便利だとは分かる(昔自分でも作ったよ)けど、O/Rマッパー全盛の時代にこれをメインに使うのはちょっとビミョーな気がする。

ただ、こういうクラスが作られた理由は何となく想像できる。

というのは、後で触れるZend_Db_TableによるO/Rマッパー機能には、PEAR DB_DataObjectには存在したデータオブジェクト同士のjoin機能がなくなっている。DB_DataObjectでは、[dbname].links.iniにリンク情報を記述しておけば、

$foo = new FooTable(); // DB_DataObjectによるfooテーブルオブジェクト
$bar = new BarTable(); // DB_DataObjectによるbarテーブルオブジェクト
$foo->joinAdd($bar); // links.iniの設定に基づいて二つのテーブルをjoinする
$foo->find(); // select文を発行

なんて感じで、joinを使ったselect文をデータオブジェクト操作のレベルで書けたけど、Zend_Db_Table(_Row)からはそういう機能はすっぱり削除された。実際にはもちろんjoinを使ったselectなんて山のように使うわけで「O/Rマッパーのレベルでのjoin機能を削った代わりに、joinを伴う複雑なselectの管理はこっちでやってね」というんで作られたのが、Zend_Db_Selectなんじゃなかろうか。

まあ確かに、DB_DataObjectのjoin機能は結構微妙なできではあったし*2、その完成度を高めるってのはかなり難しい*3ことは分かるけど、こうなっちゃうと使い勝手としてはDB_DataObjectから後退している気がするんだよなー。単体ではきれいな仕様&実装ではあると思うけど。

ちなみにZend_Db_Selectを使ったjoinのやり方は、こんな感じになる。

$select = $db->select();
$select->from('foo', '*'); // select foo.* from foo
$select->join('bar', 'foo.id = bar.id', '*'); // select bar.* ... join bar on foo.id = bar.id
$sql = $select->__toString(); // select foo.*, bar.* from foo join bar on foo.id = bar.id

うーん、俺なら素直にSQL文+placeholderで書いちゃう気がするなー。Zend_Db_Selectを使っても、結局O/RマッパーであるZend_Db_Tableとは異なるオブジェクトに対する操作になってしまうし、だったらO/Rマッパーとは異なる形でSQL文を隠蔽するZend_Db_Selectを使うよりは、まだSQL文を直接書いた方がわかりやすい気がする。Zend_Db_Selectを使う場合は、生成されるSQL文を完全に頭の中にイメージしていないと使えないわけだし。

あ、一点Zend_Db_Selectを使う確実な利点があったな。limit関係のSQLはDBMSによっていろいろ異なるんだけど、それをZend_Db_Selectを使うことで吸収してくれる模様。それが重要ならば使う価値があるかもね。

*1 +追加条件の場合はorWhereとかorHavingとかの論理演算指定含む

*2 実用レベルでは、内部的に生成されるSQL文を意識しながら使う必要があったり

*3 DBスキーマ側まで規約で縛ったりすれば何とかなるかもしれないけど

_ [PHP][PHP5][Zend Framework][Zend_Db] Zend Frameworkをどう使うか その13

「ようやくO/Rマッパーにたどり着きましたよ」のZend_Db_Table。使い方は、

// $db - Zend_Db_Adapterオブジェクト
Zend_Db_Table::setDefaultAdapter($db);
class Foo extends Zend_Db_Table {} // fooテーブルに対応するクラスの宣言
$foo = new Foo(); // $fooテーブルに対応したオブジェクト

って感じ。実行環境(static protected Zend_Db_Table::$_defaultDb)にDB接続情報をセットしておくってやり方は、DB_DataObjectがPEARのstaticPropertyを使っていたやり方と一緒だね。

ただし、上記のようにオプション設定なしでテーブルクラスを宣言した場合は、

  • クラス名 = テーブル名
  • テーブルのプライマリーキーとなるカラム名 = id

となっている必要(規約)がある。その辺を変更したい場合は、

class Table_Foo extends Zend_Db_Table
{
    protected $_name = 'foo'; // テーブル名
    protected $_primary = 'id'; // プライマリーキーとなるカラム名
}

なんて感じでクラスを定義すればいい*1。実際に使うときにはどう書くんだろうなー。適当なprefixをつけてクラス定義ファイルを自動生成しつつ、必要に応じて内容を修正する感じになるのかなー。まだ具体的な利用イメージは湧かない。

Zend_Db_Tableオブジェクトに対してできる操作は、

  • getAdapter() - Zend_Db_Adapterオブジェクトが取得できるんで、細かい操作が必要な場合はこれ経由で処理。
  • info() - テーブル情報が取得できる。テーブル名、カラム名一覧、プライマリーキーカラム名の三つを連想配列で。
  • insert(array $data) - 連想配列でデータを渡してinsertを実行。やっぱり内部的にはZend_Db_Adapter::insertが呼ばれているね。
  • update(array $data, string $where) - 連想配列でデータを、SQL文でwhere条件を渡してupdateを実行。こっちも内部的にはZend_Db_Adapter::updateが呼ばれている。
  • delete(string $where) - SQL文でwhere条件を渡してdeleteを実行。
  • find(mixed $val) - primary keyに対する検索。$valにscalarを渡すと1行検索(戻り値はZend_Db_Table_Row)、配列を渡すとin検索(戻り値はZend_Db_Table_Rowset)される。
  • fetchAll($where, $order, $count, $offset) - ふつうにselect発効。引数はすべて省略可能。戻り値はZend_Db_Table_Rowset。
  • fetchRow($where, $order) - 1行select。複数行が選択されるような$whereの場合は最初の1行。ただし、Zend_Db_Table_Rowにはnextみたいなメソッドはないから、2行目以降に移動することはできない。
  • fetchNew() - 空のZend_Db_Table_Rowを返す。Zend_Db_Table::insertではなく、Zend_Db_Table_Row::saveを使ってinsertしたいときに使うんだろう。

なんて感じ。クラス構成としては、Zend_Db_Tableはテーブル全体に対する操作を行い、レコード単位の操作は、Zend_Db_Tableのfetch系メソッドで返される、Zend_Db_Table_Row(set)で行うというイメージね。

Zend_Db_Table_Row(set)の話は後回しにして、Zend_Db_Tableの拡張の話をもうちょっと続ける。

最初の方で、テーブル名やプライマリーキーの設定を変更するためのクラス定義の話を書いたけど、それ以外にもクラスを拡張する方法はある。つっても、単にふつうのクラスの拡張の仕方と一緒で、Zend_Db_Tableを継承したクラスで、メソッドを上書き(override)するなり追加するなりすれば、機能拡張できますよ、という話。

たとえば、

class Foo extends Zend_Db_Table
{
    function insert($data)
    {
        if (empty($data['created']) {$data['created'] = time();}
        parent::insert($data);
    }
}

なんて感じで、insert時に生成時間を自動的に更新したり、

class Foo extends Zend_Db_Table
{
    function findByName($name)
    {
        $db = $this->getAdapter();
        $where = $db->quoteInto('name = ?', $name);
        return $this->fetchAll($where);
    }
}

なんて感じで独自の検索メソッドを追加したりって感じか。でもこの例なんて、DB_DataObjectだったら標準クラスを使って、

$foo = new FooTable(); // DB_DataObjectによるfooテーブルオブジェクト
$foo->name = $name;
$foo->find();

で済んだんだけどね。

*1 他の書き方もあるけど

_ [PHP][PHP5][Zend Framework][Zend_Db] Zend Frameworkをどう使うか その14

O/Rマッパー編その2。Zend_Db_Tableのfetch系メソッドで返されるZend_Db_Table_RowおよびZend_Db_Table_Rowsetについて。名前を見れば分かるとおり、Zend_Db_Table_Rowが1行レコードに相当するクラスで、Zend_Db_Table_Rowsetが複数行のレコードを管理するクラスね。

Zend_Db_Table_Rowsetは、単に内部にZend_Db_Table_Rowオブジェクトの配列を抱えているだけで、DBMSレベルでのカーソルとかは使っていない。「100万行返すZend_Db_Table::fetchAllを投げたら、100万個のZend_Db_Table_Rowオブジェクトを作ったるでぇ」という気合いの入った仕様になっている。

Zend_Db_Table_Rowsetのメソッドは、以下のような感じ。

  • current() - カレントのZend_Db_Table_Rowオブジェクトを返す。存在しない場合はfalseが返る。
  • next() - ポインタを次の行に移動し、新しいポインタを返す。
  • rewind() - ポインタを先頭に移動。
  • valid() - 有効なポインタを差しているか。
  • count() - レコード行数。
  • exists() - 有効なレコードを持つか(空のZend_Db_Table_Rowsetかどうか)
  • toArray() - 保持している全行のデータを、配列+連想配列形式(旧世代のPHPらしいデータ形式)に変換して返す。

ただし、ほとんどiteratorインターフェースを実装するためのメソッドね。ふつうのクラスライブラリ的に書けば、

$foo = new Foo(); // Zend_Db_Tableオブジェクト
$rows = $foo->fetchAll(); // 全行取得
while ($rows->valid()) {
    $row = $rows->current();
    // 各行に対する処理
    $rows->next();
}

なんて感じになるけど、iteratorを使えば、

$foo = new Foo(); // Zend_Db_Tableオブジェクト
$rows = $foo->fetchAll(); // 全行取得
foreach ($rows as $row) {
    // 各行に対する処理
}

なんて書き方になる。PHPだったらこっちの書き方の方がふつうかな。

そうやって取得した1レコード単位のZend_Db_Table_Rowのメソッドは、以下のような感じ。

  • save() - primary keyがセットされていればupdate、セットされていなければinsertを行う。
  • toArray() - データを連想配列として返す。
  • setFromArray(array $data) - 連想配列から各カラムの値をセットする

なんて感じ。もちろん各カラムの代入と参照は、上記メソッド以外にも、

$row->foo = 1;
$row->bar = 'BAR';
$row->save();

なんて感じで、__set、__getメソッドを経由したプロパティ表現でも書くことができる。

以上でZend FrameworkのO/Rマッパー周りの機能は一通り見ていったわけだけど、きれいな仕様ではあるけれども、かなり機能は少ないなーというのが、全体を通しての印象。

特にO/Rマッパーは1テーブル単位の操作でしか(検索でさえも)使えないって仕様は、結構微妙な気がする。というのはDB_DataObjectの、検索だけなら複数テーブルをまたいだ処理にもデータオブジェクト互換の操作が使える、という仕様に俺が慣れてしまっているからかもしれないけど。具体的には、

$stock = new StockTable(); // 在庫テーブル
$product = new ProductTable(); // 商品テーブル
$product->kind = 'book'; //種類は「本」
$stock->joinAdd($product); // 在庫テーブルと商品テーブルをjoin
$stock->find();
while ($stock->fetch()) {
    if ($stock->amount == 0) {
        // 在庫がない本の場合の処理
    }
}

みたいな書き方がDB_DataObjectでは簡単にできたわけだけど、これをZend_Dbを使って書くと

$select = $db->select();
$select->from('stock', '*');
$select->join('product', 'stock.product = product.id', '*');
$select->where('product.kind = ?', 'book');
$sql = $select->__toString();
$rows = $db->fetchAll($sql);
foreach ($rows as $row) {
    if ($row['amount'] == 0) {
        // 在庫がない本の場合の処理
    }
}

みたいにZend_Db_Adapter+配列による結果セットを使って書くか、あるいは、

$select = $db->select();
$select->from('product', 'id');
$select->where('kind = ?', 'book');
$sql = $select->__toString();
$rows = $db->fetchAll($sql);
$ids = array();
foreach ($rows as $row) {$ids[] = $row['id'];}

class Stock extends Zend_Db_Table {};

$stock = new Stock();
$rows = $stock->fetchAll($db->quoteInto('product in (?)', $ids));
foreach ($rows as $row) {
    if ($row->amount == 0) {
        // 在庫がない本の場合の処理
    }
}

みたいに回りくどくZend_Db_Tableを使って書くことになるんだよね? なんかDB_DataObjectの頃よりもずいぶんわかりにくくなっちゃったなーと思えてしまう。それとも俺が思いついていないだけで、現状のZend_Dbを使ってもっときれいに書く方法があるのかな?

一応補足しておくと、

Zend_Db_Adapterを使って、連想配列で結果を受け取った場合、たとえばamountというカラム名を間違って、

    if ($row['amaunt'] == 0) {

なんて書いたとしても、(E_NOTICEが有効ならばnoticeは出るけど)エラーは出ない。一方、Zend_Db_Table_Rowで結果を得る場合は、

    if ($row->amaunt == 0) {

と書くと、存在しないカラム名を使ったと言うことで、例外が投げられる。もちろん後者の方がコーディングスタイルとしてはいいよね。

_ [PHP][PHP5][Zend Framework][Zend_Db] Zend Frameworkをどう使うか その15

もう一個触れていなかったZend_Db周りのクラスがあったんで、それについても一応書いておこう。

Zend_Db_Profilerってクラスがそっと存在していて、よく見るとZend_Db_Adapterがそのオブジェクトを持っている。名前を見れば分かるとおり、DBクエリーのプロファイリングをしてくれるクラスね。

これはZend_Db_Adapterを生成するときに、

$config = array(
    ..., //DB接続設定は省略
    'profiler' => true,
);
$db = Zend_Db::factory('pdo_mysql', $config);

なんて感じで、profiler = trueな設定を渡すと有効になる。あるいは、生成したZend_Db_Adapterオブジェクトから、

$profiler = $db->getProfiler();
$profiler->setEnabled(true);

なんてやっても有効になる。

プロファイラーが有効な状態で実行されるすべてのクエリーは、その実行時間がプロファイラー内部で記録されていく。

$profiles = $profier->getQueryProfiles();
foreach ($profiles as $profile) {
    echo 'クエリー: ' . $profile->getQuery() . "\n";
    echo 'クエリー種別: ' . $profile->getQueryType() . "\n"; // Zend_Db_Profiler内で定義
    echo '実行時間: ' . $profile->getElapsedSecs() . "\n"; //micro秒単位まで
}

なんて感じで取得できる模様。

必要なところだけプロファイラーを有効にしたりできるから、ベンチマークを取りたいところだけで有効にして、その結果をログとかに吐き出したりすると、いいかもしれない。MySQLのSlow Query Log相当の機能を自前で作ったりも簡単にできそうだね。


2006-03-23 [長年日記]

_ [日常][自動車保険] ソニー損保の第一印象わろし

コンビニ払いで申し込んだのに、支払期限までに振り込まなければ申し込みは無効になるとか注意書が出ていたのに、支払期限の翌日が前の保険が切れる日なんで間に合わないと保険の空白期間ができてしまうのに、コンビニ払いの振り込み用紙が支払期限の翌日に届きやがったよ。

支払期限の前日になっても支払用紙が届かないんで、電話してクレジットカードで払い込んだから間に合ったけど、なかなか出だしからやりやがるな。休日祝日が絡む日程でぎりぎりに申し込んだっつー状況も原因の一つだろうけど、保険とか取り扱ってるところならばその辺ちゃんと管理してくれや。

_ [PHP][PHP5][Zend Framework][Smarty] Zend_View_Smartyを作ってみた

Zend_ViewをSmarty対応するためのZend_View_Smartyを作ってみた。

ただし、「Zend Frameworkをどう使うか その8」でぐだぐだ書いたとおり、現在(Zend Framework 0.1.2)のZend_View_Abstractの実装では、Viewの値を保持する変数である$_varsに、継承したクラスからアクセスできない。

そこで、Zend_View_Abstractを以下のように書き換えて使っている。

-    private $_vars = array();
+    protected $_vars = array();

Zend_View_Smartyを使うと、呼び出し側のコードは、

$config = array(
    'scriptPath' => '/path/to/views', // for Zend_View
    'compile_dir' => '/path/to/templates_c', // for Smarty
);
$view = new Zend_View_Smarty($config);
$view->foo = 'FOO';
$view->now = time();
echo $view->render('template.tpl');

というように、通常のZend_Viewを使った場合と同様に書ける。一方呼び出されるテンプレートファイルtemplate.tplは、

<p>foo is {$foo|escape}.</p>
<p>now is {$now|date_format:'%D %T'}

なんてSmarty記法を使って書ける。

Smartyのキャッシュ機構を使いたい場合は、以下のようになる。

$config = array(
    'scriptPath' => '/path/to/views', // for Zend_View
    'compile_dir' => '/path/to/templates_c', // for Smarty
    'cache_dir' => '/path/to/cache', // for Smarty
);
$view = new Zend_View_Smarty($config);
$view->getSmarty()->caching = true;
if (!$view->isCached('template.tpl')) {
    $view->foo = 'FOO';
    $view->now = time();
}
echo $view->render('template.tpl');

キャッシュ制御にcache_idやcompile_idを使いたい場合は、isCachedやrenderの引数として渡すのではなく、$view->setCacheId('[CACHE_ID]')や$view->setCompileId('[COMPILE_ID]')などのセッタメソッドを使って、あらかじめViewオブジェクトにセットする。あと上記サンプルコードにもあるけれども、$view->getSmarty()でViewの内部にもつSmartyオブジェクトにアクセスできるんで、Smartyに関する細かい制御を行いたい場合は、それ経由で行えばいい。

また、SmartyからZend_Viewの機能にアクセスするためのSmarty用のプラグインも作ってみた。helperプラグインは、

{helper name="[HELPER NAME]" params....}

という記法で、Zend_Viewのhelper機能を呼び出すことができる。また、renderプラグインは、

{render file="[TEMPLATE_FILE]"}

という記法で、Zend_Viewのrenderメソッドを呼び出すことができる。

Smartyにも標準でincludeのような外部テンプレートを呼び出す機能は用意されているが、SmartyのincludeだとSmartyオブジェクトのtemplate_dirをベースにテンプレートファイルが検索されることになる。一方Zend_View::renderではaddScriptPathなどによる複数ディレクトリからのテンプレートファイル検索が行われるので、テンプレートファイルのパスの問題を一元化するために、renderプラグインを経由した方がいい。

といった感じで、Zend_Viewのインターフェースや機能を一通り使いつつも、Smartyの機能も一通り使えるようなViewクラスを作ってみたわけだけれども、実際問題Zend Framework 0.1.2の実装ではこういう書き方はできないわけで、できればその辺を何とかしてもらいたい。もうちょっといろいろ試してみてから、MLにでもその辺の話を投げてみよう。誰か代わりに投げてもらってもいいです。英語で書くのめんどい。

MLに投げた

いい反応があるとうれしいんだけど。

_ [PHP][PHP5][Zend Framework] Zend Frameworkをどう使うか その9だっけ?

っつーかZend FrameworkのControllerとView以外の構成要素は、単に今時っぽさ+重要度の高さ順にピュアPHP5で書きおろしている、PEARライブラリの焼き直しみたいなもんだよなー。

PEARがPHP4の負の遺産を継承してしまってPHP5フル対応が進まないんで、もうPEARをPHP5用としても継続させるのはあきらめて、PEAR相当のライブラリのピュアPHP5版をこっちで再構築するつもりなのじゃなかろうか。ただ、そういっちゃうとPEAR関係者ともめそうだから、「こっちはライブラリ集ではなくて、フレームワークなんです。別物です」と言い張るために、Zend Frameworkという名前を付けたとか(邪推)。

まあそれはそれでいいんだけど、PEARよりもZend Frameworkの方がライセンスとかの扱いが怪しいのが気になるなー。各ソースのヘッダに書かれているライセンスのURLはNot Foundだし。まあ配布アーカイブにはLICENSE.txtが入っているけど。

ところでこのZend Framework Licenseって、Zend Framework全体ではなく、構成するライブラリ単体にも適用されるんだよね。Zend_View_Smartyみたいなコードは、Zend_View_Abstractを継承しているからZend Framework License 1.0のソースに依存しているわけだけど、Zend_View_Abstract自体を再配布しているわけじゃないから、Zend Framework Licenseの影響は受けないよね。Zend Framework Licenseなコード自体を改変したりして再配布する際には、Zend Framework Licenseを維持する必要があるんだろうけど。

とかいうライセンス話はおいておいて、一応ControllerとView以外の構成要素も一通り見ていくか。


2006-03-22 [長年日記]

_ [PHP][PHP5][Zend Framework] Zend Frameworkをどう使うか その4

Zend Frameworkの感触だけ確かめるつもりだったんだけど、思ったよりも深追いしすぎている気がするな。でもまあいずれやらなきゃならないことだから、今やっておいても無駄にはなるまい。

で、さすがにそろそろコードを追うだけじゃなくて、実際に動かしてみようってことで、サンプルを作って動かしてみた。ただ、標準のmod_rewriteを前提としたRouterだと気軽にテストできないんで、

<?php

Zend::loadInterface('Zend_Controller_Router_Interface');

class MyRouter implements Zend_Controller_Router_Interface
{
    public function route(Zend_Controller_Dispatcher_Interface $dispatcher)
    {
        $path = $_SERVER['PATH_INFO'];
        $path = explode('/', trim($path, '/'));

        $controller = $path[0];
        $action     = isset($path[1]) ? $path[1] : null;

        if (!strlen($controller)) {
           $controller = 'index';
           $action = 'index';
        }

        $params = array();
        for ($i=2; $i<sizeof($path); $i=$i+2) {
            $params[$path[$i]] = isset($path[$i+1]) ? $path[$i+1] : null;
        }

        $actionObj = new Zend_Controller_Dispatcher_Token($controller, $action, $params);

        if (!$dispatcher->isDispatchable($actionObj)) {
            throw new Zend_Controller_Router_Exception('Request could not be mapped to a route.');
        } else {
            return $actionObj;
        }
    }
}

?>

みたいにPATH_INFOからActionを解決するようにしたRouter(上記ソースは、REQUEST_URIの代わりにPATH_INFOを使うようにした以外は、Zend_Controller_Routerとほとんど同じ)を用意しておいて、

Zend::loadClass('Zend_Controller_Front');

$controller = Zend_Controller_Front::getInstance();
$controller->setRouter(new MyRouter());
$controller->setControllerDirectory('/path/to/app');
$controller->dispatch();

という風にRouterを差し替えて動かす。すると、

http://example.com/path/to/sample.php/[CONTROLLER]/[ACTION]

なんて感じでアクセスできるようになる。あるいはaction=[CONTROLLER]/[ACTION]とか、module=[CONTROLLER]&action=[ACTION]とかのQUERY_STRINGから解決する方がテスト用にはいいのかな。

まあそんな風にして動作させたところ、思ったような感じで動作してくれているんで、今までソースとマニュアルを読んだだけで理解してきたことは、特に大きくは外してはいない模様。

というわけで、しばらく実際にいろいろ動かして試してみることにする。

_ [PHP][PHP5][Zend Framework: Zend Frameworkをどう使うか その5

プラグインが考えたとおりに動くかどうかテスト。

<?php

Zend::loadClass('Zend_Controller_Plugin_Abstract');

class AuthPlugin extends Zend_Controller_Plugin_Abstract
{
    public function routeShutdown($action)
    {
        if (!session_id()) {session_start();}
        if ($this->_isLogin()) {return $action;}
        $action->setControllerName('login');
        $action->setActionName('index');
        return $action;
    }

    private function _isLogin()
    {
        if (isset($_SESSION['login'])) {return true;}
        if (($_POST['id'] == 'testid') && ($_POST['pwd'] == 'testpwd')) {
            $_SESSION['login'] = true;
            return true;
        }
        return false;
    }
}

?>

な感じで認証プラグインもどきを作る。routeStartupではActionが書き換えられないんで、routeShutdownの方をフック。上のコードでは、元のActionを活かして、Controller名とAction名を認証ページに書き換えているけど、こうやって全然違うActionにマップし直す場合は、

return new Zend_Controller_Dispatcher_Token([CONTROLLER], [ACTION]);

という風に、本当はToken自体を作り直した方がいいよね。でもまあそのままにしておこう。で、FrontControllerを作るときに、

Zend::loadClass('Zend_Controller_Front');

$controller = Zend_Controller_Front::getInstance();
$controller->setControllerDirectory('/path/to/controllers');
$controller->registerPlugin(new AuthPlugin());

$controller->dispatch();

なんて感じで、プラグインを登録してからdispatchする。すると、どのActionが指定された場合も、必ず事前にAuthPluginが呼ばれるようになり、認証に通っていなかったら強制的にActionはLoginController::indexActionに差し替えられるようになる。LoginControllerは

Zend::loadClass('Zend_Controller_Action');

class LoginController extends Zend_Controller_Action
{
    public function indexAction()
    {
        Zend::loadClass('Zend_View');
        $view = new Zend_View();
        $view->setScriptPath('/path/to/views');
        echo $view->render('login.php');
    }

    public function noRouteAction()
    {
        echo get_class($this) . '-&gt;' . 'noRouteAction';
        var_dump($this->_getAllParams());
    }
}

な感じで、login.phpは、

<form method="post">
ID: <input type="text" name="id" /><br />
Password: <input type="password" name="pwd" /><br />
<input type="submit" value="login" />
</form>

な感じ。これでプラグインを使った認証処理が実現できる。ただ、ここでは単に認証を通すだけだけれども、ふつうのアプリケーションならば認証されたユーザー固有の状態を保持・参照できるようにする必要がある。そういう機能を受け持つものはZend Frameworkには見あたらないっぽいんで、自前でセッションにユーザークラスとかを入れておくとか、Zend::registryあたりで持ち回すとかする必要があるだろう。

_ [PHP][PHP5][Zend Framework] Zend Frameworkをどう使うか その6

さて続いては、一番いじりがいがあって、しかも今後インターフェースが変わりそうな気配が濃厚なZend_View周りをいじってみよう。まずは、 Zend Frameworkをどう使うか その2で予想した、Zend_View::render内で$this->renderした場合に、正しくレンダリングされるかどうかのテスト。

$view = new Zend_View();
$view->setScriptPath('/path/to/views');
$view->foo = 'FOO';
$view->render('index.php');

なんて感じで呼び出し、index.phpの方で、

<p>header</p>
<?php $this->render('body.php'); ?>
<p>footer</p>

なんて書いて、body.phpでは、

<p>foo value is <?php echo $this->escape($this->foo); ?></p>

と書くと、

<p>header</p>
<p>foo value is FOO</p>
<p>footer</p>

と出力が得られた。ちゃんと入れ子のrenderも実行してくれるみたいだね。ちなみにindex.phpでは、

<p>header</p>
<?php echo $this->render('body.php'); ?>
<p>footer</p>

とrenderの戻り値をechoするようにしてもしなくても結果は同じ。というのは、Zend_View::renderは入れ子の中で呼ばれた場合は、出力結果を文字列で返さず、親のrenderでob_startされた出力バッファリングにくっつけて出力し、親のrenderが終わった段階でまとめて結果を返すようになっているから。というわけで利用者レベルではechoをつけてもつけなくてもいいわけだけど、開発者はどっちの記述法を基本にするつもりなんだろうな。まあ大した問題ではないけど。

ちなみに上記サンプルにもあるとおり、Viewにセットした値は入れ子の中のrenderで呼ばれたテンプレート(PHPコード)の中でもふつうに使える。というか、renderが入れ子になっていても、呼び出すViewオブジェクト自体は変わらない=renderされるPHPコードのスコープは変わらないから、まあ当たり前。もちろん、

<?php $view = new Zend_View(); $view->render('foo.php'); ?>

とか明示的に別のViewオブジェクトを作ったら、別のスコープでレンダリングされるけどね。パーツをレンダリングするときに、使える(見える)値を制限したい場合は、そういう使い方もあるかも。

_ [PHP][PHP5][Zend Framework] Zend Frameworkをどう使うか その7

ひとまず、その他のテンプレートエンジンを使わず、お作法に則ってZend_Viewを使うアプローチについて考えてみる。Zend Frameworkの設計は筋がいいんで、アプリケーションの処理の流れに関するコードはきれいに記述できるし、PHP言語自体テンプレート記法みたいなもんだと考えれば、HTMLテンプレート(Zend_Viewでrenderするファイル)もきちんとロジックと分離して管理できる。

けど、やっぱり<?php echo $this->escape($this->foo); ?>という記法は我慢できない。書籍でも上記のような記法は我慢できなくて、ショートタグを有効にして、<?=h($foo) ?>*1みたいな記述を採用したし、ショートタグが使えない場合の代替手段としても、<?php eh($foo); ?>*2を採用した。単にテンプレート変数を展開(エスケープ付き)するだけなら、この程度の入力しやすさ&可読性の良さを期待したい。

では、どうやってZend_Viewをベースに上記のような入力しやすさ&可読性の良さを獲得するか。アプローチはいろいろ考えつくけれども、ひとまずZend_Viewを拡張するまっとうな手段であるhelperを使って書いてみることにする。

といっても、helperは単にZend_Viewの中から$this->[helperName]で呼び出すことができるという以上の機能は持ち合わせていないんで、escapeのショートカット関数を定義するくらいにしか使えそうにない。

class Zend_View_Helper_E
{
    public function E($value)
    {
        return htmlspecialchars($value);
    }
}

なんてhelperを書いて、$view->setHelperPath('/path/to/helpers')してから、

<?php echo $this->e($this->foo); ?>

する程度が関の山か。ちょっとは短くなったけど、可読性が高いとはとても言えない。あるいは、

class Zend_View_Helper_E
{
    public function E($value)
    {
        echo htmlspecialchars($value);
    }
}

としちゃって、

<?php $this->e($this->foo); ?>

とすればもっと短くなるけど、これだとテンプレート変数を出力しているように見えなくなっちゃうんで、短くなっても可読性は落ちてる気がするしなー。まあ慣れればこれでもいいかもしれないけど。

と試してみてやっぱり、Zend_Viewがescapeを特別扱いしている意味が感じられないよなーと再確認した。escape用のhelperを標準で用意しておけばそれでいいじゃん。helperの書き換えもaddHelperPathして上書きすればいいわけだし*3。Zend_Viewがescapeを特別扱いしているのは、「escape重要!」というポリシーを知らしめるためとか、helperよりも汎用性の低い実装にすることで多少のパフォーマンス上の利点があること、くらいしか思いつかない。

*1 hはhtmlspecialchars関数へのショートカット

*2 ehはecho htmlspecialchars相当の機能へのショートカット

*3 後からaddHelperPathされたパスが優先されるんで、同名の独自のhelperを用意すれば上書きできる

_ [PHP][PHP5][Zend Framework] Zend Frameworkをどう使うか その8

さっきのテンプレート展開の例で<?=h($foo); ?>みたいな記述を実現するために、

function showTemplate($_templateFile, $_templateVars)
{
    extract($_templateVars);
    include $_templateFile;
}

みたいな関数を用意していた。同じようなことがZend_Viewでもできれば、

<?php echo $this->escape($this->foo); ?>

は、

<?php echo $this->escape($foo); ?>

と書けるようになる。$this->の連続がなくなってずいぶん見通しが良くなった気がする。この程度ならば、Zend_View_Abstractを継承した、

class MyView extends Zend_View_Abstract
{
    protected function _run()
    {
        extract($this->_vars);
        include func_get_arg(0);
    }
}

とか作れば簡単に実現できるじゃんと思ったんだけど、ダメだった。というのは、$this->_varsってZend_View_Abstractでprivate宣言されているのね。せめてprotectedにならんかなー。あるいはZend_View_Abstractに、

public function getAllVars()
{
    return $this->_vars;
}

なんてメソッドを追加してもらえないだろうか。っつーかなんでDmytro Shteflyuk’s Home &#187; Zend Framework: Using Smarty as template engineのコメント欄で紹介されていたSmarty用のViewがわざわざassignをoverrideしていたのか、ようやくわかったよ。

この仕様ってどうなんだろうなー。継承されたクラスからも、テンプレート変数一覧には直接触らせないようにしなければならないだろうか? 単に「protectedにする理由がないからprivate」にしただけならば、protectedにして欲しいなー。そうすれば、もっとまっとうなSmarty用のViewを書くこともできそうだし。

まあ最悪、Zend_View_Abstractの$this->_varsおよびその関連メソッドを丸ごとMyViewで上書きしちゃえば、俺のやりたいことはできるようになるだろうけど、できればそういうアプローチ自体を公式に意識した作りになっていて欲しい。という話はZend FrameworkのMLに投げなきゃだめなのか。英語で議論するのめんどいなー。

MLの敷居の高さは

英語云々と言うよりは、ML投稿者の誰が誰やらさっぱり分からないことだな。Zend Frameworkのソースには開発者の名前が書かれていないし、MLに発言している人も特に名乗ったりしていない。また、関係者だと分かるような署名をつけている人も見あたらない。

この状態だと、どの発言がどういう立場でのものか分からないんで、どういうアプローチで発言していいのかさっぱり分からない。たとえば、Re: [fw-general] Smarty Plugin for viewで書かれているZend_Viewをもっと抽象化して他のテンプレートエンジンに対応できるようにしよう、なんて案には賛同する*1わけだけど、人間関係的な文脈が読めないんで、どうアプローチしていいのかさっぱりわからない。ひとまず各投稿者の名前とかでググって、どういう人がどういう立場でどういう発言をしているのか把握するところからはじめないとダメなのかな。

*1 けど、俺の場合ははそこまできれいなアプローチにしなくても、単に$_varsをprotectedしてくれるだけで十分なんだけどね


2006-03-21 [長年日記]

_ [PHP][PHP5][Zend Framework] Zend Frameworkをどう使うか

PHP5への移行を決意したのはZend Frameworkの存在も大きいわけだが、Zend Frameworkがどのくらいで実用レベルに達するのか、現時点では全然見通しが立っていない。少なくとも半年程度は待たなければならないんじゃないかと思われるんで、ここ一、二ヶ月程度のスパンではZend Frameworkの存在を意識しつつも、Zend Frameworkにあまり依存しないようなアプローチでPHP5対応を行う必要があるだろう。

というわけで、いつのまにかPreview 0.1.2まで出ていたZend Frameworkを真面目にチェック。フレームワークのコアとなるControllerとViewまわりがどんな感じなのかについては、php architecttutorialが一番わかりやすいドキュメントだろう。チュートリアルなんで内部構造にはほとんど触れず、使い方のレベルの説明だけど。

で、内部構造の方は、マニュアルのControllerパートとソースを読む限りでは、結構柔軟に差し替え可能な模様。

$controller = Zend_Controller_Front::getInstance();
$controller->setRouter(new MyRouter());
$controller->setDispatcher(new MyDispatcher());
$controller->setControllerDirectory($controllerDirectory);

みたいな感じで、Router(URIからAction名を解決する)やDispatcher(Routerで解決されたAction名から、Actionオブジェクトを生成し、実行する)は差し替えることができる。

あと、

$controller->registerPlugin(new MyPlugin());

とかやると、プラグインが登録できるけど、マニュアルにはプラグインに関する記述がない。ただ、Zend_Controller_Front::dispatchと、Zend_Controller_Plugin_Interfaceを見ればだいたい挙動がわかる。各プラグインには、$controller->dispatch()中に発生する下記のイベントに対応するハンドラーメソッドを書いておき、対応したハンドラーがあればそのプラグインが実行される(なければスルー)、って感じだろう。

  • routeStartup() - $controller->getRouter()->route()前。1リクエストごとの前処理用プラグインで使うんだろう。認証とかもこの辺にかませるのが基本かな。
  • routeShutdown($action) - $controller->getRouter()->route()後。デフォルトのrouteで解決したActionを、後付けで書き換えたりセットアップ処理を追加したりするときに使うのかな?
  • $action = dispatchLoopStartup($action) - Actionのdispatch処理は、戻り値として次のActionを返すことにより、複数のActionの連鎖実行(dispatchLoop)できる。そのdispatchLoop前。最初に実行するActionが解決したあとの前処理用か。
  • $action = preDispatch($action) - Actionのdispatch前。dispatchLoop内で実行される一つのActionごとに呼ばれる。最初に実行するかどうかに限らず、特定のActionに対する前処理。
  • $action = postDispatch($action) - Actionのdispatch後。dispatchLoop内で実行される一つのActionごとに呼ばれる。preDispatchに対して、特定のActionに対する後処理用かと思いきや、この段階では$actionは次のActionに書き換えられているはずだから、plugin内で状態を保持していない限りは、次のActionの前処理と大して変わらないことしかできないな。ってことは、特定のActionに対する前処理後処理を書きたかったら、一つのプラグインでpreDispatchとpostDispatchを処理しつつ、プラグイン側で状態を保持する必要がありそう。
  • dispatchLoopShutdown() - dispatchLoop後。全体の後処理。ただし、途中でdieしたりするパターンは(少なくとも正常ルートでは)ないんだろうか? あったら使える状況は限られてしまいそうだけど。

基本的な処理パターンを、Controller、Router、Dispatcher、各Actionに記述しつつ、パターンをまたがった処理はプラグインで書いていく感じかな。なんかプラグインって名前がちょっと違和感を感じるけれども、使い勝手と柔軟性はなかなか高そう。認証とかのクラスを基本的な処理パターンに密結合させると、一気に柔軟性が失われたりするものだけど、そのへんはプラグインに出してしまおうってことなんだろう。

ただ、そうなると認証とかの状態維持はZend::registerあたりを使ってねって話なのかなー。あれはちょっと汎用的な入れ物すぎるけど、$controllerが保持するくらいだったらZend::registerでも大して変わらないしなー。かといってActionのインスタンスは実行する瞬間しか存在しないから、Actionをまたがった入れ物としては使えないし。プラグインが状態を保持するというアプローチはありかもしれないけど、プラグインのインスタンスへの直接アクセス方法がないから、いまいち使い勝手が良くない気もする。

というところで疲れてきたので休憩。

_ [PHP][PHP5][Zend Framework] Zend Frameworkをどう使うか その2

さて、次はZend_View。Controllerが比較的薄くて差し替え可能なクラスを積み重ねている感じだったんで、Viewも似たような感じかなーと思ったら、Viewの方はZend_View自体が結構機能を持っているな。

基本機能としては、

$view = new Zend_View();
$view->setScriptPath($templateDir);
$view->assign('foo', 'FOO VALUE');
echo $view->render($templateFile);

みたいな感じか。ただしassignの代わりに__setと__getをつかって、

$view->foo = 'FOO VALUE';

みたいな記法もサポートしている(内部的にはassignするのと同じく、$this->_varsにセットされる)し、連想配列とかstdclassのプロパティとかを使ってassignしてもOK。まあこの辺はPHPのテンプレート展開処理で使われるいろんな方法は一通りサポートって感じか。

で、標準のテンプレート展開処理(=PHPのコードをincludeするだけ)では、

<?php echo $this->foo; ?>

で、セットした'FOO VALUE'が出力される(エスケープされない)。

で、escapeは特別なメソッドとして用意されていて、デフォルトでは

$this->_escape = 'htmlspecialchars';

というコールバックメソッドが登録されていて、

<?php echo $this->escape($this->foo); ?>

とすると、$this->escape()の実体としてhtmlspecialcharsが呼ばれて、エスケープ出力される。これは差し替え可能なコールバックメソッドなんで、

$view->setEscape('another_escape_function');

とかすれば、$this->escape()で別のエスケープ処理が使われるようになる。けど、なんでescapeを特別扱いしているのか、いまいち理由が分からない。

というのは、Zend_Viewでは汎用的な機能拡張方法として、helperってプラグインみたいな口が用意されていて、addHelperPathとかで指定したhelper用ディレクトリに、独自のhelperクラス(たとえばMyHelper.phpとか)を用意しておけば、 $view->myHelper()なんて感じでその処理を呼び出すことができるようになっている。escapeもそれで十分表現できるんだし、それでいいんじゃないのかなー(ただ、現状のZend_View_Abstract::_loadClassでthrow new Zend_View_Exceptionしている場所がおかしいんで、helperの検索が正しく働かない気がする。と思ったらMLで報告されていた)。

ちなみに標準のhelperとしては、FormButtonとか主にフォーム部品用のhelperが用意されている模様。ZFormとは違って、こっちはちゃんと出力は必要に応じてエスケープされるようになってるな。

あと、出力結果全体にかかるfilterもセットできるようになっていて、helperと同じようにaddFilterPathとかでfilter用ディレクトリをセットしてから、$view->addFilter('filter_name')とかすると、filterが追加される。filterは複数セットすることができ、renderの最後でまとめてかけられるようになっている。

と書きながらZend_View_Abstract::renderを読んでいたら、単純にレンダリングしているだけでなく、なんか変なスタックを積んでいるな。これって特にマニュアルには記述がないみたいだけど、renderの中でさらにrenderできるようになっているっぽい。

$view->render('foo.php');

とかやっておいて、foo.phpの中で、

<?php $this->render('bar.php'); ?>
<?php $this->render('baz.php'); ?>

とかやった場合に、正しくレンダリングされる(最初のfoo.phpのrenderが終わった段階で、まとめて出力が返される)ようになっている(気がする。動作確認してないけど)。一応テンプレートを複数のパーツに分割して管理することを意識しているのね。

まあ標準のZend_View(=テンプレートはViewオブジェクトのスコープで動作するPHPのコードをそのまま書く)で使う場合は、それはそれで結構便利そう。ただ、今まで他のテンプレートエンジン(というかSmarty)を使っていた人はどうするべきだろう。いくらZend_Viewがそれなりに高機能で拡張性があっても、今更<?php echo $this->escape($this->foo); ?>なんて記述はしたくないなー。

といったところで、また調査に戻る。

_ [PHP][PHP5][Zend Framework][Template] Zend Frameworkをどう使うか その3

Zend Framework(あるいはZend_View)と既存のテンプレートエンジンとの兼ね合いについては、MLのTemplate system, form handling and time scheduleから始まるスレッドを一読しておくといい。

Zend Frameworkのアプローチとしては、マニュアルに例として掲載されているような外部テンプレートシステムとの連携を基本と考えているのだろう。最初このマニュアルの例が何を意味しているのかよく分からなかったが、これはZend_Viewからrenderで呼び出されるスクリプト内で、PHPLIBスタイルのテンプレートを利用している、という意味のようだ。Zend_Viewからrenderで呼び出されるコードはテンプレートではなく、あくまでもHTML出力用のPHPコードであり、そこでさらに外部テンプレートシステムを使って出力するコードを書け、ということだ。さすがにその書き方はきっついなーと思わないでもないが、Zend Frameworkの構成要素自体は外部テンプレートシステムから独立するというポリシーを維持するためには、それなりにまっとうなアプローチだろう。

あと、SmartyのサイトからもリンクされているZend Framework: Using Smarty as template engineという記事で紹介されているアプローチがある。これは、SmartyのオブジェクトはZend::registerに突っ込んでおいて、Actionで直接Smartyを使ってHTMLを出力してしまい、Zend_Viewなんか使わない、というものになっている。この記事の作者は、

As you can see Smarty integration with Zend Framework is very simple task. Zend_View has ability to use your own filters and helper functions, but with Smarty you don’t need them because Smarty has its own plugins, filters, modifiers. Just forget about Zend_View and use best template engine for PHP in the world!

とまとめている。機能がかぶっているZend_Viewなんかのことは忘れて、Smartyをそのまま使った方が幸せになれるよ、って感じ。Smartyオブジェクトの置き場所がZend::registryってあたりはいまいちだし、Zend Framework全体のアプローチともきれいにかみ合わないけれども、機能としては問題ないし無駄も少なく実際的なアプローチだよな。

ちなみに上記記事のコメント欄では、Zend_View_Abstractを継承して、Smarty用のViewクラスを作ってそれを使うという、クラスライブラリ的にはかなりまっとうそうなアプローチを紹介している人がいるけれども、この記事の作者は、

I tried to create something similar but I don’t see advantages of this approach. (中略)You’ve extended Zend_View_Abstract but you don’t use almost all its functionality. I don’t think this is a good object-oriented practice - to extend but not to use methods of base class.

と答えている。確かにZend_View_Abstractを継承しているけれども、Zend_View_Abstractの機能なんてほとんど使ってないじゃん、そんなんじゃ継承している意味がないじゃん、って感じか。根性入れて、Zend_ViewのhelperとかfilterとかをSmartyのpluginとかと互換モードで動かすくらい気合いの入ったSmarty用Viewを書かないと、ほとんどの機能は互換性がないからなー。表面上のassign、render周りのインターフェースが同じになるだけでもそれなりに意味はあるとは思うけど、そんなのに大した意味はないと切り捨てる意見もわからないでもない。

ちなみに現状では実用的ではないけれども、アプローチとして一番面白かったのは、Smarty Plugin for viewというMLの投稿。これは、SmartyによるレンダリングをZend_Viewのフィルターとして実行してみよう、というもの。Zend_Viewはそのまま使い、そこでふつうにSmarty用のテンプレートファイルをrenderする。もちろんZend_ViewはSmarty用のテンプレート言語は知らないから、そのままスルーする。そしてrender後のフィルター処理でSmartyのレンダリング処理が行われる。残念なことにフィルターにはZend_Viewにassignされた値が渡されないので、現状では実用的ではないけれども(テンプレート変数が渡らないんじゃレンダリングしても意味がない)、何とかその辺を解決することができればなー。

そういやさっきのMLスレッドの最後の方に、

<disclaimer>Don't quote me and blame me if I'm wrong</disclaimer>

We are aiming to have something shippable in the 2nd half of 2006. However, this will very much depend on how the development goes and if we have enough proof points to be confident that it's in good shape design wise.

ってのがあった。2nd half of 2006って2006年下半期ってことだよね。要は2006年12月いっぱいまでってことだよね。まあそんな感じかなーとは思っていたけど、やっぱり遅そうだなー。