Featured image of post The Concise Yet Powerful Macroable Macros in Laravel

The Concise Yet Powerful Macroable Macros in Laravel

In computer science, a macro is a rule or pattern that specifies how a particular input (typically a string) should be converted to a corresponding output (also usually a string) based on predefined rules. This substitution occurs during preprocessing, known as macro expansion.

From Baidu Encyclopedia:
A macro in computer science refers to batch processing rules. Generally, it’s a pattern or syntactic substitution that defines how specific inputs (usually strings) are transformed into corresponding outputs (also strings) according to predefined rules. This substitution occurs during preprocessing, called macro expansion.

  • My first encounter with macros was during a university computer basics course when the professor demonstrated Office macros. Though I didn’t fully grasp it then, I remembered macros as powerful tools that could simplify repetitive tasks.
  • Today we’ll explore macro operations in Laravel

Complete Source Code First

<?php

namespace Illuminate\Support\Traits;

use Closure;
use ReflectionClass;
use ReflectionMethod;
use BadMethodCallException;

trait Macroable
{
    /**
     * The registered string macros.
     *
     * @var array
     */
    protected static $macros = [];

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

    /**
     * Mix another object into the class.
     *
     * @param  object  $mixin
     * @return void
     */
    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));
        }
    }

    /**
     * Checks if macro is registered.
     *
     * @param  string  $name
     * @return bool
     */
    public static function hasMacro($name)
    {
        return isset(static::$macros[$name]);
    }

    /**
     * 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("Method {$method} does not exist.");
        }

        if (static::$macros[$method] instanceof Closure) {
            return call_user_func_array(Closure::bind(static::$macros[$method], null, static::class), $parameters);
        }

        return call_user_func_array(static::$macros[$method], $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("Method {$method} does not exist.");
        }

        $macro = static::$macros[$method];

        if ($macro instanceof Closure) {
            return call_user_func_array($macro->bindTo($this, static::class), $parameters);
        }

        return call_user_func_array($macro, $parameters);
    }
}

Key Components Explained

  • Macroable::macro Method
public static function macro($name, $macro)
{
    static::$macros[$name] = $macro;
}

This simple method stores macros in a static array. The $macro parameter can accept either a closure or an object (thanks to PHP’s magic methods):

class Father
{
    public function __invoke()
    {
        echo __CLASS__;
    }
}

class Child
{
    use \Illuminate\Support\Traits\Macroable;
}

// Register macro
Child::macro('show', new Father);

// Output: Father
(new Child)->show();
  • Macroable::mixin Method
    Injects methods from another object:
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));
    }
}

// Usage example
class Father
{
    public function say() { return fn() => echo 'say'; }
    protected function eat() { return fn() => echo 'eat'; }
}

Child::mixin(new Father);

// All methods become available
(new Child)->say(); // Output: say
(new Child)->eat(); // Output: eat
  • Macroable::hasMacro Method
    Simple existence check:
public static function hasMacro($name)
{
    return isset(static::$macros[$name]);
}
  • Magic Methods __call & __callStatic
    Enable dynamic method handling:
public function __call($method, $parameters)
{
    if (!static::hasMacro($method)) {
        throw new BadMethodCallException("Method {$method} does not exist.");
    }

    $macro = static::$macros[$method];

    if ($macro instanceof Closure) {
        return call_user_func_array($macro->bindTo($this, static::class), $parameters);
    }

    return call_user_func_array($macro, $parameters);
}

// Example with context binding
Child::macro('show', function() {
    echo $this->name; // Accesses Child's property
});

(new Child)->show(); // Output: father

Implementing Macros in Laravel Classes

Many Laravel classes use the Macroable trait (e.g., Illuminate\Filesystem\Filesystem). To add custom methods:

  1. Register macros in App\Providers\AppServiceProvider:
// app/Providers/AppServiceProvider.php
public function register()
{
    Filesystem::macro('customMethod', function($param) {
        return "Working with: $param";
    });
}
  1. Create test route:
// routes/web.php
Route::get('/test-macro', function() {
    return Storage::customMethod('demo');
});
  1. Visit /test-macro to see “Working with: demo” output.

This approach extends functionality without modifying core classes, demonstrating Laravel’s elegant extensibility through macros.