Application of closures of anonymous functions and calback in PHP

Lecture



Introduction to PHP 5.3 closures is one of its main innovations, and although several years have passed since the release, there is still no standard practice for using this language feature. In this article, I tried to collect all the most interesting possibilities for applying closures in PHP.

To begin, consider what it is - a closure and what are its features in 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) "" } } */ 


As you can see, the closure, like the lambda function, is an object of the Closure class, which coordinates the passed parameters. In order to call an object as a function, the magic method __invoke was introduced in PHP5.3.

 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 


Using the use construct, we inherit the variable from the parent scope to the local scope of the Lambda function.
The syntax is simple and straightforward. The use of such functionality in the development of web applications is not entirely clear. I looked at the code of several modern frameworks that use the new features of the language and tried to bring together their various uses.

Callback functions


The most obvious use of anonymous functions is to use them as callbacks. In PHP, there are many standard functions that accept a callback type or its synonym callable as entered in PHP 5.4. The most popular ones are array_filter, array_map, array_reduce. The array_map function is used for iterative processing of array elements. The callback function is applied to each element of the array and the result is the processed array. I immediately had a desire to compare the performance of the usual array processing in a loop using the built-in function. Let's experiment.

 $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 


As you can see, the overhead of a large number of function calls gives a noticeable drop in performance, which is to be expected. Although the test is synthetic, the task of processing large arrays often arises, and in this case, the use of data processing functions can become the place where your application will significantly slow down. Be careful. However, in modern applications, this approach is used very often. It allows you to make the code more concise, especially if the handler is declared somewhere else, and not when called.

In fact, in this context, the use of anonymous functions is no different from the old way of passing a string name of a function or a callback array, except for one particular feature - now we can use closures, that is, save variables from the scope when creating a function. Consider an example of processing an array of data before adding them to the database.

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


It is very convenient to use anonymous functions and to filter

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

Developments.


Closures are ideal as event handlers. for example

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


Putting logic into event handlers on the one hand makes the code cleaner, on the other hand complicates the search for errors — sometimes the behavior of the system becomes unexpected for a person who does not know which handlers are hung at the moment.

Validation


Closures essentially save some logic in a variable that can be executed or not executed in the course of the script. This is what you need to implement validators:

 $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 : “Значение не попадает в промежуток”; }; } 


In the latter case, we apply a higher order function that returns another function — a validator with predefined boundaries of values. You can use validators, for example, as follows.

 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); } … } } 


Using forms is a classic example. Also, validation can be used in setters and getters of ordinary classes, models, etc. It is true that declarative validation is considered to be a good way, when the rules are described not in the form of functions, but in the form of rules in configuration, however, sometimes this approach is very useful.

Expressions


Symfony has a very interesting use for closures. The ExprBuilder class defines an entity that allows you to build expressions like

 ... ->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() ... 


In symfony, as I understand it, this is an internal class that is used to create processing of nested configuration arrays (correct me if not right). An interesting idea is the implementation of expressions in the form of chains. In principle, it is possible to implement a class that would describe expressions in the following form:

 $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; 


Application, of course, experimentally. In essence, this is a record of some algorithm. The implementation of such a functional is quite complicated - the expression in the ideal case should store a tree of operations. An interesting concept, maybe somewhere such a construction would be useful.

Routing


In many mini frameworks, routing now works on anonymous functions.

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


Enough convenient and concise.

Caching

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


Here, the get method checks the validity of the cache using the 'users.list' key and if it is not valid, then it accesses the function for data. The third parameter determines the duration of data storage.

Initialization On Demand


Suppose we have the Mailer service, which we call in some methods. It must be configured before use. In order not to initialize it every time, we will use lazy object creation.

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


Object initialization will occur only before the very first use.

Change the behavior of objects


Sometimes it is useful to override the behavior of objects during script execution — add a method, override an old one, and so on. The closure will help us here too. In PHP5.3, you had to use different workarounds for this.

 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); //так не работает 



