PHP 7.1 : créer une Closure à partir d'un appelable

13 septembre 2016php, php-7.1

This post is also available in English.
Ceci est le septième article d’une série à propos de PHP 7.1.


Historiquement, un callable a souvent été manipulé, en PHP, sous la forme d’une chaine de caractères. Par exemple, on utilise la fonction array_map() pour appeler le callable 'trim' sur tous les éléments d’un tableau.


Cette approche souffre d’un défaut majeur : la validité de l’appelable n’est déterminée que lors de son appel !

Par exemple, considérons la portion de code suivante :

function ma_fonction($param)
{
    printf("%s(%s)\n", __FUNCTION__, $param);
}

// Pas de problème détecté ici, malgré la faute de frappe
$nom = 'ma_fonctlon';

// Ici, de nombreuses lignes de code
// et encore plein d’autres

// C’est seulement ici qu’on a un problème :
// Warning: call_user_func() expects parameter 1 to be a valid callback, function 'ma_fonctlon' not found or invalid function name
call_user_func($nom, 42);

La faute de frappe dans le nom de la fonction de rappel n’est pas détectée à l’endroit où elle est commise… mais seulement plusieurs lignes plus loin, lorsque nous essayons de l’exécuter.

Sur un exemple réel, qui s’étalerait sur plusieurs dizaines ou centaines de fichiers, peut-être même par le biais de quelques fichiers de configuration, je vous laisse imaginer l’enfer en termes de débogage ! Peut-être même l’avez-vous déjà vécu ?


PHP 7.1 répond à cette problématique en introduisant une nouvelle méthode Closure::fromCallable(). Elle prend en paramètre un appelable, le valide, puis retourne une Closure le référençant. C’est cette Closure que vous manipulerez par la suite.

Nous pouvons donc réécrire l’exemple ci-dessus en tirant profit de cette fonctionnalité. Une exception instance de TypeError est levée si l’appelable spécifié n’est pas valide :

function ma_fonction() {
    var_dump(__FUNCTION__);
}

$closure = Closure::fromCallable('ma_fonction');
$closure();  // string(11) "ma_fonction"


$closure = Closure::fromCallable('plop');
// TypeError: Failed to create closure from callable: function 'plop' not found or invalid function name

Avantage : en cas d’erreur de frappe dans le nom de la fonction de rappel, elle est détectée immédiatement — sans avoir à attendre son exécution :

function ma_fonction($param)
{
    printf("%s(%s)\n", __FUNCTION__, $param);
}

// L’erreur est immédiatement détectée
// Fatal error: Uncaught TypeError: Failed to create closure from callable: function 'ma_fonctlon' not found or invalid function name
$callable = Closure::fromCallable('ma_fonctlon');

// Ici, de nombreuses lignes de code
// et encore plein d’autres

// Pas de problème ici : sauf erreur plus haut,
// $callable est forcément valide
call_user_func($callable, 42);


Le problème causé par l’absence de validation des callables lors de leur déclaration se posait également pour d’autres types d’appelables, comme ceux désignant une méthode d’un objet :

class MaClasse
{
    protected $data;

    public function __construct($param)
    {
        $this->data = $param;
    }

    public function maMethode()
    {
        printf("%s() -> %s\n", __METHOD__, $this->data);
    }
}


// Pas d’erreur levée par PHP ici, alors que c’est sur cette ligne
// qu’on a fait une typo !
$callable = [new MaClasse(42), 'maMehtode'];


// Pas non plus d’erreur ici
call_callable($callable);


function call_callable($callable)
{
    // C’est seulement ici qu’on a une erreur !
    // Fatal error: Uncaught Error: Call to undefined method MaClasse::maMehtode()
    $callable();
}

Heureusement, nous pouvons également, avec PHP 7.1, utiliser Closure::fromCallable() dans ce cas. Nous bénéficierons ici aussi de la validation de l’appelable lors de la création de la Closure, souvent bien avant son exécution :

class MaClasse
{
    protected $data;

    public function __construct($param)
    {
        $this->data = $param;
    }

    public function maMethode()
    {
        printf("%s() -> %s\n", __METHOD__, $this->data);
    }
}


// L’erreur est immédiatement détectée
// Fatal error: Uncaught TypeError: Failed to create closure from callable: class 'MaClasse' does not have a method 'maMehtode'
$callable = Closure::fromCallable([new MaClasse(42), 'maMehtode']);


// Pas de problème ici : $callable est forcément valide
call_callable($callable);


// Bonus : on peut type-declarer
function call_callable(callable $callable)
{
    $callable();
}

Bref, voici une nouvelle méthode qui va nous permettre de valider les appelables au plus tôt, facilitant ainsi la détection et la gestion d’erreur !


‣ La RFC : Closure from callable function


Vous avez apprécié cet article ? Faites-le savoir !

Ce blog a récemment été migré vers un générateur de sites statiques et je n'ai pas encore eu le temps de remettre un mécanisme de commentaires en place.

Avec un peu de chance, je parviendrai à m'en occuper d'ici quelques semaines ;-)