Using Macros in Laravel

Macro all the things

Updated on April 14, 2017 Posted by lagbox on April 1, 2017 extending laravel tips

The concept of a macro in Laravel gives you the ability in effect to add new methods to a class at runtime. This is very useful and there could be many reasons you might want to do this. A class could provide this behavior by using the Macroable trait or defining the functionality for itself.

How it works

The magic comes from the magic methods __call and __callStatic which allow this nice behavior. Lets take a quick look at Illuminate\Support\Traits\Macroable:

/* Register a custom macro. */
public static function macro($name, callable $macro)
{
    static::$macros[$name] = $macro;
}

...

/* Dynamically handle calls to the class. */
public function __call($method, $parameters)
{
    if (! static::hasMacro($method)) {
        throw new BadMethodCallException("Method {$method} does not exist.");
    }

    if (static::$macros[$method] instanceof Closure) {
        return call_user_func_array(static::$macros[$method]->bindTo($this, static::class), $parameters);
    }

    return call_user_func_array(static::$macros[$method], $parameters);
}

When you make a call to a method that isn't accessible on the class, __call will be called which will check if a macro with the name of the method trying to be called was registered. If there is a macro defined then call that callable. (Similarly for __callStatic)

Examples

Lets create a macro on the Collection class to demonstrate how simple this is.

use Illuminate\Support\Collection;

Collection::macro('someMethod', function ($arg1 = 1, $arg2 = 1) {
    return $this->count() + $arg1 + $arg2;
});

$coll = new Collection([1, 2, 3]);
echo $coll->someMethod(1, 2);
// 6      = 3 + (1 + 2)
echo $coll->someMethod();
// 5      = 3 + (1 + 1)

In our example we are defining a macro for the method name someMethod that can take 2 arguments. The function returns the count of the collection added to the first and second arguments.

When we are passing a Closure in as our callable the Closure will be bound to the class we are calling macro on, so $this is referring to the class instance we are calling the method on and not $this from the current context of where this macro is defined (in a Service Provider for example).

There is also the example from the Laravel 5.4 Docs - Responses - Response Macros docs for adding a macro to the Illuminate\Routing\ResponseFactory class.

Lets drop into tinker and give it a go.

>>> Response::macro('caps', function ($value) { 
...    return Response::make(strtoupper($value));
... });
=> null
>>> (string) Response::caps("dollar Dollar Bills");
=> """
   HTTP/1.0 200 OK\r\n
   Cache-Control: no-cache, private\r\n
   \r\n
   DOLLAR DOLLAR BILLS
   """
>>> 

Implements

Some classes that implement this macro functionality (as of Laravel 5.4):

Other packages might implement this on some classes as well. The Laravel Collective's HTML & Form package uses the macro functionality from the Macroable trait for the HtmlBuilder and FormBuilder.

Thoughts

... they are cool?

Yes, they are cool. This functionality can be extremely handy, especially for classes that you can't easily swap out with extended versions and any helpers you come up with along the way.

There also might be situations where you might want functionality from a PR that is adding new public methods to a class and hasn't been merged. In this situation you may be able to macro these methods.

Update

After posting this I had the occasion to stumble upon a StackOverflow question that was a decent use for macros. Add lists() method in Query Builder in Laravel 5.4