Mis à jour le 08/01/2018
  • 30 heures
  • Moyenne

Ce cours est visible gratuitement en ligne.

Ce cours existe en livre papier.

Vous pouvez obtenir un certificat de réussite à l'issue de ce cours.

Vous pouvez être accompagné et mentoré par un professeur particulier par visioconférence sur ce cours.

J'ai tout compris !

Les closures

Connectez-vous ou inscrivez-vous gratuitement pour bénéficier de toutes les fonctionnalités de ce cours !

Les closures, représentant ce qu'on appelle des fonctions anonymes, sont un moyen de créer des fonctions à la volée et ont été implémentées à PHP dans sa version 5.3. Elles sont souvent utilisées en tant que fonctions de rappel comme nous allons le voir. Depuis sa version 5.4, PHP propose de lier les closures à des objets ou à des classes, rendant leur utilisation encore plus pratique.

Création de closures

Syntaxe

Une closure est une fonction particulière qui n'est pas nommée. Elle se déclare plutôt facilement, comme ceci :

<?php
function()
{
  echo 'Hello world !';
};

Quel intérêt me direz-vous ? En fait, ce qui semble être une fonction est en fait un objet (ce chapitre n'aurait pas sa place dans ce cours sinon !). En réalité, il s'agit d'une instance de la classeClosure. Si vous voulez vous en convaincre, vous n'avez qu'à essayer ce code :

<?php
$maFonction = function()
{
  echo 'Hello world !';
};

var_dump($maFonction); // On découvre ici qu'il s'agit bien d'un objet de type Closure.

Cette classeClosure possède une méthode magique que l'on a vue :__invoke. Vous souvenez-vous de son utilité ? Cette méthode est invoquée lorsque l'on se sert de notre objet comme une fonction. Vous l'aurez peut-être deviné : si l'on se sert de notre objet comme fonction, alors la fonction que l'on vient de déclarer sera invoquée. Essayez donc ceci :

<?php
$maFonction = function()
{
  echo 'Hello world !';
};

$maFonction(); // Affiche « Hello world ! »

Ça fait beaucoup de codes pour pas grand-chose, je vous l'accorde. Voyons un exemple plus concret !

Exemple d'utilisation

Les closures sont principalement utilisées en tant que fonctions de rappels. Les fonctions de rappels sont des fonctions demandées par d'autres fonctions pour effectuer des tâches spécifiques.

Prenons l'exemple de la fonction array_map. Cette fonction permet d'appeler la fonction qu'on lui passe en premier argument sur chaque élément du tableau passé en deuxième argument (comme vous le voyez, on peut passer une infinité de tableaux à traiter, mais nous resterons sur 1 tableau pour l'exemple).

La fonction que l'on doit donner à array_map doit posséder 1 paramètre : la valeur actuelle du tableau traitée pararray_map (cette fonction va appeler notre closure sur chaque élément du tableau). Notre fonction doit renvoyer la nouvelle valeur. Si l'on a un tableau de nombres et que l'on souhaite ajouter 5 à chacun d'entre eux, notre code ressemblerait à ceci :

<?php
// Notre fonction accepte 1 argument : le nombre actuellement traité par array_map
$additionneur = function($nbr)
{
  return $nbr + 5;
};

$listeNbr = [1, 2, 3, 4, 5];

$listeNbr = array_map($additionneur, $listeNbr);
// Nous obtenons alors le tableau [6, 7, 8, 9, 10]

Utilisation de variables extérieures

Actuellement, notre additionneur est assez limité. En effet, si je veux pouvoir ajouter 4 à chacun de mes nombres, je devrais créer une autre closure. Il serait donc intéressant de rendre variable le nombre ajouté (ici, 5). Pour cela, nous avons le mot-cléuse qui permet d'importer au sein de notre fonction une variable extérieure.

Son utilisation est plutôt intuitive, voyez par vous-mêmes :

<?php
$quantite = 5;
$additionneur = function($nbr) use($quantite)
{
  return $nbr + $quantite;
};

$listeNbr = [1, 2, 3, 4, 5];

$listeNbr = array_map($additionneur, $listeNbr);

var_dump($listeNbr);
// On obtient là aussi le tableau [6, 7, 8, 9, 10]

Un problème se pose maintenant : la quantité que nous avons fixée à 5 ne peut être changée, car cette variable a été importée dans notre closure dès la création de cette dernière. Ainsi, avec le code suivant, le résultat obtenu ne sera pas celui escompté :

<?php
$quantite = 5;
$additionneur = function($nbr) use($quantite)
{
  return $nbr + $quantite;
};

