Home

日記
Zend Frameworkをどう使うか その15Edit

もう一個触れていなかった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相当の機能を自前で作ったりも簡単にできそうだね。

Published At2006-03-24 00:00Updated At2006-03-24 00:00

日記
こっちの方がましか?Edit

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マッパーを使った操作は、基本的にこういった感じのアプローチで書くようになるのかな?

Published At2006-03-25 00:00Updated At2006-03-25 00:00

日記
Zend Frameworkをどう使うか その16Edit

あとふつうに使いそうなクラスといったらどれだろうなー、とつらつらながめてみて、俺的に重要度が高そうだった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に投げてみようかなー。でも俺の英語表現がつたないせいか、微妙にスルーされている気もしないでもない。

Published At2006-03-25 00:00Updated At2006-03-25 00:00

日記
Zend_Log_Adapter_Compositeを作ってみたEdit

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にも出力される。みたいな感じでログ出力側のコードは変えずに、必要に応じてログ出力先を切り替えるイメージね。

Published At2006-03-25 00:00Updated At2006-03-25 00:00

日記
Zend Frameworkをどう使うか その17Edit

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

入力フィルターの機能を受け持っているのは、汎用フィルター関数群の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周りとかを見たときにも思ったし、あれを汎用的に日本語対応するのは結構苦労した

Published At2006-03-26 00:00Updated At2006-03-26 00:00

日記
Zend Frameworkをどう使うか その18Edit

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']のアルファベット要素のみを取得

みたいなイメージね。

ただこうやっちゃうと、

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

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

Published At2006-03-27 00:00Updated At2006-03-27 00:00

日記
クスリだけもらってきたEdit

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

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

Published At2006-03-27 00:00Updated At2006-03-27 00:00

日記
ピュアPHPなアップロードステータスバー挫折Edit

PHPでは、ファイルアップロード(サーバーでの受信処理)が完了するまで、サーバー上のどのファイルがどのリクエストに対してアップロードされているファイルなのかを、PHPコードから知る方法がない(アップロード用テンポラリファイル名はPHP言語が自動的に生成する)ため、アップロード経過をAjaxで知らせるような実装は、PHP単体ではできない。PHPで実装されたその手のデモも、よく見るとファイル受信処理自体はPHP以外の言語を使っているはず。

という壁を破るいい方法を思いついた。アップロードフォームを生成する時点で、リクエスト固有のテンポラリディレクトリを作成して、upload_tmp_dirとしてセットしておき、セッションを使ってその情報を引き継ぐようにすれば、そのディレクトリ内に生成されたファイル=アップロードされているファイルになるじゃないか。

と思ったんだけど、upload_tmp_dirってPHPコードからは上書きできないのね。っつーか、実行順序としては、ファイルアップロードの受信処理が終わった(=upload_tmp_dir設定が使われた)後に、PHPで書かれたコードが実行されるんだもんな。くっそー、こうなったら.htaccessとmod_envとphp_valueを組み合わせて、強引にセッション固有のupload_tmp_dirを作成したりすれば、何とかなるかなー。って、それはもう全然PHPネイティブな方法じゃないよ。

Published At2006-03-28 00:00Updated At2006-03-28 00:00

日記
PHP 5.1.2-win+MySQL 3.23.58-winEdit

の組み合わせでMySQLにアクセスすると、mysql関数を使ってもPDOを使っても、selectをした後にApache(mod_php)が落ちる(insertとかは問題ないいや、insertだけでも落ちてたっぽい)。原因を究明するのが面倒なんで、MySQLを4.1.15に換えたら問題なくなった。まあそろそろMySQL 3系を使い続けるのもなんだなーと思いつつあったんで、ローカルテスト環境も4.1系でいいか。

Published At2006-03-29 00:00Updated At2006-03-29 00:00

日記
date.timezoneEdit

PHP 5では、php.iniにdate.timezoneという項目が追加されていて、環境変数TZがセットされていないようなシステムで、date.timezoneも設定されていない場合はwarningが出るようになったのね。

Published At2006-03-29 00:00Updated At2006-03-29 00:00