Passer au contenu principal

Qu’est-ce que le trait Macroable

Le trait Macroable permet d’ajouter dynamiquement des méthodes à une classe après coup, sans la modifier. Beaucoup de classes du cœur de Laravel l’utilisent, ce qui vous permet d’étendre les fonctionnalités sans toucher au code du cœur. L’implémentation réside dans Illuminate\Support\Traits\Macroable. En interne, les macros enregistrées sont stockées dans la propriété statique $macros, et sont invoquées via les méthodes magiques __call / __callStatic.

Classes utilisant Macroable

Laravel comporte de nombreuses classes compatibles Macroable.
ClasseRôle
Illuminate\Support\CollectionManipulation de collections
Illuminate\Support\StrManipulation de chaînes
Illuminate\Support\ArrManipulation de tableaux
Illuminate\Http\RequestRequêtes HTTP
Illuminate\Http\ResponseRéponses HTTP
Illuminate\Routing\RouterRouteur
Illuminate\Routing\ResponseFactoryFabrique de réponses
Illuminate\Database\Schema\BlueprintSchema Builder
Illuminate\Pipeline\PipelinePipeline
Illuminate\Testing\TestResponseRéponse de test

macro() — ajouter une méthode

Passez le nom de la méthode en premier argument et une closure en second à macro().
use Illuminate\Support\Collection;

Collection::macro('toSentence', function (string $separator = ', ') {
    /** @var Collection $this */
    return $this->implode($separator);
});

$result = collect(['pomme', 'orange', 'raisin'])->toSentence();
// 'pomme, orange, raisin'
Le $this de la closure est lié à l’instance qui a appelé la macro. Vous pouvez ainsi accéder directement aux propriétés et méthodes de la classe.

mixin() — ajouter plusieurs méthodes d’un coup

Pour enregistrer beaucoup de macros à la fois, utilisez mixin(). Toutes les méthodes public / protected de la classe de mixin sont enregistrées comme macros.
namespace App\Mixins;

class CollectionMixin
{
    public function toCsv(): Closure
    {
        return function (string $separator = ',') {
            /** @var \Illuminate\Support\Collection $this */
            return $this->map(function ($item) use ($separator) {
                return is_array($item) ? implode($separator, $item) : $item;
            })->implode("\n");
        };
    }

    public function filterEmpty(): Closure
    {
        return function () {
            /** @var \Illuminate\Support\Collection $this */
            return $this->filter(fn ($item) => ! empty($item))->values();
        };
    }

    public function groupByFirst(): Closure
    {
        return function (string $key) {
            /** @var \Illuminate\Support\Collection $this */
            return $this->groupBy(fn ($item) => $item[$key][0] ?? '');
        };
    }
}
Les méthodes de mixin() doivent renvoyer la closure à enregistrer comme macro. C’est la valeur de retour de la méthode qui devient l’implémentation de la macro.

Enregistrement dans un service provider

Les macros doivent être enregistrées au démarrage de l’application. La méthode boot() d’AppServiceProvider est l’endroit approprié.
namespace App\Providers;

use App\Mixins\CollectionMixin;
use Illuminate\Support\Collection;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Str;
use Illuminate\Http\Request;

class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        // Enregistrer une seule macro
        Collection::macro('toSentence', function (string $separator = ', ') {
            return $this->implode($separator);
        });

        // Enregistrer un mixin en une passe
        Collection::mixin(new CollectionMixin);

        // Macro sur Str
        Str::macro('initials', function (string $name) {
            return collect(explode(' ', $name))
                ->map(fn ($word) => strtoupper($word[0]))
                ->implode('');
        });
    }
}

Cas d’utilisation pratiques

Étendre les collections

Ajouter des méthodes personnalisées aux collections est l’usage le plus courant.
// Méthode statistique pour une collection de nombres
Collection::macro('median', function () {
    $sorted = $this->sort()->values();
    $count = $sorted->count();

    if ($count === 0) {
        return null;
    }

    $middle = (int) floor($count / 2);

    if ($count % 2 === 0) {
        return ($sorted->get($middle - 1) + $sorted->get($middle)) / 2;
    }

    return $sorted->get($middle);
});

$median = collect([3, 1, 4, 1, 5, 9, 2, 6])->median();
// 3.5

// Macro renvoyant une collection paginée
Collection::macro('paginateArray', function (int $perPage = 15, int $page = 1) {
    return $this->slice(($page - 1) * $perPage, $perPage)->values();
});

Étendre la classe Str

// Compter les caractères en tenant compte du multi-octet
Str::macro('mbLength', function (string $value) {
    return mb_strlen($value, 'UTF-8');
});

// Convertir un snake_case en notation pointée
Str::macro('toDotNotation', function (string $value) {
    return str_replace('_', '.', $value);
});

$length = Str::mbLength('こんにちは'); // 5
$dot = Str::toDotNotation('user_profile_name'); // 'user.profile.name'

Étendre la classe Request

use Illuminate\Http\Request;

Request::macro('isFromMobile', function () {
    /** @var Request $this */
    $userAgent = $this->userAgent() ?? '';

    return preg_match('/Mobile|Android|iPhone|iPad/i', $userAgent) === 1;
});

