前回と同じような話ですが、カスタムエラーハンドラ(set_error_handler)から例外を投げると、関数やメソッドの引数に使っていないただのローカル変数でも、例外から参照されてしまいます。
サンプルコード
<?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"; // カスタムエラーハンドラでPHPエラーを例外にする set_error_handler(function($errno, $errstr, $errfile, $errline) { throw new ErrorException($errstr, 0, $errno, $errfile, $errline); }); try { main3(); } catch (ErrorException $ex) { // (1) // var_dump(array_slice($ex->getTrace(), 0, 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__); user_error("xxx", E_USER_NOTICE); echo __FUNCTION__ . " end\n"; } main();
実行結果
例外を unset したタイミングで $a のデストラクタが呼ばれています。が、今回は $a は関数呼び出しには使っておらず、呼び出し履歴の引数に含まれていないはずです。
main begin main2 begin main3 begin Hoge::__construct main3 catch begin Hoge::__destruct main3 catch end main2 end main end
原因
カスタムエラーハンドラには、↑のコードに書いているものとは別に 5 番目の引数があります(PHP: set_error_handler - Manual)。
$errcontext にはエラーの発生したスコープの変数が含まれています。カスタムエラーハンドラから例外を投げると例外のスタックトレースの先頭がカスタムエラーハンドラになるため、$errcontext を経由して例外からオブジェクトが参照されてしまうのです。
ためしに (1) のコメントを解除して vardump の出力を見てみると・・・
array(1) { [0]=> array(2) { ["function"]=> string(9) "{closure}" ["args"]=> array(5) { [0]=> int(1024) [1]=> string(3) "xxx" [2]=> string(19) "/path/to/sample.php" [3]=> int(58) [4]=> array(1) { ["a"]=> object(Hoge)#2 (1) { ["_str":"Hoge":private]=> string(5) "main3" } } } } }
スタックトレースの先頭はクロージャー(カスタムエラーハンドラ)になっており」引数の 5 番目の配列に $a が含まれているのがわかります。