Friday, May 27, 2011

Using __invoke() in PHP 5.3

In PHP 5.3 one of the new magic methods was __invoke(). PHP 5.3.0 is not really a new stuff but I have seen nobody using __invoke() yet. In this post I will try to find some use cases when it can be useful.



So first of all let's overview how this method works: if you have an object thats class implements the __invoke() method, then the object it becomes a callable. Example:

class MyClass {
 
 function __invoke() {
                echo 'invoked' . PHP_EOL;
        }

        public static function factory() {
                return new MyClass;
        }
 
}

$obj = new MyClass;

$obj();
call_user_func($obj);

The object can be "called". It can be used as a function. Well, at the first glance such stuff just blows your mind. It's something really unclean - how can something be a function and an object at the same time? These are totally different things. An object is a representation encapsulated with its behavior. A function is a routine that can be called, maybe it returns something and maybe its execution has side-effect. How does it make sense to mix up these thing?

But in an OO system we often extend the original meaning of the terms class and object. We often write classes that don't represent anything, the class itself is used for something else. For example a utility class is a namespace for related helper functions, or a controller is a group for related actions that may share some common mechanisms. It's always divides PHP developers if you can, should or should not create classes for such things, and it often results in endless flames. In my personal opinion you can use classes in the above cases, but you don't have to. It's not really a good thing to create a class that you can't tell about what does it represent, but on the other hand it can help you to keep your code cleaner and more consistent, to take the advantage of autoloading instead of calling require, so I personally do put almost everything into classes. But once again: I don't care about it if you prefer writing functions, feel free to do that.

But let's get back to the topic: as I mentioned above, in so many cases objects are not created for representing something, they are used for many other things. In some of these cases they are created to do something.
  • What a request dispatcher is created for? To dispatch your request, what else?
  • What a command line task is created for? To execute itself, what else?
  • What an SQL query is created for (assuming that you use a query builder API)? To be executed, what else?
The main point in the above list is that some PHP objects are created to do something, they have a "default method", the one that you will use at most times. In these cases PHP developers name these methods execute(), exec(), run(), dispatch(), whatever. Sometimes the class has no other public method. In such cases it is a good practice to "alias" this default method to __invoke() (I mean to write __invoke() to call the default method). It can be useful after your eyes are got used to it. I don't think code maintainability is an issue here.
$result = $obj(); // it is readable
$result = $obj->run(); // I don't think it's more readable
$result = $obj->exec(); // neither this
$result = $obj->processDispatch(); // it's not readabe - it's bloated

Well, using __invoke() has some serious disadvantages:
  • an attribute of an object can be a callable object, but it can't be invoked - see issue #50029 (otherwise the same stands for lambdas)
  • if a callable object is a result of a function call, then you have to save it into a local variable before calling it. MyClass::factory()() won't work. It looks like something similar to the array dereferencing problem.

These are the reasons why I suggested aliasing the __invoke() method to the default method instead of implementing the functionality in the __invoke() method itself. These problems really reduce the usability of this magic method. Hopefully it will work better in later PHP versions.

No comments:

Post a Comment