次のようなコードがあったとして、
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回目の foo
は bar
が例外を投げますが、SplFileObject
が foo
のスコープを抜けたときにオブジェクトが破棄されるためデストラクタでファイルも閉じられており、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();
}
}
と言いたいところですが SplFileObject
に fclose
などというメソッドはありません。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);
}
}
SplFileObject
に close
とか fclose
のようなメソッドが無いのはオブジェクトが破棄されればデストラクタで勝手に閉じられるからだと思うんですけど、上記のように意図せず延命されることがあるので RAII に頼ったコードは書きにくいなーと思います。
PDO
も明示的に切断するメソッドが無いので似たようなことで困ったことがありました。もっとも、明示的に DB から切断したいことはまず滅多ありませんけど。
この問題、例外のスタックトレースに呼び出し履歴の引数がそのまま含まれているためなので、SplFileObject
のようなデストラクタでリソースの解放が行われるオブジェクトを関数の引数にしなければ回避できる問題なのですが、
set_error_handler
から ErrorException
を投げるようにしていたりすると、もっと面倒なことになります。
Notice や Warning が発生すると、関数のローカルスコープにしか存在しない変数でも関数の外まで例外に乗ってばこーんと飛んできます。