Laravel 13 ya está aquí, hoy 17 de marzo de 2026 se ha liberado. Si tienes una aplicación en Laravel 12 y quieres migrar, esta guía cubre todos los breaking changes documentados en la guía oficial, explicados con ejemplos reales para que no te lleves sorpresas.
La propia documentación oficial estima 10 minutos de migración. En la práctica depende de cuántos cambios te afecten, pero en general es una actualización limpia. Vamos a verlo todo.
¿Necesito actualizar ya?
No hay urgencia, pero sí hay una fecha que conviene tener en mente: Laravel 12 deja de recibir bug fixes en agosto de 2026 y security fixes en febrero de 2027.
Versión | Bug fixes hasta | Security fixes hasta |
|---|---|---|
Laravel 12 | Agosto 2026 | Febrero 2027 |
Laravel 13 | Q3 2027 | Q1 2028 |
Si estás arrancando un proyecto nuevo, empieza directamente en Laravel 13. Si tienes un proyecto en producción con Laravel 12, tienes margen, pero actualizar ahora tiene sentido: los breaking changes son pocos, el tiempo estimado es de 10 minutos y te pones al día antes de que la presión del soporte apriete.
Si por algún motivo no puedes actualizar PHP a 8.3 todavía, quédate en Laravel 12 sin problema. No merece la pena forzar el upgrade si el entorno no está listo.
Compatibilidad y requisitos
Antes de tocar nada, verifica que cumples estos requisitos:
Dependencia | Versión mínima requerida |
|---|---|
PHP | 8.3 |
laravel/framework | ^13.0 |
phpunit/phpunit | ^12.0 |
pestphp/pest | ^4.0 |
Nota sobre PHPUnit: PHPUnit 13 existe, pero requiere PHP 8.4+. Como Laravel 13 tiene mínimo PHP 8.3, la guía oficial mantiene
^12.0. Si ya estás en PHP 8.4 podrás actualizar PHPUnit a 13 de forma independiente, pero no es un requisito del upgrade.
Si tienes PHP 8.2, ese es tu primer bloqueante. Actualiza PHP antes de continuar.
Paso 1: comprobar conflictos antes de actualizar
Antes de tocar nada, ejecuta este comando para saber si alguna dependencia va a bloquear el upgrade:
composer why-not laravel/framework "^13.0"Si no devuelve nada, puedes continuar sin problemas. Si devuelve resultados, te lista exactamente qué paquetes están fijando una versión incompatible — normalmente paquetes de terceros que aún no tienen soporte para Laravel 13. Resuélvelos antes de continuar.
Paso 2: actualizar dependencias
composer require laravel/framework:^13.0 --no-update
composer require phpunit/phpunit:^12.0 --dev --no-update
composer require pestphp/pest:^4.0 --dev --no-update
composer updateSi usas el instalador global de Laravel:
composer global update laravel/installerImpacto alto
PreventRequestForgery: el middleware CSRF tiene nuevo nombre
Este es el cambio más importante y el que más aplicaciones van a notar, sobre todo en los tests.
El middleware VerifyCsrfToken ha sido renombrado a PreventRequestForgery. Además de cambiar de nombre, ahora incluye verificación de origen mediante la cabecera Sec-Fetch-Site, lo que añade una capa extra de protección frente a ataques CSRF en navegadores modernos.
VerifyCsrfToken y ValidateCsrfToken siguen existiendo como aliases deprecados, pero debes actualizar cualquier referencia directa, especialmente en tests donde excluyes el middleware:
<?php
use Illuminate\Foundation\Http\Middleware\PreventRequestForgery;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;
// Laravel <= 12.x
->withoutMiddleware([VerifyCsrfToken::class]);
// Laravel >= 13.x
->withoutMiddleware([PreventRequestForgery::class]);La API de configuración de middleware también se actualiza:
<?php
// Laravel >= 13.x
->preventRequestForgery(except: ['/webhook/*']);¿Dónde buscar referencias? Haz un grep en todo el proyecto:
grep -r "VerifyCsrfToken\|ValidateCsrfToken" --include="*.php" .Los ficheros más habituales donde aparece: tests de feature, bootstrap/app.php y cualquier middleware personalizado que extienda el original.
Impacto medio
Cache: nueva opción serializable_classes
La configuración de caché ahora incluye una opción serializable_classes que por defecto está en false. Este cambio endurece el comportamiento de deserialización para prevenir ataques de tipo PHP gadget chain si la APP_KEY de tu aplicación se filtra.
<?php
// config/cache.php — nuevo en Laravel 13
'serializable_classes' => false,Si tu aplicación almacena objetos PHP serializados en caché (por ejemplo, instancias de modelos, DTOs o cualquier clase), tienes dos opciones:
Opción 1: Declarar explícitamente qué clases pueden deserializarse:
<?php
'serializable_classes' => [
App\Data\CachedDashboardStats::class,
App\Support\CachedPricingSnapshot::class,
],Opción 2: Migrar esos payloads a arrays en lugar de objetos, que es la opción más segura a largo plazo.
Si no tocas objetos en caché, este cambio no te afecta.
Impacto bajo
Cache: prefijos y nombre de cookie de sesión
Los prefijos de caché y Redis ahora usan guiones en lugar de guiones bajos. El nombre de la cookie de sesión pasa a usar Str::snake():
<?php
// Laravel <= 12.x
Str::slug(env('APP_NAME', 'laravel'), '_').'_cache_';
Str::slug(env('APP_NAME', 'laravel'), '_').'_session';
// Laravel >= 13.x
Str::slug(env('APP_NAME', 'laravel')).'-cache-';
Str::snake(env('APP_NAME', 'laravel')).'_session';Esto solo afecta a aplicaciones que no tienen definidos explícitamente CACHE_PREFIX, REDIS_PREFIX y SESSION_COOKIE en el .env. La gran mayoría de proyectos en producción ya los tienen configurados y no notarán nada.
Si no los tienes definidos, añádelos al .env con los valores anteriores antes de hacer el upgrade para evitar que los usuarios pierdan la sesión:
CACHE_PREFIX=mi_app_cache_
SESSION_COOKIE=mi_app_sessionCache contracts: nuevo método touch
Si tienes una implementación personalizada de un driver de caché, debes añadir el método touch al contrato Store:
<?php
// Illuminate\Contracts\Cache\Store
public function touch($key, $seconds): bool;Si no tienes drivers de caché custom, este cambio no te aplica.
Container: Container::call con nullable defaults
Container::call ahora respeta los valores por defecto nullable cuando no existe un binding registrado, alineándose con el comportamiento de inyección en constructores introducido en Laravel 12:
<?php
$container->call(function (?Carbon $date = null) {
return $date;
});
// Laravel <= 12.x: devuelve una instancia de Carbon
// Laravel >= 13.x: devuelve nullSi tienes lógica que dependía del comportamiento anterior, revísala.
Contracts: nuevos métodos en interfaces
Si mantienes implementaciones personalizadas de los siguientes contratos, debes añadir los métodos indicados:
Contrato | Método nuevo |
|---|---|
|
|
|
|
|
|
En proyectos estándar sin implementaciones custom de estos contratos, no hay nada que hacer.
Eloquent: model booting anidado lanza excepción
Crear una instancia de un modelo mientras ese mismo modelo está siendo inicializado en boot() ahora lanza una LogicException:
<?php
protected static function boot(): void
{
parent::boot();
// Laravel >= 13.x: lanza LogicException
(new static())->getTable();
}Mueve cualquier lógica de este tipo fuera del ciclo de boot.
Eloquent: polymorphic pivot tables con nombre plural
Cuando Laravel infiere el nombre de la tabla para un morph pivot con una clase pivot personalizada, ahora genera nombres en plural. Si dependías del nombre singular inferido, define explícitamente la tabla en tu pivot:
<?php
class RoleUser extends MorphPivot
{
protected $table = 'role_user'; // definir explícitamente para mantener comportamiento anterior
}Eloquent: colecciones serializadas restauran relaciones eager-loaded
Cuando una colección de modelos se serializa y restaura (por ejemplo en jobs encolados), ahora se restauran también las relaciones eager-loaded. Si tu código dependía de que esas relaciones no estuvieran presentes tras la deserialización, revisa esa lógica.
MySQL: DELETE con JOIN, ORDER BY y LIMIT
Laravel ahora genera el SQL completo para DELETE ... JOIN incluyendo ORDER BY y LIMIT en MySQL grammar. Antes estas cláusulas se ignoraban silenciosamente. Ahora se incluyen en el SQL generado, lo que puede provocar un QueryException si el motor de base de datos no soporta esa sintaxis.
Queue: JobAttempted event — $exception reemplaza a $exceptionOccurred
El evento JobAttempted ahora expone el objeto excepción completo en lugar de un booleano:
<?php
// Laravel <= 12.x
$event->exceptionOccurred; // bool
// Laravel >= 13.x
$event->exception; // Throwable|nullActualiza cualquier listener que escuche este evento.
Queue: QueueBusy event — $connectionName reemplaza a $connection
<?php
// Laravel <= 12.x
$event->connection;
// Laravel >= 13.x
$event->connectionName;Queue contract: nuevos métodos de inspección
Si tienes un driver de queue personalizado que implementa Illuminate\Contracts\Queue\Queue, debes añadir:
<?php
public function pendingSize(): int;
public function delayedSize(): int;
public function reservedSize(): int;
public function creationTimeOfOldestPendingJob(): int|null;Routing: las rutas con dominio tienen precedencia
Las rutas con dominio explícito ahora se priorizan sobre las rutas sin dominio en la resolución. Si tu aplicación mezclaba rutas con y sin dominio y dependías del orden de registro, revisa el comportamiento de resolución.
Support: Manager::extend — cambio de binding
Los closures registrados mediante extend en los managers ahora se ejecutan con $this apuntando a la instancia del manager, no al service provider. Si pasabas datos del provider al closure vía $this, usa use (...) en su lugar:
<?php
// Laravel >= 13.x
$manager->extend('custom', function ($app) use ($someValue) {
return new CustomDriver($someValue);
});Support: Str factories se resetean entre tests
Las factories personalizadas de Str (para UUIDs, ULIDs, strings aleatorios) ahora se resetean al finalizar cada test. Si dependías de que persistieran entre métodos de test, configúralas en cada test o en el setUp.
Support: Js::from usa unicode sin escapar por defecto
Js::from() ahora incluye JSON_UNESCAPED_UNICODE por defecto. Si tienes tests que verificaban secuencias escapadas como \u00f3, actualiza las expectativas:
<?php
// Laravel <= 12.x
Js::from(['ciudad' => 'León']); // {\"ciudad\":\"Le\\u00f3n\"}
// Laravel >= 13.x
Js::from(['ciudad' => 'León']); // {\"ciudad\":\"León\"}Para proyectos con contenido en español, este cambio en realidad simplifica las cosas.
Notifications: subject del reset de contraseña
<?php
// Laravel <= 12.x
"Reset Password Notification"
// Laravel >= 13.x
"Reset your password"Actualiza los tests o traducciones que dependan del string anterior.
Views: nombres de paginación Bootstrap 3
<?php
// Laravel <= 12.x
'pagination::default'
'pagination::simple-default'
// Laravel >= 13.x
'pagination::bootstrap-3'
'pagination::simple-bootstrap-3'Si en algún punto de tu código referencias estos view names directamente, actualízalos.
Resumen de cambios por área
Área | Cambio | Impacto |
|---|---|---|
Security |
| Alto |
Cache | Nueva opción | Medio |
Cache | Formato de prefijos y cookie de sesión | Bajo |
Cache | Nuevo método | Muy bajo |
Container |
| Bajo |
Contracts | Nuevos métodos en | Muy bajo |
Database |
| Bajo |
Eloquent | Model booting anidado lanza | Muy bajo |
Eloquent | Nombres de pivot polimórficos en plural | Bajo |
Eloquent | Colecciones restauran eager-loaded relations | Bajo |
Queue |
| Bajo |
Queue |
| Bajo |
Queue | Nuevos métodos en contrato | Muy bajo |
Routing | Domain routes con precedencia | Bajo |
Support |
| Bajo |
Support |
| Bajo |
Support |
| Muy bajo |
Notifications | Subject de reset password cambiado | Muy bajo |
Views | Nombres de paginación Bootstrap 3 renombrados | Bajo |
Actualizar usando IA con Laravel Boost
Si tienes Laravel Boost instalado, puedes automatizar el upgrade usando IA. Boost es un servidor MCP de primera parte que proporciona a tu asistente de IA contexto guiado para la actualización.
Una vez instalado en tu aplicación Laravel 12, ejecuta el slash command /upgrade-laravel-v13 en Claude Code, Cursor, OpenCode, Gemini o VS Code y el asistente se encarga del proceso.
Checklist de migración
Usa esta lista para no olvidarte de nada:
[ ] Actualizar PHP a 8.3 como mínimo
[ ] Actualizar
composer.json:laravel/framework:^13.0,phpunit/phpunit:^12.0,pestphp/pest:^4.0[ ] Buscar referencias a
VerifyCsrfTokenyValidateCsrfTokeny reemplazar porPreventRequestForgery[ ] Añadir
serializable_classesenconfig/cache.php(o declarar clases permitidas si guardas objetos)[ ] Verificar que
CACHE_PREFIX,REDIS_PREFIXySESSION_COOKIEestán definidos en.env[ ] Revisar listeners del evento
JobAttempted: cambiar$exceptionOccurredpor$exception[ ] Revisar listeners del evento
QueueBusy: cambiar$connectionpor$connectionName[ ] Actualizar tests que dependan del subject de reset de contraseña
[ ] Revisar implementaciones custom de contratos (Cache Store, Queue, Dispatcher, ResponseFactory, MustVerifyEmail)
[ ] Revisar lógica en métodos
boot()de modelos que instancie el propio modelo[ ] Actualizar referencias a view names de paginación Bootstrap 3 si los usas directamente
[ ] Revisar tests que usen
Js::from()con caracteres Unicode escapados
¿Usar Laravel Shift?
Si tu proyecto es grande o quieres automatizar la mayor parte del proceso, Laravel Shift genera un PR con commits atómicos que cubren la mayoría de estos cambios de forma automática. Para proyectos medianos, el upgrade manual siguiendo esta guía es perfectamente viable.
Recursos
Curso Laravel 12
Completo 2026
El único curso 100% actualizado que incluye Laravel 12, Livewire 3, Vue 3, React 19 e Inertia 2. Aprende con proyectos reales y las últimas funcionalidades.
star Incluido en cualquier suscripción