Macros en Laravel
Los macros en Laravel son una característica que permite extender la funcionalidad del core del framework sin modificarlo directamente. En ese sentido, comparten una idea similar a los traits: reutilizar y extender comportamiento.
Conocí el concepto de macros mientras trabajaba en una aplicación con Laravel y Livewire, donde necesitaba agregar cierta funcionalidad a un componente. Al investigar soluciones, me encontré con este mecanismo que Laravel utiliza internamente y que, curiosamente, no siempre recibe la atención que merece.
En este artículo explico qué son los macros en Laravel, cómo funcionan y en qué casos resultan especialmente útiles.
¿Qué son los Macros en Laravel?
Un macro es una manera de agregar métodos personalizados a las clases “core” de Laravel al momento de ejecución (runtime). Esto significa que puedes extender la funcionalidad de las clases de Str, Collection, Request o cualquier otra que necesites modificar que pertenezca al core Laravel.
// Ejemplo para obtener las iniciales de un nombreStr::macro('initials', function ($name) { return collect(explode(' ', $name)) ->map(fn($word) => strtoupper(substr($word, 0, 1))) ->implode('');});// Ahora puedes usar ese método donde quieras en tu aplicación$initials = Str::initials('John Doe'); // regresa 'JD'Cómo crear un macro
Sintaxis básica
Para crear un macro necesitar registrarlo en el método boot(), ya que este método se ejecuta una vez que todos los service providers han sido registrados, por ejemplo View, Blade, Route etc. y ahí es el momento perfecto para agregar tu código personalizado.
<?php
namespace App\Providers;
use Illuminate\Support\Str;use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider{ public function boot() { Str::macro('initials', function ($name) { return collect(explode(' ', $name)) ->map(fn($word) => strtoupper(substr($word, 0, 1))) ->implode(''); }); }}Macros en la clase Collections
Las colecciones son ideales para generar macros personalizados:
// Agrupar por dominioCollection::macro('groupByDomain', function () { return $this->groupBy(function ($item) { return parse_url($item['url'], PHP_URL_HOST); });});
// Seleccionar opciones de la colecciónCollection::macro('toSelectOptions', function ($valueField, $labelField) { return $this->mapWithKeys(function ($item) use ($valueField, $labelField) { return [$item[$valueField] => $item[$labelField]]; })->toArray();});// Uso$urls = collect([ ['url' => 'https://example.com/page1', 'title' => 'Page 1'], ['url' => 'https://example.com/page2', 'title' => 'Page 2'], ['url' => 'https://another.com/page1', 'title' => 'Another Page'],]);
$grouped = $urls->groupByDomain();
/* Resultado esperado:[ 'example.com' => [ ['url' => 'https://example.com/page1', 'title' => 'Page 1'], ['url' => 'https://example.com/page2', 'title' => 'Page 2'], ], 'another.com' => [ ['url' => 'https://another.com/page1', 'title' => 'Another Page'], ],]*/
$options = $urls->toSelectOptions('url', 'title');/* Resultado esperado:[ 'https://example.com/page1' => 'Page 1', 'https://example.com/page2' => 'Page 2', 'https://another.com/page1' => 'Another Page',]*/Macros en la clase Request
Agregar funciones útiles a la clase Request con métodos de validación y extracción de datos.
- Validar si la petición es Ajax
// Valida si la petición es AjaxRequest::macro('isAjax', function () { return $this->headers->get('X-Requested-With') === 'XMLHttpRequest';});
// ⚠️ Nota: este macro es solo ilustrativo, ya que Laravel incluye `$request->ajax()` y `$request->expectsJson()`// Usoif ($request->isAjax()) { return response()->json(['message' => 'Ajax request detected']);}- Obtener información en específica desde un json
// Obtener la api key de la petición desde el header, si no se encuentra recupera el valor del body "api_key"Request::macro('getApiKey', function () { return $this->header('X-API-Key') ?? $this->query('api_key');});// Uso$apiKey = $request->getApiKey();- Validar IPs
// Valida si la petición (request) esta dentro del listado de IPs permitidasRequest::macro('validateIp', function ($allowedIps) { return in_array($this->ip(), $allowedIps);});// Usoif (!$request->validateIp(['127.0.0.1', '192.168.1.1'])) { abort(403);}Uso de macros en Livewire
Como mencionaba al inicio, conocí el concepto de Macros en Laravel ya que en una aplicación de Laravel con Livewire estaba implementando un componente para arrojar notificaciones en la aplicación y encontré este artículo de Fly.io donde se mencionaba el concepto de macros, lo que me llevó a investigarlos con más detalle. En si la implementación del macro es la siguiente:
// Agregar método notify a la clase Componente de LivewireComponent::macro('notify', function ($message) { // $this hace referencia a la clase del componente Liveware y no a la clase AppServiceProvider // Esto permite crear APIs internas reutilizables entre componentes $this->dispatch('notify', $message);});namespace App\Http\Livewire;
use Livewire\Component;
class ClickyButton extends Component{ public function tellme() { $messages = [ "Some", "Random", "Messages" ];
// Uso del macro $this->notify($messages[array_rand($messages)]); }}Clases Macroables
No todas las clases en Laravel pueden ser extendidas por medio de macros. Para poder extenderla la clase debe usar el trait Illuminate\Support\Traits\Macroable
Las clases macroables más comunes son:
StrCollectionRequestResponseRouterValidatorCacheFile
Para el caso de Livewire, podemos ver en la clase Component que está usando la clase Macroable en este archivo, por eso es que anteriormente pudimos agregar un macro a la clase Component de Livewire.
Macros Condicionales
Es posible registrar macros de forma condicional, por ejemplo, si estamos ejecutando la aplicación en un ambiente local se registrará el macro debug:
public function boot(){ if (app()->environment('local')) { Str::macro('debug', function ($string) { return "DEBUG: {$string}"; }); }}Recomendaciones prácticas
1. Tipa los parámetros y valor de retorno
Como si de una función se tratase, los macros aceptan parámetros y la posibilidad de tiparlos, así como al valor de retorno.
Laravel no impone tipado en los parámetros, pero PHP moderno lo permite y es recomendable usarlo.
Collection::macro('paginate', function (int $perPage = 15, ?int $page = null): LengthAwarePaginator { $page = $page ?: request()->get('page', 1); $total = $this->count();
return new LengthAwarePaginator( $this->forPage($page, $perPage)->values(), $total, $perPage, $page, ['path' => request()->url()] );});2. Organiza tus Macros
Crea un service provider dedicado por cada tipo distinto de macro:
class StringMacrosServiceProvider extends ServiceProvider{ public function boot() { Str::macro('toSlug', function ($string) { return Str::slug($string); });
Str::macro('shorten', function ($string, $length = 50) { return strlen($string) > $length ? substr($string, 0, $length) . '...' : $string; }); }}3. Documenta tus Macros
No escatimes en documentar tus macros, si trabajas con más personas te lo agradecerán tanto ellos como tu yo del futuro.
/** * Convert a string to URL-friendly slug format * * @param string $string * @return string */Str::macro('toSlug', function ($string) { return Str::slug($string);});4. Realiza testing en tus Macros
Realiza testing a tus macros, por más sencillo que parezca es una buena práctica.
class StringMacrosTest extends TestCase{ public function test_to_slug_macro() { $this->assertEquals('hello-world', Str::toSlug('Hello World')); $this->assertEquals('laravel-is-awesome', Str::toSlug('Laravel Is Awesome!')); }}5. Evitar conflictos
Puedes verificar si un macro ya ha sido registrado antes de registrarlo:
if (!Collection::hasMacro('someMacro')) { Collection::macro('someMacro', function () { // Lógica de tu macro... });}Usos prácticos
1. Macros en la clase Response
Utilidad para responder con un json de manera más sencilla.
Response::macro('api', function ($data = null, $message = 'Success', $status = 200) { return response()->json([ 'success' => $status < 400, 'message' => $message, 'data' => $data, ], $status);});// Usoreturn Response::api($user, 'User created successfully', 201);2. Macro para el sistema de archivos
Utilidad para asegurar que un directorio existe.
File::macro('ensureDirectoryExists', function ($path) { if (!File::exists($path)) { File::makeDirectory($path, 0755, true); } return $path;});Conclusion
Los macros de Laravel son una excelente forma de extender el core del framework a tu necesidad, permitiéndote:
- Agregar funcionalidad personalizada a las clases core de Laravel
- Crear métodos específicos que se sientan más natural al usarse
- Reducir la duplicidad de código en tu aplicación
- Mejorar la redacción de tu lógica de negocio en tu aplicación
Como con cualquier mecanismo de extensión, la clave está en no abusar de ellos y documentarlos correctamente para que su uso sea claro tanto para tu equipo como para tu yo del futuro.