Laravel's Higher Order Messaging

By Gavin Phillips @ Cowshed Works Ltd

Blog Picture

In my blog post from January (Laravel 5.4 released) I mentioned one of the other new features of the 5.4 update ... "Higher Order Messaging - again, magic is happening here".

I didn't really pay much attention to this beyond a cursory glance at how you'd use this new syntax. However, after some recent weekend reading around Design Patterns [1] I decided to dig into Laravel's source a bit more to spot patterns (please don't judge my hobbies). It was whilst looking at the Collection class that I stumbled across the proxies static array:

/**
     * The methods that can be proxied.
     *
     * @var array
     */
    protected static $proxies = [
        'contains', 'each', 'every', 'filter', 'first', 'map',
        'partition', 'reject', 'sortBy', 'sortByDesc', 'sum',
    ];

When you dig into where this $proxies array is used you'll see that it's these two methods tucked away at the end of the class:

/**
     * Add a method to the list of proxied methods.
     *
     * @param  string  $method
     * @return void
     */
    public static function proxy($method)
    {
        static::$proxies[] = $method;
    }

    /**
     * Dynamically access collection proxies.
     *
     * @param  string  $key
     * @return mixed
     *
     * @throws \Exception
     */
    public function __get($key)
    {
        if (! in_array($key, static::$proxies)) {
            throw new Exception("Property [{$key}] does not exist on this collection instance.");
        }

        return new HigherOrderCollectionProxy($this, $key);
    }

Here I spotted HigherOrderCollectionProxy - aha so this is how these are implemented... so now I'm off on a tangent from my pattern spotting but knowing how HigherOrderCollectionProxies work is now far more important ... here's my interpretation of what's going on.


The $proxies array

First thing's first, what are those values in that array? They look like methods on the Collection class, turns out they are, great, that's easy.

The static proxy method

This is just a static method for adding values to the array, why would you need this? Perhaps if extending the Collection class? I'm not sure, doesn't seem too important in understanding how it works though so we'll leave it.

The magic __get method

So this is where things get interesting. The __get magic method is one of PHP's 'overloading' methods. PHP's overloading isn't the same as the overloading you've probably experienced in other languages whereby you can declare the same method name but with different arguments and the correct one is used based on the provided arguments. In PHP overloading is:

Overloading in PHP provides means to dynamically "create" properties and methods. These dynamic entities are processed via magic methods one can establish in a class for various action types.

So, the __get method is actually invoked when a non-existent property is accessed on the object. In this case inside the magic __get method it's checking if the non-existent property is a member of the proxies array, if not it throws an exception, and if it is it instantiates a HigherOrderCollectionProxy object passing in itself (the Collection) and the non-existent method name.

This now starts to make a bit of sense when you look at the api for these higher order functions. For example to loop over each client in a collection the old (<5.4) way you'd do it like this:

$clients->each(function($client) {
    $client->sendManagementAccounts();
});

But with higher order functions you can do this:

$clients->each->sendManagementAccounts();

Looking at this you can see that you have a $clients collection object, you'd normally call the each method on it, but now we're accessing an each property... which doesn't exist... but is in the $proxies array.... so it's starts to make even more sense now.

Essentially what's happening is the Collection object is 'catching' getter calls for properties that don't exist but a method with the same name does actually exist, anything else throws an exception.

With this in mind it's time to dig into the HigherOrderCollectionProxy class.

This class takes the collection and method name as constructor parameters, it then sets them as properties $collection and $method.

So far then this has happened:

  1. We've written this in our code $clients->each->sendManagementAccounts();
  2. PHP has looked on the $clients Collection object for a property called each
  3. It hasn't found it, so the magic __get method is invoked
  4. The 'each' method is found in the $proxies array and a new HigherOrderCollectionProxy object is created with the collection and method names passed in, this object is returned to our chained line of code

At this point in the chain sendManagementAccounts() is called on the HigherOrderCollectionProxy object, once again this involves some PHP magic. As this method doesn't exist, the magic __call method is invoked...

/**
     * Proxy a method call onto the collection items.
     *
     * @param  string  $method
     * @param  array  $parameters
     * @return mixed
     */
    public function __call($method, $parameters)
    {
        return $this->collection->{$this->method}(function ($value) use ($method, $parameters) {
            return $value->{$method}(...$parameters);
        });
    }

This magic method passes the non-existent method name and any parameters that came with it into the method. It's easier to do another list here and pick up where we left off:

  1. sendManagementAccounts() is called on the HigherOrderCollectionProxy returned by our previous each property getter
  2. This method doesn't exist, so __call is invoked passing in sendManagementAccounts and no parameters
  3. This calls the underlying Collection object's each method (which was set earlier on instantiating this HigherOrderCollectionProxy object) and passes in a callback that's created from the data passed to the __call method, ie "sendManagementAccounts" and 0 parameters - these vars are made available to the callback with the "use" keyword.
  4. This __call method effectively does the same thing as the each method I wrote above

So, what's happening here is this:

$clients->each(function($client) {
    $client->sendManagementAccounts();
});

...is being dynamically built by PHP's magic overloading functions... this then allows you to use this shorthand...

$clients->each->sendManagementAccounts();

And that's it, a whole lot of magic going on behind the scenes to provide a more 'fluent' way to call these functions.

I'm not sure how much I'll use these as I quite like the syntax of the old style, it's easier for me to see what's happening, but that's just me, I can see why others will love the simplicity of this syntax. Also, with arrow functions in RFC [2] for a new version of PHP I think the introduction of those could render this a bit unnecessary.

Laravel's use of PHP's magic functions might upset some devs but I think it makes Eloquent great to work with and very easy for beginners to get comfortable with. I'm not sure if there are trade-offs to using PHP's magic methods, from what I can tell it looks minimal.... hmm there's another blog.


References

  1. Head First Design Patterns Paperback – 4 Nov 2004 by Eric Freeman - an excellent book, just ignore the cover.
  2. https://wiki.php.net/rfc/arrow_functions

Author: Gavin Phillips
Published: Apr 25, 2017 (1 year ago)

Work with Cowshed:Works

Ready to start your next website or app project? Give us a call or send us an email.



© Cowshed Works Ltd: UK Staffordshire-based website design, build and management

VAT: 251 4480 22 - Registered in England: 10399485

Terms: Website | Service