web-dev-qa-db-fra.com

Appel de fermeture affecté directement à la propriété d'objet

Je voudrais pouvoir appeler une fermeture que j'attribue directement à la propriété d'un objet sans réaffecter la fermeture à une variable, puis l'appeler. Est-ce possible?

Le code ci-dessous ne fonctionne pas et provoque Fatal error: Call to undefined method stdClass::callback().

$obj = new stdClass();
$obj->callback = function() {
    print "HelloWorld!";
};
$obj->callback();
98
Kendall Hopkins

Depuis PHP7, vous pouvez faire

$obj = new StdClass;
$obj->fn = function($arg) { return "Hello $arg"; };
echo ($obj->fn)('World');

ou utilisez Closure :: call () , bien que cela ne fonctionne pas sur un StdClass.


Avant PHP7, vous devez implémenter la magie __call méthode pour intercepter l'appel et appeler le rappel (ce qui n'est pas possible pour StdClass bien sûr, car vous ne pouvez pas ajouter le __call méthode)

class Foo
{
    public function __call($method, $args)
    {
        if(is_callable(array($this, $method))) {
            return call_user_func_array($this->$method, $args);
        }
        // else throw exception
    }
}

$foo = new Foo;
$foo->cb = function($who) { return "Hello $who"; };
echo $foo->cb('World');

Notez que vous ne pouvez pas faire

return call_user_func_array(array($this, $method), $args);

dans le __call body, car cela déclencherait __call dans une boucle infinie.

92
Gordon

Vous pouvez le faire en appelant __invoke à la fermeture, car c'est la méthode magique que les objets utilisent pour se comporter comme des fonctions:

$obj = new stdClass();
$obj->callback = function() {
    print "HelloWorld!";
};
$obj->callback->__invoke();

Bien sûr, cela ne fonctionnera pas si le rappel est un tableau ou une chaîne (qui peut également être des rappels valides en PHP) - juste pour les fermetures et autres objets avec un comportement __invoke.

99
Brilliand

Depuis ( PHP 7 vous pouvez faire ce qui suit:

($obj->callback)();
19
Korikulum

Puisque PHP 7 une fermeture peut être appelée en utilisant la méthode call() :

$obj->callback->call($obj);

Puisque PHP 7 il est également possible d'exécuter des opérations sur des expressions arbitraires (...) (Comme expliqué par Korikulum ):

($obj->callback)();

D'autres approches courantes PHP 5 sont:

  • en utilisant la méthode magique __invoke() (comme expliqué par Brilliand )

    $obj->callback->__invoke();
    
  • en utilisant la fonction call_user_func()

    call_user_func($obj->callback);
    
  • en utilisant une variable intermédiaire dans une expression

    ($_ = $obj->callback) && $_();
    

Chaque voie a ses avantages et ses inconvénients, mais la solution la plus radicale et définitive reste celle présentée par Gordon .

class stdKlass
{
    public function __call($method, $arguments)
    {
        // is_callable([$this, $method])
        //   returns always true when __call() is defined.

        // is_callable($this->$method)
        //   triggers a "PHP Notice: Undefined property" in case of missing property.

        if (isset($this->$method) && is_callable($this->$method)) {
            return call_user_func($this->$method, ...$arguments);
        }

        // throw exception
    }
}

$obj = new stdKlass();
$obj->callback = function() { print "HelloWorld!"; };
$obj->callback();
7
Daniele Orlando

Cela semble possible en utilisant call_user_func().

call_user_func($obj->callback);

pas élégant, cependant .... Ce que dit @Gordon est probablement la seule voie à suivre.

7
Pekka 웃

Eh bien, si vous vraiment insistez. Une autre solution serait:

$obj = new ArrayObject(array(),2);

$obj->callback = function() {
    print "HelloWorld!";
};

$obj['callback']();

Mais ce n'est pas la plus belle syntaxe.

Cependant, l'analyseur PHP traite toujours T_OBJECT_OPERATOR, IDENTIFIER, ( comme appel de méthode. Il ne semble pas y avoir de solution pour faire -> contourne la table de méthodes et accède aux attributs à la place.

7
mario

Je sais que c'est vieux, mais je pense que Traits gère bien ce problème si vous utilisez PHP 5.4+

Tout d'abord, créez un trait qui rend les propriétés appelables:

trait CallableProperty {
    public function __call($method, $args) {
        if (property_exists($this, $method) && is_callable($this->$method)) {
            return call_user_func_array($this->$method, $args);
        }
    }
}

Ensuite, vous pouvez utiliser ce trait dans vos classes:

class CallableStdClass extends stdClass {
    use CallableProperty;
}

Maintenant, vous pouvez définir des propriétés via des fonctions anonymes et les appeler directement:

$foo = new CallableStdClass();
$foo->add = function ($a, $b) { return $a + $b; };
$foo->add(2, 2); // 4
6
SteveK

Voici une autre façon d'appeler avec succès des propriétés d'objet en tant que fermeture.
Lorsque vous ne souhaitez pas modifier l'objet principal, utilisez ceci:

$obj = new AnyObject(); // with or without __invoke() method
$obj->callback = function() {
     return function () {
          print "HelloWorld!";
     };
};
$obj->callback();  

MISE À JOUR:

$obj = new AnyObject(); // with or without __invoke() method
$obj->callback = function() {
     print "HelloWorld!";
};
$callback = $obj->callback;  
$callback();
3
Mohamad Rostami

eh bien, il faut souligner que le stockage de la fermeture dans une variable, et l'appel de la variable est en fait (bizarrement) plus rapide, selon le montant de l'appel, cela devient beaucoup, avec xdebug (donc une mesure très précise), nous parlons 1,5 (le facteur, en utilisant une variable, au lieu d'appeler directement le __invoke. Donc au lieu de cela, il suffit de stocker la fermeture dans une variable et de l'appeler.

2
Kmtdk

Voici une autre alternative basée sur la réponse acceptée mais en étendant directement stdClass:

class stdClassExt extends stdClass {
    public function __call($method, $args)
    {
        if (isset($this->$method)) {
            $func = $this->$method;
            return call_user_func_array($func, $args);
        }
    }
}

Exemple d'utilisation:

$foo = new stdClassExt;
$foo->blub = 42;
$foo->whooho = function () { return 1; };
echo $foo->whooho();

Il vaut probablement mieux utiliser call_user_func ou __invoke bien que.

1
Mahn

Si vous utilisez PHP 5.4 ou supérieur, vous pouvez lier un appelable à la portée de votre objet pour invoquer un comportement personnalisé. Par exemple, si vous deviez configurer ce qui suit ..

function run_method($object, Closure $method)
{
    $prop = uniqid();
    $object->$prop = \Closure::bind($method, $object, $object);
    $object->$prop->__invoke();
    unset($object->$prop);
}

Et vous opériez sur une classe comme ça ..

class Foo
{
    private $value;
    public function getValue()
    {
        return $this->value;
    }
}

Vous pouvez exécuter votre propre logique comme si vous fonctionniez dans le cadre de votre objet

$foo = new Foo();
run_method($foo, function(){
    $this->value = 'something else';
});

echo $foo->getValue(); // prints "something else"
0
Lee Davis