PHP でリソース開放などの後処理をデストラクタに行わせるのは無理がありそうなので finally が早く使えるようになるといいなーとは常々思っていましたが、また新たに次のような期待に反してデストラクタが呼ばれないケースが見つかりました。
サンプルコード
<?php class Hoge { private $_str; public function __construct($str) { $this->_str = $str; echo __METHOD__ . " $this->_str\n"; } public function __destruct() { echo __METHOD__ . " $this->_str\n"; } } function main() { echo __FUNCTION__ . " begin\n"; main2(); echo __FUNCTION__ . " end\n"; } function main2() { echo __FUNCTION__ . " begin\n"; try { main3(); } catch (Exception $ex) { // (1) // echo "catch begin\n"; // unset($ex); // echo "catch end\n"; } echo __FUNCTION__ . " end\n"; } function main3() { echo __FUNCTION__ . " begin\n"; $a = new Hoge(__FUNCTION__); main4($a); echo __FUNCTION__ . " end\n"; } function main4($a) { // (2) // throw new Exception("except"); } main();
実行結果
そのまま実行すると次のような出力になります。
main begin main2 begin main3 begin Hoge::__construct main3 main3 end Hoge::__destruct main3 main2 end main end
main3 の $a のデストラクタは main3 を抜けたときに呼ばれており、期待した通りの動作です。
例外を投げてみる
(2) をコメントインして main4 で例外を投げるようにすると次のようになります。
main begin main2 begin main3 begin Hoge::__construct main3 main2 end Hoge::__destruct main3 main end
例外を throw して catch するまでの間に main3 を突き抜けているので、そのタイミングでデストラクタが呼ばれると思ったのですがそんなことはありませんでした。この例の場合、デストラクタは main2 を抜けた時に呼ばれています。
例外をアンセットしてみる
さらに (1) の3行をコメントインすると次のようになります。
main begin main2 begin main3 begin Hoge::__construct main3 catch begin Hoge::__destruct main3 catch end main2 end main end
$a のデストラクタは例外を unset したときに呼ばれていることがわかります。
まとめ
何故このような動作になるのかは、例外の getTrace を見れば一目瞭然でした。(1) の unset($ex) の直前に var_dump を仕込んでみます。
<?php // (1) var_dump(array_slice($ex->getTrace(), 0, 1)); echo "catch begin\n"; unset($ex); echo "catch end\n";
var_dump の出力は次のようになります。
array(1) { [0]=> array(4) { ["file"]=> string(19) "/path/to/sample.php" ["line"]=> int(57) ["function"]=> string(5) "main4" ["args"]=> array(1) { [0]=> object(Hoge)#1 (1) { ["_str":"Hoge":private]=> string(5) "main3" } } } }
例外のスタックトレースに関数の引数がそのまま保存されています。そのため、例外から間接的にオブジェクトが参照されてしまい、例外が消えるまでオブジェクトが破棄されないままになってしまうのです。