Les closure en PHP

Nous poursuivons notre série consacrée à la gestion dynamique de fonctions avec l’utilisation des closure.

Sans doute sous l’influence de Javascript, on parle de plus en plus de programmation fonctionnelle, ou en tout cas d’une réappropriation en PHP des fonctions high-order, des closure et du binding. Un module comme Pimple en exploite par exemple toute la puissance et la simplicité.

PHP 5.3 a introduit la classe Closure permettant fonctions high-order, closure ou encore currying. PHP 5.4 l’a complétée avec du binding. On peut désormais faire des choses intéressantes et ajouter une touche de fonctionnel à l’orienté objet. :-)

Définition

Définition de base

Une closure ressemble à une fonction, mais est bien plus puissante. Elle peut être assignée à une variable, passée comme argument ou encore retournée par une autre fonction. On parle alors de high-order function.

On comprend qu’il ne s’agit pas d’un statement comme une fonction traditionnelle, mais bien d’une valeur contenant une fonction en devenir. On retrouve cette distinction en Javascript entre les fonctions déclaratives et les expressions de fonction.

Aucun identifiant ne doit être défini. Une fois passée à une variable, la closure est appelée telle une fonction variable.

1
2
3
4
$foo = function(){
    echo __FUNCTION__; //affiche {closure}
};
$foo();

Assignation de variables contextuelles (use)

Il est possible de passer des variables à une closure qui proviennent d’un contexte externe, grâce au mot-clé use. Le comportement des closure de PHP rejoint alors celui de Javascript, au niveau de la notion d’espace de variables propre (bound variable). On parle alors de closure au sens premier du terme.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function foo($bar){
    return function($arg) use($bar){
        return $bar . $arg;
    };
}

$test = foo('test'); //foo retourne la closure avec la valeur 'test' encapsulée de manière statique.

//la closure garde ainsi en mémoire la valeur:
var_dump($test(1)); //string(5) "test1"
var_dump($test(2)); //string(5) "test2"

$bar = foo('bar'); //foo retourne une nouvelle closure avec la valeur 'bar' encapsulée de manière statique.
var_dump($bar(1)); //string(5) "bar1"

//mais $test conserve toujours la référence à 'test'
var_dump($test(3)); //string(5) "test3"

Il est important de bien comprendre que use fige la variable dans le contexte d’exécution de la fonction, et que sa valeur n’évolue plus en dehors de celle-ci, à l’inverse d’une variable globale qui reste libre de toute modification.

Cela permet notamment de faire du currying.

Assignation comme attribut d’objet

L’utilisation d’une closure comme valeur d’un attribut d’objet est, par contre, un peu délicate. En effet, il n’est pas permis de déclarer un attribut avec une closure comme valeur.

1
2
3
class Foo{
    public $bar = function(){}; //Parse error: syntax error, unexpected 'function' (T_FUNCTION)
}

On peut seulement lui passer après avoir instancié l’objet.

1
2
3
4
5
6
class Foo{
    public $bar;
}

$foo = new Foo();
$foo->bar = function(){};

Dommage…

create_function

La documentation de PHP nous indique que create_function permet de créer une « fonction anonyme » à la volée depuis une chaîne de caractère. Il s’agit d’une sorte d’eval() d’une fonction.

Toutefois, il ne s’agit pas d’une closure mais d’une fonction normale dont le nom est généré de manière dynamique. En effet, le résultat retourné est un identifiant unique qui peut être appelé telle une fonction variable.

1
2
3
$foo = create_function('$x, $y', 'return $x*$y;');
var_dump($foo); //string(9) "lambda_1"
var_dump($foo(2, 3)); //int(6)

Typage

En réalité, une closure n’est pas totalement une fonction.

1
2
$foo = function(){};
var_dump(gettype($foo)); //string(6) "object"

Elle n’est pas repris comme telle.

1
2
$definedFunctionNames = get_defined_functions();
var_dump($definedFunctionNames['user']); //array(0) {}

Et n’existe même pas…

1
var_dump(function_exists('foo')); //bool(false)

En fait, une closure est une instance de la classe Closure qui implémente la méthode magique __invoke.

1
get_class($foo); //Closure

C’est pourquoi d’ailleurs, dès lors qu’il s’agit d’un objet, une closure peut être typée lorsqu’elle est définie comme paramètre d’une fonction.

1
2
3
4
function bar(Closure $closure){
    echo $closure(); //affiche {closure}
}
bar($foo);

Toutefois, la classe Closure possède un comportement un peu particulier. Elle ne peut être instanciée (constructeur privé) ni même étendue (classe finale). De même, une closure est un objet qui, contrairement à un objet traditionnel, ne peut recevoir aucun attribut à la volée.

