The thin line between an Invocation

PHP language changes and until we understand them. Did this caught your attention?

The release of PHP 8.1 is quite some days ago, like November the year before last year, and when I first learned about first class callable syntax (by the RFC, but the manual has it, too) it was like this is a benefit for static code analysis (only).

This one also looks like a nice short for of Closure::fromCallable(<callable>) and comes with the same object and scope bindings:

# first class callable syntax:

strlen(...)($string);
# NOTE: the three dots ... are not an omission


# short form of closure from callable:

Closure::fromCallable("strlen")($string);

Now this example looks quite dull. We could just call strlen(string) and case closed. Obviously we only use it when we want a Closure, not a callable.


Yesterday, I revisited an older answer of mine on Stack Overflow in regard to a very simple, ground working PHP function, get_object_vars().

The question asked How to programmatically find public properties of an object from inside one of it’s class methods?

Using get_object_vars($this) returns all properties within scope, so my answer back in 2012 was to write a global function, pass the object and call get_object_vars() within:

function get_object_public_vars($object) {
    return get_object_vars($object);
}

class Foo {
    public function getPublicVars() {
        return get_object_public_vars($this);
    }
}

Sure, that’s not what the OP really has hoped for, because you have to add another global function into your utilities instead of having something at your disposal directly.

It took two more years until 2014 when Brad Kent had the one-liner answer:

    public function getPublicVars() {
        return call_user_func('get_object_vars', $this);
    }

Wait, what? – If calling get_object_vars() directly yields privates, why calling it with call_user_func() as the callable string 'get_object_vars' changes the scope? – Well we don’t know but it passed the year and then with the PHP 7.0.0 release in December 2015 these semantics changed and it was not such a thing any longer. (Change log only, sadly I still couldn’t find a related bug report.)

Now commentators below the answer then were keen to point out that it relies on a bug (again, sadly no references to it), until then sometime Brad edited in telling the required PHP version and otherwise use reflection.

Same for me, this was also my line of defense in my answer and I had reflection in it, too.


But I do not really like to rely on reflection / meta programming during programming as it often demands many more interruptions over time as reflection changes far more often as the language progresses while it is running behind actual progress in the language. Often the code that is produced by it is not robust, it does not feel well.

As create_function() is not an answer for longtime and closures could have it I was looking into them. And there I found a cadeau surprise:

    public function getPublicVars() {
        return get_object_vars(...)->__invoke($this);
    }

The __invoke method is part of PHPs’ final internal Closure class, also known for classes not extending from Closure as magic method that turns an object into a callable.

In this example get_object_vars is turned into a closure and it is bound to the scope of $this/getPublicVars() so that it can obtain all the properties.

However, triggering the __invoke() event moves it far away enough out of scope to return only the public properties similar to call_user_func('get_object_vars') once did.

So thanks to the new PHP 8.1 first class callable syntax it is now again easier to write and to rely on important code functions like get_object_vars() and to draw the thin line between invocations.



Now is this a bug as it was one in call_user_func()? Well, different to call_user_func(), which has only one form of invocation and therfore perhaps required the change, here the Closure has two forms:

# the two forms how to invoke a Closure in PHP:
get_object_vars(...)($this);
get_object_vars(...)->__invoke($this);

Therefore there is a thin line to draw here and get_object_vars(…)($this) allows to call_user_function('get_object_vars', $this) as in PHP 7+ and with __invoke($this) as it was before. For the many other functions that are not scope aware this should not make a difference and we can safely assume that the first, standard form is not affected by it. Therefore the differentiation remains to the caller offered by the implementors’ choice.

Therefore instead of calling it a bug, why not see it as implementation-specified behaviour and call it a feature. It is a one of the Closure from callable and it won’t translate out of it.

# the two forms how to invoke a Closure in PHP:
get_object_vars(...)->__invoke(...)($this);
get_object_vars(...)->__invoke(...)->__invoke($this);


You can make use of __invoke() in your own implementations and resolve the scope issue with it as well. This is PHP 7.0+ code again:

(new class {function __invoke($object) {
    return get_object_vars($object);}
})($this);

Or by relying on another method of Closure, bindTo() (again PHP 7.0+):

(function ($object) {return get_object_vars($object);})
    ->bindTo(null, null)($this);

By the way, this function constructor of a Closure doesn’t have the first class callable semantics as function is not scope sensitive, it has the scope bound:

# yields privates
(function ($object) {return get_object_vars($object);})
    ->__invoke($this);

You have to use a true native callable to unveil it.


And good old call_user_func()? Here is the PHP 5.4+ variant, you see how much syntax and with it the semantics changed:

call_user_func(
    call_user_func(
        array(
            function ($object) {return get_object_vars($object);},
            'bindTo'
        ), null, null
    ), $this                     
);

Luckily PHP 7.0 brought in more consistent function calling and method invocation syntax and we don’t need to write out call_user_func() any longer.

Not even for call_user_func('get_object_vars', $this) thanks to PHP 8.1 first class callable syntax in shis short form: get_object_vars(…)->__invoke($this).

References

Based on Théodore Géricault Study of the Head of a Youth, ca. 1821–1824 (via Wikipedia)

This entry was posted in PHP Development, Pressed, The Know Your Language Department, Uncategorized and tagged , , . Bookmark the permalink.

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.