Les nouveautés de PHP 5.3

Et comment migrer de PHP 5.2 vers PHP 5.3


précédentsommairesuivant

II. Fermetures et fonctions lambda

Cette section est une traduction de : http://www.ibm.com/developerworks/opensource/library/os-php-5.3new2/

II-A. Introduction

Dans cette deuxième partie, nous abordons les fermetures et les fonctions lambda. Elles sont conçues pour rendre la programmation plus facile en vous permettant de définir des fonctions jetables qui peuvent être utilisées dans de multiples contextes.

Les concepts de fermetures et de fonctions lambda ne sont pas nouveaux ; ils viennent tous deux du monde de la programmation fonctionnelle. La programmation fonctionnelle est un style de programmation qui déplace le centre d'intérêt depuis l'exécution de commandes à l'évaluation d'expressions. Ces expressions sont formées en utilisant des fonctions, qui sont assemblées pour trouver le résultat qui pourrait nous convenir. Ce style de programmation était plus fréquemment utilisé dans un cadre académique, mais il est également constaté dans les domaines de l'intelligence artificielle et des mathématiques, et il peut être présent dans des applications commerciales avec des langages comme Erlang, Haskell et Scheme.

Les fermetures furent développées initialement dans les années 1960 comme une partie de Scheme, l'un des plus connus des langages de programmation fonctionnelle. Les fonctions lambda et les fermetures sont souvent présentes dans les langages qui permettent aux fonctions d'être traitées comme de véritables valeurs, c'est-à-dire que les fonctions peuvent être créées à la volée et passées en tant que paramètres à d'autres structures.

Depuis lors, les fermetures et les fonctions lambda ont percé hors du monde de la programmation fonctionnelle vers des langages comme JavaScript, Python et Ruby. JavaScript est l'un des langages les plus populaires supportant les fermetures et les fonctions lambda. En fait, il les utilise comme un moyen de proposer la programmation orientée objet, où les fonctions sont imbriquées dans d'autres fonctions pour jouer le rôle de membres privés. Le Listing 1 montre un exemple d'utilisation de fermetures en JavaScript :

Listing 1. Un objet JavaScript construit avec une fermeture
Sélectionnez
var Exemple = function()
{ 
    this.public = function() 
    { 
        return "Ceci est une méthode publique"; 
    }; 

    var private = function() 
    { 
        return "Ceci est une méthode privée"; 
    };
};

Example.public()  // renvoie "Ceci est une méthode publique" 
Example.private() // erreur  ne fonctionne pas

Comme nous le voyons dans Listing 1, les fonctions membres de l'objet Exemple sont définies comme fermetures. Dans la mesure où la méthode privée a une portée de variable locale (contrairement à la méthode publique attachée à l'objet Exemple avec le mot clef this), elle n'est pas visible du monde extérieur.

Maintenant que nous avons une mesure de connaissances historiques sur l'origine de ces concepts, voyons les fonctions lambda en PHP. Le concept de fonctions lambda est la base des fermetures et il donne une bien meilleure méthode de création à la volée de fonction, par rapport à la fonction déjà existante dans PHP : create_function().

[NDT : Certaines fonctionnalités décrites ici ont été supprimées depuis la rédaction initiale de ce document. En effet, avec le cycle bêta de PHP 5.3, des incompatibilités importantes ont été décelées. Le wiki de PHPSuppression de $this dans les fermetures décrit cela. Nos lecteurs ont décrit ici une manière alternative : 13 commentaires]

II-B. Fonctions lambda

Les fonctions lambda (ou « fonctions anonymes » comme on les appelle souvent) sont simplement des fonctions jetables qui peuvent être définies à tout moment et qui sont habituellement liées à une variable. Les fonctions elles-mêmes existent uniquement dans la portée de la variable dans laquelle elles sont définies ; ainsi, lorsque cette variable devient hors de portée, la fonction disparaît également. L'idée des fonctions lambda vient d'un travail mathématique des années 1930. Connu comme « lambda calculus », il avait pour objectif d'analyser la définition et l'application des fonctions, ainsi que le concept de récursion. Le travail de lambda calculus fut utilisé pour développer des langages de programmation fonctionnelle, comme par exemple Lisp et Scheme.

