Understanding the magic of Laravel macros


Using Laravel macros is a powerful way to extend default behavior of many classes in Laravel, such as Collections, Stringables and Reponses. In this article I’m going to explain how these macros work under the hood.

What are macros?

Using macros, you can extend default methods in a class. Take for example this macro:

Collection::macro('insertBetweenEach', function ($value) {
    return $this->flatMap(fn ($item) => [$item, $value])->slice(0, -1);
});

Now if you run this code:

collect([1, 2, 3])->insertBetweenEach(4)->dd();

You’ll get this output:

[1, 4, 2, 4, 3]

As you can see, it’s very simple to add macros and thus extend classes like Collection, Str, Stringable and Response. And if you would try to change the core classes in the vendor folder, you would loose all methods when updating or deploying.

The Macroable trait

All classes that offer macro-functionality, use the Illuminate\Support\Traits\Macroable trait. You can even add this to your own classes too!

The trait adds a protected property to the class, called $macros. When you register a macro using the macro() method, it will be saved in this array.

// vendor/laravel/framework/src/Illuminate/Macroable/Traits/Macroable.php

trait Macroable
{
    protected static array $macros = [];

    public static function macro(string $name, object|callable $macro): void
    {
        static::$macros[$name] = $macro;
    }
}

Calling methods

When you call a method that does not exist on a method, PHP will check for a __call() method and runs that instead of throwing an exception. In this method Laravel will check if there is a macro registered and run that.

// vendor/laravel/framework/src/Illuminate/Macroable/Traits/Macroable.php

trait Macroable
{
    protected static array $macros = [];

    public static function macro(string $name, object|callable $macro): void
    {
        static::$macros[$name] = $macro;
    }
    
    public function __call(string $method, array $parameters): mixed
    {
        if (!isset(static::$macros[$method])) {
            throw new BadMethodCallException(sprintf('Method %s::%s does not exist.', static::class, $method));
        }

        $macro = static::$macros[$method];

        if ($macro instanceof Closure) {
            $macro = $macro->bindTo($this, static::class);
        }

        return $macro(...$parameters);
    }
}

This method exists of four parts:

  1. It checks if the macro exists, and if not it throws an exception
  2. It finds the corresponding macro
  3. If the macro is callable (a Closure), it will bind the callback to the current class, so if the macro calls $this it works as expected
  4. It will run the macro

Static methods

When a static method is called that does not exist, PHP will execute the __callStatic() method instead of __call(). This will basically do the same thing as __call(), except not binding the callback to the current instance.

// vendor/laravel/framework/src/Illuminate/Macroable/Traits/Macroable.php

trait Macroable
{
    protected static array $macros = [];

    public static function macro(string $name, object|callable $macro): void
    {
        static::$macros[$name] = $macro;
    }
    
    public function __call(string $method, array $parameters): mixed
    {
        if (!isset(static::$macros[$method])) {
            throw new BadMethodCallException(sprintf('Method %s::%s does not exist.', static::class, $method));
        }

        $macro = static::$macros[$method];

        if ($macro instanceof Closure) {
            $macro = $macro->bindTo($this, static::class);
        }

        return $macro(...$parameters);
    }
  
    public static function __callStatic(string $method, array $parameters): mixed
    {
        if (!static::hasMacro($method)) {
            throw new BadMethodCallException(sprintf('Method %s::%s does not exist.', static::class, $method));
        }

        $macro = static::$macros[$method];

        if ($macro instanceof Closure) {
            $macro = $macro->bindTo(null, static::class);
        }

        return $macro(...$parameters);
    }
}

Conclusion

Although Laravel macros may seem hard to understand, they are actually very simple to understand and use.

Laravel News Links