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

13 septembre 2016php, php-7.1
 Cet article a été rédigé il y a plusieurs années et peut ne plus être tout à fait à jour…

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