Displaying posts written in

8月 2010

CakePHPでトランザクション制御

環境: 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');
 
  //...
}

フィールドの値をシリアライズするSerializableビヘイビア

環境: 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は最も単純な作りになっているので、適当なメソッドを上書きすれば上のような動作も実現できるのではないかと思います。

動的にコールバックを設定するObservableビヘイビア

環境: 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 で戻り値を忘れてその後の処理が実行されない……といったことはありません。

PHP 5.3で共通鍵暗号(openssl)を使用する

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 ではこれを利用している。