環境: CakePHP 1.3
CakePHPのModelクラスには直接トランザクションを制御するメソッドがない。そこで例えば次のようにAppModelにメソッドを定義する。
class AppModel extends Model {
function begin() {
return $this->getDataSource()->begin($this);
}
function commit() {
return $this->getDataSource()->commit($this);
}
function rollback() {
return $this->getDataSource()->rollback($this);
}
}
しかしこの方法ではテストがやりにくい。明示的にトランザクションを制御したい時というのは、複数のテーブルにまたがる複雑なロジックを組むことが多いので、commit/rollbackされたことを確認するのにいちいちDBの状態を確認していては手間がかかりすぎる。
そもそも「commitされたらそれまでの実行結果が反映される」「rollbackされたらトランザクション開始前の状態に戻る」というのはDBの機能なので、改めてテストし直す意味はない。アプリケーションのテストとしてはcommit/rollbackが発行されたことさえ分かればいい。
そこでAppModelで定義する代わりに、専用のモデルクラスを作ってそこでメソッドの定義を行うことにした。$useTable を false にしてもDataSource自体は取得できるというのがポイントである。
class TransactionManager extends AppModel {
var $useTable = false;
function begin() {
return $this->getDataSource()->begin($this);
}
function commit() {
return $this->getDataSource()->commit($this);
}
function rollback() {
return $this->getDataSource()->rollback($this);
}
}
これでMockを使ったテストができるようになった。トランザクションを使用する際は ClassRegistry 経由でこのモデルを取得する。
$tx = ClassRegistry::init('TransactionManager');
if($tx->begin()) {
//...
if(doSomething() && $tx->commit()) {
return $values;
}
}
$tx->rollback();
return false;
テスト時には ClassRegistry にMockのインスタンスを登録しておいて、begin/commit/rollbackの各メソッドが呼び出されたことだけを調べる。もちろんそれぞれの操作が失敗した場合のテストもできる。
function startTest() {
$this->tx = new MockTransactionManager;
ClassRegistry::addObject('TransactionManager', $this->tx);
}
function testA() {
$this->tx->expectOnce('begin');
$this->tx->setReturnValue('begin', true);
$this->tx->expectOnce('commit');
$this->tx->setReturnValue('commit', true);
$this->tx->expectNever('rollback');
//...
}
環境: CakePHP 1.3
特定のフィールドの値をシリアライズしてから保存する Serializable ビヘイビアを作りました。配列などの構造を持つデータを、ひとつのフィールドに保存することができます。
serializable.php – gist
(いずれ正式なリポジトリに移すかもしれない)
fields オプションに指定したフィールドがsave時にJSONでエンコードして保存され、find時には自動的にデコードされます(PHP標準の serialize ではなくJSONを使用するのは互換性を重視しているため)。
var $actsAs = array('Serializable'
=> array('fields' => array('field1', 'field2')));
// 保存する前に自動的にJSONエンコードされる
$Model->save(array(
'field1' => array(1,2,3),
'field2' => array('assoc' => 'data')
));
// find時に自動でデコードされる
$values = $Model->find('first');
// field1 == array(1,2,3)
// field2 == array('assoc' => 'data')
この種のフィールド・シリアライズには様々なバリエーションが考えられます:
- JSON以外の形式でエンコードする
- 複数のフィールドの値をひとつにまとめて保存する
- シリアライズしたフィールドを別のモデルにマップして、hasOne関連のように扱えるようにする
このbehaviorは最も単純な作りになっているので、適当なメソッドを上書きすれば上のような動作も実現できるのではないかと思います。
環境: CakePHP 1.3
モデル用のコールバック関数を動的に登録/解除できるようにする Observable Behaviorを作りました。コールバックを使用するためだけにサブクラスを作ったりビヘイビアを作ったりする必要がなくなります。
observable.php – gist
(いずれ正式なリポジトリに移すかもしれない)
callable なオブジェクトなら何でもコールバックとして使用できますが、PHP 5.3のクロージャと組み合わせると特に便利です。
$actsAs = array('Observable');
// addObserver メソッドで登録。戻り値は一意なid
$oid = $Model->addObserver('beforeSave', function($model) {
// 第1引数はモデル、それ以後は通常のコールバック引数
$model->data[$model->alias] = ....;
// false を返すとコールバックの実行が停止する
// 何も返さなかった場合(null)は true を返したのと同じことになる
//return false;
});
// 不要になったら取り除く
$Model->removeObserver($oid);
コメント中にもある通り、明示的に引数を返さなかった場合は true と見なされるので、 beforeSave や beforeValidate で戻り値を忘れてその後の処理が実行されない……といったことはありません。
5.2以前でも使用できるmcryptについてはこちら。
PHP 5.3でopensslモジュールの機能が拡張され、共通鍵暗号による暗号化が利用可能になった。同時に待望の疑似乱数生成器も用意された。これで今までmcryptが使用できなかったWindowsでも、暗号学的強度を持った乱数が生成できる(実際に試していないが、OpenSSLはWindowsのCryptAPIから乱数を得ている)。
まだマニュアルにも詳しい情報が載っていないので、ソースを読みながら使い方を探ってみた。以下はphp-5.3.3のソースに基づく。ただしコミットログを見る限り仕様も機能もまだ安定しているとは言いがたいので、実際に使用するにはもう少し様子を見た方が良いかもしれない。
暗号化する際に決めておくべきこと:
- 鍵(以下 KEY)… マニュアルでは
$password と表記されているが、いわゆる「パスワード」ではない。暗号化に使用する鍵であり、十分にランダムでなければならない。
- 暗号アルゴリズム+モード(以下 CIPHER)… 利用可能な一覧は openssl_get_cipher_methods から得られる。
暗号化/復号を行う関数 encrypt/decrypt は次のようになる。
//実際の暗号化/復号を行う補助関数
function _callOpenSSL($func, $msg, $iv) {
$opensslFunc = "openssl_{$func}";
return $opensslFunc($msg, CIPHER, KEY, true, $iv);
};
/**
* @param string 暗号化したいメッセージ
* @return array (暗号文, 初期化ベクトル) の配列
*/
function encrypt($msg)
{
//初期化ベクトルを生成
$ivSize = openssl_cipher_iv_length(CIPHER);
$iv = openssl_random_pseudo_bytes($ivSize, $cryptStrong);
if($iv === false || !$cryptStrong) {
// 乱数生成失敗
return false;
}
//ダミーの初期化ベクトル
$dummyIV = str_repeat("x", $ivSize);
//メッセージの暗号化
$cryptMsg = _callOpenSSL('encrypt', $msg, $iv);
//初期化ベクトルの暗号化
$cryptIV = _callOpenSSL('encrypt', $iv, $dummyIV);
return array($cryptMsg, $cryptIV);
}
/**
* @param string 暗号文
* @param string 初期化ベクトル
* @return string 平文
*/
function decrypt($cryptMsg, $cryptIV)
{
//ダミーの初期化ベクトルを生成
$ivSize = openssl_cipher_iv_length(CIPHER);
$dummyIV = str_repeat("x", $ivSize);
//初期化ベクトルの復号
$iv = _callOpenSSL('decrypt', $cryptIV, $dummyIV);
//メッセージの復号
$msg = _callOpenSSL('decrypt', $cryptMsg, $iv);
return $msg;
}
実行:
define('CIPHER', 'aes-128-cbc');
define('KEY', '...暗号化のキー...');
$msg = "めっせーじ";
// 暗号化
$crypt = encrypt($msg);
var_dump(bin2hex($crypt[0]));
// 復号
$plain = decrypt($crypt[0], $crypt[1]);
var_dump($plain);
mcrytと違ってブロック長に合わせたパディング(PKCS#5パディング)はopensslが自動でやってくれるので、自前でbase64エンコードしたりする必要はない。
openssl_random_pseudo_bytes の戻り値は、乱数の生成に失敗した場合は真偽値の false になる。また十分なエントロピーが得られなかった場合は第二引数に指定した変数が false になる(ただ滅多に起こらないはずである:OpenSSL FAQ参照)。
ちなみに鍵の生成も openssl_random_pseudo_bytes で行えばいい。
#コマンドラインにて16バイト=128ビットの乱数を生成
$ php -r 'echo base64_encode(openssl_random_pseudo_bytes(16));
bONq0KiSNIO5ww1ggwdFdQ==
//定数KEYの定義
define('KEY', base64_decode('bONq0KiSNIO5ww1ggwdFdQ=='));
パスワードから鍵を生成する必要がある場合は、自分でPBKDFを実装する必要がある。『PHP PBKDF2』で検索するといくらか実装例が見つかるようだ。またRubyの pkcs5_keyivgen(openssl の EVP_BytesToKey)互換の機能が必要な場合はOpenSSL::Cipher::Cipher#pkcs5_keyivgen の中身などを参照のこと。
openssl_random_pseudo_bytes の中身
実体はOpenSSLの RAND_pseudo_bytes(rand_lib.c) -> ssleay_rand_pseudo_bytes(md_rand.c) -> ssleay_rand_bytes(md_rand.c) で、そこから先は『暗号乱数インフラの初期化処理』を参照のこと。
ssleay_rand_pseudo_bytes と ssleay_rand_bytes の違いは RAND_R_PRNG_NOT_SEEDED を見逃すか否か。エラーがあったこと自体は戻り値で判定できるので、$crypto_strong ではこれを利用している。
環境: CakePHP 1.3
CakeDCのSearchプラグインは、全体としては非常に優れたプラグインなのですが、Prgコンポーネントの機能がどうしても私の使い方には合いませんでした。具体的に言うと
- 検索パラメータの引き継ぎにnamedを使うのは問題が多い(参考)。代わりにQueryStringを使いたい。
- Searchableビヘイビアと同じような内容の $filterArgs を書くのが手間。
- そもそも値の検証や制限はモデルでやるべき、と考える。コンポーネント内でバリデーションを実行したりDBアクセスまでするのは仕事のし過ぎ。
これらの点を解消するため、昔作ったSearchPaginationプラグインを全面改訂して、Searchプラグインと連携できるようにしました。Prgコンポーネントの代わりに使うことができます。
http://github.com/tkyk/cakephp-search-pagination (旧版は 1.2 ブランチを参照)
使い方は非常に簡単で、コントローラの中で SearchPagination コンポーネントの setup メソッドを実行するだけです。
class UsersController extends AppController {
var $components = array('SearchPagination.SearchPagination');
function search() {
$this->SearchPagination->setup('User');
//...
}
}
これによってQueryStringから検索パラメータが取得され、data[ModelName] に格納されるので、あとはそれを Searchable ビヘイビアの parseCriteria に渡すなどして、検索条件を組み立てます。
function search() {
$this->SearchPagination->setup('User');
// 常に $this->data['User'] に検索パラメータが入っている
$this->paginate['conditions'] = $this->User->parseCriteria($this->data['User']);
$this->set('users', $this->paginate('User'));
$this->set('groups', $this->User->Group->find('list'));
}
ビューでは特に何もしなくても自動的に検索パラメータが引き継がれます。普通にFormヘルパーやPaginatorヘルパーを使ってください。
echo "<h2>Search Form<h2>";
echo $this->Form->create('User', array('type' => 'get'));
echo $this->Form->input('username');
echo $this->Form->input('created');
echo $this->Form->input('group_id');
echo $this->Form->end('Search');
echo $this->Paginator->numbers(array('modulus' => 10));
Formヘルパーのcreateメソッドには、typeとして ‘get’ を指定することをおすすめします。もし指定しなかった場合は(Prgコンポーネントに倣って)自動的にGET用URLを生成してリダイレクトされます。
- 症状:
- iPhone 3GSをiOS4にアップデートして以来、バッテリーの減りが異常に早くなった。
- 原因:
- (多分)スリープ中もWi-Fiが有効になる状態だったから。
- 解決策:
- (私の場合)スリープボタン+ホームボタン長押しで強制リセットしたら直った。
去年12月に買ったiPhone 3GS。以前は2日充電しなくても余裕があったのに、iOS4にアップデートして以来、1日放置しておくだけで残り15%近くまでバッテリーを消耗する状態になった。「iOS4 バッテリー」というキーワードで検索してみると、同様の症状を訴える人はたくさんいる。中でも『iOS4ではスリープ中でもWi-Fiがオフにならず、そのせいでバッテリーを早く消耗する』という情報が気になった。言われてみれば、iOS4にして以来、スリープから復帰させる時にWi-Fiのシグナルがいつも立っている。以前なら0.5秒〜1秒くらいの間があって「3G」→「Wi-Fiシグナル」と表示が切り替わっていたのに。
しかし特に問題がない人も多いところを見ると、これがiOS4の正規の挙動なのかどうかは怪しい。とりあえずものは試しと、買ってから初めて強制リセットを試してみた。方法は右上のスリープ/スリープ解除ボタンとホームボタンを押し続け、「電源オフ」のスライダが出ても押し続け、画面が暗転して銀色のリンゴが出てくるまで待つ。…すると何かの設定がリセットされたのか、Wi-Fiの挙動が以前の状態に戻り、バッテリーの異常な消耗も解消された。
必ずこの方法で復旧するとは言い切れないが、同様の問題で悩んでいる方は一度試してみると良いかと。
環境: CakePHP 1.3
CakePHPはURLパラメータのエンコードを一切行わないため、特殊な文字がパラメータに入り込むと容易にルーティングが破綻する。ここで言う「URLパラメータ」とはRoute中に埋め込まれたパラメータ、namedパラメータ、passパラメータのことを指す。
例えば以下のようなRouteがあったとする。
/controller/action/:keyword
ここでパラメータkeywordに特殊な文字を与えると…
keyword => '%'
URL: /controller/action/%
結果: URLとして不正な形式なので"400 Bad Request"になる
keyword => '?foo'
URL: /controller/action/?foo
結果: ?以降はQueryStringと見なされてルーティングから除外され、Routeにマッチしなくなる
keyword => 'a/b/c'
URL: /controller/action/a/b/c
結果: /がそのままパラメータ区切りになるのでRouteにマッチしなくなる
この問題は単純に rawurlencode/rawurldecode を使うだけでは回避できない。またCakeの設計上の問題なので、簡単な修正方法もない。よって任意の文字が含まれる可能性のあるパラメータをURLに埋め込んではいけない。最も簡単な回避策は、URLパラメータを諦めてQueryStringを使うことである。
Router::url(array('?' => array('keyword' => $any_characters)));
問題の原因と回避策の考察
なぜ rawurlencode で回避できないかというと、CakePHPが mod_rewrite 経由で $_GET['url'] から元のURLを取得しているからだ。
RewriteRule ^(.*)$ index.php?url=$1 [QSA,L]
RewriteRuleの後方参照($1の部分)では%エンコーディングのデコードが行われる。またQueryStringから $_GET に格納される際にもPHPによってデコードが行われる。つまりCakeのルーティングが実行される前に、既に2回のデコードが行われている。よってもしこの問題を rawurlencode で回避するなら、3重に rawurlencode した上で rawurldecode しなければならない、ということになる。
ちなみに RewriteRule の[B]オプションを指定することで後方参照のデコードを回避することができる。この場合は rawurlencode は2回で済む。しかしそれでもURLが汚くなることは変わりないし、検索エンジンのロボット等もURL中の文字列を認識してくれないだろう。
RewriteRule ^(.*)$ index.php?url=$1 [QSA,L,B]
別の考え方として、%エンコーディング以外の安全なエンコーディングを用いる方法もある。例えば16進数のバイナリ表現に変換すれば、[a-f0-9]しか現れないので確実に安全である。
$encoded = bin2hex($data);
//[0-9a-f]しか含まれないことを事前にチェックしておくこと
$decoded = pack('H*', $encoded);
ただしこの方法だと長さは元の文字列の2倍になるし、可読性も全く損なわれる。ここまでするくらいならやはりQueryStringを使った方が良いと思う。
7月3日に福岡で開催されたCakePHP勉強会で、『MarkupHelperの紹介および PHPによるDSL実装の可能性について』という題で発表を行いました。以下に資料をアップロードしておきます(右キーで進む、左キーで戻る、上キーでサムネイル表示切り替え)。
資料へのリンク
※Studio ODIN – 高橋メソッド風 プレゼンスクリプトを使用しています。発表本番では改造版を使ったのですが、ライセンスが不明なので本家のスクリプトに直接リンクを貼っています。
githubのMarkupHelperリポジトリはこちら:
http://github.com/tkyk/cakephp-markup-helper
補足
質疑応答より。多少内容を変更しています。
- Q. デザイナーに触ってもらうのは難しいのではないか?
-
A. このヘルパーはもともと「プログラマーがHTMLを書くのを楽にしよう」というコンセプトで作られているため、デザイナーとの連携が必要になる場面では使用できない・使用すべきでないと言えます。(デザインが比較的どうでもいい)内部向けの管理画面、ごく小さなelement、HTMLを生成する別のヘルパーの中、などで使用するのに向いています。
- 別の言い方をするとコードの分量が HTML > PHP の場合には使う意味は薄いです。コードの分量が PHP >= HTML の場合に威力を発揮します。
- Q. XHTML以外のマークアップ言語を作ることはできるか?
-
A. MarkupHelperは「正しいXHTMLのタグか否か」をチェックしていないので、基本的にはどんなマークアップでも作ることができるはずです。ただし「そのタグが閉じタグを持つか否か」という情報を $emptyElements プロパティ(public)で管理しているので、必要に応じて再定義する必要があります。
- Q. ループや条件分岐をメソッドチェーンに組み込めるか?
-
A.PHP言語の機能的な制約により、不可能です(PHP5.3のクロージャを使えば一応はできるかもしれませんが、書式が煩雑なのでDSLとしては実装できないでしょう)。
- ループや条件分岐に相当する処理を入れつつメソッドチェーンを切りたくない場合は、その部分の処理を別のヘルパーのメソッドにまとめて連結することをおすすめします。
$markup->ul->MyHelper_loop_li($arr)->end;
……
勉強会で発表するのが初めてだったので緊張してしまい、発表中は聞いている人の反応を見ている余裕がありませんでした。早口に一気にまくしたててしまったような気がして、理解してもらえたかどうか不安だったのですが、あとで感想を聞くとそれなりに好評だったようでほっとしました。それから高橋メソッドによる大きな文字は、Ustream経由でも見やすかったようです。
勉強会の進行は(いくつか想定外の事態があったにも関わらず)大変スムーズで、気持ちよく発表できました。@k1LoWさんはじめ株式会社Fusicの皆さん、素晴らしい時間をありがとうございました。
福岡は魚もお酒も美味しくて、2次会以降ついつい飲み過ぎてしまいました。酔った勢いで、特に3次会ではずいぶん大口を叩いてしまったような記憶があります…。酔っぱらいの戯言が戯言でなくなるように、もっと精進していこうと思います。
読書
| 2010 年 6 月 12 日
By tkyk |

予想どおりに不合理―行動経済学が明かす「あなたがそれを選ぶわけ」の感想。
経済学が想定する「合理性」人間とは、しばしば誤解されるような「あらゆる情報を加味して損得計算ができる」ことではなく、せいぜい「自分にとって何が特で何が損かは、自分が一番良く知っている」程度の意味だと、『世界一シンプルな経済入門』にはあった。これは一見無理のない想定に思えるが、しかし本書によれば、現実の人間はその程度の緩い想定すら満たさないほど、合理性とはかけ離れた存在であるらしい。つまり自分で自分が何をどの程度求めているか分からず、様々な要素(相対性や社会規範や一時の感情など)に支配されて容易に不合理な選択をして失敗して、しかもそのことに気がつかない。
ただし著者らによる多くの実験が明らかにするところによれば、その不合理さには秩序がある。失敗の仕方に規則性があり、予想通りの仕方で失敗する。まさに「予想通りに不合理」というわけだ。だから「合理的な」人間の代わりに「予想通りに不合理な」人間という想定を組み込むことで、経済学を改良できるのではないか。そして不合理さ故に手つかずになっている部分を改善することで、ある種の”フリーランチ”が手に入るのではないか。それが「行動経済学」というもののアプローチだという。
いくつか特に印象深かった点。
保有効果(主に7章)
人が何かを手に入れると、他の人よりもその「何か」を高く評価するようになる。早い話、愛着が湧くとそれ以前よりも良く思えるということ。またそれ故に、人は得ることよりも失うことの方に大きな感情を抱く。
この保有効果を商売に利用したものとして、組み立て式の家具(苦労するほど所有意識が高まる)、ネットオークションや大部分の広告(所有する前に所有意識を発生させる)、無料お試し期間(一旦所有意識を植え付けてしまえば離れにくくなる)、などが紹介されている。
所有意識は物質的なものに限らず、思想や宗教、アイデアなどにも当てはまる。一旦それを手に入れてしまうと、その思想なり何なりを失うことに耐えられず、実際以上にありがたがってしまう危険がある。これは「アイデアと結婚してはいけない」という『まぐれ』に書いてあった警句とも符合する。
知識、予測と経験(主に9章、10章)
美味しそうだと思って飲むと美味しく、不味そうだと思って飲むと不味くなる。「そういう意見を持つようになる」という意味ではなく、実際に味覚が変わってしまう。
「自分がどのようなステレオタイプで見られているか」認識するだけで、行動が変わるといったことも起こる。またブラセボやブラセボ手術が実際に効いてしまうのも同様の現象と言える。近年に入るまで、ほとんどの薬はブラセボだったらしい…。また薬よりさらに実験するのが難しい「手術」については、現代でも多くのブラセボが含まれている可能性があるとのこと。この点はいずれ研修医の友人に意見を聞いてみたいところだ…。
CakePHP 1.3 + SimpleTestでグローバル関数をMock化するためのラッパークラスを作ってみました。libs/ に置いて使用します。
global_function.php – gist
次のように任意の関数を呼び出せます。
//bootstrap.php 等で読み込んでおく
App::import('Lib', 'GlobalFunction');
GlobalFunction()->env('REMOTE_ADDR');
GlobalFunction()->gethostbyname('example.com');
テストケースでMockを使用したいとき、GlobalFunction::registerMock を呼び出します。戻り値はMockオブジェクトです。
function testRegisterMock() {
//使用する関数名を引数に渡す
$mock = GlobalFunction::registerMock('env', 'gethostbyname');
//通常のmockと同様に使用できる
$returnValue = '192.168.0.1';
$mock->expectOnce('env', array('REMOTE_ADDR'));
$mock->setReturnValue('env', $returnValue, array('REMOTE_ADDR'));
$mock->expectNever('gethostbyname');
$this->assertEqual($returnValue, GlobalFunction()->env('REMOTE_ADDR'));
//ClassRegistry::flush を呼び出せば元に戻る
ClassRegistry::flush();
//本物の関数envが呼ばれる
$addr = GlobalFunction()->env('REMOTE_ADDR');
}
注意:参照を引数に取る関数(array_push など)は使用できません。