The Magic of Laravel Macros

Ever wanted a piece of functionality in a part of Laravel that doesn’t exist? Let me introduce you to Laravel macros. Macros allow you to add on custom functionality to internal Laravel components.

Let’s start with a simple example on the Request facade.

Request::macro('introduce', function ($name) {
    echo 'Hello ' . $name . '!';
});
Request::introduce('Caleb'); // outputs "Hello Caleb!"

A more practical example of a Request macro would be detecting the current TLD (Top Level Domain: .com, .net, .org, etc…).

Request::macro('tldIs', function ($tld) {
    return Str::is('*.' . $tld, $this->root());
});
Request::tldIs('com') // returns true for app.com
Request::tldIs('dev') // returns false for app.com

You’ll notice Laravel automatically binds $this to the context of Request rather than the class where the macro is defined. For example:

class AppServiceProvider
{
    public function boot()
    {
        Request::macro('context', function () {
            return get_class($this);
        }
    }
...
Request::context();
// returns 'Illuminate\Http\Request'
// instead of 'App\AppServiceProvider'

Let’s look at a more advanced example. This macro conditionally adds a where statement on the model based on the current TLD.

Builder::macro('whenTldMatches', function($tld, $callback) {
    if (Request::tldIs($tld)) {
        call_user_func($callback->bindTo($this));
    }
    return $this;
});
SomeModel::whenTldMatches('org', function () {
    $this->where('id', '>', 5);
})->get();
// applies ->where() on app.org but not app.com

Where should I define them?

Service providers are a great place to define macros for your app. App\Providers\AppServiceProvider boot() is a good starting point, but can quickly become bloated. The next step is to create a App\Providers\MacrosServiceProvider and register it in config/app.php. If certain macros are related, such as the examples above, I might create a App\Providers\TldAwareServiceProvider to house all TLD related macros.

Which components are “Macroable”?

Macros can be defined on any class with the Macroable trait. Below is a list of Macroable facades & classes:

Facades

  • Cache
  • File
  • Lang
  • Request
  • Response
  • Route
  • URL

Illuminate Classes

  • Illuminate\Cache\Repository
  • Illuminate\Console\Scheduling\Event
  • Illuminate\Database\Eloquent\Builder
  • Illuminate\Database\Eloquent\Relation
  • Illuminate\Database\Query\Builder
  • Illuminate\Filesystem\Filesystem
  • Illuminate\Foundation\Testing\TestResponse
  • Illuminate\Http\RedirectResponse
  • Illuminate\Http\Request
  • Illuminate\Http\UploadedFile
  • Illuminate\Routing\ResponseFactory
  • Illuminate\Routing\Router
  • Illuminate\Routing\UrlGenerator
  • Illuminate\Support\Arr
  • Illuminate\Support\Collection
  • Illuminate\Support\Str
  • Illuminate\Translation\Translator
  • Illuminate\Validation\Rule

Takeaway

If you find yourself repeating performing logic on Laravel components throughout your system, think about using a macro for better expression and reuse. Trust me, they’re addicting.