Применение замыканий в PHP

в 8:47, , рубрики: php, замыкания, метки: ,

Введение в PHP 5.3 замыканий — одно из главных его новшеств и хотя после релиза прошло уже несколько лет, до сих пор не сложилось стандартной практики использования этой возможности языка. В этой статье я попробовал собрать все наиболее интересные возможности по применению замыканий в PHP.

Для начала рассмотрим, что же это такое — замыкание и в чем его особенности в PHP.

$g = 'test';

$c = function($a, $b) use($g){
    echo $a . $b .  $g;
};

$g = 'test2';

var_dump($c);

/*
object(Closure)#1 (2)
{
     ["static"]=> array(1) { ["g"]=> string(4) "test" } 
     ["parameter"]=> array(2) { 
          ["$a"] => string(10) "" 
          ["$b"]=> string(10) ""
      }
}
*/

Как видим, замыкание как и лямбда-функция представляют собой объект класса Closure, коорый хранит переданные параметры. Для того, чтобы вызывать объект как функцию, в PHP5.3 ввели магический метод __invoke.

function getClosure()
{
    $g = 'test';

    $c = function($a, $b) use($g){       
        echo $a . $b . $g;        
    };

    $g = 'test2';    
    
    return $c;
}

$closure = getClosure();
$closure(1, 3); //13test

getClosure()->__invoke(1, 3); //13test

Используя конструкцию use мы наследуем переменную из родительской области видимости в локальную область видимости ламбда-функции.
Ситаксис прост и понятен. Не совсем понятно применение такого функционала в разработке web-приложений. Я просмотрел код нескольких совеременных фреймворков, использующих новые возможности языка и попытался собрать вместе их различные применения.

Функции обратного вызова

Самое очевидное применение анонимных функций — использование их в качестве функций обратного вызова (callbacks). В PHP имеется множество стандартных функций, принимающих на вход тип callback или его синоним callable введенный в PHP 5.4. Самые популярные их них array_filter, array_map, array_reduce. Функция array_map служит для итеративной обработки элементов массива. Callback-функция применяется к каждому элементу массива и в качестве результата выдается обработанный массив. У меня сразу возникло желание сравнить производительность обычной обработки массива в цикле с применением встроенной функции. Давайте поэкспериментируем.


$x = range(1, 100000);
$t = microtime(1);

$x2 = array_map(function($v){
    return $v + 1;
}, $x);
//Time: 0.4395
//Memory: 22179776
//---------------------------------------

$x2 = array();
foreach($x as $v){
    $x2[] = $v + 1;
}
//Time: 0.0764
//Memory: 22174788
//---------------------------------------

function tr($v){
    return $v + 1;
}
$x2 = array();
foreach($x as $v){
    $x2[] = tr($v);
}
//Time: 0.4451
//Memory: 22180604

Как видно, накладные расходы на большое количество вызовов функций дают ощутимый спад в производительности, чего и следовало ожидать. Хотя тест синтетический, задача обработки больших массивов возникает часто, и в данном случае применение функций обработки данных может стать тем местом, которе будет существенно тормозить ваше приложение. Будьте осторожны. Тем не менее в современных приложениях такой подход используется очень часто. Он позволяет делать код более лаконичным, особенно, если обработчик объявляется где-то в другом месте, а не при вызове.

По сути в данном контексте применение анонимных функций ничем не отличается от старого способа передачи строкового имени функции или callback-массива за исключением одной особенности — теперь мы можем использовать замыкания, то есть сохранять переменные из области видимости при создании функции. Рассмотрим пример обработки массива данных перед добавлением их в базу данных.

//объявляем функцию квотирования.
$quoter = function($v) use($pdo){
	return $pdo->quote($v);//использовать эту функцию не рекомендуется, тем не менее :-)
}
$service->register(‘quoter’, $quoter);
…
//где-то в другом месте
//теперь у нас нет доступа к PDO
$data = array(...);//массив строк
$data = array_map($this->service->getQuoter(), $data);
//массив содержит обработанные данные.

Очень удобно применять анонимные функции и для фильтрации

$x = array_filter($data, function($v){ return $v > 0; });
//vs
$x = array();
foreach($data as $v)
{
	if($v > 0){$x[] = $v}
}

События.

Замыкания идеально подходят в качестве обработчиков событий. Например

