LoginSignup
21
19

More than 3 years have passed since last update.

PHP の RAII なんて幻想

Last updated at Posted at 2016-05-05

次のようなコードがあったとして、

function main()
{
    try {
        foo(false);
    } catch (Exception $ex) {
        echo $ex->getMessage() . PHP_EOL;
    }

    try {
        foo(true);
    } catch (Exception $ex) {
        echo $ex->getMessage() . PHP_EOL;
    }
}

function foo($ok)
{
    $file = new SplFileObject(__DIR__ . '/hoge.txt', 'a');
    if ($file->flock(LOCK_EX|LOCK_NB) == false) {
        throw new RuntimeException("Unable lock file");
    }
    bar($file, $ok);

    // この関数を抜けたときにファイルは閉じられるはず
}

function bar(SplFileObject $file, $ok)
{
    // なにか処理する

    if ($ok == false) {
        // 失敗したので例外を投げる
        throw new RuntimeException("oops!!!");
    }

    echo "ok\n";
}

main 関数の1回目の foobar が例外を投げますが、SplFileObjectfoo のスコープを抜けたときにオブジェクトが破棄されるためデストラクタでファイルも閉じられており、2回目の foo の実行時にはファイルのロックは解放されているはず、、、ですが、実際には2回目の foo が実行されるときにはファイルが開かれたままで、ロックも取られたままになっています。

$ php 1.php
oops!!!
Unable lock file

1回目の foo からの例外を受けた後に、例外オブジェクトを unset とかしてやればとりあえず大丈夫です。

function main()
{
    try {
        foo(false);
    } catch (Exception $ex) {
        echo $ex->getMessage() . PHP_EOL;
        unset($ex);
    }

    :
}

がしかし「例外をキャッチした後は例外オブジェクトを unset すること」などという規約が受け入れられる人は稀だと思います。ので、RAII なんて無かったことにして try...finally で明示的に閉じるほうが良いでしょう。

function foo($ok)
{
    $file = new SplFileObject(__DIR__ . '/hoge.txt', 'a');

    try {
        if ($file->flock(LOCK_EX|LOCK_NB) == false) {
            throw new RuntimeException("Unable lock file");
        }
        bar($file, $ok);
    } finally {
        $file->fclose();
    }
}

と言いたいところですが SplFileObjectfclose などというメソッドはありません。SplFileObject によって開かれているファイルを閉じるにはガベージコレクションでオブジェクトが破棄されるのに任せるしかありません。たぶんもう詰んでいます。

しいて言えば次のように finally でアンロックするとかでしょうか。ファイルは閉じれませんがロックは解放されます。ファイルは閉じれませんが。

function foo($ok)
{
    $file = new SplFileObject(__DIR__ . '/hoge.txt', 'a');

    if ($file->flock(LOCK_EX|LOCK_NB) == false) {
        throw new RuntimeException("Unable lock file");
    }

    try {
        bar($file, $ok);
    } finally {
        $file->fflush();
        $file->flock(LOCK_UN);
    }
}

SplFileObjectclose とか fclose のようなメソッドが無いのはオブジェクトが破棄されればデストラクタで勝手に閉じられるからだと思うんですけど、上記のように意図せず延命されることがあるので RAII に頼ったコードは書きにくいなーと思います。

PDO も明示的に切断するメソッドが無いので似たようなことで困ったことがありました。もっとも、明示的に DB から切断したいことはまず滅多ありませんけど。


この問題、例外のスタックトレースに呼び出し履歴の引数がそのまま含まれているためなので、SplFileObject のようなデストラクタでリソースの解放が行われるオブジェクトを関数の引数にしなければ回避できる問題なのですが、

set_error_handler から ErrorException を投げるようにしていたりすると、もっと面倒なことになります。

Notice や Warning が発生すると、関数のローカルスコープにしか存在しない変数でも関数の外まで例外に乗ってばこーんと飛んできます。

21
19
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
21
19