Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why PHP `__invoke` Not Working When Triggered from an Object Property

Tags:

php

I wonder whether this is a bug or normal. Let’s say I have a class with some magical functions:

class Foo {
    public function __toString() {
        return '`__toString` called.';
    }
    public function __get($key) {
        return '`__get(' . $key . ')` called.';
    }
    public function __invoke($x = "") {
        return '`__invoke(' . $x . ')` called.';
    }
}

And then create an instance in an object property like this:

$object = (object) [
    'foo' => 'bar',
    'baz' => new Foo
];

Then test it:

echo $object->baz;
echo $object->baz->qux;
echo $object->baz('%'); // :(

It is broken in the last echo: Call to undefined method stdClass::baz()

Currently, the only solution I can do is to store the __invoke part in a temporary variable and then call that variable as a function like this:

$x = $object->baz;
echo $x('%'); // :)

It works fine when I instantiate the class in an array property:

$array = [
    'baz' => new Foo
];

echo $array['baz'];
echo $array['baz']->qux;
echo $array['baz']('%'); // :)

By the way, I need this ability on my object for something related to API:

$foo = (object) ['bar' => new MyClass];
  • echo $foo->bar; → should trigger __toString
  • echo $foo->bar->baz; → should trigger __get
  • echo $foo->bar(); → should trigger __invoke
  • echo $foo->bar->baz(); → should trigger __call

All of them should return a string.

Can this be done in PHP completely? Thanks.

like image 965
Taufik Nurrohman Avatar asked Mar 13 '26 05:03

Taufik Nurrohman


2 Answers

No can do.

The line in question is simply ambigous, and the error message shows you how ... It is more logical to try to access the baz() method of your $object object.
That's just the context given by the parser when it sees $object->baz()

As already mentioned in the comments, you can remove that ambiguity, help the parser by telling it that $object->baz is itself an expression that needs to be executed first:

($object->baz)('arg');

PHP is also itself a program, and has to know how to execute something before executing it. If it could blindly try every possible "magic" method on every object in a $foo->bar->baz->qux chain, then it wouldn't be able to tell you what the error is when it is encountered - it would just silently crash.

like image 146
Narf Avatar answered Mar 14 '26 17:03

Narf


I have solved my problem by detecting the existence of an __invoke method inside the __call method of a class.

class MyStdClass extends stdClass {
    protected $data = [];
    public function __construct(array $array) {
        $this->data = $array;
    }
    public function __get($key) {
        return isset($this->data[$key]) ? $this->data[$key] : null;
    }
    public function __call($key, $args = []) {
        if (isset($this->data[$key])) {
            $test = $this->data[$key];
            // not an object = not an instance, skip!
            if (!is_object($test)) {
                return $this->__get($key);
            }
            if (!empty($args) && get_class($test) && method_exists($test, '__invoke')) {
                // or `return $test(...$args)`
                return call_user_func([$test, '__invoke'], ...$args);
            } 
        }
        return $this->__get($key);
    }
    public function __set($key, $value = null) {
        $this->data[$key] = $value;
    }
    public function __toString() {
        return json_encode($this->data);
    }
    public function __isset($key) {}
    public function __unset($key) {}
}

So, instead of converting the array into object with (object), here I use:

$object = new MyStdClass([
    'foo' => 'bar',
    'baz' => new Foo
]);
like image 26
Taufik Nurrohman Avatar answered Mar 14 '26 19:03

Taufik Nurrohman



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!