1
$foo->bar = 'bar'; //Catchable fatal error: Closure object cannot have properties

Enfin, Closure ne peut pas être sérialisé.

1
serialize($foo); //Exception: Serialization of 'Closure' is not allowed

Invocation

Fonction variable ou callback

Une closure peut indifféremment être appelée comme une fonction variable, ou comme une callback.

1
2
3
4
5
function bar(Closure $closure){
    $closure(); //appel de type fonction variable
    call_user_func($closure); //appel de type callback
}
bar($foo);

D’ailleurs, une closure peut également être associée à une callback au niveau du typage. En somme, une closure est une forme particulière de de callback.

1
2
3
4
function bar(callable $closure){ //on type la closure comme une callable
    call_user_func($closure); //affiche {closure}
}
bar($foo);

Variable temporaire

On peut juste regretter que PHP ne permette pas encore ce genre de syntaxe à la Javascript:

1
2
3
4
5
function bar(){
    return function(){}
}

bar()(); //Parse error: syntax error, unexpected '('

Il faut impérativement passer par une variable temporaire (et donc inutile). Dommage quand on veut faire du currying, car cela engendre du bruit supplémentaire…

Attribut d’objet

Dommage aussi qu’on ne puisse pas appeler directement une closure affectée à un attribut d’objet sans, à nouveau, passer par une variable temporaire (et donc inutile).

1
2
3
4
5
6
7
8
9
10
11
//définition de la classe
class Foo{
    public $bar;
}

//affectation de la closure
$foo = new Foo();
$foo->bar = function(){};

//appel direct de la closure
$foo->bar(); //Fatal error:  Call to undefined method Foo::bar()

On comprend que PHP veut ainsi garder une distinction forte entre les propriétés (qui ne peuvent jamais être invoquées) et les méthodes, à la différence de Javascript où les méthodes ne sont rien d’autre que des propriétés contenant des fonctions.

Binding

Depuis PHP 5.4, la classe Closure bénéficie de méthodes natives permettant le binding, à l’instar de Javascript. Le binding est une technique qui vise à redéfinir le contexte d’une fonction, en lui passant explicitement un objet ou une classe. Concrètement, PHP duplique une closure, tout en lui affectant un contexte d’objet ou de classe.

Binding d’objet

Si j’exécute une fonction avec un contexte d’objet, PHP retourne bien sûr une erreur.

1
2
3
4
5
$getProp = function(){
    return $this->prop;
};

$getProp(); //Fatal error: Using $this when not in object context

Il n’est pas non plus possible de passer le $this dans le use().

1
2
3
4
5
6
7
8
9
10
class Bar{

    private $a = 'a';

    function foo(){
        return function() use($this){ //Fatal error: Cannot use $this as lexical variable
            return $this->a;
        };
    }
}

Par contre, la méthode Closure::bind permet d’associer la fonction à ce contexte manquant.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Bar{

    private $a = 'a';

    function foo($str){
        return Closure::bind(
            function($add) use($str){
                return $this->a.$str.$add;
            },
            $this
        );
    }
}

$bar = new Bar();
$foo = $bar->foo('b'); //foo retourne la closure le contexte de la fonction.
echo $foo('c'); //Affiche abc => l'appel à $this fonctionne

Attention au dernier argument qui permet d’accéder aux propriétés privées depuis l’extérieur.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class A{
    private $prop = 'a';
}

$getProp = function(){
    return $this->prop;
};

//Comportement correct
$getProp = Closure::bind($getProp, new A(), 'A');
var_dump($getProp()); //string 'a' (length=1)

//Erreur
$getProp = Closure::bind($getProp, new A()); //on ne précise pas la classe
var_dump($getProp()); //Fatal error: Cannot access private property A::$prop

Binding de classe

Le principe est identique pour les méthodes de classe, si ce n’est que la closure se définit à l’aide su mot-clé static, et qu’aucun objet n’est passé à bind.

1
2
3
class A{
    static private $prop = 'a';
}
1
2
3
4
5
6
$getProp = static function(){
    return self::$prop;
};

$getProp = Closure::bind($getProp, null, 'A');
var_dump($getProp()); //string 'a' (length=1)

Reflection

Les closure étant des objets, c’est ReflectionClass qui doit être utilisée plutôt que ReflectionFunction. Toutefois, ReflectionClass ne fait que refléter la classe Closure, même si la closure est bindée à un autre objet. Les informations retournées sont donc assez pauvres.

1
2
3
$closure = Closure::bind(function(){}, new Bar(), 'Bar');
$reflectedClosure = new \ReflectionClass($closure);
var_dump($reflectedClosure->getName()); //string(7) "Closure"

