Route Metadata en Laravel 13: Añade Datos a tus Rutas
Route Metadata en Laravel 13: Añade Datos a tus Rutas
Laravel 13.17 introduce una característica poderosa que muchos desarrolladores han estado esperando: Route Metadata Support. Esta funcionalidad te permite asociar datos personalizados directamente a tus rutas, lo que abre un mundo de posibilidades para organizar mejor tu código, crear middlewares más inteligentes y construir sistemas de autorización más granulares.
En este artículo exploraremos cómo usar Route Metadata, casos de uso reales y patrones que mejorarán significativamente la arquitectura de tus aplicaciones Laravel.
¿Qué es Route Metadata?
Route Metadata es simplemente información adicional que asocias a una ruta sin que afecte su comportamiento directo. Piensa en ello como “etiquetas” o “propiedades” que puedes leer en middlewares, controladores o listeners para tomar decisiones dinámicas.
Antes de Laravel 13.17, si necesitabas asociar información a una ruta, debías recurrir a:
- Constantes en controladores
- Arrays de configuración separados
- Parámetros en middlewares
- Convenciones de nombres
Con Route Metadata, todo es de primera clase y centralizado.
Sintaxis Básica de Route Metadata
La sintaxis es elegante y directa. Usas el método ->metadata() en tus rutas:
// routes/web.php
Route::get('/dashboard', [DashboardController::class, 'index'])
->name('dashboard')
->metadata('module', 'analytics')
->metadata('requires-mfa', true);
Route::post('/users', [UserController::class, 'store'])
->metadata([
'module' => 'user-management',
'audit' => true,
'rate-limit' => 100,
]);
También puedes usar arrays para asignar múltiples metadatos de una vez:
Route::delete('/users/{id}', [UserController::class, 'destroy'])
->middleware('admin')
->metadata([
'module' => 'user-management',
'action' => 'delete-user',
'requires-audit' => true,
'api-documented' => true,
]);
Accediendo a Route Metadata
Una vez que hayas definido metadatos, accederás a ellos a través del objeto Route actual:
En Middlewares
Los middlewares son el lugar ideal para aprovechar Route Metadata:
// app/Http/Middleware/AuditingMiddleware.php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class AuditingMiddleware
{
public function handle(Request $request, Closure $next)
{
$route = $request->route();
if ($route && $route->metadata('requires-audit')) {
// Registrar la acción en un log de auditoría
\App\Models\AuditLog::create([
'user_id' => auth()->id(),
'action' => $route->metadata('action'),
'module' => $route->metadata('module'),
'ip_address' => $request->ip(),
'timestamp' => now(),
]);
}
return $next($request);
}
}
Luego registra este middleware en bootstrap/app.php:
->withMiddleware(function (Middleware $middleware) {
$middleware->append(AuditingMiddleware::class);
})
En Controladores
También puedes acceder a metadatos dentro de tus controladores:
// app/Http/Controllers/UserController.php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class UserController extends Controller
{
public function destroy(Request $request, $id)
{
$route = $request->route();
$module = $route->metadata('module');
$action = $route->metadata('action');
// Usar la información en la lógica de negocio
$user = User::findOrFail($id);
$user->delete();
return redirect()->route('users.index')
->with('success', "Usuario eliminado desde módulo: $module");
}
}
En Event Listeners
Puedes acceder a metadatos incluso desde listeners de eventos:
// app/Listeners/LogUserAction.php
namespace App\Listeners;
use Illuminate\Http\Request;
class LogUserAction
{
public function __construct(private Request $request) {}
public function handle($event)
{
$route = $this->request->route();
if ($route?->metadata('requires-audit')) {
Log::channel('audit')->info('User action', [
'user_id' => auth()->id(),
'action' => $route->metadata('action'),
'module' => $route->metadata('module'),
'data' => $event->data,
]);
}
}
}
Casos de Uso Prácticos
1. Sistema de Auditoría Automática
// routes/web.php
Route::group(['middleware' => 'audit'], function () {
Route::patch('/users/{id}', [UserController::class, 'update'])
->metadata([
'audit' => true,
'action' => 'update-user',
'module' => 'user-management',
]);
Route::post('/products', [ProductController::class, 'store'])
->metadata([
'audit' => true,
'action' => 'create-product',
'module' => 'inventory',
]);
});
// app/Http/Middleware/AuditingMiddleware.php
class AuditingMiddleware
{
public function handle(Request $request, Closure $next)
{
$response = $next($request);
if ($request->route()?->metadata('audit') && $response->status() === 200) {
AuditLog::create([
'user_id' => auth()->id(),
'action' => $request->route()->metadata('action'),
'module' => $request->route()->metadata('module'),
'method' => $request->method(),
'url' => $request->path(),
'status' => $response->status(),
]);
}
return $response;
}
}
2. Control de Acceso Basado en Módulos
// routes/api.php
Route::group(['prefix' => 'api', 'middleware' => 'module-access'], function () {
Route::get('/reports', [ReportController::class, 'index'])
->metadata('module', 'analytics')
->metadata('permission', 'reports.view');
Route::post('/exports', [ExportController::class, 'store'])
->metadata('module', 'analytics')
->metadata('permission', 'exports.create');
});
// app/Http/Middleware/ModuleAccessMiddleware.php
class ModuleAccessMiddleware
{
public function handle(Request $request, Closure $next)
{
$route = $request->route();
if ($module = $route?->metadata('module')) {
$permission = $route->metadata('permission');
if (!auth()->user()->hasModuleAccess($module) ||
($permission && !auth()->user()->can($permission))) {
abort(403, "No tienes acceso al módulo: $module");
}
}
return $next($request);
}
}
3. Documentación de API Automática
// routes/api.php
Route::get('/api/products', [ProductController::class, 'index'])
->metadata([
'doc' => true,
'description' => 'Obtiene el listado de productos',
'response' => 'ProductCollection',
'rate-limit' => 60,
]);
Route::post('/api/products', [ProductController::class, 'store'])
->metadata([
'doc' => true,
'description' => 'Crea un nuevo producto',
'body' => 'ProductStoreRequest',
'response' => 'ProductResource',
'rate-limit' => 30,
]);
// app/Console/Commands/GenerateApiDocs.php
class GenerateApiDocs extends Command
{
public function handle()
{
$documentation = [];
foreach (Route::getRoutes() as $route) {
if ($route->metadata('doc')) {
$documentation[] = [
'method' => implode('|', $route->methods()),
'path' => $route->uri,
'description' => $route->metadata('description'),
'request' => $route->metadata('body'),
'response' => $route->metadata('response'),
'rate_limit' => $route->metadata('rate-limit'),
];
}
}
File::put(
base_path('docs/api.json'),
json_encode($documentation, JSON_PRETTY_PRINT)
);
$this->info('Documentación de API generada exitosamente');
}
}
4. Rate Limiting Inteligente
// routes/api.php
Route::get('/api/public-data', [DataController::class, 'public'])
->metadata('rate-limit', 1000) // 1000 requests por hora
->withoutMiddleware('throttle');
Route::get('/api/premium-data', [DataController::class, 'premium'])
->middleware('auth:sanctum')
->metadata('rate-limit', 10000);
// app/Http/Middleware/SmartRateLimitMiddleware.php
class SmartRateLimitMiddleware
{
public function handle(Request $request, Closure $next)
{
$route = $request->route();
if ($limit = $route?->metadata('rate-limit')) {
$user = auth()->user();
$key = $user ? "user:{$user->id}" : "ip:{$request->ip()}";
$current = Cache::get($key, 0);
if ($current >= $limit) {
return response()->json([
'error' => 'Rate limit exceeded',
], 429);
}
Cache::put($key, $current + 1, now()->addHour());
}
return $next($request);
}
}
5. Autenticación Multifactor Condicional
// routes/web.php
Route::group(['middleware' => 'mfa-check'], function () {
Route::get('/settings', [SettingsController::class, 'index'])
->metadata('requires-mfa', true)
->metadata('risk-level', 'medium');
Route::delete('/account', [AccountController::class, 'destroy'])
->metadata('requires-mfa', true)
->metadata('risk-level', 'high');
Route::get('/analytics/sensitive', [AnalyticsController::class, 'sensitive'])
->metadata('requires-mfa', false)
->metadata('risk-level', 'low');
});
// app/Http/Middleware/MfaCheckMiddleware.php
class MfaCheckMiddleware
{
public function handle(Request $request, Closure $next)
{
if ($request->route()?->metadata('requires-mfa')) {
if (!session()->has('mfa-verified-at')) {
return redirect()->route('mfa.verify')
->with('intended', url()->current());
}
// Verificar que la verificación siga siendo válida (últimas 2 horas)
if (now()->diffInHours(session('mfa-verified-at')) > 2) {
session()->forget('mfa-verified-at');
return redirect()->route('mfa.verify');
}
}
return $next($request);
}
}
Validación de Metadatos
Es una buena práctica validar que los metadatos tengan los valores esperados:
// app/Http/Middleware/ValidateMetadata.php
class ValidateMetadata
{
protected array $validModules = ['user-management', 'analytics', 'inventory'];
protected array $validActions = ['create', 'read', 'update', 'delete'];
public function handle(Request $request, Closure $next)
{
$route = $request->route();
if ($module = $route?->metadata('module')) {
if (!in_array($module, $this->validModules)) {
throw new InvalidArgumentException(
"Módulo inválido en ruta: {$route->uri}"
);
}
}
return $next($request);
}
}
Mejores Prácticas
1. Mantén Metadatos Simples
Evita metadatos demasiado complejos. Usa arrays anidados con moderación:
// ✅ Bien
->metadata('module', 'analytics')
->metadata('permission', 'reports.view')
// ❌ Evita
->metadata('complex-data', [
'nested' => [
'deeply' => [
'structures' => 'are-hard-to-maintain'
]
]
])
2. Define Constantes para Metadatos Recurrentes
// app/Support/RouteMetadata.php
class RouteMetadata
{
const MODULES = [
'ANALYTICS' => 'analytics',
'USER_MANAGEMENT' => 'user-management',
'INVENTORY' => 'inventory',
];
const ACTIONS = [
'CREATE' => 'create',
'READ' => 'read',
'UPDATE' => 'update',
'DELETE' => 'delete',
];
const RISK_LEVELS = [
'LOW' => 'low',
'MEDIUM' => 'medium',
'HIGH' => 'high',
];
}
// En tus rutas
Route::post('/users', [UserController::class, 'store'])
->metadata('module', RouteMetadata::MODULES['USER_MANAGEMENT'])
->metadata('action', RouteMetadata::ACTIONS['CREATE']);
3. Documenta tus Metadatos
/**
* POST /api/users
*
* Metadata:
* - module: Identificador del módulo funcional
* - action: Tipo de acción (create, update, delete)
* - audit: Si se debe registrar en auditoría
* - requires-mfa: Si requiere autenticación multifactor
*/
Route::post('/users', [UserController::class, 'store'])
->metadata('module', 'user-management')
->metadata('action', 'create')
->metadata('audit', true)
->metadata('requires-mfa', true);
Depuración de Route Metadata
Usa el comando route:list de Artisan para visualizar tus rutas y sus metadatos:
php artisan route:list
Para una inspección más profunda, crea un comando personalizado:
// app/Console/Commands/DebugRouteMetadata.php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Route;
class DebugRouteMetadata extends Command
{
protected $signature = 'route:metadata {--module= : Filtrar por módulo}';
public function handle()