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

109

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();
Kendall Hopkins
la source
1
C'est exactement ce dont vous avez besoin: github.com/ptrofimov/jslikeobject Encore plus: vous pouvez utiliser $ this à l'intérieur des fermetures et utiliser l'héritage. Seulement PHP> = 5.4!
Renato Cuccinotto

Réponses:

106

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 deviez implémenter la __callméthode magique pour intercepter l'appel et invoquer le rappel (ce qui n'est pas possible pour StdClassbien sûr, car vous ne pouvez pas ajouter la __callmé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 __callcorps, car cela se déclencherait __calldans une boucle infinie.

Gordon
la source
2
Parfois, vous trouverez cette syntaxe utile - call_user_func_array ($ this -> $ property, $ args); quand il s'agit d'une propriété de classe appelable, pas d'une méthode.
Nikita Gopkalo
105

Vous pouvez le faire en appelant __invoke sur 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 peuvent également être des rappels valides en PHP) - juste pour les fermetures et autres objets avec le comportement __invoke.

Brilliand
la source
3
@marcioAlmada très moche cependant.
Mahn
1
@Mahn Je pense que c'est plus explicite que la réponse acceptée. Explicite est mieux dans ce cas. Si vous aimez vraiment une solution "mignonne", ce call_user_func($obj->callback)n'est pas si mal.
marcio
Mais call_user_funcfonctionne aussi avec des cordes et ce n'est pas toujours pratique
Gherman
2
@cerebriform sémantiquement cela n'a aucun sens d'avoir à faire $obj->callback->__invoke();quand on s'attend à ce que ce soit le cas $obj->callback(). C'est juste une question de cohérence.
Mahn
3
@Mahn: Certes, ce n'est pas cohérent. Cependant, la cohérence n'a jamais vraiment été le point fort de PHP. :) Mais je pense qu'il ne sens des marques, si l' on considère la nature de l' objet: $obj->callback instanceof \Closure.
évêque
24

À partir de PHP 7, vous pouvez effectuer les opérations suivantes:

($obj->callback)();
Korikulum
la source
Un tel bon sens, mais c'est purement dur à cuire. La grande puissance de PHP7!
tfont
10

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

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

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

($obj->callback)();

Les autres approches courantes de PHP 5 sont:

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

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

    call_user_func($obj->callback);
  • utilisation d'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 la plus 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();
Daniele Orlando
la source
7

Il semble possible d'utiliser call_user_func().

call_user_func($obj->callback);

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

Pekka
la source
7

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

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

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

$obj['callback']();

Mais ce n'est pas la meilleure syntaxe.

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

mario
la source
7

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;
}

Désormais, 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
SteveK
la source
Wow :) C'est beaucoup plus élégant que ce que j'essayais de faire. Ma seule question est la même que pour les éléments de connecteur british hot / cold tap: POURQUOI n'est-il pas déjà intégré ??
dkellner
Merci :). Ce n'est probablement pas intégré en raison de l'ambiguïté qu'il crée. Imaginez dans mon exemple, s'il y avait en fait une fonction appelée "add" et une propriété appelée "add". La présence de parenthèses indique à PHP de rechercher une fonction portant ce nom.
SteveK
2

eh bien, il faut bien comprendre que stocker la fermeture dans une variable, et appeler la variable est en fait (bizarrement) plus rapide, en fonction du montant de l'appel, ça devient pas mal, avec xdebug (donc mesure très précise), on parle 1,5 (le facteur, en utilisant une variable, au lieu d'appeler directement __invoke. Donc à la place, stockez simplement la fermeture dans une variable et appelez-la.

Kmtdk
la source
2

Actualisé:

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

PHP> = 7:

($obj->callback)();

PHP> = 5.4:

$callback = $obj->callback;  
$callback();
M Rostami
la source
Avez-vous essayé cela? Ça ne marche pas. Call to undefined method AnyObject::callback()(La classe AnyObject existe, bien sûr.)
Kontrollfreak
1

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();

Vous êtes probablement mieux d'utiliser call_user_funcou __invokecependant.

Mahn
la source
0

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 devez configurer la configuration suivante.

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

Et vous travailliez 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 opériez dans le cadre de votre objet

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

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

Je note que cela fonctionne en PHP5.5

$a = array();
$a['callback'] = function() {
    print "HelloWorld!";
};
$a['callback']();

Permet de créer une collection de fermetures psuedo-object.

Gordon Rouse
la source