gallu’s blog

エンジニアでゲーマーで講師で占い師なおいちゃんのブログです。

CAS実装……の前提のお話

ふと、色々と紆余曲折があった末として「MagicWeaponのdata_clumpで、cas形式の楽観的ロック、実装してみようかしらん?」というような発想がありまして。
その前提として「cas tokenど〜やって実装しよう?」から、比較的現実的な可能性があるあたりを妄想して、その辺の実装の欠片を思いついたので、おおむね、メモ用途でw


端的には「bigint(8byte) unsigned相当の整数」をランダムで持たせたら、大体、トークン足りえるんじゃないかなぁ? と、妄想をしまして。
8byteなら、index切っても、メモリ空間の圧迫が「そんなもんだろう」程度、だろうし。
渡し方については……MySQLであれば……二種類あるなぁ。

mysql> SELECT CAST(0xffffffffffffffff AS UNSIGNED);

                                                                              • +
CAST(0xffffffffffffffff AS UNSIGNED)
                                                                              • +
18446744073709551615
                                                                              • +

mysql> SELECT CONV('ffffffffffffffff', 16, 10);

                                                                      • +
CONV('ffffffffffffffff', 16, 10)
                                                                      • +
18446744073709551615
                                                                      • +


………ん?

mysql> SELECT CAST('0xffffffffffffffff' AS UNSIGNED);

                                                                                  • +
CAST('0xffffffffffffffff' AS UNSIGNED)
                                                                                  • +
0
                                                                                  • +

あぁ文字列にすると駄目なのか。
って事は、CONV関数だな。こっちなら「文字列」として渡せる。
まぁ、MySQLに方法があるんだ、ほかのRDBでもあるだろうw(ざつ)


んで。
だとするとあとは「8byte整数相当の値をランダムにとってくる」と、なんとなく行けるっぽくて。
後は「重複しねぇよなぁ」を、雑にテストしてみるとよいんじゃなかろうか、って思う。


Linux的に手っ取り早いのは /dev/urandom デバイスでしょ、ってんで、確認かねて、以下のコードを動かしてみた。

<?php

$data = [];
for($i = 0; $i < 100000; ++$i) {
    $h = bin2hex(file_get_contents('/dev/urandom', false, NULL, 0, 8));
    if (false === isset($data[$h])) {
        $data[$h] = 1;
    } else {
        echo "ng... \n";
    }
}

重複が発生したら教えてくれる、的な子。
とりあえず10万回。
……ま〜ま〜時間かかるのな。ほんのわずかにびっくりしたおw


何度か動かしてみましたが、(おおよそ予想通り)一度もぶつからなかったので。
多分、いけるんじゃないかなぁ、的な。


どこかで、気が向いたり体力が向いたりしたら、CASを実際に実装してみませう。
って事で、とりあえず「実装するために一番重要な部分」を、軽くテストしてみました。


突っ込みなどあったら歓迎いたしますので、是非。

before_filter的なほにゃらら、の想定

元ネタは、友人の

会員登録に住所が必要な場合の「数字は全角で」「数字は半角で」もそろそろどうにかならんのか

ってあたりの発言。


まぁ実際「住所入力全角のみ」で、番地数半角ではねられていらついたとかいうケースは割と後を絶たないわけでして。
その辺りを考えると、入力値受け取る時に
・半角になぁれ
・全角になぁれ
・カンマ(,)はいらねぇっつってんだろ!!(数値入力)
など、いくつか定型的な「事前に置換したり除去したりしたいパターン」ってのはあるような気がするんですね。
記憶をほっくり返しても確かに組んでた記憶があるし。


んで「なら、set_valueあたりでフックして、自動で置換したり除去したりすればいいんぢゃね?」って思いましたのが事の発端。
実装イメージ的にはvalidateと似ている感じかなぁ。


設定は、set_element() 辺りで

  $this->push_before_filter('hoge', 定数 | 定数); // 
  $this->push_before_filter('foo',  定数 | 定数); // 
  $this->push_before_filter('bar',  定数 | 定数); // 

的な感じで。


後は、普通にset_valueするタイミングで逐次、filterが適用される的な。
微妙に悩んでるのは、以下のあたり。


本来的には「手で入力した際」のフック用、なのだよねぇ。
それを考えると、なんとなし「set_from_cgi*()」がフック場所なのだけど。
「手動で普通にごりごりとset_valueしてるコード」の存在もあり得るので、そうすると「フック場所はset_value」のほうが、性能を考えなければ、より確実。
ただ「すでにfilter済みのデータも毎回filter」ってのも些か処理的に重いのでは? ってのもあって。
そうすると「フック場所はset_valueで、引数にフラグを追加、"明示的にfilterをすっ飛ばす"オプションがある」にするとよいのかなぁと思うんだけど、「手間」なのと「DBからの入力なんだけど信用できない」とかいう特殊ケースをどうすっかなぁ? 的な問題。


