Laravel Macros Postmortem

Founder & CEO, Contrive Solutions 15+ years in software engineering, SaaS, AI automation and enterprise application development
Posted on (updated ) by Rohan Jalil

Laravel gives you the ability to add methods to a class at runtime by using the Macroable (Illuminate\Support\Traits\Macroable) Trait. You need to use the Macroable trait inside the class in which you want to add methods at run time.

Here is an example from the Laravel Documentation. The following code adds a toUpper method to the Collection class:

use Illuminate\Support\Collection;
use Illuminate\Support\Str;

Collection::macro('toUpper', function () {
 return $this->map(function ($value) {
 return Str::upper($value);
 });
});

$collection = collect(['first', 'second']);

$upper = $collection->toUpper();

// ['FIRST', 'SECOND']

How to Register Your Macros?

Laravel’s Macroable trait offers 2 ways to register your macros.

  1. Using Macroable::macro($name, $macro) method.
  2. Using Macroable::mixin($mixin, $replace) method.

How the Magic Stuff Works?

For the magic stuff to work, you need to have Magic Methods. Macros magic is based on two magic methods provided by PHP: __call() and __callStatic().

How it all works behind the scenes is that if you call a method on a class object which doesn’t exist, and you have defined the __call() magic method, control will go to this method. Here you can capture the request and perform any task you want, for example log it and throw an exception.

Laravel uses this concept very intelligently and neatly. Laravel has defined these magic methods inside the Macroable trait. First the Macroable trait is used inside a class where we want to add dynamic methods.

use Illuminate\Support\Traits\Macroable;

class JsonResponse extends BaseJsonResponse
{
 use ResponseTrait, Macroable {
 Macroable::__call as macroCall;
 }
}

Then we register our method by calling the macro static method. For example, let’s add functionality in our Laravel Response class to capitalize a value. We can do that with the following piece of code, an example taken from the Laravel Documentation.

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

Implementation of the macro function is pretty straightforward. It maintains a mapping of the name of the newly added function and the callback for it.

/**
 * Register a custom macro.
 *
 * @param string $name
 * @param object|callable $macro
 * @return void
 */
public static function macro($name, $macro)
{
 static::$macros[$name] = $macro;
}

After registering your macro, it is time to call it!

Once you call your registered macro, for example Response::caps('laravel'), the request flow will be the following:

  • The call first goes to the Response class, and the PHP engine checks for the definition of the called method. In our case it checks for a caps function definition in the Response class (Illuminate/Routing/ResponseFactory).
  • If the method is found, it is executed. Otherwise the PHP engine checks for the __call() and __callStatic() magic methods, depending on whether the function was called statically or not.
  • In our case, the Response class (Illuminate/Routing/ResponseFactory) does not have a caps method defined, so the PHP engine forwards control to either __call() or __callStatic() (depending on the call type) defined in the Macroable trait. Here we first check whether the called function is registered as a macro. If not, a BadMethodCallException is thrown. If it is, we bind the macro callback to the class reference and execute it.
/**
 * Dynamically handle calls to the class.
 *
 * @param string $method
 * @param array $parameters
 * @return mixed
 *
 * @throws \BadMethodCallException
 */
public static function __callStatic($method, $parameters)
{
 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) {
 return call_user_func_array(Closure::bind($macro, null, static::class), $parameters);
 }

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

/**
 * Dynamically handle calls to the class.
 *
 * @param string $method
 * @param array $parameters
 * @return mixed
 *
 * @throws \BadMethodCallException
 */
public function __call($method, $parameters)
{
 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) {
 return call_user_func_array($macro->bindTo($this, static::class), $parameters);
 }

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

What if I Want to Define Multiple Macros?

Laravel to the rescue again! You can define multiple macros at once using the Macroable::mixin($mixin, $replace) method. The $mixin argument is a class object whose methods return callables.

class MixinClass
{
 public function firstMixin(): Closure
 {
 return function () {
 return 'I am first Mixin!';
 };
 }

 public function secondMixin(): Closure
 {
 return function () {
 return 'I am Second Mixin!';
 };
 }
}

$mixin = new MixinClass();

$macroClass->mixin($mixin);

$macroClass->firstMixin(); // I am first Mixin!

$macroClass->secondMixin(); // I am Second Mixin!

Scope of Macros

One important thing to note about macros is that they are bound to the class scope in which the Macroable trait is used, not the scope from which they are registered. For example, let’s add a whoami method to our Laravel Response class (Illuminate/Routing/ResponseFactory) to check the scope of $this.

class AppServiceProvider extends ServiceProvider
{
 /**
 * Bootstrap any application services.
 *
 * @return void
 */
 public function boot()
 {
 Response::macro('whoAmi', function () {
 return get_class($this);
 });
 }
}

When we call this in our test route, we get a result of Illuminate\Routing\ResponseFactory, not AppServiceProvider.

Route::get('test', function () {
 return Illuminate\Support\Facades\Response::whoAmi();
});

// Output is Illuminate\Routing\ResponseFactory

The secret behind this is the bindTo method of the PHP Closure class. The bindTo method binds the callback to the called class scope. Inside your macro callback, $this is the called class scope, and you can access other accessible methods and attributes.


Where Can Macros Be Defined?

You can define macros inside Laravel’s Service Providers. App\Providers\AppServiceProvider‘s boot() method is a good starting point. If you have many macros in your application, the recommended approach is to create your own Service Provider class for macros and register it in config/app.php.


If you found this useful, the same walk-down-to-the-SQL treatment is applied to Laravel 13’s new semantic search in the Laravel Vector Search Postmortem.

Founded
2014
Years in business
12
People
66
Headquarters
Lahore, Pakistan
Projects delivered
250+
AI systems in production
10