Understanding Laravel’s Macroable trait

Laravel makes heavy use of the Macroable trait throughout its codebase, but the official documentation only mentions it in passing. There’s no explanation of its purpose, or when you should (and shouldn’t) use it. Let’s dig in.

What is the purpose of the Macroable trait?

The sole purpose of the Macroable trait is to allow you to extend the functionality of (some of) the built in Laravel classes.

I like to think of a “macroable” class as supporting ad-hoc traits. That is, you can add “traits” to a class you don’t own, without having to extend it.

This has a couple of advantages:

  1. It’s a lot simpler to add some functionality using a macro than it is to extend-and-override the Laravel class.
  2. It keeps the Laravel codebase clean, without limiting developer freedom. Desperately want to add a tail method to the Collection class? No problem.

Should you use the Macroable trait in your own classes?

The only reason to use the Macroable trait in your own classes is if you’re building them for re-use. This could be as a distributed package, or privately, in your code library.

How does the Macroable trait work?

When you look under the hood, you find that the Macroable trait is pretty simple.

In essence, it maintains an associative array of “macro” methods, where the array key is the macro name, and the array value is a callable.

The trait captures any unhandled instance and method calls, using the __call and __callStatic magic methods. If your class already implements the __call or __callStatic methods, you’ll need to do a bit of extra work in order use the Macroable trait.

If the requested function name exists in the macros array, the Macroable trait calls it, and returns the result. If it doesn’t exist, the Macroable trait throws a BadMethodCallException.

How do you add a macro using the Macroable trait?

There are two ways to add functionality to a macroable class:

  1. Using the Macroable::macro method.
  2. Using the Macroable::mixin method.

How to use the Macroable::macro method

The Macroable::macro method is the most common way of adding functionality to a macroable class.

Taking the canonical example from the documentation, the following code adds a caps method to the Response class:

Response::macro('caps', function ($value) {
    return Response::make(strtoupper($value));
});

You can also create a class-based macro, if you prefer. This is particularly handy if you’d like to unit test it.

<?php

namespace App\Macros;

use Illuminate\Support\Facades\Response;

class CapsResponse
{
    public function handle(string $value)
    {
        return Response::make(strtoupper($value));
    }
}

You register a class-based macro as follows:

Response::macro('caps', [\App\Macros\CapsResponse::class, 'handle']);

How to use the Macroable::mixin method

If you want to declare a number of related methods, you may prefer to use the Macroable::mixin method.

The mixin method can be confusing, so let’s take a moment to break it down.

public static function mixin($mixin)
{
    $methods = (new ReflectionClass($mixin))->getMethods(
        ReflectionMethod::IS_PUBLIC | ReflectionMethod::IS_PROTECTED
    );

    foreach ($methods as $method) {
        $method->setAccessible(true);

        static::macro($method->name, $method->invoke($mixin));
    }
}

Here’s how it works, step by step:

  1. The mixin method accepts an object—typically a class instance—and assigns it to the $mixin variable.
  2. Laravel retrieves every non-private method from the $mixin object, using reflection.
  3. Laravel sets each method to be ”accessible“.
  4. Laravel calls each method, and uses its return value as the registered macro callable.

That last point is the thing that tends to trip people up. They imagine that their mixin class should look something like this:

// Incorrect example
class ResponseMixin
{
    public function caps(string $value)
    {
        return Response::make(strtoupper($value));
    }
}

Whereas in reality, their mixin class should look like this:

// Correct example
class ResponseMixin
{
    public function caps(): Closure
    {
        /**
         * This is the function that will run when we call
         * Response::caps
         */
        return function (string $value) {
            return Response::make(strtoupper($value));
        }
    }
}

Macroable usage examples

At time of writing, 30 of Laravel’s core classes are macroable. Here are a couple of examples of how you can use this feature to clean up your code.

API responses

Many API responses are very similar, which can result in a lot of unnecessary busywork in your controllers. Macros are an excellent solution to this problem.

For example, we can use a macro to easily generate a response to an OPTIONS request:

Response::macro('options', function (
    array $methods,
    int $status = 200,
    array $headers = []
): JsonResponse {
    $methods = array_sort($methods);
    
    $headers = array_merge($headers, [
        'allow' => implode(',', $methods),
    ]);
    
    return response()->json(
        ['options' => $methods],
        $status,
        $headers
    );
});

Now our controller code can be as simple as:

return response()->options(['GET', 'HEAD', 'OPTIONS']);

Database migrations

Sometimes you may wish to check for the existence of a foreign key, before attempting to drop it. Laravel doesn’t provide this functionality out-the-box1, but luckily for us, the Illuminate\Database\Schema\Blueprint class is macroable:

Blueprint::macro('hasForeign', function ($index) {
    $indexString = is_array($index) ? $this->createIndexName('foreign', $index) : $index;
    
    $doctrineTable = Schema::getConnection()
        ->getDoctrineSchemaManager()
        ->listTableDetails($this->table);

    return $doctrineTable->hasIndex($indexString);
});

With this in place, your migrations remain clean and readable:

Schema::table('users', function (Blueprint $table) {
    if ($table->hasForeign(['roles'])) {
        $table->dropForeign(['roles']);
    }
});

Where to go next

Pay attention to any Laravel method calls which seem to require a lot of “prep” work. A good indicator of that is if you have a separate “build data” method, which you call prior to calling the Laravel method.

This is an excellent opportunity to clean up your code, by moving the prep work to a macro. The result will be cleaner, more readable, and no less testable.

Footnotes

  1. Presumably because it’s not compatible with all database engines.

Sign up for my newsletter

A monthly round-up of blog posts, projects, and internet oddments.