Par contre, chose plus intéressante, il est possible de sortir une méthode d’un objet pour la récupérer en tant que closure. On peut ainsi appliquer du binding sur une méthode d’objet plutôt que sur une fonction: Une méthode de la classe A peut être invoquée avec le contexte de la classe B.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//définition des classes
class A{

    private $prop = 'a';

    function getProp(){
        return $this->prop;
    }
}

class B{

    private $prop = 'b';

}
1
2
3
4
5
6
7
8
9
10
11
12
13
//récupération de la méthode de la classe A
$a = new A();
$reflectA = new ReflectionClass($a);
$closureA = $reflectA->getMethod('getProp')->getClosure($a);

//la méthode peut être appelée comme une fonction, avec le contexte de la classe A.
var_dump($closureA()); //string 'a' (length=1)

//binding vers la classe B
$closureB = Closure::bind($closureA, new B(), 'B');

//le contexte de la méthode est désormais B
var_dump($closureB()); //string 'b' (length=1)

Méthode à la volée

Si je trouve PHP extrêmement puissant, j’étais, jusqu’à présent, quelque peu étonné de ne pas avoir trouvé de fonction native permettant d’ajouter à la volée une méthode à un objet ou une classe. (On parle aussi de Monkey patch.)

En réalité, il existe une extension (donc à installer) expérimentale (donc pas exploitable en production) qui peut affecter le comportement des classes. Cela dépasse quelque peu le cadre des Closure, alors examinons plutôt ce que nous pouvons déjà faire avec ces dernières.

J’ai essayé de contourner ce problème en m’inspirant de Javascript, pour passer une closure à un attribut d’objet.

1
2
3
4
5
6
7
class Bar{
    public $foo;
}

$bar = new Bar();
$bar->foo = Closure::bind(function(){ return get_class($this); }, $bar);
var_dump($bar); //object(Bar)[1] { public 'foo' => object(Closure)[2] }

On constate que l’attribut foo est bien une closure. Mais, malheureusement, comme dit plus haut, il n’est pas possible d’appeler cette fonction directement, tel qu’on l’aurait fait avec une variable normale. PHP imagine avoir affaire à une méthode…

1
$bar->foo(); //Fatal error: Call to undefined method Bar::foo()

Il est nécessaire de passer par une variable intermédiaire. Autrement dit, l’attribut ne sert que de lieu de stockage à la closure et n’émule aucunement une méthode…

1
2
$foo = $bar->foo;
var_dump($foo()); //string 'Bar' (length=3)

Quant aux classes Reflection, aucune ne propose d’ajouter ou même de modifier une méthode.

On peut néanmoins construire un objet qui gère les closure de manière dynamique, lesquelles sont appelées via la méthode __call() que nous avions vue dans notre précédent article.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class ClosureAdder
{

    /**
     * liste de méthodes
     * nom de méthode => closure
     *
     * @var array
     */

    private $closures = array();

    /**
     * implémentation de __call
     * permet d'appeler dynamiquement les méthodes de $this->closures
     *
     * @param string $methodName
     * @param array $args
     * @return mixed
     */

    public function __call($methodName, array $args)
    {
        return call_user_func_array($this->closures[$methodName], $args);
    }

    /**
     * ajoute une méthode à l'objet.
     * les méthodes ajoutées sont appelables dynamiquement via __call
     *
     * @param string $name
     * @param callable $closure
     */

    public function addClosure($name, Closure $closure)
    {
        $this->closures[$name] = Closure::bind($closure, $this, get_class($this));
    }

}
1
2
3
4
5
$closureAdder = new ClosureAdder();
$closureAdder->addClosure('foo', function(){
    return __METHOD__;
});
var_dump($closureAdder->foo()); //ClosureAdder::foo

Conclusion

Les closure sont surtout utilisées en tant que callback, comme nous le verrons dans notre prochain article. Mais PHP a mis en place un système intéressant de binding qui peut se révéler très puissant et qui est certainement encore trop méconnu.

On peut juste regretter que la syntaxe d’appel ne soit pas encore assez souple pour être utilisée de manière aussi légère qu’en Javascript.

13 réflexions au sujet de « Les closure en PHP »

  1. Ping : Appel dynamique de fonctions en PHP | The Dark Side Of The Web

  2. Ping : Les callback en PHP | The Dark Side Of The Web

  3. Ping : Mocker une méthode avec une closure en PHP

  4. Ping : Closure, callback et fonctions dynamiques en PHP

  5. IMO : ici

    1
    2
    3
    4
    5
    function foo($bar){
        return function($arg) use($bar){
            return $bar+$arg;
        };
    }

    ça devrait être :

    1
    2
    3
    4
    5
    function foo($bar){
        return function($arg) use($bar){
            return $bar . $arg;
        };
    }

    Merci

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Vous pouvez utiliser ces balises et attributs HTML : <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>