Php лямбда функции this

Применение замыканий в 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 

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

Читайте также:  Php xml response to array

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

Источник

Оцените статью