session_start と __wakeup で無限ループ。

PHP 5.2.6 でのお話。

PHP のクラスでは __wakeup っていう特殊メソッドが使えます。
例えば、セッションに保存したオブジェクトを復元する時に、
変数を復元するだけじゃなく、何か計算をしたり、
データベースから最新の値を取ってきたりしたい時があります。
そんな時、そのクラスに __wakeup 関数を作って、その中に必要な処理を書いておけば、
オブジェクトが復元される時に、自動的にその処理が実行されます。便利です。

__wakeup について、詳しくはマニュアルを見てね。
PHP: マジックメソッド – Manual

今回は、
session_start と __wakeup を一緒に使うと無限ループしちゃう事があるから気を付けよう。
というお話。

短いサンプルコードです。無限ループしちゃいます。


<?php

class 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() が呼ばれた場合でも、無視する。」っていう動きに変わってくれたら楽でいいな。
と思いました。

プログラミングPHP 第2版

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です