Using closures in PHP

    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 features for using 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, a closure, like a lambda function, is an object of the Closure class, which stores the passed parameters. In order to call the object as a function, the __invoke magic method 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 for several modern frameworks using the new features of the language and tried to put together their various applications.

    Callback functions


    The most obvious use of anonymous functions is to use them as callbacks. PHP has many standard functions that accept callback type or its synonym callable introduced in PHP 5.4. The most popular of them are array_filter, array_map, array_reduce. The array_map function is used for iterative processing of array elements. A callback function is applied to each element of the array and the processed array is returned as the result. I immediately had a desire to compare the performance of conventional 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 decline in performance, which is to be expected. Although the test is synthetic, the task of processing large arrays arises often, and in this case, the application of data processing functions can become the place that will significantly slow down your application. Be careful. Nevertheless, 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 the string name of a function or callback array, except for one feature - now we can use closures, that is, save variables from 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 for filtering.

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

    Events.


    Closures are ideal as event handlers. for instance

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

    Removing 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 currently attached.

    Validation


    Closures essentially preserve some logic in a variable that may or may not be executed as the script runs. 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 use a higher-order function that returns another function - a validator with predefined value boundaries. Validators can be used, 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. Validation can also be used in setters and getters of ordinary classes, models, etc. True, declarative validation is considered good practice, when the rules are described not in the form of functions, but in the form of rules during 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 of the form

    ...
    ->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 for 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 quite possible to implement a class that describes expressions in this 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, is experimental. In fact, this is a record of some algorithm. The implementation of such a functional is quite complicated - the expression in the ideal case should store the tree of operations. Interesting concept, maybe somewhere such a design will 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!’);
    });
    

    Comfortable enough and concise.

    Caching


    On a habr it was already discussed, nevertheless.

    $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 key 'users.list' and if it is not valid, it accesses the function for data. The third parameter determines the duration of data storage.

    On-Demand Initialization


    Suppose we have a Mailer service that we call in some methods. Before use, it must be configured. 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(...);
    

    Initialization of the object will occur only before the very first use.

    Changing Object Behavior


    Sometimes it is useful to override the behavior of objects during the execution of a script - add a method, override the old one, etc. Closing will help us here. In PHP5.3, you had to use various 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, you can redefine the old method, but only if it was defined in a similar way. Not very convenient. 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 did not work, however, the closure gets access to private functions and properties.

    Pass as default parameters to data access methods


    An example of getting a value from a GET array. If it is absent, the value will be obtained by calling the function.
    $name = Input::get('name', function() {return 'Fred';});
    


    Higher Order Functions


    There has already been 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, which can then be used to write a message to the cache.

    Pass to templates


    Sometimes it’s convenient to transfer not just data to the 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, several links to user entities were generated in the template, and their login appeared in the addresses of these links.

    Recursive closure definition


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

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


    Many of the examples look taut. How many years have lived without them - and nothing. However, sometimes applying 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 bit to the new paradigm and everything will fall into place. In general, I recommend comparing how such things are used in other languages ​​such as Python. I hope someone found something new for themselves here. And of course, if someone knows any other interesting applications of closures, then I really look forward to your comments. Thanks!

    Also popular now: