Les callback en PHP

Ce troisième article de notre série consacrée à la gestion dynamique de fonctions aborde les callback.

Une fonction de rappel (callback) est une fonction ou une méthode en puissance. Il s’agit d’une valeur contenant l’identifiant d’une fonction existante, laquelle peut ainsi être exécutée dans un second temps.

Définition

Une callback peut prendre différentes formes, selon qu’il s’agisse d’une fonction, d’une méthode d’instance ou d’une méthode de classe:

  1. Identifiant de fonction sous forme de chaîne de caractères (voir fonction dynamique)
    1
    is_callable('in_array'); //true
  2. Fonction anonyme (voir closure) (PHP 5.3.0)
    1
    is_callable(function(){}); //true
  3. Chaîne de caractères contenant le nom d’une classe et le nom d’une méthode statique (PHP 5.2.3)
    1
    is_callable('DateTime::createFromFormat'); //true
  4. Tableau contenant:
    • index 0: le nom d’une classe sous forme de chaîne de caractères
    • index 1: le nom d’une méthode statique sous forme de chaîne de caractères
    1
    is_callable(array('DateTime', 'createFromFormat')); //true
  5. Tableau contenant:
    • index 0: une instance de classe
    • index 1: le nom d’une méthode d’instance sous forme de chaîne de caractères
    1
    is_callable(array(new \DateTime(),'add')); //true
  6. Objet implémentant la méthode magique __invoke()
    1
    2
    3
    4
    5
    6
    7
    8
    9
    class Bar {

        function __invoke(){
            echo __METHOD__;
        }

    }

    is_callable(new Bar()); //true

A noter qu’il est possible de préciser qu’il s’agit d’une méthode ou classe parente (voir plus bas, la section liage statique) (PHP 5.3.0).

Typage

Depuis PHP 5.4, il est possible de typer ces fonctions comme étant callable.

1
2
3
function foo(callable $callback){
    //code
}

Avec ce typage, PHP agit comme pour la fonction is_callable (du moins, lorsque le deuxième argument de cette fonction est à false, comme par défaut) et s’assure que la valeur passée corresponde effectivement à une fonction ou une méthode appelable.

1
2
is_callable('none'); //false
foo('none'); //Catchable fatal error: Argument 1 passed to foo() must be callable, string given,

De même, les fonctions d’appel que nous allons étudier dans la prochaine section nécessitent de recevoir des callback valides.

1
call_user_func('none'); //Warning: call_user_func() expects parameter 1 to be a valid callback, function 'none' not found or invalid function name

Invocation

Appel de base

Il est très important de bien comprendre qu’une callback est, en PHP, à la fois une fonction et une méthode. Or, si une fonction aurait pu être appelée avec des parenthèses comme une fonction variable, une méthode, de son côté, implique un objet ou une classe.

1
2
3
4
5
6
7
function foo(callable $callback){
    return $callback();  
}

var_dump(foo('time')); //int 1370789465

var_dump(foo('DateTime::getLastErrors')); //Fatal error: Call to undefined function DateTime::getLastErrors()

C’est pour cette raison qu’il est recommandé de systématiser les appels de callbacks avec les fonctions call_user_func et call_user_func_array.

call_user_func permet d’appeler la fonction avec des arguments passés les uns à la suite des autres.

1
call_user_func('in_array', 'bar', array('bar'), true);

Tandis que call_user_func_array reçoit les arguments à faire passer sous forme de tableau.

1
2
3
4
5
6
$args = array(
            'bar',
            array('bar'),
            true
        );
call_user_func_array('in_array', $args);

Méthodes privées

A noter que PHP permet d’appeler une méthode privée d’un objet si l’on se trouve dans la classe de celui-ci, et même s’il ne s’agit pas de la même instance. En effet, l’encapsulation privée, plutôt que d’empêcher l’accessibilité depuis l’extérieur de l’instance comme on pourrait le penser, n’implique qu’une restriction depuis l’extérieur de la classe. Un objet peut ainsi accéder à une méthode privée d’une autre instance de la même classe. Et cela est très utile si l’on désire appeler une callback privée depuis une classe.

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

    function run(){
        call_user_func(array($this, 'foo'));
    }

    private function foo(){
        echo __METHOD__;
    }
}

$bar = new Bar();
$bar->run(); //affiche Bar::foo => run connaît bien foo.

Passage par référence

Attention que call_user_func ne supporte pas le passage d’arguments par référence.

1
2
3
$array = array(1, 2, 3);
call_user_func('array_shift', $array); //Warning:  Parameter 1 to array_shift() expected to be a reference, value given
print_r($array); //Array(1, 2, 3)

Mais, nous savons tous qu’utiliser des références démontre avant tout un problème conceptuel qui peut entraîner de graves désagréments.

Sinon, Jean-françois Lépine propose une solution pour contourner ce problème en utilisant call_user_func_array tout en forçant la référence.

