Tags: 技術日記

技術日記
MySQLのクエリーキャッシュが効かないEdit

なんかやけにパフォーマンスが出ないなーと思ったら、MySQLのクエリーキャッシュが全然効いていなかった。なんでだろうと思ったら、

  • Zend_Db_Adapter_Pdo_Mysqlは必ずプリペアードステートメントとしてMySQLにクエリーを流す
  • プリペアードステートメントに対しては、MySQLのクエリーキャッシュが効かない

という合わせ技だった。

そんな罠があったのかよ。MySQL 4.1のマニュアルには書いてなかったけど、MySQL 5.0のマニュアルには「It was issued as a prepared statement, even if no placeholders were employed. 」と書いてあったよ。せめてプレースホルダーがないパターンだけでもキャッシュしてくれればいいのに。

プリペアードステートメントを使わないバージョンの、Zend_Db_Adapter_Mysqlってどこかに転がってないかなー。がんばって書くしかないかなー。と思いつつも、ひとまずアプリケーションレイヤーのキャッシュを追加して対応。でも面倒くさい。

続報あり

Published At2007-02-06 00:00Updated At2019-12-31 00:02

技術日記
estmasterの_logがEdit

HyperEstraierのノードマスタの_logが2Gバイトに達して、「File size limit exceeded」でこけていた。っつーかなにが「File size limit exceeded」なのかなかなか気づけなかったorz……。

Published At2007-01-22 00:00Updated At2019-12-31 00:02

技術日記
Zend_Viewの仕様変更Edit

PHPプロ!マガジン Vol.2のZend Frameworkの記事に関して、以前読者の方から「テンプレートファイル内で別テンプレートをレンダリングするときには、<?php $this->render('path/to/template'); ?>じゃなくて、<?php echo $this->render('path/to/template'); ?>じゃないの?」という問い合わせがきたんだけど、そのときは手元のZend Framework Preview 0.2.0の実装を見て、

 通常のZend_View::render()では、結果を出力するためにecho文等が必要になりますが、テンプレートファイル内で$this->render()する際には、echoは不要となります。

 Zend_View_Abstract::render()の実装を見ると分かりますが、入れ子になったテンプレートのレンダリング処理では、出力内容は戻り値として返されません。

※子テンプレートのレンダリング結果は、親テンプレートと同一の出力バッファ(ob_*関数参照)に保存され、親テンプレートのレンダリング処理が終わった後に、まとめて結果として返されます

 出力バッファを利用した処理となっているので、

<?php echo $this->render('headerMenu.php') ?>

 と書いても特に問題はありませんが、echoを書く意味はありません(この場合のechoは「echo NULL;」相当になります)。

と答えたんだけど、Zend Framework Preview 0.6.0バージョンのZend_Viewを見たら、いつのまにかZend_View::render()メソッドは必ず結果を戻り値として返すようになっていて、テンプレート内から他のテンプレートを呼び出す際にもecho文が必須になっちゃっているよ!

というわけで、問い合わせをいただいた方&私の今まで書いたサンプルコードを見ていた方は、今後はテンプレート内で$this->render()する場合にも必ずechoする必要がありますんで、注意してください。

Published At2007-01-17 00:00Updated At2019-12-31 00:02

技術日記
PHPプロ!MagazineにZend Frameworkの記事を書きましたEdit

PHPプロ!Magazine2007年冬号にZend Frameworkの記事を書きました。内容は、Preview 0.2.0を使ったサンプルblogアプリケーションの解説、今後の動向(ロードマップ解説)などになります。サンプルアプリのコードは、この辺に転がっています。

svn co http://svn.ishinao.net/webxp/PHPPro06winter/sample1/

とかでどうぞ。一般論的な紹介は前にWEB+DB PRESS誌に書いたので、今回はサンプルアプリのコード解説を中心に書いています。詳細に書きすぎて依頼されたページ数を大幅に越える30ページ近くも書いてしまいましたが、PDF雑誌なのでそのまま載せてもらいました。Synfony、Ethna、Piece、Cakeの記事もあるんで、PHPフレームワークの選定に迷っている方は是非どうぞ。立ち読みもできるんですが、Zend Frameworkの記事は出だしの一般論的紹介部分しか読めないんで、そこだけ読んでもあんまり面白くないと思います。