Les fonctions lambda sont pratiques dans un certain nombre de situations, en particulier pour les fonctions PHP qui nécessitent une fonction de rappel. L'une de ces fonctions est array_map(), qui nous permet de parcourir un tableau et d'appliquer une fonction de rappel à chacun de ses éléments. Dans les versions précédentes de PHP, le plus gros problème était qu'il n'y avait pas de moyen propre de créer la fonction de rappel ; nous étions contraints à utiliser l'une de ces approches au problème :

  1. Nous pouvons définir la fonction de rappel ailleurs dans le code de manière à savoir qu'elle est disponible. C'est déroutant car cela déplace ailleurs une partie de l'implémentation de l'appel, ce qui est peu pratique pour la lisibilité et pour la maintenance, surtout si nous n'utilisons cette fonction à aucun autre moment.
  2. Nous pouvons définir la fonction de rappel dans le même bloc de code, mais avec un nom. Tandis que cela aide à la cohésion du code, nous devons ajouter un bloc conditionnel autour de la définition afin d'éviter les collisions d'espace de noms. Listing 2 est un exemple de cette approche.
  3. Nous pouvons utiliser create_function(), qui fait partie de PHP depuis sa version 4, pour créer la fonction à l'exécution. Tandis que d'un point de vue fonctionnel, cela donne l'effet désiré, il y a quelques inconvénients. L'un des problèmes majeurs est que la compilation a lieu ors de l'exécution au lieu de lors de la compilation, ce qui empêche les caches d'opcodes de mettre la fonction en cache. C'est également une syntaxe assez laide et la coloration de code de la plupart des EDI ne fonctionne pas.
Listing 2. Définir une fonction de rappel nommée dans le même bloc de code
Sélectionnez
function quoteWords()
{
     if (!function_exists ('quoteWordsHelper')) {
         function quoteWordsHelper($string) {
             return preg_replace('/(\w)/','"$1"',$string);
         }
      }
      return array_map('quoteWordsHelper', $text);
}

Tandis que les fonctions acceptant des fonctions de rappel sont puissantes, il n'y a aucun moyen de créer une fonction de rappel d'une seule traite sans du code très inélégant. Avec PHP 5.3, nous pouvons utiliser les fonctions lambda pour refaire l'exemple ci-dessus d'une manière bien plus propre :

Listing 3. quoteWords() utilisant une fonction lambda pour le rappel
Sélectionnez
function quoteWords()
{
     return array_map('quoteWordsHelper',
            function ($string) {
                return preg_replace('/(\w)/','"$1"',$string);
            });
}

Nous voyons une syntaxe bien plus propre pour définir ces fonctions, qui peuvent être optimisées pour les performances par les caches d'opcodes. Nous avons également amélioré la lisibilité et la compatibilité avec la coloration syntaxique. Maintenant, partons de là pour rebondir sur le concept des fermetures en PHP.

II-C. Fermetures

Les fonctions lambda par elles-mêmes ajoutent peu de choses par rapport à ce que nous pouvions faire jusque-là. Comme nous l'avons vu, il était possible de faire la même chose avec create_function(), bien qu'avec une syntaxe plus laide et des performances moins qu'idéales. Cependant, ce sont également des fonctions jetables et elles ne maintiennent aucune sorte d'état, ce qui limite leur utilité. Voici où les fermetures entrent en scène et poussent les fonctions lambda au niveau supérieur.

Une fermeture est une fonction évaluée dans son propre environnement, qui a une ou plusieurs variables liées, disponibles au moment où la fonction est appelée. Les fermetures viennent du monde de la programmation fonctionnelle, où les développeurs jonglent avec un certain nombre de concepts. Les fermetures sont comme des fonctions lambda mais elles sont plus intelligentes dans la mesure où elles ont la capacité d'interagir avec des variables hors de l'environnement dans lequel la fermeture est définie.

