Пользовательские обработчики с использованием объекта $this (методы bindTo и bind в замыканиях)

15 июля 2018
Документация по bindTo: https://www.php.net/manual/ru/closure.bindto.php
Документация по bind: https://www.php.net/manual/ru/closure.bind.php

Пользовательские обработчики, реализуемые через замыкания (анонимные функции), за пределами класса в теле функции не могут использовать свойства и методы текущего объекта. Но им можно разрешить это делать с помощью метода bindTo($obj, $scope) класса замыканий Closure.

Данный метод создаёт и возвращает копию замыкания с передачей в функцию объекта и новой области доступа. К переданному объекту (аргумент $obj) в теле функции можно обращаться через ключевое слово $this.

Аргумент $scope определяет имеет ли функция доступ к защищённым (protected) и закрытым (private) свойствам и методам объекта. Если в аргументе указать название класса, то функции будет разрешён доступ к защищённым и закрытым структурам объекта, а если в аргументе указать строчное значение static, то область видимости останется без изменений и функция будет иметь доступ только к открытым (public) свойствам и методам.

Пример кода:
final class Hello
{
	private $ru = 'Привет';		//русский
	private $en = 'Hi';			//английский
	private $de = 'Guten Tag';	//немецкий
	private $hy = 'Բարեւ'; 		//армянский

	private $procedure = false;	//Обработчик вывода

	function __construct()
	{
		//Вывод по-умолчанию
   		$this->procedure = function()
   		{
   			echo $this->ru.'<br>';
   		};
	}

	// Устанавливает новый (если задан) и вызывает обработчик вывода
	// @param object|bool - новый обработчик или false, если обработчик не нужно менять
	public function write_1($func=false)
	{
		//Установка нового обработчика
		if(is_object($func))
		{
			$this->procedure = $func->bindTo($this, __CLASS__);
		}

		//Вызов обработчика
		if(is_callable($this->procedure))
		{
			/*
			Если вызвать замыкание из свойства просто как $this->procedure, то интерпретатор ничего не выведет.
			А если вызвать замыкание как $this->procedure(), то будет ошибка, что метода не существует.
			Поэтому замыкание в свойстве нужно выводить, обводя свойство в скобки
			*/
			($this->procedure)();
		}
	}

	// Устанавливает новый (если задан) и вызывает обработчик вывода
	// @param object|bool - новый обработчик или false, если обработчик не нужно менять
	public function write_2($func=false)
	{
		//Установка нового обработчика
		if(is_object($func)) $this->procedure = $func;

		//Вызов обработчика
		if(is_callable($this->procedure)) ($this->procedure)();
	}

}

$a = new Hello();

//Выведет 'Привет'
$a->write_1();

//Выведет 'Hi'
$a->write_1(function(){
	echo $this->en.'<br>';
});

/*
Выведет ошибку 'Uncaught Error: Using $this when not in object context', так как тело функции
не видит переменную $this. Для того, что бы функция видела объект нужно создавать копию функции с передачей объекта.
Всё это делается через метод bindTo
*/
$a->write_2(function(){
	echo $this->de.'<br>';
});

Статический bind

В предыдущем примере сначала создавалась анонимная функции, а затем для неё вызывался метод bindTo.
Существует статический метод bind($closure, $obj, $scope), который повторяет функциональность bindTo. В аргументе $closure указывается замыкание.

Пример использования:
class Day
{
	private $str = 'День';
}

$a = new Day();

$b = Closure::bind(function(){
	echo 'Хороший '.$this->str;
}, $a, 'Day');

$b(); //Выведет 'Хороший День'