とりあえず今のところ何となく
・フック場所はset_value
・引数に「明示的に指定すると"filterを通さない"意思表示が出来る」ように改造
・data_clumpがもっている、いくつかのメソッドは「filterを通さない」「filterを通さなくする事ができる」ように改造
って方向かなぁ、と思ってる。
あとは作ってみて使ってみて改良かなぁ?


filterは、候補としては
・数値を全て半角にする
・数値を全て全角にする
・英字を全て半角にする
・英字を全て全角にする
・英字を全て「大文字」にする(最終データが半角データのみ)
・英字を全て「小文字」にする(最終データが半角データのみ)
・全角スペースを「半角」に変換
・半角スペースを「全角」に変換
・全角カタカナを「全角ひらがな」に変換
・全角ひらがなを「全角カタカナ」に変換
・半角カタカナを「全角ひらがな」に変換
・半角カタカナを「全角カタカナ」に変換
・全角の長音の類い(調べないと…)をハイフン(半角)に変換
・全角の長音の類い(調べないと…)を長音(ー)に変換:揃える、とも言う
・ハイフン(半角)を長音(ー)に変換
・カンマを除去する(半角、全角とも)
・スペースを除去する(半角全角とも)
・改行を除去する(\r\nとも)
くらいかなぁ?
まぁ必要に応じてこの辺も追加。


単純に「必要要素」だけだと面倒な気もするので。
セットメニュー的なのも作っておくと良さそうかも。
・「数値を全て半角」+「英字を全て半角にする」+「全角スペースを「半角」に変換」+「全角の長音の類い(調べないと…)をハイフン(半角)に変換」の、「半角セット」
とかとか。
セット的には
・半角セット
・全角セット
・半角数値入力セット(半角数値系+カンマ除去)
あたりがとりあえず有力かなぁ。


あと、もっと大前提として「filter」でいいのかしらん? 的な、命名の問題。

「before_filter」って名前も、微妙におよそと混ざりそうで、幾分躊躇してる感じw
なんかいい命名ないかしらん?
意味合いとしては「入力されたデータを、置換したり一部除去したりして整えたり揃えたりする」感じ。


…なんてことを、もわもわ。
意見コメント突っ込みその他、ありましたらお気軽に是非!!

phpのsession関数を使ってのギミック

PHPのセッションは、便利な反面色々と気になる事もあるのですが。
そのうち
・「セッション有効時間」をきっちりしたい(有効時間が切れたら問答無用で切断したい)
・アカウントロックの実装をかけて、ロックしたら「ログイン出来ない」だけじゃなくて「現在ログイン中のアカウントもシャット」したい
のあたりを解決しようかなぁ、と思いました。


細かい所をかくと
・「自分のアカウントに紐付いているセッションIDのうち、現在のセッション以外を全部遮断」(gmailの「アカウント アクティビティ」の「他のすべてのWebセッションからログアウト」)
も実装したいのだけど…ここに手を入れるのは些か難儀なので、一旦見送り。


で、実装方法を簡単に。
基本は、いわゆる「認可(authorization):ログイン中のユーザかどうか」の判断ロジックのところで、少しギミックを入れました。
その手前として、ログイン(認証:authentication)のタイミングでも、ギミック追加しています。


寿命については、大まかには
・ログイン時に、$_SESSIONに「有効時間」を入れる
・認可の度に
・・有効時間を超えてたら問答無用でログアウトさせる
・・有効時間範囲内なら「有効時間を更新して」認可する
ってロジック。


また合わせて、認可のタイミングのラスト付近で「ロックテーブル」なるテーブルを1枚用意して。最低限としては「ユーザID」だけがカラムにあればよくて、判断としては
・ロックテーブルに「認可を試みてるユーザのユーザIDが存在してたら、認可しない」
って感じで「問答無用でロック」が出来るようにしました。
…DBアクセスが1つ増えるのが微妙っちゃぁ微妙なんだけど、まぁその辺はトレードオフかなぁ、と。
基本「KVS的なアクセス」でよいので、その辺で色々と小細工をしてみてもよいだろうし。


たいした実装でもないのですが、色々と「ちょっともにょってた」ところが実装できたので、割と満足でございます。


