読者です 読者をやめる 読者になる 読者になる

PHP 5.4 は クロージャーに $this が暗黙的に束縛されるため 5.3 と動作が異なってしまうことがある

PHP

PHP 5.4 ではクラスのインスタンスメソッド内で定義したクロージャーには $this が暗黙的に束縛されてしまうため、次のコードは 5.3 と 5.4 で動作が異なります。

<?php
class AAA
{
	public function getFunction()
	{
		// クロージャー!
		return function ($a, $b) {
			return $a + $b;
		};
	}
	
	public function __destruct()
	{
		echo __METHOD__  . PHP_EOL;
	}
}

class BBB
{
	public function hoge()
	{
		echo __METHOD__ . " BEGIN" . PHP_EOL;
		$aa = new AAA;
		$func =  $aa->getFunction();
		echo __METHOD__ . " END" . PHP_EOL;
		return $func;
	}
	
	public function fuga()
	{
		echo __METHOD__ . " BEGIN" . PHP_EOL;
		$func = $this->hoge();
		$func(1, 2);
		echo __METHOD__ . " END" . PHP_EOL;
	}
}

echo "BEGIN" . PHP_EOL;
$bb = new BBB;
$bb->fuga();
echo "END" . PHP_EOL;
PHP 5.3.16 の実行結果

AAA::getFunction() が返すクロージャーに AAA のインスタンスが束縛されていないので、BBB::hoge() のスコープを抜けた時に AAA のデストラクタが呼ばれます。

BEGIN
BBB::fuga BEGIN
BBB::hoge BEGIN
BBB::hoge END
AAA::__destruct
BBB::fuga END
END
php 5.4.6 の実行結果

AAA::getFunction() が返すクロージャーに AAA のインスタンスが束縛されているため、BBB::hoge() のスコープを抜けてもまだクロージャーが生きているので AAA のデストラクタは呼ばれません。BBB::fuga() のスコープを抜けるとクロージャー($func)も消えるので AAA のデストラクタが呼ばれます。

BEGIN
BBB::fuga BEGIN
BBB::hoge BEGIN
BBB::hoge END
BBB::fuga END
AAA::__destruct
END

5.3 でも 5.4 でも同じ動作にする方法

PHP 5.4 でもクロージャーの定義に static をつければ $this は束縛されなくなりますが、もちろん PHP 5.3 でそんなことをしてもパースエラーになるだけです。


$this が束縛されるのはインスタンスメソッドの場合だけなので、$this を束縛したくない場合は静的メソッドでクロージャーを定義すると良さそうです。例えば↑の例では次のように書き換えれば 5.3 でも 5.4 でも同じ動きになります。

<?php
class AAA
{
	public function getFunction()
	{
		return self::getStaticFunction();
	}
	
	public static function getStaticFunction()
	{
		return function ($a, $b) {
			return $a + $b;
		};
	}
	
	public function __destruct()
	{
		echo __METHOD__  . PHP_EOL;
	}
}

class BBB
{
	public function hoge()
	{
		echo __METHOD__ . " BEGIN" . PHP_EOL;
		$aa = new AAA;
		$func =  $aa->getFunction();
		echo __METHOD__ . " END" . PHP_EOL;
		return $func;
	}
	
	public function fuga()
	{
		echo __METHOD__ . " BEGIN" . PHP_EOL;
		$func = $this->hoge();
		$func(1, 2);
		echo __METHOD__ . " END" . PHP_EOL;
	}
}

echo "BEGIN" . PHP_EOL;
$bb = new BBB;
$bb->fuga();
echo "END" . PHP_EOL;


滅多に $this が束縛されたら困るようなことは無いと思いますが・・・↓のようにのようにエラーハンドラにクロージャーを仕込んでデストラクタでエラーハンドラをリストアする処理が PHP 5.4 では意図通りに動かなくなったりしました。