Vai al contenuto principale

Cos’è il trait Macroable

Il trait Macroable permette di aggiungere dinamicamente metodi a una classe, senza modificarla. Molte delle classi core di Laravel usano questo trait, quindi puoi estenderne le funzionalità senza toccare il codice del core. Il trait vero e proprio si trova in Illuminate\Support\Traits\Macroable. Internamente memorizza le macro registrate nella proprietà statica $macros e le richiama tramite le magic method __call / __callStatic.

Classi che usano Macroable

In Laravel ci sono molte classi che supportano Macroable.
ClasseUso
Illuminate\Support\CollectionManipolazione delle collection
Illuminate\Support\StrManipolazione delle stringhe
Illuminate\Support\ArrManipolazione degli array
Illuminate\Http\RequestRichiesta HTTP
Illuminate\Http\ResponseRisposta HTTP
Illuminate\Routing\RouterRouter
Illuminate\Routing\ResponseFactoryResponse factory
Illuminate\Database\Schema\BlueprintSchema builder
Illuminate\Pipeline\PipelinePipeline
Illuminate\Testing\TestResponseTest response

macro() — aggiungere un metodo

A macro() passi come primo argomento il nome del metodo e come secondo argomento una closure.
use Illuminate\Support\Collection;

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

$result = collect(['mela', 'arancia', 'uva'])->toSentence();
// 'mela, arancia, uva'
Il $this all’interno della closure viene collegato all’istanza che ha invocato la macro. In questo modo puoi accedere direttamente a proprietà e metodi della classe.

mixin() — aggiungere più metodi in blocco

Per registrare tante macro insieme usa mixin(). Tutti i metodi public / protected della classe mixin vengono registrati come macro.
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] ?? '');
        };
    }
}
I metodi del mixin() devono restituire la closure da registrare come macro. Il valore restituito dal metodo diventa l’implementazione della macro.

Registrazione nel service provider

Le macro devono essere registrate all’avvio dell’applicazione. Il metodo boot() di AppServiceProvider è il posto giusto.
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
    {
        // registra una singola macro
        Collection::macro('toSentence', function (string $separator = ', ') {
            return $this->implode($separator);
        });

        // registra un mixin in blocco
        Collection::mixin(new CollectionMixin);

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

Casi d’uso pratici

Estendere Collection

L’aggiunta di metodi personalizzati alle collection è l’uso più comune.
// metodi statistici per collection numeriche
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 che restituisce una collection con informazioni di paginazione
Collection::macro('paginateArray', function (int $perPage = 15, int $page = 1) {
    return $this->slice(($page - 1) * $perPage, $perPage)->values();
});

Estendere la classe Str

// conteggio caratteri (compatibile multi-byte)
Str::macro('mbLength', function (string $value) {
    return mb_strlen($value, 'UTF-8');
});

// converte snake_case in notazione a punti
Str::macro('toDotNotation', function (string $value) {
    return str_replace('_', '.', $value);
});

$length = Str::mbLength('ciao'); // 4
$dot = Str::toDotNotation('user_profile_name'); // 'user.profile.name'

Estendere 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 = ['it', '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';
});
// utilizzo nel controller
public function index(Request $request)
{
    if ($request->isFromMobile()) {
        return response()->json($this->getMobileData());
    }

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

Estendere Blueprint (migrazioni)

Se raggruppi in macro le definizioni delle colonne, mantieni un design del DB uniforme.
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();
});
// utilizzo in una migrazione
Schema::create('posts', function (Blueprint $table) {
    $table->id();
    $table->string('title');
    $table->text('body');
    $table->addTimestampsWithTimezone();
    $table->addUserTracking();
});

Estendere le test response

Puoi aggiungere metodi di assertion specifici per i test.
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);
});
// utilizzo nei test
$this->getJson('/api/posts')->assertPaginated();
$this->postJson('/api/orders', $data)->assertApiSuccess();

hasMacro() — verificare l’esistenza di una macro

use Illuminate\Support\Collection;

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

flushMacros() — resettare le macro

Utile quando vuoi resettare le macro nei test.
use Illuminate\Support\Collection;

// reset dentro un test
Collection::flushMacros();
flushMacros() rimuove tutte le macro della classe. Talvolta viene chiamato in tearDown() per garantire l’indipendenza tra i test, ma attenzione: cancella anche macro registrate da altri test.

Macro statiche

Le macro funzionano non solo come metodi d’istanza ma anche come metodi statici. Sono gestite da __callStatic.
Str::macro('randomHex', function (int $length = 8) {
    return substr(bin2hex(random_bytes($length)), 0, $length);
});

// chiamata statica
$hex = Str::randomHex(16);

Usare Macroable nelle tue classi

Puoi integrare Macroable anche in classi che scrivi tu.
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);
    }
}
// estensione nel service provider
ReportBuilder::macro('withSummary', function (string $title) {
    /** @var ReportBuilder $this */
    return $this->addSection('summary', fn () => [
        'title' => $title,
        'generated_at' => now()->toIso8601String(),
    ]);
});

// esempio d'uso
$report = app(ReportBuilder::class)
    ->withSummary('Report mensile')
    ->addSection('data', fn () => ['rows' => 42])
    ->build();

Dettagli dell’implementazione interna

// implementazione di __call (chiamata a metodo d'istanza)
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) {
        // con bindTo collega $this all'istanza
        $macro = $macro->bindTo($this, static::class);
    }

    return $macro(...$parameters);
}
La closure viene collegata all’istanza con Closure::bindTo(). In questo modo $this fa riferimento all’oggetto che ha chiamato la macro. Per elementi non closure (ad esempio oggetti invokable) il bind non viene eseguito.
Per ottenere supporto dall’IDE puoi definire le annotazioni delle macro con un Docblock che usa @mixin, oppure generare automaticamente un file helper con il pacchetto Laravel IdeHelper.

Prossimi passi

Pattern Pipeline

Impara a comporre in serie più step di elaborazione con il pattern pipeline.
Ultima modifica il 13 luglio 2026