How to use mixins in Laravel with Macroable
When you find yourself with a growing list of Request::macro(…) calls in a service provider, it doesn’t feel great. It starts to look like a bag of global helpers, just with nicer branding and less discoverability.
I hit that problem building an advanced data table with Inertia and React. I wanted reusable request parsing for sorts and filters, but I didn’t want to copy/paste the same “take a query param and normalise it” logic across multiple controllers. The solution was a mixin: group related macros into a single class and register them once.
Mixins aren’t a brand new Laravel trick. They’re built on the long-standing Macroable trait, which is how Laravel enables macro() and mixin() in the first place. Under the hood, it’s closures + reflection + magic method dispatch — simple enough, but powerful when used intentionally.
By the end of this post, you’ll know:
- when it makes sense to reach for a mixin rather than individual macros,
- how
mixin()works on classes usingMacroable, - how to keep it tidy (and keep your tools happy).
Why individual macros don’t scale
A handful of macros is fine. A dozen starts to feel messy. Two dozen and you’re basically maintaining a mini framework in AppServiceProvider.
The issue isn’t the feature, it’s the ergonomics:
- you lose grouping (what belongs together?),
- you lose narrative (“why do these exist?”),
- you increase boot-time clutter,
- and you increase the likelihood of name collisions or drift over time.
If you’re already thinking “I’ll just put them in another provider”… you’re halfway to a mixin anyway.
What a Laravel mixin actually is
A macro is a named closure stored on the class, dispatched via __call / __callStatic by the Macroable trait.
A mixin is just a convenience wrapper:
- you create a class whose public methods return closures,
- you call
SomeMacroableClass::mixin(new YourMixin()), - Laravel reflects over the public methods and registers them as macros.
So instead of this:
Request::macro('getSorts', …)Request::macro('getFilters', …)Request::macro('…', …)
You do one registration call and keep related behaviour in one place.
A real example: extending the request for data tables
When I hit this while building an advanced data table, the core issue was consistency. The frontend (a React table powered by TanStack Table) needed sorts and filters in a very specific shape, and I wanted that shape to be enforced in one place. Rather than duplicating query-string parsing or pushing the logic into controllers, I created the below RequestMixin that normalises sorts and filters into the exact format the table expects.
Note: @mixin Request is doing real work here for tooling — it helps IDEs and static analysers understand what the mixin is meant to extend.
<?php
declare(strict_types=1);
namespace App\Mixins;
use Closure;
use Illuminate\Http\Request;
/**
* @mixin Request
*/
final class RequestMixin
{
public function getSorts(): Closure
{
/**
* @return array<int, array{id: string, desc: bool}>
*/
return function (?string $default = null): array {
$sortQuery = $this->query('sort', $default);
if (is_string($sortQuery)) {
$sorts = explode(',', $sortQuery);
$sorts = array_map(function (string $value): array {
$value = trim($value);
$desc = str_starts_with($value, '-');
$id = ltrim($value, '-');
return [
'id' => $id,
'desc' => $desc,
];
}, $sorts);
return array_filter($sorts, fn (array $sort): bool => $sort['id'] !== '');
}
return [];
};
}
public function getFilters(): Closure
{
/**
* @return array<int, array{id: string, value: string}>
*/
return function (?array $default = null): array {
$filters = $this->query('filter', $default);
if (is_array($filters)) {
$filters = array_map(fn (mixed $value, string $key): array => [
'id' => $key,
'value' => is_array($value) ? implode(',', $value) : $value,
], $filters, array_keys($filters));
return array_filter($filters, fn (array $filter): bool => $filter['id'] !== '');
}
return [];
};
}
}
Registering the mixin once in a provider
The mixin got registered in AppServiceProvider, which is a perfectly sensible default when there are only a few. If this grows (Request, Collection, Response, Str, etc.), a dedicated provider is a nice next step.
use App\Mixins\RequestMixin;
use Illuminate\Http\Request;
public function boot(): void
{
Request::mixin(new RequestMixin());
}
This works because Request uses Macroable and exposes macro() and mixin() accordingly.
Using the mixin in a controller
The payoff is that the controller stops caring about query-string parsing details. It just asks the request for “sorts” and “filters”, and the mixin is the single place those rules live.
public function index(Request $request): Response
{
$users = User::paginate();
return Inertia::render('dashboard/index', [
'users' => UserResource::collection($users),
'sorts' => $request->getSorts(),
'filters' => $request->getFilters(),
]);
}
That “feels” like a framework feature because, functionally, it is. It’s just one you own.
Type safety, IDEs, and PHPStan
Macros and mixins are runtime features, which means static analysis tools don’t understand them automatically. Without a bit of care, that can lead to missing autocomplete, false positives in analysis, or the impression that methods “don’t exist”.
The fix is mostly about being explicit. Keeping mixins strict, returning closures with clear signatures, and using PHPDoc where PHP’s type system falls short makes a big difference. The @mixin Request annotation, in particular, gives IDEs and static analysers enough context to treat the mixed-in methods as if they were native to the class.
With that in place, mixins can work comfortably alongside strict static analysis. Typed return values, shaped array annotations, and predictable method names allow tools like PHPStan (with the Laravel plugin enabled) to reason about the code without resorting to broad ignores or workarounds.
The result is that mixins stop feeling like “magic” and start behaving like a deliberate extension point: discoverable, analysable, and safe to evolve over time.
When not to use mixins
Mixins are a good organisational unit. They are not a license to add random helpers everywhere.
I avoid them when:
- the method isn’t truly “a capability of that class” (it’s really a service concern),
- it’s not cohesive (one mixin becomes “UtilsButOnRequest”),
- it risks naming collisions with framework upgrades.
If you’re extending Request with behaviours that are actually domain-specific (like table sorts/filters), you’re on solid ground.
What to watch out for
- Overstuffed mixins. If the class starts to look like a dumping ground, split by concept.
- Method name collisions. You can override existing methods unintentionally. Pick names that are specific to your domain.
- Tooling confusion. Without docblocks, teammates may assume the method “doesn’t exist” and spend time chasing ghosts.
- Hidden coupling. If your macro assumes certain query string formats, document them near the method and keep tests around it.
Conclusion
If you only have one or two macros, keep it simple. But once you start accumulating framework extensions, mixins are the maintainable unit: they keep your providers tidy, your intent grouped, and your runtime magic easier to reason about.
And if you’re already running strict static analysis, the combination of strict typing + shaped array docs + @mixin annotations can make this pattern feel surprisingly “normal” (which is the whole point).