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:
- It’s a lot simpler to add some functionality using a macro than it is to extend-and-override the Laravel class.
- It keeps the Laravel codebase clean, without limiting developer freedom. Desperately want to add a
tail
method to theCollection
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:
- Using the
Macroable::macro
method. - 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:
- The
mixin
method accepts an object—typically a class instance—and assigns it to the$mixin
variable. - Laravel retrieves every non-private method from the
$mixin
object, using reflection. - Laravel sets each method to be ”accessible“.
- 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
-
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.