Macro provide convenient way to attach new behavior to a class at run time. In this article, we will uncover some basic examples, the $this instance in macro and how they work behind the scenes.
Pre-requisites
- Basic knowledge about the framework
- You should know what a closure is
First of all some basics! Laravel has a Macroable trait that makes it possible to attach some behavior to a class through a closure. Logically speaking this trait can be used with any class to extend its behavior but the real fruit of it lies extending the framework behavior since we shouldn’t modify the classes such as collections, arrays and so on… Below is a basic example of the Macro in action to make you and myself understand how this magic happens. Afterwards, we will deep dive into how it all comes together behind the scenes.
Basic Macro Example
Suppose we have a class MacroExample. To be able to attach new behavior at run time, we must use Macroable trait
class MacroExample {
use Macroable;
}Macros are usually defined in service providers so that is what we will do next
public function boot(): void
{
MacroExample::macro('greet', function () {
return 'hello from macro';
});
}We define a closure function that should print hello from macro and seem it to assign greet keyword (don’t worry it will be explained later). Calling greet function on MacroExample class should now return hello from macro. Go ahead and try it
To simplify it for you, macros allow us to call any given function in the Macroable class context (MacroExample class in this case). You might be wondering if that is the case, how $this keyword behaves inside the closure function since we are supposed to be in MacroExample context.
To demonstrate how $this keyword behaves, let’s add a simple function to the MacroExample class
class MacroExample {
use Macroable;
public function time(): string
{
return 'the time is' . now()->toTimeString();
}
}Also modify the example in service provider as following
MacroExample::macro('greet', function () {
return 'hello from macro.' . $this->time();
});and voila! it returns the expected output with time. But, you might be confused how does $this is executed in the context of MacroExample class and you’d be absolutely right to question it. To answer this question we will have to dip our toes into the framework code
Uncovering behind the scenes…
If you remember we previously used the Macroable trait on our class. Let’s pull out our detective gloves and go deep into this trait. The macro was defined as following
MacroExample::macro('greet', function () {
return 'hello from macro. ' . $this->time();
});If we look into the Macroable, we can find this function inside of there
public static function macro($name, $macro)
{
static::$macros[$name] = $macro;
}it looks like it is doing nothing but storing callback functions with its associated name for later usage. Bear in mind that is also using $macros as static constant. The idea is to make these functions available globally instead of against a specific class instance of MacroExample class for-example
If you remember what happened next was that we were able to call greet function on the MacroExample afterwards. But the question arises; how are we able to call this function if it does not even exist on MacroExample class and you’d be absolutely right to question it. The magic lies in php __call function. Whenever, a function is invoked which does not exist on the class, it calls the __call function. If we look inside the Macroable trait, sure enough we find this function AND this is where all the magic happens. So, let’s take a look
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) {
$macro = $macro->bindTo($this, static::class);
}
return $macro(...$parameters);
}after checking if a ‘macro’ function has been defined, it fetches the callback function and stores it inside the $macro variable. Afterwards, it binds the closure context to $this variable. Hmmm…. what’s going on here?
How does bindTo work?
If you go to php docs to find out about bindTo, you might find some complex definition to what is but basically bindTo ties the $this inside of closure to some object and it returns a new closure. In our case “some object ” $this inside bindTo happens to refer to the MacroExample class because it is the class which is responsible for calling of __call function. Hence why, when you used $this inside the closure in service provider, you could still call the time function in MacroExample. Neat… isn’t it?
and at the end, we just call the callback function and pass in the arguments. To put it simply bindTo makes closure “think” that it is inside of MacroExample class itself
What’s next
That was the basic overview of how macro work in Laravel. To be macroable yourself 😛 you might find some advanced examples on the laravel documentation itself for-example, laravel documentation shows this example of macro on the collection class
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
Collection::macro('toUpper', function () {
return $this->map(function (string $value) {
return Str::upper($value);
});
});
$collection = collect(['first', 'second']);
$upper = $collection->toUpper();
// ['FIRST', 'SECOND']You can read more about it and one more example here. Keep leveling yourself up and Happy Coding 🎉🎉