illuminate/macroable
The Illuminate Macroable trait lets you add custom methods to a class at runtime without modifying its source. Register macros once and call them like native methods, making Laravel components and your own classes easily extensible.
Reality check first: there is no meaningful standalone install here. The trait you actually use is Illuminate\Support\Traits\Macroable, which ships inside illuminate/support — i.e. it is already in every Laravel app via laravel/framework. Do not add a composer require illuminate/macroable step to your setup docs or onboarding; it's a subtree-split artifact of the monorepo, not a dependency you adopt. If you're on Laravel, you have it.
Minimal setup, three steps:
vendor/laravel/framework/src/Illuminate/Support/Traits/Macroable.php and look at macro(), mixin(), hasMacro(), flushMacros(), __call(), __callStatic(). Everything below follows from those six methods.Macroable to Laravel's own classes — Str, Stringable, Collection, Request, Response, Rule, Blueprint, Carbon, and Eloquent factories are already macroable. You only add the trait to your own classes:
use Illuminate\Support\Traits\Macroable;
class Invoice
{
use Macroable;
}
boot(), not inline at call sites:
// app/Providers/AppServiceProvider.php
public function boot(): void
{
Str::macro('initials', function (string $name): string {
return collect(explode(' ', $name))
->map(fn ($part) => Str::upper(Str::substr($part, 0, 1)))
->implode('');
});
}
Then Str::of('Ada Lovelace')->initials() works anywhere in the app.First real use case: a formatting/domain helper you'd otherwise duplicate. Instead of a Helpers.php full of static functions nobody owns, you hang Response::macro('apiOk', ...) or Collection::macro('activeUsers', ...) off the class it conceptually belongs to. That's the whole value proposition: discoverable, namespaced, testable, no inheritance.
Where to look first when something's off: Laravel's docs page on Extending Laravel / Macros, plus debug_backtrace() inside the closure. Your IDE will not find macro methods — plan for that on day one (see Gotchas).
Dedicated macro service provider. Stop stuffing macros into AppServiceProvider past the second one. Create App\Providers\MacroServiceProvider (or one per surface: StrMacros, CollectionMacros, ResponseMacros) and group them by the class they extend. This gives reviewers one place to grep and one place to delete.
Mixin classes for grouped registration. When you have 5+ macros on one target, register them via a mixin object. Each public method must return a closure:
namespace App\Support\Macros;
class StrMacros
{
public function initials(): callable
{
return function (string $name): string {
return collect(explode(' ', $name))
->map(fn ($p) => Str::upper(Str::substr($p, 0, 1)))
->implode('');
};
}
}
// in the provider
Str::mixin(new StrMacros, replace: false);
Pass replace: false in app code so a package or a teammate can't silently clobber your macro.
Instance vs. static semantics. Macros are bound to $this when called on an instance, and to a fresh instance when called statically. So write macros that read instance state on instances (Collection::macro), and macros that take all their input as arguments when you expect static-style usage (Str::macro).
Guard against collisions and double-registration. macro() silently overwrites and there is no error. Wrap idempotent registrations:
if (! Collection::hasMacro('activeUsers')) {
Collection::macro('activeUsers', fn () => $this->where('status', 'active'));
}
This matters because in Octane/queued workers your provider's booted() can run more than once per process lifetime depending on setup.
Use it at the HTTP boundary for API envelopes. A classic day-to-day win — one macro, consistent JSON everywhere:
Response::macro('api', function (mixed $data = null, int $status = 200, array $meta = []) {
return response()->json(['data' => $data, 'meta' => $meta], $status);
});
Schema/rule macros for project conventions. Blueprint::macro('tenantColumns', ...) and Rule::macro('phoneNumber', ...) keep migrations and validation from re-deriving the same rules per table/form.
Test macros like first-class code. They are functions; give them a test file. Use flushMacros() in tearDown() (or a Pest afterEach) so one test's macro can't leak into another and hide a missing registration:
protected function tearDown(): void
{
Str::flushMacros();
Collection::flushMacros();
parent::tearDown();
}
Prefer a macro over a static helper when: the method reads naturally as a verb on the object ($order->totalWithTax()), the target class is already macroable, and it's used in more than one place. Prefer a plain class/service when: the logic is heavy, needs DI (DB, HTTP client), or you want static analysis. A macro is a shorthand, not an architecture.
It isn't a package — don't pin it. Adding illuminate/macroable to composer.json is either a no-op from the split repo or a version-drift hazard against laravel/framework. If you need the trait, you need illuminate/support, and you already have it. This is the single most common misstep when this "package" gets proposed in a ticket.
Overwrites are silent. macro('foo', ...) twice = last one wins, no warning, no exception. Combined with mixin()'s $replace = true default, a package can quietly redefine your Collection::sum-adjacent helper. Always hasMacro()-guard in app code; always pass replace: false to mixin().
Collision with a future framework method is the real long-term risk. __call only fires when the method doesn't exist. If Laravel ships a real Str::initials() in a later release, your macro becomes dead code and behavior changes with zero errors. Mitigate by prefixing business macros (acmeInitials) or at least keeping a test that asserts the expected output — that test will fail loudly on the upgrade.
$this is bound with the class scope, not just the instance. The closure is bound to static::class, which means a macro can touch protected members it has no business touching. Convenient, and a great way to couple your helper to internals that get refactored. Treat macros as public API of the target class.
Static invocation gives you a throwaway instance. MyClass::someMacro() binds to new static, so any macro reading instance properties ($this->subtotal) will blow up or return nonsense in static context. If a class has a required constructor, don't offer static-style macros on it.
IDE and static analysis blindness. No autocomplete, no @method inference, no unused-method detection. Fix it once, properly:
@method annotations to your own macroable classes,.phpstorm.meta.php or a PHPStan baseline/stub, or use a Laravel-aware plugin (Laravel Idea, larastan's macro support),hasMacro() assertion in the test suite so a deleted registration fails CI rather than 500ing in prod.Octane and long-running workers. Macros live in a protected static $macros array for the lifetime of the PHP process. In Octane, that's the worker's lifetime, not the request's. Never capture request-scoped objects (Request, models, user instances, DB connections) in a macro closure registered at boot — you'll leak memory across requests. Register only stateless closures in boot(); if you genuinely need request data, pass it as an argument.
Queues inherit the same rule. A job running on another worker only has the macro if the provider booted there too. Keep macro registration in boot(), never in a controller, route file, or test bootstrap — otherwise jobs and artisan commands behave differently from HTTP.
mixin() reflects everything visible. It registers all public and protected methods of the mixin class, so a stray helper method or a constructor on your mixin becomes a bogus macro. Keep mixin classes to nothing but closure-returning methods; don't give them constructors or shared state.
Trait conflicts. If a class uses Macroable and another trait with macro()/hasMacro()/__call(), you get a fatal error — resolve with insteadof, and never define your own __call() alongside the trait. You also can't override the trait's methods meaningfully; wrap or compose instead.
Debugging a "method does not exist" that clearly exists. Three usual causes, in order: (1) the provider didn't boot in that context (console vs. HTTP, or a package provider ordering issue), (2) you called it before boot() ran — e.g. inside register() of another provider, (3) the name is already a real method so __call never fires. Check with Class::hasMacro('name') first; it instantly separates 1–2 from 3.
Performance is usually fine, but not for tight loops. Each call does a macro lookup plus a closure bind. That's negligible for request-level helpers and genuinely wasteful if invoked per-row over 100k rows — inline those or use a real collection callback.
Extension point: flushMacros() is your escape hatch. Besides tests, it's how you swap macro sets at runtime (feature flags, multi-tenant behavior differences) — flush, then re-register the tenant's variant inside a scoped block, and flush again in a finally.
How can I help you explore Laravel packages today?