$listeNbr = [1, 2, 3, 4, 5];

$listeNbr = array_map($additionneur, $listeNbr);
var_dump($listeNbr);
// On a : $listeNbr = [6, 7, 8, 9, 10]

$quantite = 4;

$listeNbr = array_map($additionneur, $listeNbr);
var_dump($listeNbr);
// On a : $listeNbr = [11, 12, 13, 14, 15] au lieu de [10, 11, 12, 13, 14]

Dans ce cas-là, nous allons passer par une fonction chargée de renvoyer une closure. Cette fonction prendra 1 argument : la quantité à ajouter. Nous importerons ainsi cette quantité dans notre closure, puis la retournerons. Ainsi, array_map recevra une nouvelle closure, dont la quantité importée sera à chaque fois différente (si on en spécifie une différente bien entendue).

Vous ne voyez peut-être pas exactement comment se présentera notre code. Le voici donc, suivi de plus amples explications :

<?php
function creerAdditionneur($quantite)
{
  return function($nbr) use($quantite)
  {
    return $nbr + $quantite;
  };
}

$listeNbr = [1, 2, 3, 4, 5];

$listeNbr = array_map(creerAdditionneur(5), $listeNbr);
var_dump($listeNbr);
// On a : $listeNbr = [6, 7, 8, 9, 10]

$listeNbr = array_map(creerAdditionneur(4), $listeNbr);
var_dump($listeNbr);
// Cette fois-ci, on a bien : $listeNbr = [10, 11, 12, 13, 14]

Ici, nous avons une fonctioncreerAdditionneur chargée de renvoyer l'additionneur quearray_map utilisera. Cet additionneur importe la quantité à additionner dès sa création. Or, cet additionneur est créé lorsque l'on appellecreerAdditionneur avec la quantité à additionner. Suivant l'argument donné àcreerAdditionneur, la quantité additionnée ne sera pas la même, d'où le résultat final !

Lier une closure

Lier une closure à un objet

Actuellement, nous sommes capables de créer des closures dénuées de tout contexte. Sachez qu'il est possible, une fois notre closure créée, d'en obtenir une copie qui sera liée à un objet. En d'autres termes, notre closure fera partie de notre objet, lui permettant un accès à ses attributs et méthodes.

Reprenons notre additionneur basique, mais modifions-le pour qu'il ajoute 5 à un attribut d'un objet. Notre additionneur ressemblerait donc à ceci :

<?php
$additionneur = function()
{
  $this->_nbr += 5;
};

class MaClasse
{
  private $_nbr = 0;

  public function nbr()
  {
    return $this->_nbr;
  }
}

$obj = new MaClasse;

Le but de la manoeuvre sera donc d'ajouter cette fonction à notre objet$obj afin de pouvoir modifier ce nombre. Pour cela, nous allons nous servir de la méthode bindTo de la classeClosure. Cette méthode accepte 2 arguments. Le premier est l'objet auquel on veut lier notre closure (ici, ce sera donc$obj). Le deuxième argument est le contexte dans lequel la méthode sera invoquée. Ici, nous souhaitons modifier un attribut privé, il est donc important que la méthode soit invoquée au sein de la classe MaClasse. Il y a 2 façons de l'indiquer : soit par le biais d'une chaine de caractères valant « MaClasse », soit par un objet du type MaClasse (comme$obj tout simplement).

Notre code ressemblera donc à ceci :

<?php
$additionneur = function()
{
  $this->_nbr += 5;
};

class MaClasse
{
  private $_nbr = 0;

  public function nbr()
  {
    return $this->_nbr;
  }
}

$obj = new MaClasse;

// On obtient une copie de notre closure qui sera liée à notre objet $obj
// Cette nouvelle closure sera appelée en tant que méthode de MaClasse
// On aurait tout aussi bien pu passer $obj en second argument
$additionneur = $additionneur->bindTo($obj, 'MaClasse');
$additionneur();

echo $obj->nbr(); // Affiche bien 5

Il est important de comprendre cette histoire de contexte. En effet, je vous laisse essayer ce code :

<?php
$additionneur = function()
{
  $this->_nbr += 5;
};

class MaClasse
{
  private $_nbr = 0;

  public function nbr()
  {
    return $this->_nbr;
  }
}

class AutreClasse { }

$obj = new MaClasse;

$additionneur = $additionneur->bindTo($obj, 'AutreClasse');
$additionneur();

// L'appel de nbr générera une erreur stipulant que vous accédez à $_nbr alors que vous n'avez pas le droit
echo $obj->nbr();

