Macros in Laravel
This post was published 2 years ago. Some of the information might be outdated!
Laravel's Illuminate\Support\Macroable
trait allows you extend classes at runtime with custom methods. The idea is that you provide a named Closure
, or callable
value, which can then be invoked using PHP's __call()
and __callStatic()
magic methods.
Let's take a look at an example using a Collection
.
$collection = collect([
'foo' => [
'bar' => [
'baz' => 'bob',
],
],
]);
$collection->get('foo.bar.baz')
I want this code to take the dot-notated path and return the value. The Collection
object doesn't support this though. One solution would be swapping out the Collection::get()
call for a data_get()
call.
$baz = data_get($collection, 'foo.bar.baz');
That does actually work since the data_get()
function has some special logic for Collection
objects. From a readability point of view, it's not quite as nice though. The Collection
object is known for its fluency and method chaining and this bit of code doesn't fit into that pattern.
Let's add a macro the Collection
object that does this logic for us.
use Illuminate\Support\Collection;
Collection::macro('path', function (string $path) {
return data_get($this, $path);
});
The Macroable::macro()
method accepts 2 arguments. The first is the name of the macro and the second is a Closure
or callable
value.
We can now call a magic path()
method on the $collection
and it will return the value found at the path provided.
$baz = collect([
// ...
])
->path('foo.bar.baz');
Behind the scenes the method call is being delegated to the __call()
method implemented by the Macroable
trait. It will check if the macro exists and invoke the callback if it does.
What is special about the callback is that we can use $this
and reference the object we're calling the macro on instead of the $this
context where the macro is defined.
Inside of the Closure
, $this
is actually the Collection
object that we created. To help our IDE understand the context a little more, we can add a DocBlock at the top of the Closure
.
Collection::macro('path', function (string $path) {
/** @var \Illuminate\Support\Collection $this */
return data_get($this, $path);
});
Intellisense
Since macros are defined at runtime, your IDE likely won't be able to provide any autocomplete or intellisense. I've found the best way to fix this problem is by creating a "stubs" file that has a carcass definition of your macros.
This file has multiple purposes:
- It lets your IDE and static analysis tools discover the macros your define.
- It serves as a single-source of truth for the macros that you define in your project.
I typically create a .stubs.php
file in the root of my project. The path()
macro might be stubbed out like below.
<?php
namespace Illuminate\Support
{
class Collection
{
public function path(string $path): mixed {}
}
}
If your tooling is able to scan that file, it should add the Collection::path()
definition to its indexes and provide some autocomplete for you.