Zum Hauptinhalt springen

Was ist der Macroable-Trait?

Der Trait Macroable erlaubt es, einer Klasse nachträglich dynamisch Methoden hinzuzufügen, ohne sie zu verändern. Viele Kernklassen von Laravel setzen diesen Trait ein — Sie können also Funktionalität erweitern, ohne den Framework-Code selbst anzufassen. Die eigentliche Implementierung liegt in Illuminate\Support\Traits\Macroable. Intern werden registrierte Macros in der statischen Property $macros gespeichert und über die Magic Methods __call und __callStatic aufgerufen.

Klassen, die Macroable nutzen

Laravel enthält zahlreiche Klassen mit Macroable-Unterstützung.
KlasseVerwendungszweck
Illuminate\Support\CollectionOperationen auf Collections
Illuminate\Support\StrString-Operationen
Illuminate\Support\ArrArray-Operationen
Illuminate\Http\RequestHTTP-Requests
Illuminate\Http\ResponseHTTP-Responses
Illuminate\Routing\RouterRouter
Illuminate\Routing\ResponseFactoryResponse-Factory
Illuminate\Database\Schema\BlueprintSchema Builder
Illuminate\Pipeline\PipelinePipeline
Illuminate\Testing\TestResponseTest-Response

macro() — Methode hinzufügen

Als erstes Argument von macro() übergeben Sie den Methodennamen, als zweites eine Closure.
use Illuminate\Support\Collection;

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

$result = collect(['Apfel', 'Orange', 'Traube'])->toSentence();
// 'Apfel, Orange, Traube'
Innerhalb der Closure ist $this an die Instanz gebunden, auf der das Macro aufgerufen wurde. Damit können Sie direkt auf Klassen-Properties und -Methoden zugreifen.

mixin() — mehrere Methoden gleichzeitig hinzufügen

Wenn Sie viele Macros gemeinsam registrieren möchten, nutzen Sie mixin(). Alle public- und protected-Methoden der Mixin-Klasse werden als Macros registriert.
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] ?? '');
        };
    }
}
Die Methoden für mixin() müssen eine Closure zurückgeben, die als Macro registriert wird. Der Rückgabewert der Methode selbst ist die eigentliche Macro-Implementierung.

Registrierung im Service Provider

Macros müssen beim Anwendungsstart registriert werden. Ein geeigneter Ort ist die Methode boot() des AppServiceProvider.
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
    {
        // Einzelnes Macro registrieren
        Collection::macro('toSentence', function (string $separator = ', ') {
            return $this->implode($separator);
        });

        // Mixin gesammelt registrieren
        Collection::mixin(new CollectionMixin);

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

Praktische Anwendungsfälle

Erweiterung von Collections

Eigene Methoden auf Collections zu ergänzen ist der häufigste Anwendungsfall.
// Statistikmethode für numerische Collections
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, das eine paginierte Collection zurückgibt
Collection::macro('paginateArray', function (int $perPage = 15, int $page = 1) {
    return $this->slice(($page - 1) * $perPage, $perPage)->values();
});

Erweiterung der Str-Klasse

// Mehrbyte-fähiges Zählen japanischer Zeichen
Str::macro('mbLength', function (string $value) {
    return mb_strlen($value, 'UTF-8');
});

// Snake Case in Dot-Notation umwandeln
Str::macro('toDotNotation', function (string $value) {
    return str_replace('_', '.', $value);
});

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

Erweiterung der Request-Klasse

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 = ['ja', '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';
});
// Einsatz im Controller
public function index(Request $request)
{
    if ($request->isFromMobile()) {
        return response()->json($this->getMobileData());
    }

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

Erweiterung von Blueprint (Migrationen)

Wenn Sie Spaltendefinitionen als Macro bündeln, bleiben Ihre Datenbankdesigns konsistent.
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();
});
// Einsatz in der Migration
Schema::create('posts', function (Blueprint $table) {
    $table->id();
    $table->string('title');
    $table->text('body');
    $table->addTimestampsWithTimezone();
    $table->addUserTracking();
});

Erweiterung der Test-Response

Sie können Test-spezifische Assertion-Methoden ergänzen.
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);
});
// Verwendung in Tests
$this->getJson('/api/posts')->assertPaginated();
$this->postJson('/api/orders', $data)->assertApiSuccess();

hasMacro() — Existenz eines Macros prüfen

use Illuminate\Support\Collection;

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

flushMacros() — Macros zurücksetzen

Nützlich, wenn Sie Macros in einem Test zurücksetzen möchten.
use Illuminate\Support\Collection;

// Reset innerhalb eines Tests
Collection::flushMacros();
flushMacros() löscht sämtliche Macros dieser Klasse. Ein Aufruf in tearDown() isoliert Tests voneinander, entfernt aber auch Macros, die andere Tests registriert haben — Vorsicht.

Statische Macros

Macros funktionieren nicht nur als Instanzmethoden, sondern auch als statische Methoden. Verantwortlich ist __callStatic.
Str::macro('randomHex', function (int $length = 8) {
    return substr(bin2hex(random_bytes($length)), 0, $length);
});

// Statischer Aufruf
$hex = Str::randomHex(16);

Macroable in eigenen Klassen nutzen

Sie können Macroable auch in Ihren eigenen Klassen einsetzen.
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);
    }
}
// Erweiterung im Service Provider
ReportBuilder::macro('withSummary', function (string $title) {
    /** @var ReportBuilder $this */
    return $this->addSection('summary', fn () => [
        'title' => $title,
        'generated_at' => now()->toIso8601String(),
    ]);
});

// Anwendungsbeispiel
$report = app(ReportBuilder::class)
    ->withSummary('Monatsbericht')
    ->addSection('data', fn () => ['rows' => 42])
    ->build();

Details der Implementierung

// Implementierung von __call (Instanzmethodenaufruf)
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) {
        // $this über bindTo an die Instanz binden
        $macro = $macro->bindTo($this, static::class);
    }

    return $macro(...$parameters);
}
Die Closure wird mittels Closure::bindTo() an die Instanz gebunden. Dadurch zeigt $this auf das Objekt, auf dem das Macro aufgerufen wurde. Andere Werte (etwa invokable Objekte) werden nicht gebunden.
Damit die IDE-Unterstützung funktioniert, definieren Sie Macro-Annotationen entweder per Docblock mit @mixin oder generieren mit dem Paket Laravel IdeHelper automatisch entsprechende Helper-Dateien.

Nächste Schritte

Pipeline-Muster

Lernen Sie, wie Sie mit dem Pipeline-Muster mehrere Verarbeitungsschritte seriell verketten.
Zuletzt geändert am 13. Juli 2026