PHP 5.2.6 でのお話。
PHP のクラスでは __wakeup っていう特殊メソッドが使えます。
例えば、セッションに保存したオブジェクトを復元する時に、
変数を復元するだけじゃなく、何か計算をしたり、
データベースから最新の値を取ってきたりしたい時があります。
そんな時、そのクラスに __wakeup 関数を作って、その中に必要な処理を書いておけば、
オブジェクトが復元される時に、自動的にその処理が実行されます。便利です。
__wakeup について、詳しくはマニュアルを見てね。
PHP: マジックメソッド - Manual
今回は、
session_start と __wakeup を一緒に使うと無限ループしちゃう事があるから気を付けよう。
というお話。
短いサンプルコードです。無限ループしちゃいます。
<?phpclass Test
{
var $x = 0;
function __wakeup()
{
session_start();
}
}session_start();
if ( ! $_SESSION[ "test" ] )
{
$_SESSION[ "test" ] = new Test();
}$test = $_SESSION[ "test" ];
$test->x++;echo “x は {$test->x} だよ。\n”;
?>
上のプログラムをサーバーに置いて、ブラウザでアクセスすると、
1回目のアクセスでは「x は 1 だよ。」と表示されて正しく終了しますが、
2回目にアクセスした時に無限ループになって画面が表示されなくなります。
原因は、Test::__wakeup() の中で session_start() を呼んでしまっている事です。
細かく動きを追って行くと・・・
1 回目のアクセスで
1.1. session_start() が呼ばれる。この時点でセッションは空。
1.2. Test オブジェクトを生成してセッションに保存される。
2 回目のアクセスで
2.1. session_start() が呼ばれる。
2.2. セッション情報に Test オブジェクトがあるから・・・
2.3. Test オブジェクトを復元するため、PHP によって unserialize() が自動的に呼ばれる。
2.4. unserialize() から Test::__wakeup() が自動的に呼ばれる。
2.5. Test::__wakeup() の中で session_start() を呼んでるから・・・
2.6. 2.1 に戻る。
と言う具合です。
session_start() のマニュアルを見ると、
「2回目以降に session_start() が呼ばれた場合は、単に無視される。」と書いてますが、
今回のサンプルプログラムの場合は、
1 回目の session_start() の途中で、その中から再起的に 2 回目以降の session_start() が呼ばれているので、
無視されずに実行されれてしまうようです。
PHP: session_start - Manual
昔の記事 ( As of session_start() ) で、
「2回目以降の session_start() は勝手に無視してくれるから、
$_SESSION を使う時はとりあえず session_start() を呼んでおくと安心。」
とか書きましたが、運悪くその処理が __wakeup から呼ばれていると、無限ループになっちゃう事が判明です。
サンプルみたいに、__wakeup() の中で直接 session_start() を呼ぶ事は無いと思いますが、
__wakeup() から別の関数を呼んでて、その関数がさらに別の関数を呼んでて、
その関数の中で何か session_start() 呼んでましたー。うわー!みたいな事は十分ありえます。
とりあえず、対策としては、
session_start() は色んな所で呼ばず、プログラムの開始時に1回だけ呼ばれるようにする。
で良いと思います。
が。
PHP の session_start() が、
「1回 session_start() が呼ばれたら、その中から再起的に2回目以降の session_start() が呼ばれた場合でも、無視する。」っていう動きに変わってくれたら楽でいいな。
と思いました。
