Для начала рассмотрим, что же это такое — замыкание и в чем его особенности в 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 function($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. Надеюсь, что кто-нибудь нашел для себя здесь что-то новое. И конечно, если кто-то знает еще какие-нибудь интересные применения замыканий, то очень жду ваши комментарии. Спасибо!

