Laravel 13 ya está disponible. Se lanzó el 17 de marzo de 2026, junto con la primera versión estable de Laravel AI SDK. Siguiendo el ciclo anual de releases que Laravel mantiene desde hace varios años, esta versión llega con cambios enfocados en modernización, limpieza interna y algunas nuevas funcionalidades.
En este artículo repasamos todo lo que incluye basándonos exclusivamente en fuentes oficiales y el repositorio de GitHub de Laravel.
Requisitos y compatibilidad
Antes de hablar de nuevas funcionalidades, lo más importante:
PHP 8.3 como versión mínima
Laravel 13 requiere PHP 8.3 como mínimo (PR #54763), subiendo desde PHP 8.2 en Laravel 12. El soporte se extiende hasta PHP 8.5.
Si tu servidor o entorno de desarrollo todavía ejecuta PHP 8.2, necesitarás actualizar antes de migrar a Laravel 13. Si no puedes actualizar PHP, puedes quedarte en Laravel 12 sin problema, ya que recibirá correcciones de seguridad hasta febrero de 2027.
Soporte para Symfony 7.4 y 8.0
Laravel 13 actualiza sus dependencias para ser compatible con Symfony 7.4 y 8.0 (PR #56029). Esto afecta a los componentes internos que Laravel utiliza de Symfony como HTTP, Console o Routing.
Línea temporal de soporte
Versión | PHP | Lanzamiento | Bug fixes hasta | Security fixes hasta |
|---|---|---|---|---|
11 | 8.2 - 8.4 | Marzo 2024 | Septiembre 2025 | Marzo 2026 |
12 | 8.2 - 8.5 | Febrero 2025 | Agosto 2026 | Febrero 2027 |
13 | 8.3 - 8.5 | Marzo 2026 | Septiembre 2027 | Marzo 2028 |
No hay prisa por actualizar. Laravel 12 seguirá recibiendo soporte durante bastante tiempo.
Nuevas funcionalidades
PHP Attributes como alternativa a propiedades
El cambio más visible de Laravel 13 es la introducción de PHP 8 Attributes como alternativa a las propiedades de clase para configurar componentes del framework (PR #58578).
Es importante destacar que este cambio es 100% opcional y no-breaking. Las propiedades de toda la vida ($fillable, $hidden, $table, etc.) siguen funcionando exactamente igual.
Modelos Eloquent
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\Hidden;
use Illuminate\Database\Eloquent\Attributes\Table;
#[Table('users', key: 'user_id', keyType: 'string', incrementing: false)]
#[Hidden(['password'])]
#[Fillable(['name', 'email'])]
class User extends Model {}Los atributos disponibles para modelos son: #[Appends], #[Connection], #[Fillable], #[Guarded], #[Hidden], #[Table], #[Touches], #[Unguarded] y #[Visible].
Jobs de colas
<?php
namespace App\Jobs;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\Attributes\Connection;
use Illuminate\Queue\Attributes\Queue;
use Illuminate\Queue\Attributes\Tries;
use Illuminate\Queue\Attributes\Timeout;
#[Connection('redis')]
#[Queue('podcasts')]
#[Tries(3)]
#[Timeout(120)]
class ProcessPodcast implements ShouldQueue {}Los atributos de colas disponibles son: #[Backoff], #[Connection], #[FailOnTimeout], #[MaxExceptions], #[Queue], #[Timeout], #[Tries] y #[UniqueFor].
Estos atributos también aplican a listeners, notifications, mailables y broadcast events.
Comandos de Artisan
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Console\Attributes\Signature;
use Illuminate\Console\Attributes\Description;
#[Signature('mail:send {user} {--queue}')]
#[Description('Send a marketing email to a user')]
class SendMailCommand extends Command {}Otros componentes
Los Attributes también están disponibles para Form Requests (#[RedirectTo], #[StopOnFirstFailure]), API Resources (#[Collects], #[PreserveKeys]), Factories (#[UseModel]) y Seeders en tests (#[Seed], #[Seeder]).
Cache::touch()
El método Cache::touch() (PR #55954) permite extender el TTL de un elemento en caché sin necesidad de leerlo ni reescribirlo:
<?php
// Extender por segundos
Cache::touch('user_session:123', 3600);
// Extender con DateTime
Cache::touch('analytics_data', now()->addHours(6));
// Extender indefinidamente
Cache::touch('report_cache', null);Hasta ahora, para extender el TTL de un valor en caché necesitabas hacer un get seguido de un put, lo que implicaba transferir el valor por la red innecesariamente. Cache::touch() lo resuelve de forma eficiente: en Redis usa un único comando EXPIRE, en Memcached usa TOUCH y en el driver de base de datos ejecuta un solo UPDATE.
El método devuelve true si la operación fue exitosa y false si la clave no existe. Está implementado en todos los drivers de caché.
PreventRequestForgery — Nueva protección CSRF
Laravel 13 introduce un nuevo middleware PreventRequestForgery que reemplaza al clásico VerifyCsrfToken (PR #58400). No es solo un rename — añade verificación del header Sec-Fetch-Site, siguiendo un enfoque similar al adoptado recientemente por Rails.
Los navegadores modernos envían automáticamente este header en cada petición, indicando si viene del mismo origen (same-origin), del mismo sitio (same-site) o de un sitio externo (cross-site). Este header no se puede falsear con JavaScript, lo que lo convierte en una señal fiable para protección CSRF.
El middleware comprueba este header primero. Si la petición tiene un origen válido, pasa directamente sin comprobar el token CSRF. Si el header no está presente o no es válido, hace fallback a la validación clásica del token para mantener la retrocompatibilidad con navegadores antiguos y conexiones HTTP no seguras.
<?php
protected function hasValidOrigin($request)
{
$secFetchSite = $request->header('Sec-Fetch-Site');
if ($secFetchSite === 'same-origin') {
return true;
}
if ($secFetchSite === 'same-site' && static::$allowSameSite) {
return true;
}
if (static::$originOnly) {
throw new OriginMismatchException('Origin mismatch.');
}
return false;
}El antiguo VerifyCsrfToken queda como un alias deprecado que extiende de PreventRequestForgery:
<?php
/**
* @deprecated Use PreventRequestForgery instead.
*/
class VerifyCsrfToken extends PreventRequestForgery
{
//
}Si en tu código usas withoutMiddleware(VerifyCsrfToken::class), tendrás que actualizar la referencia a PreventRequestForgery.
WorkerStopReason — Observabilidad en colas
Laravel 13 añade un nuevo Enum WorkerStopReason (PR #58341) que indica exactamente por qué se detuvo un worker de colas. Hasta ahora solo tenías un código de estado genérico. Con este Enum, puedes saber si paró por memoria, tiempo, señal del sistema o cola vacía:
<?php
enum WorkerStopReason: string
{
case Interrupted = 'interrupted';
case MaxJobsExceeded = 'max_jobs';
case MaxMemoryExceeded = 'memory';
case MaxTimeExceeded = 'max_time';
case QueueEmpty = 'empty';
case ReceivedRestartSignal = 'restart_signal';
}Si gestionas colas en producción, esto es muy útil para observabilidad. Puedes escuchar el evento WorkerStopping y registrar el motivo exacto de la parada para tus sistemas de monitorización.
markEmailAsUnverified() — Desverificar emails
El contrato MustVerifyEmail incorpora un nuevo método markEmailAsUnverified() (PR #58701). Hasta ahora existía markEmailAsVerified() para verificar un email, pero no había una forma estándar de hacer lo contrario.
El caso de uso más habitual es cuando un usuario cambia su dirección de email y necesitas forzar una nueva verificación:
<?php
// Cuando el usuario actualiza su email
$user->email = $request->validated('email');
$user->markEmailAsUnverified();
$user->save();
$user->sendEmailVerificationNotification();Sin este método, tenías que hacer manualmente $user->email_verified_at = null, lo cual no era consistente con el patrón que ya establecía markEmailAsVerified().
Js::from() con JSON_UNESCAPED_UNICODE
La clase Js::from() ahora incluye JSON_UNESCAPED_UNICODE por defecto (PR #58471). Esto afecta directamente a los desarrolladores que trabajan con idiomas que usan caracteres especiales como acentos, eñes o caracteres asiáticos.
Hasta ahora, si usabas Js::from() en una vista Blade con texto en español, los caracteres se escapaban:
<?php
// ANTES — Los caracteres se escapaban
Js::from(['mensaje' => 'Año nuevo']);
// Resultado: {\"mensaje\":\"A\\u00f1o nuevo\"}
// AHORA — Se renderizan directamente
Js::from(['mensaje' => 'Año nuevo']);
// Resultado: {\"mensaje\":\"Año nuevo\"}Si tienes tests que verifican el output escapado de Js::from(), necesitarás actualizarlos para reflejar este cambio.
Cambios internos y breaking changes
Además de las funcionalidades nuevas, Laravel 13 incluye varios cambios internos:
Concurrencia por defecto en HTTP pools:
PendingRequest::pool()ahora tiene un valor por defecto de 2 para la concurrencia (PR #57972), evitando que las peticiones se ejecuten en serie cuando no se especifica el valor.Alineación con Symfony en Request::get(): Se ajusta el comportamiento para mantener compatibilidad con los componentes de Symfony (PR #58081).
DELETE JOIN completo en MySQL: La gramática de MySQL ahora compila queries
DELETE ... JOINincluyendoORDER BYyLIMIT(PR #57196).Manager extend() consistente: Los callbacks de
extend()reciben una instancia del manager de forma consistente en todas las clases manager (PR #57173). Este es un breaking change.Eliminación de containsOneItem y containsManyItems: Se reemplazan por
hasSole()yhasMany()en la interfaz Enumerable (PR #58594).Ordenación de rutas con subdominios: Las rutas con subdominio se registran antes que las rutas sin dominio (PR #55921).
Enums como claves de caché: Todo el contrato de Cache ahora acepta
BackedEnumyUnitEnumcomo claves (PR #58481). Puedes hacerCache::get(MiEnum::CLAVE)directamente, sin necesidad de castear a string.Preparación para PHP 8.5: Reemplazo interno de
__wakeuppor__unserialize, que será deprecado en PHP 8.5.
Actualización desde Laravel 12
Si tienes un proyecto en Laravel 12 y quieres migrar, tenemos una guía completa de actualización de Laravel 12 a Laravel 13 con todos los breaking changes explicados con ejemplos de código y un checklist de migración.
También puedes usar Laravel Shift, que genera un PR con commits atómicos para revisar los cambios necesarios.
Los puntos clave a tener en cuenta son:
Asegurarte de que tu servidor ejecuta PHP 8.3 o superior.
Revisar que tus paquetes de terceros son compatibles.
Actualizar los métodos deprecados en colecciones (
containsOneItem→hasSole,containsManyItems→hasMany).Revisar el comportamiento de
Manager::extend()si tienes drivers custom.Actualizar referencias a
VerifyCsrfTokenporPreventRequestForgerysi usaswithoutMiddleware().Revisar tests que verifiquen el output de
Js::from()con caracteres unicode.
Conclusión
Laravel 13 es un release centrado en la modernización del framework. No hay grandes cambios revolucionarios, pero sí una base más limpia con PHP 8.3 como mínimo, compatibilidad con Symfony 8.0 y mejoras de calidad de vida como Cache::touch().
Los PHP Attributes son la novedad más visible, aunque su uso es completamente opcional. Si prefieres seguir usando propiedades de clase como hasta ahora, puedes hacerlo sin ningún problema.
La nueva protección CSRF con PreventRequestForgery y el header Sec-Fetch-Site es una mejora de seguridad real que merece más atención de la que está recibiendo. Y las mejoras de observabilidad en colas con WorkerStopReason aportan valor directo para entornos de producción.
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