In principle, it is possible to override the old method, but only if it was defined in a similar way. Not quite comfortable. Therefore, in PHP 5.4, it became possible to associate a closure with an object.

 $closure = function(){ return $this->privateMethod(); } $closure->bindTo($b, $b); //второй параметр определяет область видимости $closure(); 


Of course, the modification of the object failed, however, the closure gains access to private functions and properties.

Passing as default parameters to data access methods


An example of getting a value from a GET array. In case of its absence, the value will be obtained by calling the function.

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

Higher order functions


There was already an example of creating a validator. I will give an example from the lithium framework

 /** * 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']); }; } 


The method returns a closure that can then be used to write the message to the cache.

Transfer to templates


Sometimes it is convenient to transfer not just data to a template, but, for example, a configured function that can be called from the template code to get any values.

 //В контроллере $layout->setLink = function($setId) use ($userLogin) { return '/users/' . $userLogin . '/set/' . $setId; }; //В шаблоне setLink->__invoke($id);?>>Set name 


In this case, the template generated several links to the user's entities, and his login appeared in the addresses of these links.

Recursive closure definition


Finally, how to set recursive closures. To do this, pass a reference to the closure in use, and call it in code. Do not forget about the condition of the termination of recursion

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



Many of the examples look stiff. How many years have lived without them - and nothing. However, sometimes using closures is natural enough for PHP. Skillful use of this feature will make the code more readable and increase the efficiency of the programmer. You just need to adjust your thinking a little under the new paradigm and everything will fall into place. In general, I recommend to compare how such things are used in other languages ​​like Python. I hope that someone has found something new here. And of course, if someone else knows any interesting applications of closures, then I’m really looking forward to your comments. Thank!

This small tutorial is designed to cover the topic of closures in PHP. First of all, it is worth explaining what a closure is. One definition of closure:

A closure is an anonymous function, that is, a function that does not have a name .

To some extent this reflects the essence of the closure. It is important to understand that closure is a kind of independent entity from a theoretical point of view and not to confuse the very concept of closure with how it is specifically implemented. Below I will describe what the difference is.

About applicability
In order to better understand the scope of closures, you should recall (or find out if you have not done so already) that there is a so-called data pseudotype in PHP - callback. In essence, variables of this type are, from the point of view of the interpreter, the entities for the call. In other words, data of this type can be used as functions. Strictly speaking, this type is wider than just closures; it can also include string names of functions. I will give a complete list:

0. Actually, the circuit
1. Strings that are the names of custom or standard functions (for example, "trim")
2. Names of methods, including static

It should be remembered that in the latter case, the actual callback data will be an array indicating the object in the element with a zero index and the name of the method in the element with the first index. For example:
PHP:
copy code to clipboard

  1. // Proper callback variable of type 0:

  2. $ fnCallback = function ($ sItem)

  3. {

  4. return trim ($ sItem);

  5. };

  6. // Proper callback variable of type 1:

  7. $ fnCallback = "trim";

  8. // Proper callback variable of type 2 (indicating that the data will be processed as Foo-> bar)

  9. $ fnCallback = array ("Foo", "bar");



- the first time I specify the syntax of closures for the first time, which is probably premature, but if you don’t understand it, skip and return to the example later. Callback data can be used in functions, for example, array_walk or call_user_func .

Little formality
Now it's time to talk about how closures are built. Depending on the PHP version, they can:

0. Not supported at all, PHP version <4.0.1
1. Only supported via create_function, PHP version 4> = 4.0.1 or PHP 5 <5.3
2. To be supported both through create_function and through special syntax.

The option when closures are not supported at all, we are not interested, and therefore I will talk about two ways to create closures (those in paragraphs 1. and 2.).
So, creating closures with create_function :
Of course, the most complete description of this function is on the official site, but I will do some summary.
The function takes two parameters and both are string. The line-list of arguments separated by a comma is indicated by the first parameter, the PHP code is indicated by the second parameter.
I will give an example:

PHP:
copy code to clipboard

  1. $ fnCallback = create_function ('$ a, $ b', 'return $ a + $ b;');



- in essence, creates a function that takes two parameters and returns their sum. And what exactly does $ fnCallback look like? Everything is simple here - create_function will return a string for the created closure. In essence, this will be some unique identifier for the closure in the global namespace. Modify the previous example and see:

PHP:
copy code to clipboard

  1. $ fnCallback = create_function ('$ a, $ b', 'return $ a + $ b;');

  2. var_dump ($ fnCallback);


- at the exit

PHP:
copy code to clipboard

  1. string (9) "lambda_1"


In the future, the newly created closure can be used in functions that expect callback parameters. Use the newly created closure:

PHP:
copy code to clipboard

  1. $ rgOdd = array (1,3,5,7);

  2. $ rgEven = array (2,4,6,8);

  3. $ rgResult = array_map ($ fnCallback, $ rgOdd, $ rgEven);

  4. var_dump ($ rgResult);


- will give an array containing the sum of the elements in the same indexes:

PHP:
copy code to clipboard

  1. array (4) {

  2. [0] =>

  3. int (3)

  4. [1] =>

  5. int (7)

  6. [2] =>

  7. int (11)

  8. [3] =>

  9. int (15)

  10. }


I will tell about one reef which exists for create_function. Since both of its arguments are strings, the use of double quotes will cause the interpreter to substitute the values ​​of variables (instead of interpreting the written code as PHP code). Almost always, this is not what is needed, and to avoid this behavior, you need to either escape the "$" signs or use single quotes.

The second way to create closures is with the function keyword.
Only available from PHP 5.3. In this case, the closure is created similarly to the function declaration. There are two significant differences - firstly, such a function does not have a name, secondly, it can refer to its context using the keyword use . This is similar to the use of the global keyword , but not identical to it. I will give the syntax:

PHP:
copy code to clipboard

  1. $ fnCallback = function ($ a, $ b) use ($ c)

  2. {

  3. return $ c * ($ a + $ b);

  4. };


As you can see, the function still takes two parameters, and also relies on some context variable $ c. A context variable means that its value will be as it is at the time the closure was created. To make it clearer, I will give two examples:
the first:

PHP:
copy code to clipboard

  1. $ fnCallback = function ($ a, $ b) use ($ c)

  2. {

  3. return $ c * ($ a + $ b);

  4. };

  5. $ c = 2;

  6. $ rgOdd = array (1,3,5,7);

  7. $ rgEven = array (2,4,6,8);

  8. $ rgResult = array_map ($ fnCallback, $ rgOdd, $ rgEven);

  9. var_dump ($ rgResult);


and second:

PHP:
copy code to clipboard

  1. $ fnCallback = function ($ a, $ b)

  2. {

  3. global $ c;

  4. return $ c * ($ a + $ b);

  5. };

  6. $ c = 2;

  7. $ rgOdd = array (1,3,5,7);

  8. $ rgEven = array (2,4,6,8);

  9. $ rgResult = array_map ($ fnCallback, $ rgOdd, $ rgEven);

  10. var_dump ($ rgResult);


- As you can see, in the second version I replaced use with global . Since in the first case, the $ c variable was not declared at the time of the closure, it simply does not exist in the context and therefore the first example will produce something like this:

PHP:
copy code to clipboard

  1. Notice: Undefined variable: c in ... on line ...

  2. array (4) {

  3. [0] =>

  4. int (0)

  5. [1] =>

  6. int (0)

  7. [2] =>

  8. int (0)

  9. [3] =>

  10. int (0)

  11. }


At the same time, the second example uses $ c from the global namespace, which will give the result:

PHP:
copy code to clipboard

  1. array (4) {

  2. [0] =>

  3. int (6)

  4. [1] =>

  5. int (14)

  6. [2] =>

  7. int (22)

  8. [3] =>

  9. int (30)

  10. }


However, it should be remembered that, as a rule, closures make sense in context with any data, the use of global greatly degrades the readability of the code (I suggest to think about why).
What will happen if both global and use are specified? Obviously, we use the global keyword inside the body of the closure and therefore it will override the context transmitted via use .

What can be done in closures
- As in the usual functions, in closures it is possible to receive links with the help of the & sign. Moreover, the closure can modify its parameters if they are passed by reference (exactly the same as a regular function).
- If PHP version 5.3 and higher, then closures created using the function keyword will not be strings, but instances (objects) of the special class Closure . In fact, this class is made for compatibility and, for example, it is impossible to create an instance of it other than creating a closure (because its constructor prohibits doing this). Thus, this is another difference from create_function . And the opportunity, which is achieved due to the fact that the closure is an object of the class - since PHP 5.4, this class has got methods that allow controlling anonymous functions after they are created. This is a squeeze of documentation.
- Closures can be made recursive. To do this, it is enough to apply a simple trick - to accept callback data as a parameter, on which to rely inside the circuit. For example, the array_walk_recursive function can be replaced with the following code:

PHP:
copy code to clipboard

  1. $ fnWalk = function ($ rgInput, $ fnCallback) use (& $ fnWalk)

  2. {

  3. foreach ($ rgInput as $ mKey => $ mValue)

  4. {

  5. if (is_array ($ mValue))

  6. {

  7. $ fnWalk ($ mValue, $ fnCallback);

  8. }

  9. else

  10. {

  11. $ fnCallback ($ mValue, $ mKey);

  12. }

  13. }

  14. };


Notice the context variable.
- If PHP version 5.4, then it is possible to use $ this inside the closure body. This is similar to referring to properties inside a class.
- If PHP version 5.4, then it is permissible to define "aliases" for the parameters of closures, according to which they will be available within the closure itself. It looks like this:

PHP:
copy code to clipboard

  1. $ fnCallback = function ($ x) use ($ y as $ fMultipier)

  2. {

  3. return $ x * $ this-> fMultiplier;

  4. }


The example gives an idea about the use of $ this

“As a shortcoming of closures, I’ll give you the fact that data of this type cannot be serialized by the standard serialize function, which makes it inconvenient to be present as object properties.

Some recommendations
Так зачем же использовать лямбда-функции? Это - воможность языка, эта конструкция в большинстве случаев может быть заменена другими приемами.
Прежде всего - замыкания полезны при обработке массивов. Очень часто бывает необходимо что-либо сделать для всех элементов массива. В этом случае замыкания - лаконичный и наглядный способ это сделать. Однако если происходит обработка массива, которая не предполагает обработку всех его элементов, то, как правило, от использования замыканий стоит воздержаться. Например, задача - обнаружить, есть ли в массиве значение, которое содержит только символы латиницы az.
При помощи цикла:

PHP:
скопировать код в буфер обмена

  1. $rgData=array('foo12a', 'test', 'bar13b', 'baz14c');

  2. $bFound=false;

  3. foreach($rgData as $sData)

  4. {

  5. $bFound = $bFound || preg_match('/^[az]+$/', $sData);

  6. if($bFound)

  7. {

  8. break;

  9. }

  10. }


И при помощи замыкания:

PHP:
скопировать код в буфер обмена

  1. $rgData=array('foo12a', 'test', 'bar13b', 'baz14c');

  2. $bFound=false;

  3. array_walk($rgData, function($sValue) use (&$bFound)

  4. {

  5. $bFound = $bFound || preg_match('/^[az]+$/', $sValue);

  6. });


- как видим, второй вариант несколько "красивее". Однако же - в первом случае цикл прекратит свое выполнение уже на втором элементе, тогда как во втором случае будет пройден весь массив. В случае, если элементов 4 особой разницы во времени нет, но если счет идет на десятки тысяч, ситуация может измениться.
Поэтому моя общая рекомендация - как и все остальное, замыкания хороши при их использовании в меру. Не стоит гнаться за красотой кода, если при этом явно пострадает производительность.

В качестве заключения, как обычно, приведу вопросы к уроку :
0. Напишите замыкание, которое меняет местами ключи и значения массива
1. Можно ли использовать замыкания в качестве параметров замыканий? Если да/нет то почему?
2. What happens if the loop context variable is also used as its parameter?


Comments


To leave a comment
If you have any suggestion, idea, thanks or comment, feel free to write. We really value feedback and are glad to hear your opinion.
To reply

Running server side scripts using PHP as an example (LAMP)

Terms: Running server side scripts using PHP as an example (LAMP)