Request::macro('preferredLocale', function (array $available = ['fr', 'en']) {
    /** @var Request $this */
    foreach ($this->getLanguages() as $lang) {
        $short = substr($lang, 0, 2);
        if (in_array($short, $available)) {
            return $short;
        }
    }

    return $available[0] ?? 'en';
});
// Utilisation dans un contrôleur
public function index(Request $request)
{
    if ($request->isFromMobile()) {
        return response()->json($this->getMobileData());
    }

    $locale = $request->preferredLocale(['fr', 'en', 'zh']);
    // ...
}

Étendre Blueprint (migrations)

Regrouper des définitions de colonnes sous forme de macros permet de garder une conception de base de données cohérente.
use Illuminate\Database\Schema\Blueprint;

Blueprint::macro('addTimestampsWithTimezone', function () {
    /** @var Blueprint $this */
    $this->timestampsTz();
    $this->softDeletesTz();
});

Blueprint::macro('addUserTracking', function () {
    /** @var Blueprint $this */
    $this->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
    $this->foreignId('updated_by')->nullable()->constrained('users')->nullOnDelete();
});
// Utilisation dans une migration
Schema::create('posts', function (Blueprint $table) {
    $table->id();
    $table->string('title');
    $table->text('body');
    $table->addTimestampsWithTimezone();
    $table->addUserTracking();
});

Étendre les réponses de test

Vous pouvez ajouter des méthodes d’assertion propres aux tests.
use Illuminate\Testing\TestResponse;

TestResponse::macro('assertPaginated', function () {
    /** @var TestResponse $this */
    return $this->assertJsonStructure([
        'data',
        'meta' => ['current_page', 'last_page', 'per_page', 'total'],
        'links' => ['first', 'last', 'prev', 'next'],
    ]);
});

TestResponse::macro('assertApiSuccess', function () {
    /** @var TestResponse $this */
    return $this->assertOk()->assertJsonPath('success', true);
});
// Utilisation dans les tests
$this->getJson('/api/posts')->assertPaginated();
$this->postJson('/api/orders', $data)->assertApiSuccess();

hasMacro() — vérifier l’existence d’une macro

use Illuminate\Support\Collection;

if (Collection::hasMacro('toSentence')) {
    $result = collect(['a', 'b'])->toSentence();
}

flushMacros() — réinitialiser les macros

À utiliser pour réinitialiser les macros dans les tests.
use Illuminate\Support\Collection;

// Réinitialisation dans un test
Collection::flushMacros();
flushMacros() supprime toutes les macros de la classe. On peut l’appeler dans tearDown() pour préserver l’indépendance des tests, mais attention : les macros enregistrées par d’autres tests sont aussi supprimées.

Macros statiques

Les macros fonctionnent aussi bien comme méthodes d’instance que comme méthodes statiques. C’est __callStatic qui les gère.
Str::macro('randomHex', function (int $length = 8) {
    return substr(bin2hex(random_bytes($length)), 0, $length);
});

// Appel statique
$hex = Str::randomHex(16);

Utiliser le trait Macroable dans vos propres classes

Vous pouvez également intégrer Macroable à vos classes.
namespace App\Services;

use Illuminate\Support\Traits\Macroable;

class ReportBuilder
{
    use Macroable;

    protected array $sections = [];

    public function addSection(string $name, callable $content): static
    {
        $this->sections[$name] = $content;

        return $this;
    }

    public function build(): array
    {
        return array_map(fn ($fn) => $fn(), $this->sections);
    }
}
// Extension dans un service provider
ReportBuilder::macro('withSummary', function (string $title) {
    /** @var ReportBuilder $this */
    return $this->addSection('summary', fn () => [
        'title' => $title,
        'generated_at' => now()->toIso8601String(),
    ]);
});

// Exemple d'utilisation
$report = app(ReportBuilder::class)
    ->withSummary('Rapport mensuel')
    ->addSection('data', fn () => ['rows' => 42])
    ->build();

Détails de l’implémentation interne

// Implémentation de __call (appel de méthode d'instance)
public function __call($method, $parameters)
{
    if (! static::hasMacro($method)) {
        throw new BadMethodCallException(sprintf(
            'Method %s::%s does not exist.', static::class, $method
        ));
    }

    $macro = static::$macros[$method];

    if ($macro instanceof Closure) {
        // Liaison de $this à l'instance via bindTo
        $macro = $macro->bindTo($this, static::class);
    }

    return $macro(...$parameters);
}
La closure est liée à l’instance via Closure::bindTo(). Ainsi, $this pointe vers l’objet appelant. Pour d’autres types (objets invokables, par exemple), aucune liaison n’est effectuée.
Pour obtenir la prise en charge de l’IDE, vous pouvez déclarer les macros via une annotation @mixin dans un bloc doc, ou utiliser le package Laravel IdeHelper pour générer automatiquement un fichier d’aide.

Étapes suivantes

Pattern Pipeline

Découvrez comment enchaîner en série plusieurs étapes de traitement grâce au pattern pipeline.
Dernière modification le 13 juillet 2026