実装サンプルはこちらからどんぞ。
https://github.com/gallu/MagicWeaponTest
の「lib/common/base_model_admin_base.inc」と、
https://github.com/gallu/MagicWeapon
の「authorization_session.inc」
あたりを追いかけていくと、大体、処理が点在しています(笑


あとは…微妙っちゃぁ微妙なのですが
・そもそも「Anonymouse セッション(ログインしてなくても始まるセッション)」ってどうなんだろう?(セッションアダプションはやっぱりちょっと気になる)
・「他のすべてのWebセッションからログアウト」入れたいよねぇ
あたりになると、セッション周りを「自作しなおすかねぇ( http://d.hatena.ne.jp/gallu/20160402/p1 )」という話になって。
ただ、この辺になると上述で言うところの「authorization_session.inc」の処理にも手を加える(ってか多分、別クラスにする)ってのもあって…ってのが、「セッションIDを作ったり設定したりする」辺りを全部自前実装にする気ぃ満々なのでw
興味はあるんだけど少し時間がかかりそうで……次回短期入院の時の課題かなぁ、とw


まぁ「なければ作る」はUNIX系列の伝統的作法だと思うので。
便利なPHPの機能は最大限「そのまま使いつつ」、不便な所をラップしていくのが最近のおいちゃんの好みかなぁ、という辺りでの邁進を想定しておりまするる。

data_clumpの使い方(ざっくり版)

ほぼ私信のようなもんですがw
細かくはまたマニュアル( https://github.com/gallu/MagicWeaponManual/blob/master/table_of_contents.md )のほうに書きますが、一端、メモ程度に。

本質的なところ

data_clumpは「データの塊」です。
例えば「1つのmail form」は一塊のデータだと思われますし、DBの1テーブルなんてのも、一塊のデータだと思われます。
そんな「一塊」ごとに1クラスをアサインしているのがdata_clumpです。

下ごしらえ

とりあえず「data_clump継承クラス」を作成してください。
場所は、MagicWeaponの流儀をそのまま持ち込むのであれば、libディレクトリにclumpってディレクトリを切って、そこに置くとよいです。
また必須ではないのですが、clumpディレクトリの中にbaseってディレクトリを切っておくと、「Generation Gapパターン」的な意味でよりよいです。


例えば。
http://furu.mwtest.gjmj.net/
で動きを見ていただけて、ソースコード
https://github.com/gallu/MagicWeaponTest
にあるのですが。
「1つのform」を構成するクラスは、
https://github.com/gallu/MagicWeaponTest/blob/master/lib/clump/base/form_test_clump_base.inc
https://github.com/gallu/MagicWeaponTest/blob/master/lib/clump/form_test_clump.inc
の2つのクラスに分解すると、色々とはかどります。


「clump/base/*_clump_base.inc」は、大まかには「このデータの塊は具体的にどんな要素をもっているのか」が記述してあります。
「clump/*_clump_base.inc」は、data_clumpの本体になります。
formだと難しいのですが、「DBのテーブル」単位でclumpを作る場合は、MagicWeaponをインストールしたディレクトリのtool/soak_up_information.php( https://github.com/gallu/MagicWeapon/blob/master/tools/soak_up_information.php )ってのを叩くと、自動でコードを生成してくれます。
もうちょっと正確に書くと「baseは常に上書きで生成(するからこのファイルは追記とかしないようにしたほうが無難)」「clumpは"なければ作る"」って動きをします。


以下、上述のファイルを作ってる前提で話をすすめます。

formからのデータの取得とかvalidateとか出力とかその辺

ものすごく大まかには、以下のようなコードで大体処理をします。
https://github.com/gallu/MagicWeaponTest/blob/master/lib/form_confirm.inc
(ちょっとコメント変えてます)

  // data_clumpインスタンスを取得
  $form_test_c = $this->get_clump('form_test_clump');

  // 「cgi requestから」データを取得
  $form_test_c->set_from_cgi($this->get_cgi_request());

  // validate(定型のみ)
  if (false === $form_test_c->is_valid($this->get_conv())) {
    $this->forward('form_input');
    return ;
  }

  // セッションに保存
  $_SESSION['form_test'] = $form_test_c->get_all_data_to_hash();

  // 表示
  $this->get_conv()->set('form', $form_test_c->get_all_view_values());

インスタンスは「newで作る」でもよいのですが、MagicWeaponのMVCを使っている場合は「model(ほかのフレームワークのcontroller/action)から取得可能」なので、そこで取得してます。
base_model#get_clump()で取得すると「newした後でDBハンドルぶち込んどいてくれる」程度に便利です…が、上述のような「formからの取得だけ」なら、あんまり意味はないです(笑
まぁ「他とそろえた方が見やすい」程度かなぁ。


data_clumpは「一塊のデータ」なので「cgi requestから、一塊を一式取得しといて」は、1メソッド set_from_cgi() で片付けます。
同様に、validateも「どう? valid?」なので、1メソッド。
ちなみにvalidateについては、「定型で片付く」範疇までなら、protected function set_element()ん中のpush_validateで定数とか設定しておけば、後は自動。
「もうちょっとややこしいvalidateが絡む」場合は、is_valid_insert()またはis_valid_update()を上書きします。先にparent::で「定型処理のvalidate」呼んでもらって、残りの「ややこい」のは自力で適宜追記実装してください。


validaだった場合。
data_clumpは、内部的に「data」と「view_values」を微妙に使い分けているので、使い分けながら「セッションに情報を保存しつつ」「表示しつつ」します。
dataは「内部データ構造」。なので、DBとのやりとりとか、いわゆる「生データ」がここになります。
一方でview_valuesは「表示用のデータ」。
dataと等しいケースもあるけど「表示用にちょっと色々小細工してみたい」なんてケースもあるので、その場合はview_valuesメソッドを上書きして、parent::で情報取得した後に「小細工いれてreturn」とかやると、色々と便利です。


で、データを最終的に受けとるのはこっち。
https://github.com/gallu/MagicWeaponTest/blob/master/lib/form_fin.inc

  // clumpに一度通してからviewへ
  $form_test_c = $this->get_clump('form_test_clump');
  $form_test_c->set_all_data_from_hash($_SESSION['form_test']);

  // 表示
  $this->get_conv()->set('form', $form_test_c->get_all_view_values());

表示するので、言い方を変えると「view_valuesの処理を通したい」ので。
set_all_data_from_hash()で「生データをぶち込んで」から、get_all_view_values()でviewに変数を一式セット。
これを定型でやっておくと「なんか表示上の変更があっても、get_all_view_values()とテンプレート修正すればOK」なので、DRYに近い感じでよいのです。


上述コードは「ほかはなんもやってない」のですが、実際には、mailを作って送ったり、とかするんだと思う。
get_all_view_values()があるからあんまりこの状態だと使わない気がするんだけど、例えば上述で「text_dataのデータ単体が欲しい」場合は、「get_value('text_data')」で取得可能。

formからのデータの取得をDBにぶち込む系

大体いっしょ。
サンプルコードがないのでざっくり概念だけ書くと、finのタイミングで

  // clumpに一度通してからviewへ
  $form_test_c = $this->get_clump('form_test_clump');
  $form_test_c->set_all_data_from_hash($_SESSION['form_test']);

  // DBにinsert
  $r = $form_test_c = $this->insert();
  if (false === $r) {
    // insertできなかったお orz
  }

  // 表示
  $this->get_conv()->set('form', $form_test_c->get_all_view_values());

これくらい。
set()ってメソッドもあって、いわゆるupsertなんだけど、最近あんまり使わないようにしてるので多分そのうち非推奨になる予定w
ちなみにinsertの場合、「set_insert_date_name()」ってのが設定されていると、設定されたカラム名に「現在日付」が入る感じ。


で…DBにinsertする場合。
set_from_cgi(正確にはset_from_cgi_insert)でデータを取得するときに、例えば「idはauto incrementなのに、パラメタインジェクションで外部から指定されちゃってショック!!」ってのが、起きないとも限らない。
なので、そーゆー時はset_from_cgi_insert()を上書きしてくださいませ。
set_from_cgi_insertは内部的に「set_from_cgi_detailをcallしている」だけで、set_from_cgi_detailは

 * @param cgi_request $req cgi_requestクラスのインスタンス
 * @param vector $target 対象とするcgi name attribute値
 * @param boolean $empty_overwrite_flg 空文字の上書きフラグ trueにすると空なら空文字を上書きする

なので。
第二引数の「$target」から、例えば「自動で設定したいPKのカラム名」を抜いたりすればOK。
$this->get_all_names() で「全パラメタ名」が配列で取得できるので、そこから「抜きたいのを削除する」ってロジックを書くと楽。
おいちゃんは
・array_flipで値とkeyを反転
・unsetでkeyを指定して削除したいカラム名を削除
・array_flipで戻す
ってやり方をよくやるかな。
これなら「後でカラム名が増えても」気にせずやっていけるからw

update系

処理としては似てるんだけど。
やり方的には
・keyに対応する情報をDBから取得
cgi requestで「修正すべき値」が投げられてくる
・DBをupdate
って手順になると思う。


雑にソースコードを書くと

  // clumpインスタンス作成
  $hoge_c = $this->get_clump('hoge_clump');

  // keyを設定
  // XXX IDが空かどうかのチェックは省略
  $hoge_c->set_value('hoge_id', $this->get_cgi_request()->find('hoge_id'));

  // DBから取得
  $r = $hoge_c->get();
  if (false === $r) {
    // データないってよ!!
    適宜エラー処理各種
    return ;
  }

  // 表示
  $this->get_conv()->set('hoge', $hoge_c->get_all_view_values());

で、まず表示。
修正内容取得&(確認画面抜きにして)修正なら

  // clumpインスタンス作成
  $hoge_c = $this->get_clump('hoge_clump');

  // 「cgi requestから」データを取得
  $hoge_c->set_from_cgi_update($this->get_cgi_request());

  // validate(定型のみ)
  if (false === $hoge_c->is_valid_update($this->get_conv())) {
    適宜エラー処理各種
    return ;
  }

  // DBの内容編集
  $hoge_c->update();

  // 表示
  $this->get_conv()->set('hoge', $hoge_c->get_all_view_values());

こんな感じ。
set_insert_date_name()とほぼ一緒な感じで、set_update_date_name()ってメソッドがあるので。
「修正日」とかいうカラムがある系なら、ご利用くださいませ、的な。


あと「set_from_cgi_updateでデータを取得するときに、変更させたくないパラメタなのにパラメタインジェクションで外部から指定されちゃってショック!!」ってのが、起きないとも限らないので、的な、insertと同じお話。
set_from_cgi_updateは内部的に「set_from_cgi_detailをcallしている」だけなのと、 $this->get_all_no_key_names() で「pk以外のカラム名一式」が取得できるので、insertん時と同じように「適宜、抜くべきカラム名は抜いて」あげてくださいませ。

「PK以外のデータを指定して」情報を引っ張ってきたい場合

例えばユーザデータなんかで
・IDはint
・emailがユニーク
・emailからユーザを引っ張ってきたい
なんてケース。

  // clumpインスタンス作成
  $users_c = $this->get_clump('users_clump');

  // keyを設定
  // XXX IDが空かどうかのチェックは省略
  $users_c->set_value('email', $this->get_cgi_request()->find('email'));

  // DBから取得
  $r = $hoge_c->get_nopk();
  if (false === $r) {
    // データないってよ!!
    適宜エラー処理各種
    return ;
  }

ようは、get()がget_nopk()に変わるだけ。
ただこれ「複数引っかかる場合、なにが引っかかるかは保障されない」ので、注意してね。

「一覧」とかを処理する系

MagicWeaponの基本の一つは「SQLは書いて」なのでw
その辺を前提に、一覧系を、やっぱり雑なコードで簡単に解説。

  // なんか「特定のstatus」を持ってるユーザの一覧とか検索
  $mw_sql = new mw_sql();
  $mw_sql->set_sql('SELECT * FROM users WHERE status=:status;'); // プリペアドステートメントを設定
  $mw_sql->bind(':status', $this->get_cgi_request()->find('status')); // 値をbind
  $res = $this->get_db()->query($mw_sql); // SQLの発行
  $res->set_fetch_type_hash(); // fetchのタイプをhash(カラム名)に変更

  // data_clumpのインスタンスを再利用してちょっとだけメモリ節約用:昔は結構重要だった。今はどうかなぁ?
  $users_c = null;

  // データがなくなるまでぶん回す
  $users_list = [];
  while($res->fetch()) {
    $users_c = $this->get_clump('users_clump', $users_c); // 第二引数がnullならnew、nullでなければiniti叩いてインスタンス初期化して再利用

    // データを「db_dataインスタンスから」取得
    $users_c->set_from_dbdata($res);

    // view用データを蓄積
    $users_list[] = $users_c->get_all_view_values();
  }

  // viewに設定
  $this->get_conv()->set('users_list', $users_list);


大体こんな感じ。

その他雑多で「まぁまぁ使う」子たち

del()
データの削除。単純にdeleteなんだけど、
「もし、テーブル名 + '_delete'っていう名前のテーブルが存在する」場合、そちらへのinsertを同時にやってくれる、ってあたりがちょっとだけ小細工。
テーブル名_deleteのテーブルには、テーブル名のカラム+「delete_date」ってカラム、が必須。


set_value_nowdate()
set_valueとほぼ等価なんだけど、値は「日付が自動で入る」ので、稀に便利。


set_value_token()
set_valueとほぼ等価なんだけど、値は「tokenが自動で入る」ので、稀に便利。
ちなみにtokenは、tokenizerクラス( https://github.com/gallu/MagicWeapon/blob/master/tokenizer.inc )の値。


set_value_token_with_ip()
set_value_token()とほぼ一緒なんだけど「IPアドレス付き」になるので、複数サーバでも無問題。


update_calculation()
「1つのカラム」の数値を加減算できるメソッド。


set_insert_id()
auto_increment時に、insertの後でこのメソッドをcallすると「IDがclumpの中に入ってくる」ので、後の取り回しが楽、かも。


あとは、data_clumpは「実はデータをmemcachedに入れられる」とか「実はデータをAPCん中に入れられる」とか、細かいギミックがいくつか。


…うん思った以上に長くなった(苦笑
まぁなんだかんだ、ある程度「MagicWeaponに特徴的なクラス」なので、細かくは色々な機能がありますw
でもまぁベースにあるのは「データを一塊で扱う」以上終了、なので。


ソースコードは、またどっかのタイミングで整理しないとねぇ(苦笑


PS
この文章は本気で「見直しをしていない」ので、突っ込みは大歓迎w
多分、定期的に修正いれますw

おおざっぱに「がちゃ用の選択プログラム」を組んでみた

以前にも estab_table というクラスを組むには組んでいたのですが。
ほんのりと思うところがありまして、新規に組んでみました。


https://github.com/gallu/MagicWeapon/blob/master/probability.inc
https://github.com/gallu/MagicWeapon/blob/master/probability_T.php (超大まかな使い方)


基本的には
・確率はすべて整数で入れる
・別に「トータルが100であるかどうか」は気にしない(のでデータ作るときにでもチェックしてくれぃ)
という感じ。
pushメソッドの第二引数「確率」の合計は、100でもいいし、1000でもいいし、って感じ。まぁ昨今の「0.002%」とかってのを受け入れると合計の数値が100,000とかになっちゃうんだろうけど、まぁそれはそれで。


使い方としては
・pushで一通り突っ込んで
・choiceで取り出す
だけなのですが。ロジック上、pushは「確率が高いものから順番にpushする」と、幾分効率がよくなると思われたりします。
大体がちゃって「マスターがテーブル上にある」ケースが多いので。引っ張ってくる時にorder byでもしていただければ、って感じかねぇ。


で、以下備忘録的に少し。


まずは確率の検証。モンテカルロ法的な手法をとってみます。

<?php

require_once('probability.inc');

$obj = new probability();
//
$obj->push('item_1', 50);
$obj->push('item_2', 30);
$obj->push('item_3', 20);
//var_dump($obj);

//
$data = array();

//
for($i = 0; $i < 100000; $i ++) {
  @$data[ $obj->choice() ] += 1;
}
//
printf("item_1 is %.4f\n", $data['item_1'] / 100000 * 100);
printf("item_2 is %.4f\n", $data['item_2'] / 100000 * 100);
printf("item_3 is %.4f\n", $data['item_3'] / 100000 * 100);


あるタイミングにおける結果は、こんな感じでした。

[gallu@localhost wk]$ php probability_T2.php
item_1 is 50.1870
item_2 is 29.7750
item_3 is 20.0380
[gallu@localhost wk]$ php probability_T2.php
item_1 is 49.5910
item_2 is 30.3550
item_3 is 20.0540
[gallu@localhost wk]$ php probability_T2.php
item_1 is 49.9310
item_2 is 29.9220
item_3 is 20.1470
[gallu@localhost wk]$ php probability_T2.php
item_1 is 50.0470
item_2 is 29.9730
item_3 is 19.9800
[gallu@localhost wk]$ php probability_T2.php
item_1 is 50.1910
item_2 is 29.7650
item_3 is 20.0440

まぁ大体意図通りだねぇ、っと。


次に性能。

<?php

require_once('probability.inc');

$t = microtime(true);

for($j = 0; $j < 1000; $j ++) {
  $obj = new probability();
  //
  for($i = 0; $i < 200; $i ++) {
    $obj->push("item_{$i}", 1);
  }
  $obj->choice();
}
$t = microtime(true) - $t;
var_dump($t);

これで「1000回」ぶん回してみませう。1000回だとちょうど「1秒が実際には1ミリ秒」になるからわかりやすいのよ、的な(笑
実際に「1つのがちゃに200種類」は些か盛りすぎな気がせんでもないのですが、まぁそれくらいが確認にはよろしかろう、的な。


ノーマル

[gallu@localhost wk]$ php probability_TTT.php
float(1.2731449604034)
[gallu@localhost wk]$ php probability_TTT.php
float(1.272558927536)
[gallu@localhost wk]$ php probability_TTT.php
float(1.2700932025909)
[gallu@localhost wk]$ php probability_TTT.php
float(1.2739930152893)
[gallu@localhost wk]$ php probability_TTT.php
float(1.2681269645691)

実質的には大体1.27ミリ秒。気にならないで済む程度の速度なのではないかなぁ、と。


念の為に「配列の一番最後」を意図的にチョイスするように小細工した時の速度。

[gallu@localhost wk]$ php probability_TTT.php
float(1.3437149524689)
[gallu@localhost wk]$ php probability_TTT.php
float(1.3538720607758)
[gallu@localhost wk]$ php probability_TTT.php
float(1.3482151031494)
[gallu@localhost wk]$ php probability_TTT.php
float(1.343001127243)
[gallu@localhost wk]$ php probability_TTT.php
float(1.3433270454407)

それでも1.34ミリ秒。


せっかくなんで「配列の先頭」を意図的にチョイスするように小細工した時の速度。

[gallu@localhost wk]$ php probability_TTT.php
float(1.2022018432617)
[gallu@localhost wk]$ php probability_TTT.php
float(1.2001230716705)
[gallu@localhost wk]$ php probability_TTT.php
float(1.1996169090271)
[gallu@localhost wk]$ php probability_TTT.php
float(1.2015750408173)
[gallu@localhost wk]$ php probability_TTT.php
float(1.1977519989014)

1.2ミリ秒。
まぁまぁ。


もうちょっと速度を気にするんなら、もしかするとだけど「あらかじめpushしたインスタンス」をシリアライズしておいてDBにぶち込んで、実際に使う時には「アンシリアライズして使う」とかやると、もっと早いかも…と思ったのでさっそく検証。

<?php

require_once('probability.inc');


$obj = new probability();
//
for($i = 0; $i < 200; $i ++) {
  $obj->push("item_{$i}", 1);
}
$s = serialize($obj);
$obj = null;

$t = microtime(true);
for($j = 0; $j < 1000; $j ++) {
  $obj = unserialize($s);
  $obj->choice();
}
$t = microtime(true) - $t;
var_dump($t);


結果

[gallu@localhost wk]$ php probability_TTT2.php
float(0.24353098869324)
[gallu@localhost wk]$ php probability_TTT2.php
float(0.23857712745667)
[gallu@localhost wk]$ php probability_TTT2.php
float(0.24080300331116)
[gallu@localhost wk]$ php probability_TTT2.php
float(0.24097180366516)
[gallu@localhost wk]$ php probability_TTT2.php
float(0.24446702003479)

アベレージ、0.24ミリ秒。
む、思った以上に早いでやんの。
「多少の運用コストかけてでもマシンコストを下げたい」ってニーズであれば、こっちのほうがいいかもしんまい…けどまぁ「微々たるもん」って気もするので、その辺はお好みかなぁ。


「ユーザ毎に確率を変える」とかその手の小細工が必要になるとこのコードは使えないのですが*1、そうでなければ、割と使えるんじゃないかなぁ、という感触。
まぁ以前も似たようなコードを組んでたのですが、なんかふと「降りてきた」ので、一気に書き上げてみましたとさ。


感想その他ありましたらお気軽に。

*1:それをやっていいのか悪いのかはまた別問題 B-p

セッション関連、作ろうかなぁ?

もっそ雑に調べたのですが。

<?php

$_SESSION = array();
$session = array();

function hoge() {
  $_SESSION['test'] = 999;
  $session['test'] = 999;
}

hoge();
var_dump($_SESSION);
var_dump($session);

が、まぁ予想通り

$ php ttt.php
array(1) {
["test"]=>
int(999)
}
array(0) {
}

となって、なにかってぇとつまり「$_SESSIONは別にsession_start()しなくても普通にスーパーグローバル変数として使えるぽい」ので。


そうすると…いくつか、PHPのセッションで気になる事もあって(…まぁ、PHPC言語ソースコードを一番初めに読んだ理由もそこだったしなぁ)。
特に一番気になるのが「セッションIDが存在しなかったら新しくつくる」あたり。
これは「session_start()時に何も情報がなかったら新しく自動で作る」も「session_start()時にCookieからID渡されたんだけどこっちには情報がない時に自動で作る」も、どっちの意味も含めて。
もちろん「anonymouseなセッションが欲しい」場合もあるんだけど、一方で「純粋にログイン認証者にのみ発行したいセッション」もあるので。


なんか別に「PHPのセッションのままでもいい」ような気もしているのですが、なんとなく思考が走ったので、いったん、備忘録的にクラスの作りを記述しておこうかなぁ、っと。
ニーズが多かったら作りますw

// 初期設定系:set系は「start()」call以降は例外でも吐きましょうかねぇ
mw_session::set_name($name); // session_name()で引数与えたのと同じ挙動
mw_session::get_name(); //  session_name()と同じ挙動
// XXX以下面倒なんでsetterとgetter混雑で記述
mw_session::[sg]et_expire(int); // セッション寿命。寿命が来たら基本的には「問答無用で殺す」。引数は分単位?
mw_session::[sg]et_session_data_rw_class_name(string); // セッション情報を読み書きする、いわゆる「SessionHandlerInterfaceクラス」のクラス名:Interfaceで縛る:デフォはRDBかしら?
// Cookie関連
mw_session::[sg]et_cookie_lifetime(int); // Cookieの寿命:デフォは0かねぇ?
mw_session::[sg]et_cookie_path(string); // Cookieで設定するpath:デフォは / かねぇ?
mw_session::cookie_secure_on(); // Cookieのセキュア属性を有効にする
mw_session::cookie_secure_off(); // Cookieのセキュア属性を無効にする:デフォは、まださすがにこっちかねぇ?
mw_session::is_cookie_secure(); // Cookieのセキュア属性を確認
mw_session::cookie_httponly_on(); // httponly属性をonに試みる:デフォこっちで
mw_session::cookie_httponly_off(); // httponly属性をoffに試みる:…このメソッドいるか? そもそも
mw_session::is_cookie_httponly(); // httponly属性を確認
// 特殊
mw_session::injection_expire_logic(object); // セッションの寿命判定を「特殊なものにしたい場合」用のロジック注入口。Interfaceで縛る?
mw_session::injection_create_session_id_logic(object); // セッションIDの作成方法を「特殊なものにしたい場合」用のロジック注入口。Interfaceで縛る? デフォはDIIDかしら?
mw_session::injection_serializer_logic(object); // セッションデータのシリアライズ方法を「特殊なものにしたい場合」用のロジック注入口。Interfaceで縛る? デフォはPHPの普通のserializeになると思う

// ----------------------------------------
// セッションのstart。「セッションテーブルにIDが存在しない」場合、falseが帰ってくる&新規セッションは作らない
mw_session::start();
  // XXX register_shutdown_function()関数つかって、mw_session::end()関数をcallできるようにしておく。end()で、セッションを書き込む
  // 引数で明示的にtrueを指定したら「anonymouseなセッションを許容する(存在しないIDなら作る)」とか、やっとくか? いらないか?

// XXX データはそのまま$_SESSIONに書き込む

// 終了時はシャットダウン関数経由で書き込みが行われる

-------------------
// セッションIDの張り替え。session_regenerate_id(true)と同じように動かす
mw_session::regenerate_id();

-------------------
// セッションの作成。「ログイン成功時」とかこれだよねぇ多分
mw_session::create();

-------------------
// セッションの破棄。セッションIDを亡き者にする。ついでにdataも壊すよねぇふつう。
mw_session::create();


あとはいわゆる「SessionHandlerInterfaceクラス」相当のやつのインタフェースだなぁ。
多分、こんな感じ。

abstract class session_data_rw {
  // セッションIDの設定
  public function set_id($id) { $this->id_ = $id; }

  // セッション開始時
  abstract public function read();

  // セッション終了時
  abstract public function write();

  // セッション削除時
  abstract public function delete();

  // セッション作成時
  abstract public function create();

//private:
private $id_;
}


ここを起点にして。
「ファイルベース」の場合、多分メソッドに「set_save_path(string)」とかいうメソッドが追加されて。
RDBベース」の場合、多分メソッドに「[sg]et_dbh($dbh)」と、「[sg]et_session_clump_name(string)」が追加されて。


ふむ…イメージはわりとがっちょり固まったなぁw
あとは作る気力なんだけど……まぁ「ログイン時のみ有効なセッション ≒ サーバ側が作ってないセッションIDを頑なに受け入れない」というセッション機構は実際欲しいシーンはあったりするので。
なんとなく、そのうち上の実装、やってるような気がするなぁ…実装のイメージつくしなぁ実装そのものは多分1〜2時間とか、テスト入れても、簡易なテストくらいなら半日かからんだろうし。


軽く賛否伺いたかったりするので、ご意見がありましたら、ぜひ。
「はよ作れ」も歓迎いたしまするw


追記
register_shutdown_function()関数微妙に不安が走った…。
まぁ基本的には「MagicWeaponで使う」想定なので、Weaponのcontrollerのfin処理…か、そうかmodelのfin処理でend()叩いてもいいんだな。二重callはmodel側でも禁則処理書けるし、end()ん中でも書いてもいいし。
どのみち「Viewでセッション変数いじる」とか死んでもやらせる気ねぇしw

set_from_cgi_update()のオーバライド用覚え書き

MagicWeaponには、data_clumpっていう、おいちゃんが勝手に「OTM(Object Tbale Mapper)」って呼称している、割合特徴的なクラスがあります。
で、この子は割と「データの塊」を扱うので。
例えば「formから情報を一式とってきてインスタンスん中にいれる」場合、set_from_cgi()っていうメソッドがあります。
厳密には set_from_cgi_insert() と set_from_cgi_update() になるのですが。set_from_cgi()の場合、insertのほうが呼ばれます。


insertの場合基本的には「全カラムがformからぶちこまれる」で、updateの場合は「pk以外の全カラムが以下略」なのですが。
勿論状況によっては「updateで、このカラムは外から受け取らない」ってのがあります。
そこで「formにそのinput要素を作らない」…だけだとクラックされるので。
もしかしたらどっかで「便利メソッド」書くかもしれませんが、現在は大体、こんな感じで書いてます。

// update用のkeyを固定する
public function set_from_cgi_update($req, $empty_overwrite_flg = false)
{
  // キーを把握、除外したいのを除外しておく
  $awk = array_flip($this->get_all_no_key_names());
  unset($awk['除外したいキー(カラム)名']);
  unset($awk['除外したいキー(カラム)名']);
  unset($awk['除外したいキー(カラム)名']);
  unset($awk['除外したいキー(カラム)名']);
  //
  $awk = array_flip($awk);
//var_dump($awk);

  // 「明示的に除外した項目」以外を対象にする
  return $this->set_from_cgi_detail($req, $awk, $empty_overwrite_flg);
}


いや別に「固定で指定」してもよいのですが。
MagicWeaponの発想のひとつって「DBのカラムなんて、ちょいちょい追加されたりするよねぇ」に対する利便性があるのと。
その場合「このカラムだけ更新したい」ってよりは「このカラムは更新の情報受け取んな!」ってほうが状況的に多いので、こゆ書き方しています。
「このカラムだけ更新したい」なら、単純にここで「固定で配列をreturn」すりゃいいだけなんで、まぁ状況に応じての使い分け、ですな。


この辺も「今度マニュアル書く」予定ですが、とりあえず、備忘録兼ねて、めも。