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):
-
Illuminate\Database\Query\Builder
-
Illuminate\Database\Eloquent\Builder
-
Illuminate\Database\Eloquent\Relations\Relation
-
Illuminate\Http\Request
-
Illuminate\Http\RedirectResponse
-
Illuminate\Http\UploadedFile
-
Illuminate\Routing\Router
-
Illuminate\Routing\ResponseFactory
-
Illuminate\Routing\UrlGenerator
-
Illuminate\Support\Arr
-
Illuminate\Support\Str
-
Illuminate\Support\Collection
-
Illuminate\Cache\Repository
-
Illuminate\Console\Scheduling\Event
-
Illuminate\Filesystem\Filesystem
-
Illuminate\Foundation\Testing\TestResponse
-
Illuminate\Translation\Translator
-
Illuminate\Validation\Rule
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