バリデーターあれこれ
今更ながら。
バリデーターを書く場所について悩んでいる。
- バリデーター(or フィルター)
- アクション
- ビジネスロジック
という階層があるとき、ビジネスロジックで何らかのエラーがあった場合はエラー画面を出したい。
具体的には、
+----------+ |ユーザーID| +----------+ | ・・・ | +----------+
というテーブルがあったとき、ユーザー情報の操作画面ではフィルタなりバリデーターなりでセッション情報を確認して不備があればエラー画面を出す。
これはビジネスロジックじゃなくてもよい。セッションはユーザーIDに基づいているのでセッションが正常ならそのIDに基づいたユーザー情報の操作も正常だ。
ところが
+----------+ |ユーザーID| +----------+ | ・・・ | +----------+ | +----------+ | 住所ID | +----------+ |ユーザーID| | ・・・ | +----------+
こういうテーブルがあったとき、住所情報の操作画面では、フォームからの入力値として住所IDを受け取って何らかの処理をしたいのだが、一緒にユーザーIDのチェックもしないといけない。
ここで、住所情報の操作はビジネスロジックのクラスだとすると、ユーザーIDのチェックもビジネスロジックになる。あるいは、取得した住所情報をアクションに持ってきたときにチェックするか。
どちらにせよ、まともにリンクをたどった場合は起こり得ない状況がURL直打ちなどを考慮すると起こり得るので余計な(?)チェックが必要になる。
達成したい処理は住所情報に関する操作なのだがそこにユーザーセッションの処理が加わるのは何となくしっくりこない。
フォームからの入力チェックなのでアクションに移る前にバリデーターで処理したいところだが、住所情報を取得しないことにはユーザーIDと住所IDが正しいのかどうかが分からない。住所情報を取得するためにはビジネスロジックを通過する。
何度でもDBを見に行ってもいいとするならば、バリデーターでユーザーIDと住所IDの検証をすることもできるが、これは無駄すぎる気がする。
AOP的なことをしようかっていうのも考えたが…あまりいい案も浮かばず。
書きながら、Haskellのプログラムを思い出した。
Haskellではフォーム処理やDB処理などのIOを一カ所に集めないと面倒なことになる。
Haskell的考えでいくと、そもそもビジネスロジックからDBに接続にいくのがおかしいということになるのかな。
http://www.symfony-project.org/book/1_2/02-Exploring-Symfony-s-Code
だいたいはここのFigure 2-2のように考えていた。この考えでいくと、全てがIOになるんじゃないかな。まぁそれがPHP的と言えばPHP的だけれども。
で、自分がHaskellで作った小さなWebプログラムを見直してみる。
そこでは
type Action = Context -> IO Context type ItemAction = (Context, Item) -> IO (Context, Item) filterItem :: ItemAction -> Action filterItem a = do ... -- DB接続してItemを取得、正常に取得できなければエラー用Actionを返し ... -- 取得できれば a を実行して結果からItemを除いて返却する。 modifyAndView :: Action modifyAndView = filterItem (modifyItem >=> viewItem) modifyItem :: ItemAction modifyItem (c, i) = do ... viewItem :: ItemAction ...
のように作っていた。
ここでは、Itemを操作するアクションはItemを受け取るようになっていて、何らかのエラーでItemが受け取れない場合はfilterItemによってエラー画面に遷移する。
つまりアクション実行時にはフォームやDBを含めた全てのデータが正常であることが保証されている。この構成だと本当に実装したい処理に集中できる。
ここで外部キーを使ってItemInItemテーブルを追加した場合はどうなるのだろう。たぶん
type ItemInItemAction :: (Context, ItemInItem) -> IO (Context, ItemInItem) filterItemInItem :: ItemInItemAction -> Action ... modifyItemInItem :: ItemInItemAction
というように作っていくのかな。filterItemInItem が ItemとItemInItemの外部キーをチェックするようにして。
まだPHPでどうするかは思い付かないが、何かこの辺にヒントがある気がする。
サブドメインをまたいだセッション管理ができなくて長時間ハマった
サブドメインをまたいだらセッションがクリアされて、色々試していた。
どうも異なるホストだとクッキーを共有できないような動きになっていた。
原因はsuhosinパッチ?extension?だった。
入れたつもりはないけれどaptで自動で入っていたみたい。
オプションをデフォルトから変更したらサブドメインをまたいでセッションとクッキーをやりとりできるようになった。
cat /etc/php5/conf.d/suhosin.ini ; configuration for php suhosin module extension=suhosin.so suhosin.cookie.cryptdocroot = Off suhosin.session.cryptdocroot = Off
検索しても日本語の説明は全然なかった。まぁ名前から想像した通りだと思ってオフに。
ハッシュ関数がよくわからない
cryptはdesがどうのこうのでダメだという話がよくあるが、cryptとmd5やsha1やhashの違いがよくわからない。
<?php $str = 'foobarbaz'; echo md5($str) . "\n"; echo hash('md5', $str) . "\n"; echo crypt($str) . "\n"; echo sha1($str) . "\n"; echo hash('sha1', $str) . "\n";
php hash.php 6df23dc03f9b54cc38a0fc1483df6e21 6df23dc03f9b54cc38a0fc1483df6e21 $1$2vFnj56C$KGzeZFN3T4qTKvH8Wl5hE/ 5f5513f8822fdbe5145af33b64d8d970dcf95c6e 5f5513f8822fdbe5145af33b64d8d970dcf95c6e
うーん。どれを使えばいいのやら。
cryptだけ何らかの暗号化方式を使って更に加工しているみたい。
0x00 パスワードを pw, 塩を salt, "$1$"を magic と定義します。
0x01 [pw, salt, pw]という文字列を作ります。これを S1 とします。
0x02 S1 を MD5で暗号化します。出力結果を M1 とします。
0x03 [pw, magic, salt]という文字列を作ります。これを S2 とします。
0x04 S2 に M1 を、ある条件(仮に条件を F1 とする) F1 に従って追加します。
0x05 S2 に さらにある条件 F2 を使って、文字列を追加します。
0x06 S2 を MD5で暗号化します。出力結果を M2 とします。
0x07 pw, M2, salt をある条件 F3 を使用して組み合わせて文字列にし暗号化します。これを M_0001 とします。
0x08 0x07 を M_0001 〜 M_1000 まで 1000回くり返します。M2 はそのつど M_xxxx に変わります。
0x09 M_1000 を M3 とします。
0x0a M3 を ある条件 F4 を使用して 22bytes の文字列に変換します。これが暗号文です。仮に Z と定義します。
0x0b 最終的に[magic, salt, "$", Z]という文字列を出力します。
条件 F1 〜 F4 はソースコード見た方が良いと思います。
http://ruffnex.oc.to/kenji/xrea/md5crypt.txt
なかなか複雑な処理を行っています。
うーん。さっぱり。
Crypt::SaltedHash が生成する値は、LDAP のパスワードを扱う方法を定義した RFC-3112 に準拠した形式になるというメリットがあります (ただし SHA-1, MD5 を使用した場合のみ)。
ある日突然、ユーザアカウントの管理を LDAP に移行したい! ということになっても、パスワードカラムの値をそのまま使えるのです。素晴らしいですね。
パスワード保存のお供に Crypt::SaltedHash - JPerl Advent Calendar 2009
これが主な違い?
あとsqueezeや http://php53.dotdeb.org のPHPならcryptでもsha256とかが使えるらしい。
crypt() で $5$ や $6$ から始まる salt を指定することで、SHA-256 や SHA-512 が使用できるようになりました。
<?php echo 'SHA-256' . crypt( 'test', '$5$rounds=5000$salt$' ) . "\n"; echo 'SHA-512' . crypt( 'test', '$6$rounds=5000$salt$' ) . "\n";結果は以下のようになります。
SHA-256: $5$rounds=5000$salt$lNwM/2EW94.5e484ZZwetUClB7.Z/Z3buPmQvXdPEj4 SHA-512: $6$rounds=5000$salt$xdLuw21n.5WciQUUpHTTPfR6QwS..Z1Q/4xGfiyYa51WSQktzSXYXSk2zBp.Is5r9WiXrGqRmHpEG0iG0HaSk.PHP 5.3.2 での修正点や機能追加について - t_komuraの日記
で、結局パスワードやセッションIDを生成するのは何がいいんだろうね。
cryptでSHA-512を使いつつsaltを自動生成とかできないのかな。
こういうのもあった。
<?php function mkPasswd_SSHA($clearPass, $salt=null) { if(!isset($salt)){ mt_srand((double)microtime()*1000000); $salt = substr(md5(mt_rand()), 4, 8); } if(function_exists('sha1')) { $hash = pack("H*", sha1($clearPass . $salt)); }else if(function_exists('mHash')){ $hash = mHash(MHASH_SHA1, $clearPass . $salt); }else{ die("Error: Can not use Function:(SHA or SSHA). Use PHP Ver. 4.3.0 over."); } return base64_encode($hash . $salt); }http://www.developlus.jp/topics/password.html
うーむ。
saltを自動生成する部分だけ自作してcryptを使うのがいいのかな。どうなんだろ。
PHPのcryptをsha256変換方式で使う場合の情報って全然ないなーと思ったら本家にあった。
PHP: crypt - Manual
- CRYPT_SHA256 - SHA-256 hash with a sixteen character salt prefixed with $5$. If the salt string starts with 'rounds=
$', the numeric value of N is used to indicate how many times the hashing loop should be executed, much like the cost parameter on Blowfish. The default number of rounds is 5000, there is a minimum of 1000 and a maximum of 999,999,999. Any selection of N outside this range will be truncated to the nearest limit. - CRYPT_SHA512 - SHA-512 hash with a sixteen character salt prefixed with $6$. If the salt string starts with 'rounds=
$', the numeric value of N is used to indicate how many times the hashing loop should be executed, much like the cost parameter on Blowfish. The default number of rounds is 5000, there is a minimum of 1000 and a maximum of 999,999,999. Any selection of N outside this range will be truncated to the nearest limit.
日本語訳されてなかっただけだった…。
実装ネタ元のリンクもあった。
http://people.redhat.com/drepper/SHA-crypt.txt
長い…。
しかしさすがPHPのマニュアルだ。説明がないと思ったけどちゃんと書いてた。
続きはまた後でやろう。
テストコード
http://d.hatena.ne.jp/shimooka/20100422/1271905286
これ凄いな。
今のプロジェクトで測ってみると、20000:4000だった。0.2倍…。
バグを出せない数値計算系しかテストしてないからなんだけど、、普通は何倍もテストコード書いてるのかな。普通ってアレだが。
今作ってるものはテストコードゼロ…ファイル名とかディレクトリの構造とかをかなり変えながらなんで難しい…。
http://github.com/nishimura/laiz/commit/860e5bc49005df56739409ded32c607ed657b1c8
だいぶファイルを移動した。
名前とかディレクトリ構造とか、なかなか良い案が思い付かないね。
タイプヒンティングからオブジェクトの配列をインジェクションするためにネームスペースを使って動的にクラスを生成する
オートロード関数を登録する。
動的にネームスペースを生成してクラスを定義してオブジェクトを生成する。
<?php namespace laiz\lib\aggregate; use \laiz\autoloader\Register; use \laiz\util\Inflector; class Autoloader implements Register { public function autoload($name) { $pattern = preg_quote(__NAMESPACE__, '/'); if (!preg_match("/^$pattern/", $name)) return; $pattern = preg_quote('\\'); if (!preg_match("/(${pattern}[^$pattern]+)$/", $name, $matches)) return; $className = $matches[1]; $fullNamespace = str_replace($className, '', $name); $realNamespace = str_replace(__NAMESPACE__ , '', $fullNamespace); $interface = $realNamespace . Inflector::singularize($className); $interface = ltrim($interface, '\\'); $className = ltrim($className, '\\'); // fullNamespace: laiz\lib\aggregate\path\to // realNamespace: \path\to // interface : path\to\Object // className: Objects // debug //var_dump($fullNamespace, $realNamespace, $interface, $className); eval(" namespace $fullNamespace; use \\ArrayObject; use \\laiz\\builder\\Container; class $className extends ArrayObject { public function __construct(\$input = null, \$flag = 0, \$iterator = 'ArrayIterator') { if (\$input === null){ \$input = Container::getInstance()->getComponents('$interface'); } parent::__construct(\$input, \$flag, \$iterator); } } "); } }
Registerはオートロード関数を登録するためのインターフェースで、getComponents(interface)はインターフェースを実装しているクラスを全て読み込んで生成して配列で返すメソッド。
laiz\lib\aggregate; 以下のネームスペースを利用しようとすると、このオートロードが動く。
<?php use \laiz\lib\aggregate\anyfeature\Filters; $filters = new Filters();
とすると、anyfeature\Filter を実装したオブジェクトの配列(ArrayObject)を取得できる。
なんでこんなまどろっこしいことをやってるのかというと…
<?php use \laiz\lib\aggregate\anyfeature\Filters; class MyAction { public function act(MyObject $obj, Filters $filters) { foreach ($filters as $filter){ $filter->filter($obj) } } }
これでタイプヒンティングからオブジェクトの配列をインジェクションできるようになった。
上のMyObjectは今までも自動生成していたが、配列の場合はタイプヒンティングがArrayになるから判別不能だった。
namespaceを使う前はやっつけで Array $Anyfeature_Filters などと書いて変数名から判別していたが、バックスラッシュが絡むとどうにもこうにも。
これ書きながら、わざわざnamespaceを隔離して新しい場所に作らない方法もあるなと思いつつ…とりあえずこれでしばらく使ってみる。
namespaceとかクロージャとかで検索したら、PHP5.3の発表前後のマニュアル的な記事が上位に上がってくる。
まだ普通に使われるところまでいってないのかな。
オレオレフレームワークのメモ
Maple再起動で振り返る国内PHPフレームワーク戦争の歴史 – この先生きのこるには
ここにフレームワークの歴史が。
今もここの第3世代のまま進んでる感じなんだろうか。もう何年も前の記事だけれども。
- xFrameworkPX
- コアの開発は一人だったような…たしか。
- http://www.xframeworkpx.com/
- http://code.xenophy.com/?cat=15
今検索してヒットしたもの。
- PokoX
- 3eyes
- Petitwork Framework
思ったよりヒットしない。というか俺俺フレームワークで検索してもここのはてダもヒットしないし、どうやって探そう。できれば日本語ブログがついてるのがいいんだけど。
もっと探してまとめるつもりだったけど時間がないので途中のままメモ。
xcacheを入れてみる
キャッシュ機構が欲しいけれどmemcachedまでは要らないかな、と思ってaptitude searchを眺めているとxcacheというのがあることに気付いた。
aptitude install php5-xcache
使い方は検索しても全然出てこない。どこもインストールと速度調査の記事ばかり。
aptで入れるからインストールは一瞬。その代わり.soファイルとそれを読み込むiniファイルしかない。READMEもサンプル設定ファイルもない。
仕方がないので本家を見る。
http://xcache.lighttpd.net/
パッケージのxcacheはバージョン1.3で、PHP5.3に対応したバージョンらしい。
管理用のファイルがないので取ってきて適当な場所に置く。
wget http://xcache.lighttpd.net/pub/Releases/1.3.0/xcache-1.3.0.tar.gz mv xcache-1.3.0/admin ~/public_html/xcacheadmin cd ~/public_html/xcacheadmin/
http://localhost/xcacheadmin/mkpassword.php にアクセスしてmd5形式のパスワードを生成する。
そして/etc/php5/conf.d/xcache.ini を編集する。
; configuration for php xcache module extension=xcache.so xcache.admin.user = "admin" xcache.admin.pass = "md5 password"
これでWeb管理画面が使える。
iniファイルの説明はここ。
http://xcache.lighttpd.net/wiki/XcacheIni
取り敢えずキャッシュをオンにしてみる。
xcache.size = 100M xcache.var_size = 50M
キャッシュのサンプルコードはここ。
http://xcache.lighttpd.net/wiki/XcacheApi
試しに使ってみる。
<?php $d = new DateTime(); xcache_set('date', $d); var_dump(xcache_get('date'));
同じセッションだとオブジェクトそのままでも使えるが、別のセッションでxcache_getだけ使ってオブジェクトを取得しようとするとSegmentation fault (11)で落ちる。そりゃそうか…。
<?php $d = new DateTime(); xcache_set('date', serialize($d)); var_dump(unserialize(xcache_get('date')));
これならいけそう。
ここまでやってから他の方法を探してみた。サーバ一台なら通常ファイルが一番いい気がしてきた。
- http://www.tsujita.jp/blojsom/blog/default/PHP/2007/01/12/memcached%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%9FPHP%E3%81%AE%E3%82%B7%E3%83%B3%E3%82%B0%E3%83%AB%E3%83%88%E3%83%B3%E5%AE%9F%E8%A3%85.html
- memcachedを使ったPHPのシングルトン実装? - yoyaのメモ
- memcachedを用いた関数キャッシュ - Blog::koyhoge::Tech
うーむ。
素直にPythonとかRubyとかを使うべきなのかなぁ。
PHPでマゾいことをやるか…。
検索してたらEthnaのソースコードがあった。
http://svn.sourceforge.jp/svnroot/ethna/ethna/branches/ETHNA_UTF8_BRANCH/class/Ethna_ClassFactory.php
コメントが読みやすいね。