Saltar al contenido principal

Qué es el trait Macroable

El trait Macroable es un mecanismo que permite añadir métodos dinámicamente a una clase después de haberla escrito, sin modificarla. Muchas clases del core de Laravel lo utilizan, así que puedes extender sus funcionalidades sin tocar el código del framework. La implementación real del trait está en Illuminate\Support\Traits\Macroable. Internamente guarda las macros registradas en una propiedad estática $macros y las invoca a través de los métodos mágicos __call / __callStatic.

Clases que usan Macroable

Laravel tiene muchas clases compatibles con Macroable.
ClaseUso
Illuminate\Support\CollectionOperaciones sobre colecciones.
Illuminate\Support\StrOperaciones sobre cadenas.
Illuminate\Support\ArrOperaciones sobre arrays.
Illuminate\Http\RequestPeticiones HTTP.
Illuminate\Http\ResponseRespuestas HTTP.
Illuminate\Routing\RouterRouter.
Illuminate\Routing\ResponseFactoryFactory de respuestas.
Illuminate\Database\Schema\BlueprintConstructor de esquema.
Illuminate\Pipeline\PipelinePipeline.
Illuminate\Testing\TestResponseRespuestas en tests.

macro() — añadir un método

Al macro() le pasas el nombre del método como primer argumento y un closure como segundo.
use Illuminate\Support\Collection;

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

$result = collect(['manzana', 'naranja', 'uva'])->toSentence();
// 'manzana, naranja, uva'
El $this dentro del closure queda vinculado a la instancia que invocó la macro. Así puedes acceder directamente a las propiedades y métodos de la clase.

mixin() — añadir varios métodos a la vez

Para registrar muchas macros en bloque, usa mixin(). Todos los métodos public / protected de la clase mixin se registran como 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] ?? '');
        };
    }
}
Los métodos del mixin() deben devolver el closure que se registrará como macro. El valor devuelto por el método es la implementación real de la macro.

Registro en un service provider

Las macros deben registrarse durante el arranque de la aplicación. El sitio adecuado es el método boot() de 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
    {
        // Registro de una macro individual
        Collection::macro('toSentence', function (string $separator = ', ') {
            return $this->implode($separator);
        });

        // Registro de un mixin completo
        Collection::mixin(new CollectionMixin);

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

Casos de uso prácticos

Extensión de Collection

Añadir métodos personalizados a las colecciones es el caso más habitual.
// Método estadístico para colecciones numéricas
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 que devuelve la colección paginada
Collection::macro('paginateArray', function (int $perPage = 15, int $page = 1) {
    return $this->slice(($page - 1) * $perPage, $perPage)->values();
});

Extensión de la clase Str

// Contar caracteres multibyte
Str::macro('mbLength', function (string $value) {
    return mb_strlen($value, 'UTF-8');
});

// Convertir snake_case a notación con puntos
Str::macro('toDotNotation', function (string $value) {
    return str_replace('_', '.', $value);
});

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

Extensión de la clase 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 = ['es', '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';
});
// Uso en un controlador
public function index(Request $request)
{
    if ($request->isFromMobile()) {
        return response()->json($this->getMobileData());
    }

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

Extensión de Blueprint (migraciones)

Al convertir en macros las definiciones de columnas del esquema, mantienes un diseño de BD consistente.
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();
});
// Uso en una migración
Schema::create('posts', function (Blueprint $table) {
    $table->id();
    $table->string('title');
    $table->text('body');
    $table->addTimestampsWithTimezone();
    $table->addUserTracking();
});

Extensión de TestResponse

Puedes añadir métodos de assertion específicos para 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);
});
// Uso en un test
$this->getJson('/api/posts')->assertPaginated();
$this->postJson('/api/orders', $data)->assertApiSuccess();

hasMacro() — comprobar si existe una macro

use Illuminate\Support\Collection;

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

flushMacros() — resetear las macros

Se usa cuando quieres resetear las macros en un test.
use Illuminate\Support\Collection;

// Reset dentro del test
Collection::flushMacros();
flushMacros() elimina todas las macros de esa clase. A veces se llama en tearDown() para mantener aisladas las pruebas, pero también borrará las macros registradas por otros tests, así que hay que tener cuidado.

Macros estáticas

Las macros funcionan también como métodos estáticos, no solo como métodos de instancia. Las gestiona __callStatic.
Str::macro('randomHex', function (int $length = 8) {
    return substr(bin2hex(random_bytes($length)), 0, $length);
});

// Llamada estática
$hex = Str::randomHex(16);

Usar Macroable en tus propias clases

Puedes incorporar Macroable también a tus propias clases.
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);
    }
}
// Extensión desde el service provider
ReportBuilder::macro('withSummary', function (string $title) {
    /** @var ReportBuilder $this */
    return $this->addSection('summary', fn () => [
        'title' => $title,
        'generated_at' => now()->toIso8601String(),
    ]);
});

// Ejemplo de uso
$report = app(ReportBuilder::class)
    ->withSummary('Informe mensual')
    ->addSection('data', fn () => ['rows' => 42])
    ->build();

Detalles de la implementación interna

// Implementación de __call (llamadas a métodos de instancia)
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) {
        // bindTo vincula $this a la instancia
        $macro = $macro->bindTo($this, static::class);
    }

    return $macro(...$parameters);
}
El closure se vincula a la instancia con Closure::bindTo(). Así, dentro de la macro, $this apunta al objeto que la invocó. Si el macro no es un closure (por ejemplo, un objeto invokable), no se realiza el bind.
Para obtener soporte del IDE, define anotaciones de las macros con @mixin en el docblock o utiliza el paquete Laravel IdeHelper para generar los archivos helper automáticamente.

Próximos pasos

Patrón Pipeline

Aprende cómo componer varios pasos de procesamiento en serie usando el patrón Pipeline.
Última modificación el 13 de julio de 2026