Refactoring with Laravel Collection Macros: The Complete Guide
Refactoring with Laravel Collection Macros
Laravel Collections are arguably the best feature of the framework. They transform messy array manipulation into fluent, readable chains. But as your application grows, you often find yourself repeating the same map, filter, and reduce chains in multiple places.
You could create a Helper class. You could put it in a Service. But the most "Laravel way" is to teach the Collection class new tricks using Macros.
What is a Macro?
The Macroable trait (used by Collections, Request, Response, and many others) allows you to register custom methods at runtime. Essentially, you can "monkey patch" the framework's core classes safely.
Step 1: Cleaning up AppServiceProvider
Tutorials often tell you to put macros in AppServiceProvider::boot(). In a real project, this gets messy fast.
Create a dedicated provider:
php artisan make:provider CollectionMacroServiceProvider
Register it in bootstrap/providers.php (or config/app.php).
// app/Providers/CollectionMacroServiceProvider.php
use Illuminate\Support\Collection;
use Illuminate\Support\ServiceProvider;
class CollectionMacroServiceProvider extends ServiceProvider
{
public function boot(): void
{
// Define your macros here
Collection::macro('toUpper', function () {
return $this->map(fn ($value) => strtoupper($value));
});
}
}
Real-World Examples
1. The "Standardize Keys" Macro
APIs often return messy keys (First_Name, last-name). Let's fix them to snake_case.
Collection::macro('standardizeKeys', function () {
return $this->mapWithKeys(function ($value, $key) {
return [Str::snake($key) => $value];
});
});
// Usage
$data = collect(['First_Name' => 'John', 'user-ID' => 1]);
$data->standardizeKeys(); // ['first_name' => 'John', 'user_id' => 1]
2. The "If Empty" Fallback
Sometimes you want to return a default if the collection is empty, but you want to stay in the chain.
Collection::macro('ifEmpty', function (callable $callback) {
if ($this->isEmpty()) {
$callback($this);
}
return $this;
});
3. Complex Business Logic: Cartesian Products
Macros are perfect for complex math that you don't want to copy-paste.
Collection::macro('cartesianProduct', function ($collection) {
return $this->flatMap(function ($a) use ($collection) {
return $collection->map(function ($b) use ($a) {
return [$a, $b];
});
});
});
Organizing with Mixins
If you have 20 macros, one file is still too big. Use Mixins. A Mixin is a class where methods return Closures.
// app/Macros/CollectionMixins.php
class CollectionMixins
{
public function paginate()
{
return function ($perPage = 15, $page = null, $options = []) {
$page = $page ?: (Paginator::resolveCurrentPage() ?: 1);
return new LengthAwarePaginator(
$this->forPage($page, $perPage),
$this->count(),
$perPage,
$page,
$options
);
};
}
public function extractEmails()
{
return function () {
return $this->pluck('email')->filter()->unique();
};
}
}
Registering it is cleaner:
Collection::mixin(new \App\Macros\CollectionMixins());
Solving the IDE Autocomplete Problem
The biggest downside of macros is that PHPStorm/VS Code doesn't know they exist. You have two solutions:
- Laravel IDE Helper: The popular package usually picks them up if you regenerate docs.
- Manual PHPDoc: Create a
_ide_macros.phpfile in your root (not loaded by autoloader).
// _ide_macros.php
namespace Illuminate\Support {
/**
* @method self standardizeKeys()
* @method LengthAwarePaginator paginate(int $perPage = 15)
*/
class Collection {}
}
Unit Testing Macros
Macros are global state, so testing them effectively requires care.
// tests/Unit/CollectionMacroTest.php
test('standardize keys macro works', function () {
$collection = collect(['My Key' => 1]);
expect($collection->standardizeKeys()->toArray())
->toBe(['my_key' => 1]);
});
Because Collection is loaded by the framework boot process, your macros are available in all tests extending TestCase.
Summary
- Don't clutter controllers with repetition.
- Do move logic to Macros or Mixins.
- Do create a dedicated ServiceProvider.
- Do help your IDE with PHPDocs.
Macros turn your application's specific domain language into first-class citizens of the framework.