//где-то при конфигурации приложения.
$this->events->on(User::EVENT_REGISTER, function($user){
	//обновить счетчик логинов для пользователя и т.д.
});

$this->events->on(User::EVENT_REGISTER’, function($user){
	//выслать email для подтверждения.
});

//в обработчике формы регистрации
$this->events->trigger(User::EVENT_REGISTER, $user);

Вынос логики в обработчики событий с одной стороны делает код более чистым, с другой стороны — усложняет поиск ошибок — поведение системы иногда становится неожиданным для человека, который не знает, какие обработчики навешаны в данный момент.

Валидация

Замыкания по сути сохраняют некоторую логику в переменной, которая может быть выполнена или не выполнена в по ходу работы скрипта. Это то, что нужно для реализации валидаторов:

$notEmpty = function($v){ return strlen($v) > 0 ? true : “Значение не может быть пустым”; };
$greaterZero = function($v){ return $v > 0 ? true : “Значение должно быть больше нуля”; };

function getRangeValidator($min, $max){
	return frunction($v) use ($min, $max){
		return ($v >= $min && $v <= $max) 
                         ? true 
                         : “Значение не попадает в промежуток”;
	};
}

В последнем случае мы применяем функцию высшего порядка, которая возвращает другую функцию — валидатор с предустановленными границами значений. Применять валидаторы можно, например, так.

class UserForm extends BaseForm{

    public function __constructor()
    {
        $this->addValidator(‘email’, Validators::$notEmpty);
        $this->addValidator(‘age’, Validators::getRangeValidator(18, 55));
        $this->addValidator(‘custom’, function($v){
		//some logic
        });
    }

    /**
    * Находится в базовом классе.
    */
    public function isValid()
    {
        …
        $validationResult = $validator($value);
        if($validationResult !== true){
            $this->addValidationError($field, $validationResult);
        }
        …
    }
}

Использование в формах классический пример. Также валидация может использоваться в сеттерах и геттерах обычных классов, моделях и т.д. Хорошим тоном, правда, считается декларативная валидация, когда правила описаны не в форме функций, а в форме правил при конфигурации, тем не менее, иногда такой подход очень кстати.

Выражения

В Symfony встречается очень интересное применение замыканий. Класс ExprBuilder опеделяет сущность, которая позволяет строить выражения вида

...
->beforeNormalization()
    ->ifTrue(function($v) { return is_array($v) && is_int(key($v)); })
    ->then(function($v) { return array_map(function($v) { return array('name' => $v); }, $v); })
 ->end()
...

В Symfony как я понял это внутренний класс, который используется для создания обработки вложенных конфигурационных массивов (поправьте меня, если не прав). Интересна идея реализации выражений в виде цепочек. В принципе вполне можно реализовать класс, который бы описывал выражения в таком виде:

$expr = new Expression();

$expr
->if(function(){ return $this->v == 4;})
->then(function(){$this->v = 42;})
->else(function(){})
	->elseif(function(){})
->end()
->while(function(){$this->v >=42})
	->do(function(){
		$this->v --;
})
->end()
->apply(function(){/*Some code*/});

$expr->v = 4;
$expr->exec();
echo $expr->v;

Применение, конечно, экспериментально. По сути — это запись некоторого алгоритма. Реализация такого функционала достаточно сложна — выражение в идеальном случае должно хранить дерево операций. Инетересна концепция, может быть где-то такая конструкция будет полезна.

Роутинг

Во многих мини-фреймворках роутинг сейчас работает на анонимных функциях.

App::router(‘GET /users’, function() use($app){
    $app->response->write(‘Hello, World!’);
});

Достаточно удобно и лаконично.

Кеширование

На хабре это уже обсуждалось, тем не менее.

$someHtml = $this->cashe->get(‘users.list’, function() use($app){
	$users = $app->db->table(‘users)->all();
	return $app->template->render(‘users.list’, $isers);
}, 1000);

Здесь метод get проверяет валидность кеша по ключу ‘users.list’ и если он не валиден, то обращается к функции за данными. Третий параметр определяет длительность хранения данных.

Инициализация по требованию

Допустим, у нас есть сервис Mailer, который мы вызываем в некоторых методах. Перед использованием он должен быть сконфигурирован. Чтобы не инициализировать его каждый раз, будем использовать ленивое создание объекта.

