前回と同じような話ですが、カスタムエラーハンドラ(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 が含まれているのがわかります。