laravelroutingeloquentatributos-php

RouteKey en Laravel 13.21: Binding Elegante sin Configuración

Introducción

Laravel 13.21 introduce el atributo #[RouteKey], una característica que revoluciona cómo definimos el route model binding en nuestras aplicaciones. Durante años, hemos configurado explícitamente qué columna usar para resolver modelos en nuestras rutas. Ahora, con este nuevo atributo, podemos delegar esa responsabilidad directamente al modelo Eloquent, logrando código más limpio, tipado y mantenible.

Si trabajas con rutas que incluyen modelos como parámetros (ej: /posts/{post}), este artículo te mostrará cómo usar #[RouteKey] para evitar configuraciones redundantes y hacer tus rutas más expresivas.

¿Qué es el Route Model Binding?

Antes de sumergirnos en #[RouteKey], recordemos qué es el route model binding. Es un mecanismo que transforma un parámetro de ruta en una instancia de modelo Eloquent automáticamente.

Ejemplo tradicional (antes de Laravel 13.21):

// routes/web.php
Route::get('/posts/{post}', [PostController::class, 'show']);

// Esto automáticamente resuelve:
// POST /posts/123 → busca Post::find(123)

El binding por defecto usa la clave primaria, pero a menudo queremos usar slugs u otros identificadores:

// routes/web.php
Route::get('/posts/{post:slug}', [PostController::class, 'show']);

// Ahora busca por slug: Post::where('slug', $value)->first()

El problema: esta configuración vive en la ruta, no en el modelo. Si cambias el identificador o tienes múltiples rutas, repites la lógica.

El Atributo #[RouteKey] de Laravel 13.21

El atributo #[RouteKey] centraliza esta configuración en el modelo. Define qué columna debe usar Laravel para resolver el modelo en cualquier ruta, sin necesidad de especificarlo cada vez.

Sintaxis básica

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Attributes\RouteKey;

#[RouteKey('slug')]
class Post extends Model
{
    protected $fillable = ['title', 'slug', 'content'];
}

Ahora, en tus rutas:

// routes/web.php
Route::get('/posts/{post}', [PostController::class, 'show']);
// Laravel automáticamente busca por slug, no por ID

No necesitas escribir {post:slug}. El binding se resuelve automáticamente.

Ejemplos prácticos

Ejemplo 1: Blog con slugs

Imagina un blog donde los posts se acceden por slug (ej: /posts/como-aprender-laravel).

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Attributes\RouteKey;

#[RouteKey('slug')]
class Post extends Model
{
    protected $fillable = ['title', 'slug', 'content'];
    
    protected function casts(): array
    {
        return [
            'published_at' => 'datetime',
        ];
    }
    
    // Generar slug automáticamente
    protected static function booted(): void
    {
        static::creating(function ($post) {
            if (!$post->slug) {
                $post->slug = \Str::slug($post->title);
            }
        });
    }
}

Tu controlador es directo:

<?php

namespace App\Http\Controllers;

use App\Models\Post;

class PostController extends Controller
{
    public function show(Post $post)
    {
        // $post ya está resuelto por slug, gracias a #[RouteKey('slug')]
        return view('posts.show', ['post' => $post]);
    }
}

Rutas simples:

// routes/web.php
Route::get('/posts/{post}', [PostController::class, 'show'])->name('posts.show');
Route::post('/posts/{post}/comments', [CommentController::class, 'store'])->name('comments.store');

Sin #[RouteKey], necesitarías escribir {post:slug} en cada ruta.

Ejemplo 2: API REST con UUIDs

Muchas APIs modernas usan UUIDs en lugar de IDs secuenciales.

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Attributes\RouteKey;
use Illuminate\Database\Eloquent\Concerns\HasUuids;

#[RouteKey('uuid')]
class Order extends Model
{
    use HasUuids;
    
    protected $fillable = ['user_id', 'total', 'status'];
}

Ahora tus rutas API usan UUIDs automáticamente:

// routes/api.php
Route::apiResource('orders', OrderController::class);

// GET /api/orders/550e8400-e29b-41d4-a716-446655440000
// Laravel resuelve por UUID sin configuración extra

Ejemplo 3: Parámetros personalizados

¿Qué si quieres un identificador personalizado, como un código de pedido?

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Attributes\RouteKey;

#[RouteKey('order_code')]
class Invoice extends Model
{
    protected $fillable = ['order_code', 'amount', 'issued_at'];
}

Ruta simplificada:

Route::get('/invoices/{invoice}', [InvoiceController::class, 'show']);
// POST /invoices/INV-2024-00123 resuelve por order_code

Casos de uso avanzados

Múltiples modelos con RouteKey

Cuando tienes rutas anidadas con varios modelos:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Attributes\RouteKey;

#[RouteKey('slug')]
class Category extends Model
{
    protected $fillable = ['name', 'slug'];
}