//Где-то в конфигурационном файле.
$service->register(‘Mailer’, function(){
	return new Mailer(‘user’, ‘password’, ‘url’);
});

//Где-то в контроллере
$this->service(‘Mailer’)->mail(...);

Инициализация объекта произойдет только перед самым первым использованием.

Изменение поведения объектов

Иногда бывает полезно переопределить поведение объектов в процессе выполнения скрипта — добавить метод, переопределить старый, и т.д. Замыкание поможет нам и здесь. В PHP5.3 для этого нужно было использовать различные обходные пути.

class Base{
    
    public function publicMethod(){echo 'public';}
    private function privateMethod(){echo 'private';}    
    //будем перехватывать обращение к замыканию и вызывать его.
    public function __call($name, $arguments) {
        if($this->$name instanceof Closure){
            return call_user_func_array($this->$name, array_merge(array($this), $arguments));
        }
    }
}

$b = new Base; 

//создаем новый метод
$b->method = function($self){
    echo 'I am a new dynamic method';
   $self->publicMethod(); //есть доступ только к публичным свойствам и методам
};

$b->method->__invoke($b); //вызов через магический метод

$b->method(); //вызов через перехват обращения к методу

//call_user_func($b->{'method'}, $b); //так не работает

В принципе можно и переопределять старый метод, однако только в случае если он был определен подобным путем. Не совсем удобно. Поэтому в PHP 5.4 появилось возможность связать замыкание с объектом.

$closure = function(){
	return $this->privateMethod();
}

$closure->bindTo($b,  $b); //второй параметр определяет область видимости
$closure();

Конечно, модификации объекта не получилось, тем не менее замыкание получает доступ к приватным функциям и свойствам.

Передача как параметры по умолчанию в методы доступа к данным

Пример получения значения из массива GET. В случае его отсутствия значение будет получено путем вызова функции.

$name = Input::get('name', function() {return 'Fred';});

Функции высшего порядка

Здесь уже был пример создания валидатора. Приведу пример из фреймворка lithium

/**
 * Writes the message to the configured cache adapter.
 *
 * @param string $type
 * @param string $message
 * @return closure Function returning boolean `true` on successful write, `false` otherwise.
 */
public function write($type, $message) {
	$config = $this->_config + $this->_classes;

	return function($self, $params) use ($config) {
		$params += array('timestamp' => strtotime('now'));
		$key = $config['key'];
		$key = is_callable($key) ? $key($params) : String::insert($key, $params);

		$cache = $config['cache'];
		return $cache::write($config['config'], $key, $params['message'], $config['expiry']);
	};
}

Метод возвращает замыкание, которое может быть использовано потом для записи сообщения в кеш.

Передача в шаблоны

Иногда в шаблон удобно передавать не просто данные, а, например, сконфигурированную функцию, которую можно вызвать из кода шаблона для получения каких либо значений.

//В контроллере
$layout->setLink = function($setId) use ($userLogin)
{
    return '/users/' . $userLogin . '/set/' . $setId;
};

//В шаблоне
<a href=<?=$this->setLink->__invoke($id);?>>Set name</a>

В данном случае в шаблоне генерировалось несколько ссылок на сущности пользователя и в адресах этих ссылок фигурировал его логин.

Рекурсивное определение замыкания

Напоследок о том, как можно задавать рекурсивные замыкания. Для этого нужно передавать в use ссылку на замыкание, и вызывать ее в коде. Не забывайте об условии прекращения рекурсии

$factorial = function( $n ) use ( &$factorial ) {
    if( $n == 1 ) return 1;
    return $factorial( $n - 1 ) * $n;
};

print $factorial( 5 );

Многие из примеров выглядят натянуто. Сколько лет жили без них — и ничего. Тем не менее иногда применение замыкания достаточно естественно и для PHP. Умелое использование этой возможности позволит сделать код более читаемым и увеличить эффективность работы программиста. Просто нужно немного подстроить свое мышление под новую парадигму и все станет на свои места. А вообще рекомендую сравнить, как используются такие вещи в других языках типа Python. Надеюсь, что кто-нибудь нашел для себя здесь что-то новое. И конечно, если кто-то знает еще какие-нибудь интересные применения замыканий, то очень жду ваши комментарии. Спасибо!

Автор: neyronius

* - обязательные к заполнению поля