Le contexte n'est pas un argument obligatoire. Si vous ne le spécifiez pas, le contexte sera inchangé, c'est-à-dire que le contexte de la nouvelle closure sera le même que celui de l'ancienne.

Lier temporairement une closure à un objet

Pour de petites opérations, la solution précédente peut s'avérer lourde. Depuis la version 7 de PHP, il est possible de lier la closure à un objet le temps d'un appel. Considérons l'exemple suivant :

<?php

class Nombre
{
  private $_nbr;
  
  public function __construct($nbr)
  {
    $this->_nbr = $nbr;
  }
}

$closure = function() {
  var_dump($this->_nbr + 5);
};

$two = new Nombre(2);
$three = new Nombre(3);

$closure->call($two);
$closure->call($three);

Vous découvrirez que le premier appel donnera un résultat de 7, tandis que le second donnera un résultat de 8. Que s'est-il passé ?

  1. Une closure a été créée, ayant pour rôle d'afficher le résultat de l'addition de l'attribut_nbr avec 5.

  2. Nous avons 2 instances différents de la même classe. L'attribut_nbr de la première instance vaut 2, tandis qu'il vaut 3 dans la seconde instance.

  3. Nous invoquons la closure en la liant au premier objet. De cette façon, l'expression$this->_nbr contenue dans la closure vaut 2. Le résultat est donc de 7.

  4. Nous invoquons ensuite la closure en la liant au second objet. Cette fois-ci, l'expression$this->_nbr contenue dans la closure vaut 3. Le résultat est donc de 8.

Lier une closure à une classe

Voilà qui devrait vous interpeller. Est-ce que cela aurait un sens de lier notre additionneur à une classe ? Non, pour la simple et bonne raison que notre classe accède à un attribut, et un attribut tel que celui qu'on a manipulé appartient à un objet et non à une classe. Voyez-vous où je veux en venir ? Il est ainsi possible de lier une closure statique à une classe.

Reprenons notre additionneur ainsi que notre classeMaClasse et adaptons-les pour que notre closure fonctionne dans un contexte statique.

<?php
$additionneur = function()
{
  self::$_nbr += 5;
};

class MaClasse
{
  private static $_nbr = 0;

  public static function nbr()
  {
    return self::$_nbr;
  }
}

Cette fois-ci, l'appel debindTo sera légérement différent. En effet, nous ne souhaitons pas lier notre closure à un objet. Nous allons donc passernull en premier argument. Le second argument, lui, sera toujours le même, car nous souhaitons toujours invoquer notre closure en tant que méthode deMaClasse.

<?php
$additionneur = function()
{
  self::$_nbr += 5;
};

class MaClasse
{
  private static $_nbr = 0;

  public static function nbr()
  {
    return self::$_nbr;
  }
}

$additionneur = $additionneur->bindTo(null, 'MaClasse');
$additionneur();

echo MaClasse::nbr(); // Affiche bien 5

Il reste un dernier petit détail à régler. Actuellement, notre closure peut toujours être liée à un objet. Si vous souhaitez imposer à votre closure de ne pouvoir être liée qu'à une classe, il faut la déclarer statique avec le mot-clé...static !

<?php
// Nous déclarons ici une closure statique
$additionneur = static function()
{
  self::$_nbr += 5;
};

class MaClasse
{
  private static $_nbr = 0;

  public static function nbr()
  {
    return self::$_nbr;
  }
}

$additionneur = $additionneur->bindTo(null, 'MaClasse');
$additionneur();

echo MaClasse::nbr();

Dorénavant, si vous essayez de la lier à un objet, PHP vous dira gentillement que ce n'est pas possible.

Les liaisons automatiques

Jusqu'à maintenant, nous avons toujours déclaré nos closures dans la partie globale du script, et nous devions les lier manuellement aux objets ou classes souhaitées. En fait, si vous déclarez une closure à l'intérieur d'une méthode, cette closure adoptera le contexte dans lequel a été appelée cette méthode, et sera directement liée à l'objet concerné si la méthode n'est pas statique.

<?php
class MaClasse
{
  private $_nbr = 0;

  public function getAdditionneur()
  {
    return function()
    {
      $this->_nbr += 5;
    };
  }

  public function nbr()
  {
    return $this->_nbr;
  }
}

$obj = new MaClasse;

$additionneur = $obj->getAdditionneur();
$additionneur();

echo $obj->nbr();
// Affiche bien 5 car notre closure est bien liée à $obj depuis MaClasse