Voyons comment définir une fermeture en PHP. Le listing 4 montre un exemple de fermeture qui importe une variable de l'environnement externe et qui l'affiche simplement à l'écran :

Listing 4. Exemple de fermeture simple
Sélectionnez
$chaine = "Hello World!";
$fermeture = function() use ($chaine) { echo $chaine; };

$fermeture();

Affiche :
Hello World!

Les variables importées de l'environnement externe sont spécifiées dans la clause use de la définition de la fermeture. Par défaut, elles sont transmises par valeur, ce qui signifie que modifier la valeur passée dans la définition de la fermeture ne modifie pas la valeur externe. Nous pouvons cependant le faire en préfixant la variable par l'opérateur &, qui est utilisé dans les définitions de fonctions pour indiquer le passage par référence. Le listing 5 montre un exemple de cela :

Listing 5. Fermeture passant une variable par référence
Sélectionnez
$x = 1;
$closure = function() use (&$x) { ++$x; };

echo $x . "\n";
$closure();
echo $x . "\n";
$closure();
echo $x . "\n";

Affiche :
1
2
3

Nous voyons ici une fermeture utilisant une variable externe $x et l'incrémentant chaque fois qu'elle est appelée. Nous pouvons aisément mélanger les variables passées par valeur et par référence dans la clause use, et elles seront gérées sans problème. Nous pouvons également avoir des fonctions qui renvoient directement une fermeture, comme le montre le Listing 6. Dans ce cas, la durée de vie de la fermeture sera plus longue que celle de la méthode qui l'a définie :

Listing 6. Fermeture retournée par une fonction
Sélectionnez
function getAppender($baseString)
{
      return function($appendString) use ($baseString) {
         return $baseString . $appendString;
      };
}

II-D. Fermetures et objets

Les fermetures peuvent être un outil très pratique, non seulement pour la programmation fonctionnelle mais aussi pour la programmation orientée objet. Dans cette situation, utiliser les fermetures a le même objectif qu'à l'extérieur d'une classe : contenir une fonction spécifique liée à une petite portée. Par ailleurs, elles sont tout aussi simples à utiliser dans nos objets qu'à l'extérieur.

Lorsqu'elles sont définies dans un objet, un aspect intéressant est que la fermeture peut utiliser la totalité de l'objet à travers la variable $this sans nécessiter un import explicite. Le Listing 7 le montre :

Listing 7. Fermeture dans un objet
Sélectionnez
class Chien
{
    public $_nom;
    public $_couleur;

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

    public function saluer($salut)
    {
		$self = $this;
        return function() use ($salut, $self) {
             echo "$salut, je suis un chien {$self->_couleur}"
               ." appelé {$self->_nom}.";
         };
    }
}

$chien = new Chien("Rover","rouge");
$fermeture = $chien->saluer("Bonjour");
$fermeture();

Affiche :
Bonjour, je suis un chien rouge appelé Rover.

Ici, nous utilisons de manière explicite depuis la fermeture le salut donné à la méthode saluer() qui définit la fermeture. Dans la fermeture, nous récupérons également la couleur et le nom du chien transmis au constructeur et enregistrés dans l'objet.

Les fermetures définies dans une classe sont fondamentalement les mêmes que celles définies à l'extérieur d'un objet. La seule différence est l'importation automatique de l'objet par la variable $this. Nous pouvons désactiver ce comportement en définissant la fermeture comme static.

Listing 8. Fermeture statique
Sélectionnez
class Maison
{
     public function peindre($couleur)
     {
         return static function() use ($couleur) {
            echo "Peindre la maison en $couleur...";
         };
     }
}

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

Affiche :
Peindre la maison en rouge...

Cet exemple est similaire à la classe Chien définie dans le Listing 5. La grande différence est que l'on n'utilise aucune propriété de l'objet depuis la fermeture puisqu'elle est définie static.