Published At2006-12-25 00:00Updated At2019-12-31 00:02

技術日記
Zend_Mail_Transport_Smtpで「.」を含むメール本文が正しく送られないバグEdit

メール本文中に記載しているURLの「.」の一部が欠けてしまうというバグに遭遇し、しばらくはまっていたんだけど、SMTP transport is not prepending dots to lines beginning with a dotが原因だった。

Published At2006-12-19 00:00Updated At2019-12-31 00:02

技術日記
Zend Framework 0.2.0調査メモEdit

0.2.0のアーカイブをながめながら、気になったところをメモ。

Zend_Cacheがincubatorからlibraryに格上げされている。incubatorの頃から使っているんで、特に俺的には新鮮みはないけれども、一応まとめておく。キャッシュの利用方法を規定するFrontendと、キャッシュの格納方法を規定するBackendの二つのクラスで構成される。Frontendはクラスのメソッド呼び出しをキャッシュするZend_Cache_Frontend_Class、ファイルの読み込みをキャッシュするZend_Cache_Frontend_File、関数呼び出しをキャッシュするZend_Cache_Frontend_Function、外部(HTML)出力をキャッシュするZend_Cache_Frontend_Output、ページ単位の出力をキャッシュするZend_Cache_Frontend_Page(Outputキャッシュとほとんど同じで、キャッシュIDをURL等から自動生成する)が用意されている。BackendはファイルキャッシュのZend_Cache_Backend_File、APCを使ったメモリキャッシュのZend_Cache_Backend_APC、memcachedを使ったメモリキャッシュのZend_Cache_Backend_Memcached、SQLiteを使ったZend_Cache_Backend_Sqliteが用意されている。ただしAPCとMemcachedはタグ機能がサポートされていないんで、機能限定版だと思っておいた方がいい。ちなみに自分ではMySQLバックエンドを書いて使っている。

Zend_Configもincubatorからlibraryに格上げされている。階層構造を持つ設定情報を、PHPのstdclassでプロパティアクセスするようなインターフェースにマッピングして扱えるようにするクラス。連想配列なんかよりも扱いやすくて便利そうなんだけど、データソースとして現状でINIファイル、XMLファイル、PHPの連想配列にしか対応していないんで使ってない。Spycとかのラッパーを書いてYAML対応してもいいんだけど、それならSpycをそのまま使ったって別にかまわないしなー。

ルーターは、Zend_Controller_RewriteRouterがlibraryに格上げされている。ってのは0.1.4からだっけ? RewriteRouter自体は悪くないんだけど、これの完成度が上がる前に、自前で似たようなルーターを書いちゃったから今のところ使ってない。将来的なメンテナンス性とかを考えると、そのうちRewriteRouterに乗り換えるかもしれないけど。RewriteRouterは機能的にはあまり文句はないんだけど、YAMLで設定を書けないところが面倒くさいよなー。っつーか、Zend Frameworkは早いところYAML対応して欲しいなー。ゼロから書き起こす縛りがなければ、YAML対応するのなんて簡単だろうに。

Zend_Http_Clientは、昔incubatorで開発されていたバージョンは没になって、構成がずいぶん変更されているっぽい。Zend_Http_Requestが導入されたんで、Zend_Http_Client本体を高機能に拡張するのはやめて、Zend_Http_Client、Zend_Http_Request、Zend_Http_Responseで役割を分割するようにしたのかな。っつーことは、旧incubatorバージョンを採用していた俺のコードは、すべて書き直す必要がありそうだ……。まあどちらにしろ、Zend_Http_Request対応する必要があるから、そのあたりは大規模に書き換える必要があるけど。

ちなみにincubatorではコントローラ周りにも結構大きな仕様変更が行われつつある。ルーティング結果を保持するZend_Controller_Dispatcher_Tokenがなくなり、代わりにZend_Controller_Request_Httpが採用される。これは、従来のTokenと同様にURLから解決されたコントローラ名、アクション名、パラメータを保持するだけでなく、$_GET、$_POSTなどの入力値も管理する機能を持つ。今まではURLから解決できる値はTokenが持ち、それ以外の入力値はZend_Filter_Inputあたりを使って解決するという、中途半端な仕様になっていたんだけど、Zend_Controller_Request_Httpが導入されることによって、ようやく入力値を統一的に扱うことができるようになる。また、こうやって入力値を統一的に扱うことができるようになると、アクションレイヤーのテストなんかで仮想的なRequestを渡してテストを実行できるようになったりするんで、そういう意味でも便利。ちなみにZend_Controller_Request_HttpはZend_Http_Requestを内部にもっているんで、その辺が前段落の話とつながってくる。ちなみに俺ルーターはすでに同様の機能を持っちゃってるのが、今後Zend Framework本家版に合流するのを難しくしてしまっている。