Bien entendu, le principe reste exactement le même pour un contexte statique :

<?php
class MaClasse
{
  private static $_nbr = 0;

  public static function getAdditionneur()
  {
    return function()
    {
      self::$_nbr += 5;
    };
  }

  public static function nbr()
  {
    return self::$_nbr;
  }
}

$additionneur = MaClasse::getAdditionneur();
$additionneur();

echo MaClasse::nbr(); // Affiche bien 5

Implémentation du pattern Observer à l'aide de closures

Vous souvenez-vous du design pattern Observer ? Il s'agit d'un patron de conception nous permettant de placer sur écoute un objet afin qu'il notifie d'autres objets lors d'un événement précis. Je ne vais pas réexpliquer ici le fonctionnement de ce pattern. Ainsi, si vous ne vous en souvenez plus, je vous invite à relire le chapitre sur les design pattern, et plus particulièrement la partie dédiée au pattern Observer.

Pour rappel, nous avions une classe observée qui implémentait l'interfaceSplSubject. Pour l'exemple, nous allons nous appuyer sur celle-ci :

<?php
class Observed implements SplSubject
{
  protected $name;
  protected $observers = [];

  public function attach(SplObserver $observer)
  {
    $this->observers[] = $observer;
    return $this;
  }

  public function detach(SplObserver $observer)
  {
    if (is_int($key = array_search($observer, $this->observers, true)))
    {
	  unset($this->observers[$key]);
	}
  }

  public function notify()
  {
    foreach ($this->observers as $observer)
    {
      $observer->update($this);
    }
  }

  public function name()
  {
  	return $this->name;
  }

  public function setName($name)
  {
  	$this->name = $name;
  	$this->notify();
  }
}

Si votre mémoire est bonne, vous vous souvenez que nous écrivions une classe observatrice pour chaque fonction à notifier. Chacune de ces classes implémentaitSplObserver et implémentait ainsi une méthode update qui se chargeait d'effectuer les opérations nécessaires en cas de notification de la part de l'objet observé.

Notre code était un peu lourd dans la mesure où la seule partie qui changeait était le contenu de la méthodeupdate, alors que nous créâmes une classe à chaque fois. Le but ici est de faire en sorte de n'avoir à créer qu'une classe générique implémentant l'interfaceSplObserver et à laquelle nous donnerons la closure à notifier.

Chaque nouvelle closure sera liée à un nouvel objet observateur. Vous devriez être capables de pouvoir écrire la classe dont les objets observateurs seront des instances :

<?php
class Observer implements SplObserver
{
  protected $name;
  protected $closure;

  public function __construct(Closure $closure, $name)
  {
    // On lie la closure à l'objet actuel et on lui spécifie le contexte à utiliser
    // (Ici, il s'agit du même contexte que $this)
    $this->closure = $closure->bindTo($this, $this);
    $this->name = $name;
  }

  public function update(SplSubject $subject)
  {
    // En cas de notification, on récupère la closure et on l'appelle
    $closure = $this->closure;
    $closure($subject);
  }
}

Maintenant, il n'y a plus qu'à tester notre système ! Pour cela, nous allons créer une instance deObserved ainsi que 2 closures à notifier. Une fois rattachées à l'objet observé, nous allons changer l'attribut name de notre objet observé afin de déclencher les notifications.

Si vous avez sous vos yeux le chapitre sur les design pattern, vous devriez vous en sortir. Voici la correction :

<?php
$o = new Observed;

$observer1 = function(SplSubject $subject)
{
  echo $this->name, ' a été notifié ! Nouvelle valeur de name : ', $subject->name(), "\n";
};

$observer2 = function(SplSubject $subject)
{
  echo $this->name, ' a été notifié ! Nouvelle valeur de name : ', $subject->name(), "\n";
};

$o->attach(new Observer($observer1, 'Observer1'))
  ->attach(new Observer($observer2, 'Observer2'));

$o->setName('Victor');
// Ce qui affiche :
// Observer1 a été notifié ! Nouvelle valeur de name : Victor
// Observer2 a été notifié ! Nouvelle valeur de name : Victor

En résumé

  • Les closures permettent de représenter des fonctions anonymes.

  • Les closures sont souvent utilisées en tant que fonctions de rappels.

  • Il est possible de lier une closure à un objet ou à une classe grâce àbindTo.

Vous êtes demandeur d'emploi ?
Sans diplôme post-bac ?

Devenez Développeur web junior

Je postule
Formation
en ligne
Financée
à 100%
Exemple de certificat de réussite
Exemple de certificat de réussite