Saltar al contenido

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 update

Si usas el instalador global de Laravel:

composer global update laravel/installer

Impacto 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_session

Cache 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 null

Si 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

Illuminate\Contracts\Bus\Dispatcher

dispatchAfterResponse($command, $handler = null)

Illuminate\Contracts\Routing\ResponseFactory

eventStream(...)

Illuminate\Contracts\Auth\MustVerifyEmail

markEmailAsUnverified()

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|null

Actualiza 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

VerifyCsrfTokenPreventRequestForgery

Alto

Cache

Nueva opción serializable_classes

Medio

Cache

Formato de prefijos y cookie de sesión

Bajo

Cache

Nuevo método touch en contratos

Muy bajo

Container

Container::call con nullable defaults

Bajo

Contracts

Nuevos métodos en Dispatcher, ResponseFactory, MustVerifyEmail

Muy bajo

Database

DELETE + JOIN + ORDER BY / LIMIT en MySQL

Bajo

Eloquent

Model booting anidado lanza LogicException

Muy bajo

Eloquent

Nombres de pivot polimórficos en plural

Bajo

Eloquent

Colecciones restauran eager-loaded relations

Bajo

Queue

JobAttempted::$exceptionOccurred$exception

Bajo

Queue

QueueBusy::$connection$connectionName

Bajo

Queue

Nuevos métodos en contrato Queue

Muy bajo

Routing

Domain routes con precedencia

Bajo

Support

Manager::extend binding cambia a manager

Bajo

Support

Str factories se resetean entre tests

Bajo

Support

Js::from con JSON_UNESCAPED_UNICODE por defecto

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 VerifyCsrfToken y ValidateCsrfToken y reemplazar por PreventRequestForgery

  • [ ] Añadir serializable_classes en config/cache.php (o declarar clases permitidas si guardas objetos)

  • [ ] Verificar que CACHE_PREFIX, REDIS_PREFIX y SESSION_COOKIE están definidos en .env

  • [ ] Revisar listeners del evento JobAttempted: cambiar $exceptionOccurred por $exception

  • [ ] Revisar listeners del evento QueueBusy: cambiar $connection por $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

school Curso completo

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.

access_time 8 horas de contenido
layers 4 tecnologías en 1
update 100% actualizado
code Proyectos prácticos
Ver Curso Laravel 12 arrow_forward

star Incluido en cualquier suscripción

Rutas de aprendizaje

Newsletter

Recibe nuevos cursos, actualizaciones, artículos del blog y promociones en tu correo.