php

L’une des fonctionnalités que présente la version PHP 5.3 sont les Lambda functions et les closures. Une fonction lambda est une fonction PHP anonyme (déclarée a la volée, sans nom, un peu a la Java Script) qui peut être stockée dans une variable et passée comme argument a d’autres fonctions ou méthodes. Une Closure est une fonction lambda qui est « consciente » de son contexte.

Illustrons ceci via des exemples :

Utilisations avec les fonctions array_map(), array_reduce() et array_filter()

L’exemple suivant filtre le tableau $input en enlevant toutes les valeurs supérieures a 5

$input = array(1, 2, 3, 4, 5, 6, 7);
$output = array_filter($input, function ($value) { return $value > 5; });

« function ($v) { return $value > 2; } » est une fonction lambda, qui peut être stockée dans une variable pour etre rutilisee :

$comparator = function ($value) { return $value > 5; };
$input = array(1, 2, 3, 4, 5, 6, 7);
$output = array_filter($input, $comparator);

Supposons maintenant que je veuille changer le nombre permis dans le tableau filtré? Je pourrais éventuellement créer une autre fonction lambda ou utiliser une closure :

$comparator = function ($max)
{
  return function ($value) use ($max) { return $value > $max; };
};

$input = array(1, 2, 3, 4, 5, 6, 7);
$output = array_filter($input, $max_comparator(2));

La fonction $comparator prend en paramètre le nombre maximum permis et retourne une fonction différente selon ce nombre. Bien que l’exemple est simple, vous imaginez déjà le grand pouvoir.

passage de variables par reference

Prenons cet exemple :

$x = 1
$closure = function() use (& $x) { ++$x; }

echo $x . PHP_EOL;
$closure();
echo $x . PHP_EOL;
$closure();
echo $x . PHP_EOL;

Output:
1
2
3

Nous pouvons voir que la closure utilise une variable externe $x et l’incrémente a chaque appel. Nous pouvons mixer des variables passes avec et sans référence avec la clause « use ». Nous pouvons aussi avoir des fonctions qui retournent des closures. Dans ce cas, la durée de vie des closures est plus longue que la méthode qui les a définis. Par exemple :

function getAppender($baseString)
{
      return function($appendString) use ($baseString) { return $baseString . $appendString; };
}

Les closures et les objets

Les closures fournissent un outil intéressant dans le contexte objet. Exemple :

class Chien
{
    private $_nom;
    protected $_couleur;

    public function __construct($nom, $couleur)
    {
         $this->_nom = $nom;
         $this->_couleur = $couleur;
    }

    public function bonjour($salutation)
    {
	 $self = $this;
         return function() use ($self, $salutation)
	 {
             echo "$salutation, je m'appelle {$self->_nom} et je suis un chien {$self->_couleur}.";
         };
    }
}

$chien = new Chien("Georges","rouge");
$chien->bonjour("Bonjour");

Resultat:
Bonjour, je m'appelle Georges et je suis un chien rouge.

Les closures peuvent aussi utilisées dans un contexte statique :

class Maison
{
     public function peint($couleur)
     {
         return static function() use ($couleur) { echo "Peindre la maison en $couleur ...."; };
     }
}

$maison = new Maison();
$maison->peint('rouge');

Resultat:
Peindre la maison en rouge ....

La closure ne charge pas l’objet dans ce cas, ce qui fait que la méthode peint ne soit pas gourmande en mémoire.

La methode __invoke()

La méthode magique __invoke() permet a un objet de se faire appeler comme une closure. par exemple :

class Chien
{
    public function __invoke()
    {
         echo "Je suis un chien!";
    }
}
$dog = new Chien();
$dog();
Resultat :
Je suis un chien!

En appelant $dog en tant que fonction ($dog()), la methode __invoke() est automatiquement appelee transformant la classe en une closure.

La reflection et les closures

L’API de reflection de PHP permet de faire un reverse-engineer sur les classes, interfaces, fonctions, méthodes etc. Puisque les closures sont des fonctions anonymes, ils n’apparaîtront alors pas via les appels de reflection. Par contre, une nouvelle méthode getClosure() a été ajoutée aux classes ReflectionMethod et ReflectionFunction afin de créer une closure depuis une fonction ou une méthode spécifique. Voici un exemple :

class Compteur
{
      private $x;

      public function __construct()
      {
           $this->x = 0;
      }

      public function incremente()
      {
           $this->x++;
      }

      public function valeurCourante()
      {
           echo $this->x . PHP_EOL;
      }
}
$classe = new ReflectionClass('Compteur');
$methode = $classe->getMethod('valeurCourante');
$closure = $methode->getClosure()
$closure();
$classe->incremente();
$closure();

Resultat :
0
1

Notez ici qu’il y a un effet de bord : la méthode getClosure nous permet d’accéder aux membres private et protected, ceci peut être intéressant dans des scénarios de tests unitaires. Exemple :

class Exemple
{
     private static function secret()
     {
          echo "Je suis une methode secrete!";
     }
} 

$classe = new ReflectionClass('Exemple');
$methode = $classe->getMethod('secret');
$closure = $methode->getClosure()
$closure();
Resultat :
Je suis une methode secrete!

Nous pouvons bien évidemment utiliser l’API de reflection pour inspecter la closure elle meme en passant tout simplement la closure au constructeur de la classe ReflectionMethod :

$closure = function ($x, $y = 1) {};
$methodeClosure = new ReflectionMethod($closure);
Reflection::export ($methodeClosure);
Output:
Method [  public method __invoke ] {
  - Parameters [2] {
    Parameter #0 [  $x ]
    Parameter #1 [  $y ]
  }
}

Utilite des closures ?

Comme nous avons vu, l’un des utilisations les plus communes des closures est dans les quelques fonctions qui acceptent une fonction de callback comme paramètres. En effet, les closures peuvent être extrêmement utiles dans n’importe quel contexte la ou on veut encapsuler une logique a l’intérieur de son scope même. En voici un exemple plus concret :

$db = mysqli_connect("server","user","pass");
Logger::log('debug','database','Connected to database');
$db->query('insert into mytable (title, description) values ('Closures','tres interessant');
Logger::log('debug','database','Inserted Closures in mytable');
$db->query('insert into mytable (title, description) values ('Lambda','aussi interessant');
Logger::log('debug','database','Inserted lambda in mytable');
$db->query('insert into mytable (title, description) values ('Anonym functions','like JavaScript');
Logger::log('debug','database','Inserted Anonym Functions in mytable');

Notez ici que chaque appel a Logger::log() passe toujours les mêmes deux premiers paramètres. Nous pouvons améliorer ceci en poussant cet appel dans une closure :

$logdb = function ($string) { Logger::log('debug','database',$string); };

$db = mysqli_connect("server","user","pass");
$logdb('Connected to database'); 

$db->query('insert into mytable (title, description) values ('Closures','tres interessant');
$logdb('Inserted Closures in mytable');
$db->query('insert into mytable (title, description) values ('Lambda','aussi interessant');
$logdb('Inserted lambda in mytable');
$db->query('insert into mytable (title, description) values ('Anonym functions','like JavaScript');
$logdb('Inserted Anonym Functions in mytable');

L’utilisation des closures presente plusieurs avantages dans cet exemple : notamment le code est plus lisible et clair, mais aussi, il est plus facile de changer le niveau du log puisqu’il est implemente a une place unique (la closure).