- PVSM.RU - https://www.pvsm.ru -
Введение в 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 [1] опеделяет сущность, которая позволяет строить выражения вида
...
->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 [2]
/**
* 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. Умелое использование этой возможности позволит сделать код более читаемым и увеличить эффективность работы программиста. Просто нужно немного подстроить свое
Автор: neyronius
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/php-2/11280
Ссылки в тексте:
[1] ExprBuilder: https://github.com/symfony/symfony/blob/master/src/Symfony/Component/Config/Definition/Builder/ExprBuilder.php
[2] lithium: http://lithify.me/
[3] мышление: http://www.braintools.ru
Нажмите здесь для печати.