#[RouteKey('slug')]
class Product extends Model
{
    protected $fillable = ['category_id', 'name', 'slug'];
    
    public function category()
    {
        return $this->belongsTo(Category::class);
    }
}

Rutas anidadas elegantes:

Route::get('/categories/{category}/products/{product}', 
    [ProductController::class, 'show']
);

// GET /categories/electronics/products/iphone-15
// Ambos se resuelven por slug automáticamente

Controlador:

public function show(Category $category, Product $product)
{
    // Ambos modelos ya están resueltos
    // No necesitas validar que product pertenece a category
    return view('products.show', [
        'category' => $category,
        'product' => $product,
    ]);
}

Combinar RouteKey con scoping

Aunque #[RouteKey] define el identificador, aún puedes usar scoped binding para mayor seguridad:

Route::get('/categories/{category}/products/{product}', function (Category $category, Product $product) {
    // Aunque ambos usen slug, Laravel resuelve product dentro de category
})
->scopeBindings(); // Valida relación implícita

Ventajas sobre la configuración tradicional

AspectoSin #[RouteKey]Con #[RouteKey]
Dónde se defineEn cada ruta ({post:slug})En el modelo
RepeticiónSí, en múltiples rutasNo, centralizado
Cambios futurosActualizar todas las rutasSolo el modelo
LegibilidadRutas más largasRutas concisas
TipadoImplícitoExplícito con atributo

Compatibilidad y consideraciones

Versión requerida: Laravel 13.21 o superior

Notas importantes:

  • El atributo es opcional. Puedes seguir usando {post:slug} en rutas si prefieres.
  • Si defines #[RouteKey] pero especificas otra columna en la ruta ({post:id}), la ruta tiene prioridad.
  • El atributo no afecta métodos como Model::find() o Model::findOrFail().

Debugging y validación

Si el binding no funciona como esperas, verifica:

  1. La columna existe y tiene datos:
// En Tinker
php artisan tinker
>>> Post::first()->slug
  1. El atributo está correctamente importado:
use Illuminate\Database\Eloquent\Attributes\RouteKey;
  1. El valor en la URL coincide exactamente (sensible a mayúsculas):
// Si slug es 'como-aprender-laravel'
// La URL debe ser /posts/como-aprender-laravel
// No /posts/Como-Aprender-Laravel
  1. Usa artisan route:list para verificar bindings:
php artisan route:list

Verás qué columna usa cada ruta.

Integración con API Resources

Si usas API Resources, #[RouteKey] también funciona:

<?php

namespace App\Http\Resources;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

class PostResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'slug' => $this->slug,
            'title' => $this->title,
            'content' => $this->content,
            'created_at' => $this->created_at,
        ];
    }
}

Las rutas API heredan automáticamente el binding:

Route::apiResource('posts', PostController::class);
// POST /api/posts/como-aprender-laravel resuelve por slug

Migración desde configuración antigua

Si tienes un proyecto con binding configurable en rutas:

Antes:

Route::get('/users/{user:username}', [UserController::class, 'show']);
Route::post('/users/{user:username}/posts', [PostController::class, 'store']);
Route::put('/users/{user:username}', [UserController::class, 'update']);

Después:

// En Model: User.php
#[RouteKey('username')]
class User extends Model { ... }

// En rutas
Route::get('/users/{user}', [UserController::class, 'show']);
Route::post('/users/{user}/posts', [PostController::class, 'store']);
Route::put('/users/{user}', [UserController::class, 'update']);

Más limpio y DRY.

Conclusión

El atributo #[RouteKey] de Laravel 13.21 representa un paso adelante en la elegancia y mantenibilidad del código. Centralizar la configuración de binding en el modelo donde pertenece logra:

  • Menos repetición: Define una vez, usa en todas partes
  • Mayor claridad: El modelo declara explícitamente cómo se resuelve
  • Código moderno: Aprovecha atributos PHP nativos
  • Menos errores: No olvidas especificar :slug en una ruta

Si trabajas con rutas parametrizadas, especialmente con slugs o identificadores personalizados, este atributo debería estar en tu caja de herramientas.

Puntos clave

  • #[RouteKey] centraliza la configuración de binding en el modelo Eloquent
  • ✅ Elimina la necesidad de escribir {model:columna} en cada ruta
  • ✅ Compatible con slugs, UUIDs, códigos personalizados y cualquier columna única
  • ✅ La configuración en rutas aún tiene prioridad si la especificas
  • ✅ Funciona con rutas anidadas, scoped binding y API Resources
  • ✅ Requiere Laravel 13.21 o superior
  • ✅ Usa atributos PHP nativos, mejorando la expresividad del código
  • ✅ Facilita refactoring futuro: cambiar identificadores solo afecta el modelo