カスタムエラーハンドラ(set_error_handler)から例外を投げるとスコープ内の変数が例外から参照される

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