1
2
3
$array = array(1, 2, 3);
call_user_func_array('array_shift', array(&$array));
print_r($array); //Array(2, 3)

Résolution d’espace de nom

L’appel des callback via les méthodes spécifiques ne tient pas compte de l’espace de nom courant. Il est donc nécessaire de préciser le chemin absolu.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
namespace Bar1{

    function foo(){
        echo __FUNCTION__;
    }
   
}

namespace Bar2{

    use Bar1;
   
    call_user_func('Bar1\foo'); //affiche Bar1\foo
    call_user_func('foo'); //Warning: call_user_func() expects parameter 1 to be a valid callback, function 'foo' not found or invalid function name

}

Liage statique

call_user_func et call_user_func_array ne conservent le liage statique que lorsque le parent est précisé de manière explicite. forward_static_call et forward_static_call_array conservent quant à elles toujours ce liage (PHP 5.3).

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
class A {

    const NAME = 'A';

    public static function getName() { 
        echo __METHOD__ .' => '.self::NAME.' - '.static::NAME."\n";
    }
}

class B extends A {

    const NAME = 'B';
   
    public static function getName() {
        echo __METHOD__ .' => '.self::NAME.' - '.static::NAME."\n";
    }

    public static function test() {

        //call_user_func sans préciser le parent      
        call_user_func(array('A', 'getName')); //A::getName => A - A
        echo '<hr />';

        //trois façons d'utiliser parent/static/self avec call_user_func
       
        //1
        call_user_func(array('parent', 'getName')); //A::getName => A - B
        call_user_func(array('self', 'getName')); //B::getName => B - B
        call_user_func(array('static', 'getName')); //B::getName => B - B
        echo '<hr />';
       
        //2.A
        //call_user_func(array('A', 'parent::getName')); Warning:  call_user_func() expects parameter 1 to be a valid callback, cannot access parent:: when current class scope has no parent
        call_user_func(array('A', 'self::getName')); //A::getName => A - B
        //call_user_func(array('A', 'static::getName')); Warning:  call_user_func() expects parameter 1 to be a valid callback, class 'A' is not a subclass of 'B'
        echo '<hr />';

        //2.B
        call_user_func(array('B', 'parent::getName')); //A::getName => A - B
        call_user_func(array('B', 'self::getName')); //B::getName => B - B
        call_user_func(array('B', 'static::getName')); //B::getName => B - B
        echo '<hr />';
       
        //3
        call_user_func('parent::getName'); //A::getName => A - B
        call_user_func('self::getName'); //B::getName => B - B
        call_user_func('static::getName'); //B::getName => B - B
        echo '<hr />';

        //forward_static_call
        forward_static_call(array('A', 'getName')); //A::getName => A - B  
       
    }
}

B::test();

Appel dynamique d’une chaîne de méthodes

Avec un peu d’imagination, on peut facilement chaîner les méthodes les unes aux autres de manière dynamique.

methodA.methodB.methodC...
1
2
3
4
5
6
7
8
9
10
11
function call(array $callback, $delimiter='.'){ //note: callable ne fonctionne pas!
    foreach(explode($delimiter, $callback[1]) as $method){
        $callback[0] = $callback[0]->$method();
    }
    return $callback[0];
}

$dateTime = new DateTime('now', new DateTimeZone('Antarctica/South_Pole'));
$callback = array($dateTime , 'getTimezone.getName'); //notation en chaîne de méthode

var_dump(call($callback)); //affiche string 'Antarctica/South_Pole' (length=21)

Reflection

Il n’existe pas de classe de reflection d’une callback qui soit implémentée de manière native par PHP. Bien sûr, le troisième argument de is_callable permet de retrouver une chaîne de caractère contenant le nom de la callback, mais c’est à peu près tout…

1
2
3
$name = '';
is_callable(array(new \DateTime(),'add'), false, $name);
var_dump($name); //string(13) "DateTime::add"

Sinon, voici une classe de Reflection maison qui permet d’identifier chaque type possible tels que nous les avons vus.

1
2
3
$closure = function(){};
$reflect = new CallableReflection($closure);
var_dump($reflect->isClosure()); //true
1
2
3
4
5
6
$object = new \DateTime();
$reflect = new CallableReflection(array($object, 'add'));
var_dump($reflect->isInstanceMethod()); //true
var_dump($reflect->getClassName()); //'DateTime'
var_dump($reflect->getObject()===$object); //true
var_dump($reflect->getMethodName()); //'add'

Conclusion

Les callback sont extrêmement puissantes en programmation, et PHP nous propose d’exploiter tant les fonctions que les méthodes.

La seule difficulté réside dans les différentes formes de syntaxe qu’une callback peut prendre, ainsi que dans la nécessité de passer par des fonctions d’appel spécifiques.

3 réflexions au sujet de « Les callback en PHP »

  1. Ping : Les closure en PHP | The Dark Side Of The Web

  2. Ping : Appel dynamique de fonctions en PHP

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

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>