Macros: Extending the Framework

Add your own methods to Collections, Str, Request, Response and more — without touching core.

Many Laravel classes are macroable: you can bolt your own methods onto them at runtime. Collection, Str, Stringable, Request, Response, Arr, the query Builder and more all use the Macroable trait. This is how you add a reusable helper that feels native.

Defining a macro

Call ::macro('name', $closure) in a service provider's boot(). Inside the closure, $this is bound to the instance — so a Collection macro can call $this->map(...).

use Illuminate\Support\Collection;

Collection::macro('toUpperKeys', function () {
    return $this->keyBy(fn ($v, $k) => strtoupper($k));
});

collect(['a' => 1])->toUpperKeys();

Mixins

When you have several related macros, group them into a class and register them all at once with ::mixin(new MyMixin) — each public method becomes a macro. Cleaner than a wall of closures.

Example

Example · php
<?php

namespace App\Providers;

use Illuminate\Http\Client\PendingRequest;
use Illuminate\Support\Collection;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Stringable;

class MacroServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        // Collection: split into N roughly-even columns for a grid view.
        Collection::macro('columns', function (int $count) {
            /** @var Collection $this */
            return $this->chunk((int) ceil($this->count() / $count));
        });

        // Stringable: a domain-specific transformer, chainable like the rest.
        Stringable::macro('initials', function () {
            /** @var Stringable $this */
            return $this->explode(' ')
                ->map(fn ($p) => \Illuminate\Support\Str::substr($p, 0, 1))
                ->implode('');
        });

        // Register a whole group of HTTP-client macros via a mixin class.
        PendingRequest::mixin(new \App\Http\Macros\HttpClientMacros());
    }
}

// --- Usage reads as if it were built in ---
// collect($users)->columns(3);
// str('Ada Lovelace')->initials();   // 'AL'

When to use it

  • A team adds a macro to the Collection class to add a toAssoc() helper that converts a flat list of key-value pairs into an associative array, reusing it across multiple modules.
  • A developer extends the Request class with a macro that extracts and validates a JSON sub-key from nested request data, keeping controllers clean.
  • A package publishes a macro on the Builder class that adds a whereDateRange() convenience method available to every Eloquent model in the application.

More examples

Add a macro to Collection

Collection::macro() registers a custom method on the Collection class globally, available on any collection instance in the application.

Example · php
// In AppServiceProvider::boot()
use Illuminate\Support\Collection;

Collection::macro('toAssoc', function () {
    return $this->reduce(function ($carry, $item) {
        $carry[$item['key']] = $item['value'];
        return $carry;
    }, []);
});

// Usage
$assoc = collect([
    ['key' => 'color', 'value' => 'red'],
    ['key' => 'size',  'value' => 'L'],
])->toAssoc();
// => ['color' => 'red', 'size' => 'L']

Macro on the Query Builder

Adding a macro to the Query Builder makes the method chainable on any Eloquent model query, encapsulating the date-range pattern in one place.

Example · php
use Illuminate\Database\Query\Builder;

Builder::macro('whereDateRange', function (string $column, $from, $to) {
    return $this->whereBetween($column, [
        $from instanceof \Carbon\Carbon ? $from : \Carbon\Carbon::parse($from),
        $to   instanceof \Carbon\Carbon ? $to   : \Carbon\Carbon::parse($to),
    ]);
});

// Available on every Eloquent model
$orders = Order::whereDateRange('created_at', '2025-01-01', '2025-03-31')->get();

Mixin: register many macros at once

A Mixin class groups multiple macro definitions; Collection::mixin() registers all of them at once, keeping the service provider tidy.

Example · php
// app/Mixins/CollectionMixin.php
class CollectionMixin
{
    public function toCsv(): Closure
    {
        return function (array $columns) {
            return $this->map(fn($item) =>
                implode(',', array_map(fn($col) => $item[$col], $columns))
            )->implode("\n");
        };
    }
}

// AppServiceProvider::boot()
Collection::mixin(new CollectionMixin());

Discussion

  • Be the first to comment on this lesson.