Le grand avantage d'utiliser une fermeture statique par rapport à une fermeture non statique à l'intérieur d'un objet, est pour les économies de mémoire. Puisqu'il n'est pas nécessaire d'importer l'objet dans la fermeture, nous pouvons économiser une jolie quantité de mémoire, surtout si nous avons de nombreuses fermetures qui n'ont pas besoin de cette fonctionnalité.

[NDT : Les fermetures static ont été désactivées depuis l'écriture de cet article : http://wiki.php.net/rfc/closures/removal-of-thisRemoval of $this in closures]

Un autre aspect intéressant pour les objets est l'ajout d'une méthode magique __invoke(), qui permet à l'objet lui-même d'être appelé comme une fermeture. Si cette méthode est définie, elle est utilisée lorsque l'objet est appelé dans ce contexte. Le Listing 9 le montre :

Listing 9. Utiliser la méthode __invoke()
Sélectionnez
class Chien
{
    public function __invoke()
    {
         echo "Je suis un chien !";
    }
}

$chien = new Chien();
$chien();

Appeler la référence d'objet montrée en Listing 9 comme une fonction appelle automatiquement la méthode magique __invoke(), provoquant un comportement de fermeture par la classe elle-même.

Les fermetures peuvent s'intégrer dans du code orienté objet aussi bien que dans du code procédural. Voyons maintenant les interactions avec la puissante API de réflexion de PHP.

II-E. Fermetures et réflexion

[NDT : Les méthodes getClosure() et getClosureThis() ont été désactivées depuis l'écriture de cet article : http://wiki.php.net/rfc/closures/removal-of-thisRemoval of $this in closures]

PHP dispose d'une fonctionnalité bien utile de réflexion qui nous permet de rétro concevoir des classes, des interfaces, des fonctions et des méthodes. De conception, les fermetures sont des fonctions anonymes, ce qui signifie qu'elles n'apparaissent pas dans l'API de réflexion.

Cependant, une nouvelle méthode getClosure() a été ajoutée aux classes ReflectionMethod et ReflectionFunction afin de permettre à PHP de créer dynamiquement une fermeture à partir de la fonction ou de la méthode spécifiée. Elle agit comme une macro dans ce contexte, où appeler une méthode ou une fonction par l'intermédiaire de la fermeture génère l'appel de fonction dans le contexte dans lequel elle est définie. Le Listing 10 montre comment cela fonctionne :

Listing 10. Utiliser la méthode getClosure()
Sélectionnez
class Compteur
{
      private $x;

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

      public function incrémente()
      {
           $this->x++;
      }

      public function valeurActuelle()
      {
           echo $this->x . "\n";
      }
}
$classe = new ReflectionClass('Compteur');
$méthode = $classe->getMethod('valeurActuelle');
$fermeture = $méthode->getClosure();
$fermeture();
$classe->incrémente();
$fermeture();

Affiche :
0
1

Un effet de bord intéressant de cette approche est que cela nous permet d'avoir accès à des membres privés et protégés d'une classe par l'intermédiaire de la fermeture, ce qui peut s'avérer pratique pour les tests unitaires des classes. Le Listing 11 est un exemple d'accès à une méthode privée dans une classe.

Listing 11. Utiliser une méthode privée d'une classe
Sélectionnez
class Exemple 
{ 
     ....
     private static function secret() 
     { 
          echo "Je suis une méthode qui se cache !"; 
     } 
     ...
} 

$classe = new ReflectionClass('Exemple');
$méthode = $class->getMethod('secret');
$fermeture = $méthode->getClosure();
$fermeture();

Affiche :
Je suis une méthode qui se cache !

De plus, nous pouvons utiliser l'API de réflexion pour inspecter une fermeture elle-même, comme le montre le Listing 12. Nous transmettons simplement la référence à la variable à la fermeture dans le constructeur de la classe ReflectionMethod.

Listing 12. Inspection d'une fermeture en utilisant l'API de réflexion
Sélectionnez
$fermeture = function ($x, $y = 1) {}; 
$m = new ReflectionMethod($fermeture); 
Reflection::export ($m);

Affiche :
Method [ <internal> public method __invoke ] {
  - Parameters [2] {
    Parameter #0 [ <required> $x ]
    Parameter #1 [ <optional> $y ]
  }
}

En termes de compatibilité avec le code existant, le nom de classe Closure est désormais réservé par le moteur PHP pour enregistrer les fermetures. Ainsi, toute classe utilisant ce nom devra être renommée.

L'API de réflexion a un excellent support des fermetures, comme nous l'avons vu, dans la mesure où elle est capable de créer dynamiquement des fonctions et des méthodes. Elle peut également inspecter une fermeture comme le peut n'importe quelle fonction.

II-F. Pourquoi les fermetures ?

Comme nous l'avons vu dans les exemples pour les fonctions lambda, l'une des utilisations les plus évidentes des fermetures est dans les quelques fonctions PHP qui acceptent une fonction de rappel en paramètre. En revanche, les fermetures peuvent être utiles dans tout contexte où nous avons besoin d'encapsuler de la logique dans sa propre portée. Un exemple de cela est la réorganisation de code pour aider à sa simplification et à le rendre davantage lisible. Prenons l'exemple suivant qui montre un enregistreur utilisé pendant l'exécution de requêtes SQL :

Listing 13. Code archivant des requêtes SQL
Sélectionnez
$db = mysqli_connect("serveur","utilisateur","mot de passe"); 
Enregistreur::archive('debug','database','Connecté à la base de données'); 

$db->query("insert into parts (part, description)
   values ('Marteau','Martèle les ongles')"); 
Enregistreur::archive('debug','database','Insérer Marteau dans la table parts'); 

$db->query("insert into parts (part, description)
   values ('Vrille','Fait des trous dans du bois')");
Enregistreur::archive('debug','database','Insérer Vrille dans la table parts'); 

$db->query("insert into parts (part, description)
   values ('Scie','Coupe du bois')"); 
Enregistreur::archive('debug','database','Insérer Scie dans la table parts'); 

Une chose qui se démarque dans le Listing 13 est le nombre de fois que l'on répète les mêmes opérations. Chaque appel à Enregistreur::archiver() a les deux mêmes premiers arguments. Afin d'y remédier, nous pouvons déplacer cet appel de méthode dans une fermeture et faire les appels à cette fermeture. Le code résultant est montré ci-dessous :

Listing 14. Code remanié d'archivage de requêtes SQL
Sélectionnez
$logdb = function ($string) {
   Enregistreur::archive('debug','database',$string);
};

$db = mysqli_connect("serveur","utilisateur","mot de passe"); 
$logdb('Connecté à la base de données'); 

$db->query("insert into parts (part, description)
   values ('Marteau','Martèle les ongles')"); 
$logdb('Insérer Marteau dans la table parts'); 

$db->query("insert into parts (part, description) 
   values ('Vrille','Fait des trous dans le bois')");
$logdb('Insérer Vrille dans la table parts'); 

$db->query("insert into parts (part, description)
   values ('Scie','Coupe le bois')"); 
$logdb('Insérer Scie dans la table parts'); 

En plus d'avoir donné une apparence plus propre au code, nous avons simplifié le niveau d'archivage des requêtes SQL car nous n'avons plus à effectuer la modification que dans un endroit.

II-G. Conclusion

Cet article montre l'utilité des fermetures comme une structure de programmation fonctionnelle dans du code PHP 5.3. Nous avons parlé des fonctions lambda et des avantages qu'ont les fermetures à leur encontre. Les objets et les fermetures s'entendent à merveille, tel que nous l'avons vu dans la gestion particulière des fermetures dans du code orienté objet. Nous avons vu que l'API de réflexion est très utile pour créer des fermetures dynamiques ainsi que pour inspecter des fermetures.


précédentsommairesuivant

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2008 John Mertic et Guillaume Rossolini. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.