- The Closure class
- Class synopsis
- Table of Contents
- User Contributed Notes 4 notes
- Применение замыканий в PHP
- Функции обратного вызова
- События.
- Валидация
- Выражения
- Роутинг
- Кеширование
- Инициализация по требованию
- Изменение поведения объектов
- Передача как параметры по умолчанию в методы доступа к данным
- Функции высшего порядка
- Передача в шаблоны
- Рекурсивное определение замыкания
The Closure class
Anonymous functions yield objects of this type. This class has methods that allow further control of the anonymous function after it has been created.
Besides the methods listed here, this class also has an __invoke method. This is for consistency with other classes that implement calling magic, as this method is not used for calling the function.
Class synopsis
public static bind ( Closure $closure , ? object $newThis , object | string | null $newScope = «static» ): ? Closure
Table of Contents
- Closure::__construct — Constructor that disallows instantiation
- Closure::bind — Duplicates a closure with a specific bound object and class scope
- Closure::bindTo — Duplicates the closure with a new bound object and class scope
- Closure::call — Binds and calls the closure
- Closure::fromCallable — Converts a callable into a closure
User Contributed Notes 4 notes
This caused me some confusion a while back when I was still learning what closures were and how to use them, but what is referred to as a closure in PHP isn’t the same thing as what they call closures in other languages (E.G. JavaScript).
In JavaScript, a closure can be thought of as a scope, when you define a function, it silently inherits the scope it’s defined in, which is called its closure, and it retains that no matter where it’s used. It’s possible for multiple functions to share the same closure, and they can have access to multiple closures as long as they are within their accessible scope.
In PHP, a closure is a callable class, to which you’ve bound your parameters manually.
It’s a slight distinction but one I feel bears mentioning.
Small little trick. You can use a closures in itself via reference.
Example to delete a directory with all subdirectories and files:
Применение замыканий в 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 function($v) use ($min, $max)< return ($v >= $min && $v ; >
В последнем случае мы применяем функцию высшего порядка, которая возвращает другую функцию — валидатор с предустановленными границами значений. Применять валидаторы можно, например, так.
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()v = 42;>) ->else(function()<>) ->elseif(function()<>) ->end() ->while(function()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()private function privateMethod() //будем перехватывать обращение к замыканию и вызывать его. 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->, $b); //так не работает
В принципе можно и переопределять старый метод, однако только в случае если он был определен подобным путем. Не совсем удобно. Поэтому в PHP 5.4 появилось возможность связать замыкание с объектом.
$closure = function()< return $this->privateMethod(); > $closure->bindTo($b, $b); //второй параметр определяет область видимости $closure();
Конечно, модификации объекта не получилось, тем не менее замыкание получает доступ к приватным функциям и свойствам.
Передача как параметры по умолчанию в методы доступа к данным
Пример получения значения из массива GET. В случае его отсутствия значение будет получено путем вызова функции.
$name = Input::get('name', function() );
Функции высшего порядка
Здесь уже был пример создания валидатора. Приведу пример из фреймворка 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']); >; >
Метод возвращает замыкание, которое может быть использовано потом для записи сообщения в кеш.
Передача в шаблоны
Иногда в шаблон удобно передавать не просто данные, а, например, сконфигурированную функцию, которую можно вызвать из кода шаблона для получения каких либо значений.
В данном случае в шаблоне генерировалось несколько ссылок на сущности пользователя и в адресах этих ссылок фигурировал его логин.
Рекурсивное определение замыкания
Напоследок о том, как можно задавать рекурсивные замыкания. Для этого нужно передавать в use ссылку на замыкание, и вызывать ее в коде. Не забывайте об условии прекращения рекурсии
$factorial = function( $n ) use ( &$factorial ) < if( $n == 1 ) return 1; return $factorial( $n - 1 ) * $n; >; print $factorial( 5 );
Многие из примеров выглядят натянуто. Сколько лет жили без них — и ничего. Тем не менее иногда применение замыкания достаточно естественно и для PHP. Умелое использование этой возможности позволит сделать код более читаемым и увеличить эффективность работы программиста. Просто нужно немного подстроить свое мышление под новую парадигму и все станет на свои места. А вообще рекомендую сравнить, как используются такие вещи в других языках типа Python. Надеюсь, что кто-нибудь нашел для себя здесь что-то новое. И конечно, если кто-то знает еще какие-нибудь интересные применения замыканий, то очень жду ваши комментарии. Спасибо!