Zend_Aclはまだincubator止まりだな。これはいつになったら格上げされるんだろう? ちなみにZend_Aclの構成としては、アクセス制御される対象物を管理するACL(Zend_Acl。$acl = new Zend_Acl();)と、アクセスを行う実行者グループを管理するARO(Zend_Acl_Aro。$aro = $acl->aroRegistry();)があり、まずAROとして実行者グループを登録し($aro->add('somegroup'); // somegroupというグループを登録。$aro->add('someperson', $aro->somegroup); // somegroupに所属するsomepersonを登録)、続いてACLにどの対象物に対してどのAROがどういう権限を持つか設定していく($acl->deny(); // デフォルトは拒否。$acl->someobject->allow('somegroup'); // someobjectという対象物に対してsomegroupのアクセスは許可する。$acl->someobject->somepart->deny('someperson'); // someobjectの一部であるsomepartに対してsomepersonのアクセスを拒否する)。権限は基本的には許可(allow)と拒否(deny)だけど、対象物+アクションに対して権限を設定することもできる($acl->someobject->deny('somegroup', 'hit'); // 叩いちゃダメ)。で、実際にアクセス権があるかどうかはvalidメソッドでテストする(if ($acl->someobject->valid('someperson') { // アクセス権がある } )。

ZFormは進展なしな模様。っつーかいまだに命名規則すらZend_Formになってないし。

Zend_Json_Serverとかいうものがincubatorに入っているな。っつーかこれは、Zend_Serverってのが増えた中の実装の一つか。XMLRPCサーバーとかSOAPサーバーとかを書くためのZend_Serverってのができて、その実装の一つとしてJSONサーバーがあるのね。コードを読んだ限りでは、$_REQUEST['method']で実行したいメソッド名を渡し、$_REQUEST内からメソッド引数を抽出し、サーバーに登録されているメソッドだったらそのメソッド+引数を実行。結果はapplication/jsonとして出力するって感じらしい。こういう仕様のJSONサーバーって、どこかで規格化されていたりするのかな? RESTサーバーの返しはXMLになっている模様。SOAPとXMLRPCはまあちゃんと規格があるからコードは読まなくていいだろう。

Zend_Sessionもincubatorに入っている。ネームスペースの導入、バリデーションの共通インターフェース、時間もしくは回数による有効期限設定あたりが主な拡張かな。セッションハンドラーを拡張するインターフェースは用意されているけれども、まだハンドラーの実装はない模様。有効期限については、1回有効期限のセッションを利用すれば、RoRとかのFlashみたいな使い方ができるな。

ってあたりを押さえておけば、0.1.4から0.2.0へのアップデートに関してはだいたい大丈夫かな? その他の修正やアップデートに関しては、実際に使ってみた何かあったらチェックしよう。

Published At2006-11-21 00:00Updated At2019-12-31 00:03

技術日記
PHPで安全なセッション管理を実現する方法に対する高木さんのコメントへのフォローEdit

高木さんのはてなブックマークコメントに、

[セキュリティ][乱数][暗号][PHP][moderate] PHPはセッションID生成にsecureな擬似乱数生成系を使用していないようだ。さすがPHPらしい駄目っぷり。

とあって、そこから人がたくさん来ているらしいんで、ちょっとだけフォロー。PHPのセッションID生成は、

sprintf(buf, "%.15s%ld%ld%0.8f", remote_addr ? remote_addr : "",
tv.tv_sec, (long int)tv.tv_usec, php_combined_lcg(TSRMLS_C) * 10);

なんて感じで、マイクロ秒単位の現在時刻+ユーザーのリモートアドレス+combined-LCG(線形合同法による乱数2つを組み合わせているらしい。線形合同法自体は、疑似乱数生成方法としてはセキュアな方法ではないとされている)による乱数を使って生成されているんだけど、php.iniとかで、

session.entropy_file = /dev/urandom
session.entropy_length = 16

とか設定しておけば、そっちも組み合わせて使われます(/dev/urandomとかが使える環境ならば)。なんで、この設定を有効にしておけば、PHPのセッションID生成もセキュアな感じになるんじゃないでしょうか。

ただわたしはセッションID生成におけるセキュリティの問題として、「マイクロ秒単位の現在時刻+ユーザーのリモートアドレス+combined-LCGによる乱数」という方法にはどの程度の問題があり、それに「/dev/urandom」を組み合わせることでどのくらい安全性が高まるのか、とかよく分かってませんが。

PHP 5.2.0の該当コード(ext/session/session.c)を貼っておきますよ

entropy_lengthとentropy_fileがphp.iniなどからやってくるsession.entropy_lengthとsession.entropy_file設定。デフォルトはどちらも空。hash_funcはsession.hash_function設定(PHP 5以降で有効)から来ていてデフォルトはMD5(値としては0)になっている。

PHPAPI char *php_session_create_id(PS_CREATE_SID_ARGS)
{
PHP_MD5_CTX md5_context;
PHP_SHA1_CTX sha1_context;
unsigned char digest[21];
int digest_len;
int j;
char *buf;
struct timeval tv;
zval **array;
zval **token;
char *remote_addr = NULL;
gettimeofday(&tv, NULL);
if (zend_hash_find(&EG(symbol_table), "_SERVER",
sizeof("_SERVER"), (void **) &array) == SUCCESS &&
Z_TYPE_PP(array) == IS_ARRAY &&
zend_hash_find(Z_ARRVAL_PP(array), "REMOTE_ADDR",
sizeof("REMOTE_ADDR"), (void **) &token) == SUCCESS) {
remote_addr = Z_STRVAL_PP(token);
}
buf = emalloc(100);
/* maximum 15+19+19+10 bytes */
sprintf(buf, "%.15s%ld%ld%0.8f", remote_addr ? remote_addr : "",
tv.tv_sec, (long int)tv.tv_usec, php_combined_lcg(TSRMLS_C) * 10);
switch (PS(hash_func)) {
case PS_HASH_FUNC_MD5:
PHP_MD5Init(&md5_context);
PHP_MD5Update(&md5_context, (unsigned char *) buf, strlen(buf));
digest_len = 16;
break;
case PS_HASH_FUNC_SHA1:
PHP_SHA1Init(&sha1_context);
PHP_SHA1Update(&sha1_context, (unsigned char *) buf, strlen(buf));
digest_len = 20;
break;
default:
php_error_docref(NULL TSRMLS_CC, E_ERROR, "Invalid session hash function");
efree(buf);
return NULL;
}
if (PS(entropy_length) > 0) {
int fd;
fd = VCWD_OPEN(PS(entropy_file), O_RDONLY);
if (fd >= 0) {
unsigned char rbuf[2048];
int n;
int to_read = PS(entropy_length);
while (to_read > 0) {
n = read(fd, rbuf, MIN(to_read, sizeof(rbuf)));
if (n <= 0) break;
switch (PS(hash_func)) {
case PS_HASH_FUNC_MD5:
PHP_MD5Update(&md5_context, rbuf, n);
break;
case PS_HASH_FUNC_SHA1:
PHP_SHA1Update(&sha1_context, rbuf, n);
break;
}
to_read -= n;
}
close(fd);
}
}
switch (PS(hash_func)) {
case PS_HASH_FUNC_MD5:
PHP_MD5Final(digest, &md5_context);
break;
case PS_HASH_FUNC_SHA1:
PHP_SHA1Final(digest, &sha1_context);
break;
}
if (PS(hash_bits_per_character) < 4
|| PS(hash_bits_per_character) > 6) {
PS(hash_bits_per_character) = 4;
php_error_docref(NULL TSRMLS_CC, E_WARNING, "The ini setting hash_bits_per_character is out of range (should be 4, 5, or 6) - using 4 for now");
}
j = (int) (bin_to_readable((char *)digest, digest_len, buf, PS(hash_bits_per_character)) - buf);
if (newlen)
*newlen = j;
return buf;
}

Published At2006-11-20 00:00Updated At2019-12-31 00:03