Saltar al contenido principal

Introducción

Laravel 9 se lanzó el 8 de febrero de 2022. En esta guía se resumen los pasos para actualizar de Laravel 8.x a 9.x y los cambios de mayor impacto.
El tiempo estimado necesario para la actualización es de unos 30 minutos. Sin embargo, la carga de trabajo puede aumentar según el envío de emails, el almacenamiento de archivos, los casts personalizados y las sobrescrituras de clases core del framework.

Actualización automatizada con Laravel Shift

Puedes automatizar la actualización con Laravel Shift. Shift ayuda a actualizar composer.json y los archivos de configuración, y resulta útil como punto de partida para revisar diffs.

Cambios por nivel de impacto

Impacto: alto

  • Actualización de dependencias
  • Migración a Flysystem 3.x
  • Migración a Symfony Mailer

Impacto: medio

  • Métodos firstOrNew / firstOrCreate / updateOrCreate de BelongsToMany
  • Casts personalizados y comportamiento con null
  • Timeout por defecto del cliente HTTP
  • Añadido de PHP Return Types
  • Cambio de nombre de configuración schema de Postgres
  • Deprecación del método assertDeleted
  • Movida del directorio lang
  • Cambios en las reglas de contraseña
  • Cambios en los métodos when / unless
  • Tratamiento de claves de array no validadas en la validación

Pasos de actualización

Actualización de dependencias

Impacto: alto Laravel 9 requiere PHP 8.0.2 o superior. Empieza revisando las dependencias en composer.json.
{
  "require": {
    "php": "^8.0.2",
    "laravel/framework": "^9.0",
    "spatie/laravel-ignition": "^1.0"
  },
  "require-dev": {
    "nunomaduro/collision": "^6.1"
  }
}
Además, según aplicación, pueden ser necesarias las siguientes actualizaciones.
  • Elimina facade/ignition y sustitúyelo por spatie/laravel-ignition:^1.0
  • Si usas pusher/pusher-php-server, actualízalo a ^5.0
  • Comprueba que los paquetes de terceros que uses tengan versión compatible con Laravel 9
  • Si usas el canal de notificaciones de Vonage, revisa también su guía de actualización específica
Después, instala las dependencias.
composer update

Requisitos de versión de PHP

Impacto: alto Laravel 9 requiere PHP 8.0.2 o superior. Antes de proceder con la actualización, alinea la versión de PHP en CI, entorno de desarrollo local y producción.

Migración a Symfony Mailer

Impacto: alto Uno de los grandes cambios de Laravel 9 es la migración de SwiftMailer (cuyo mantenimiento terminó en diciembre de 2021) a Symfony Mailer. Las aplicaciones que solo usan Mail::to()->send() tienen poco impacto, pero si tocas directamente APIs de bajo nivel de SwiftMailer, revísalo.

Dependencias de drivers

# Añadir solo si usas Mailgun
composer require symfony/mailgun-mailer symfony/http-client

# Si usas Postmark, elimina el paquete de SwiftMailer y sustitúyelo
composer remove wildbit/swiftmailer-postmark
composer require symfony/postmark-mailer symfony/http-client

De withSwiftMessage a withSymfonyMessage

// Laravel 8.x: basado en SwiftMailer
$this->withSwiftMessage(function ($message) {
    $message->getHeaders()->addTextHeader('Custom-Header', 'Header Value');
});

// Laravel 9.x: basado en Symfony Mailer
use Symfony\Component\Mime\Email;

$this->withSymfonyMessage(function (Email $message) {
    $message->getHeaders()->addTextHeader('Custom-Header', 'Header Value');
});
send, html, raw y plain de Illuminate\Mail\Mailer ahora devuelven Illuminate\Mail\SentMessage en lugar de void. Además, la propiedad message del evento MessageSent pasa a ser Symfony\Component\Mime\Email en lugar de Swift_Message.

Revisión de la configuración SMTP

En Symfony Mailer se ha eliminado la opción stream de SMTP y las configuraciones soportadas se mueven al nivel superior.
return [
    'mailers' => [
        'smtp' => [
            // Configuración de Laravel 8.x
            'stream' => [
                'ssl' => [
                    'verify_peer' => false,
                ],
            ],

            // Configuración de Laravel 9.x
            'verify_peer' => false,
        ],
    ],
];
Ya no es necesario indicar auth_mode explícitamente. Es más seguro migrar a un flujo en el que las direcciones inválidas se validan antes del envío, en lugar de recuperarlas después.

Migración a Flysystem 3.x

Impacto: alto Laravel 9 ha actualizado la implementación interna de la facade Storage de Flysystem 1.x a 3.x. Los métodos de operaciones de archivo mantienen la compatibilidad en la medida de lo posible, pero hay diferencias en excepciones, valores de retorno y registro de adapters.

Instalación adicional de drivers

# Driver de Amazon S3
composer require -W league/flysystem-aws-s3-v3 "^3.0"

# Driver de FTP
composer require league/flysystem-ftp "^3.0"

# Driver de SFTP
composer require league/flysystem-sftp-v3 "^3.0"

Principales cambios de comportamiento de Storage

  • put, write y writeStream sobrescriben por defecto los archivos existentes
  • Si falla la escritura, devuelven false en lugar de lanzar excepción
  • La lectura de archivos inexistentes devuelve null en lugar de lanzar excepción
  • delete sobre un archivo inexistente devuelve true
  • El cached adapter se ha eliminado, así que puedes eliminar la clave cache de la configuración de disk
Si quieres que la escritura fallida lance excepción como antes, activa la opción throw.
return [
    'disks' => [
        'public' => [
            'driver' => 'local',
            // Actívalo solo si quieres que se lance excepción al fallar la escritura
            'throw' => true,
        ],
    ],
];
Si registras drivers de filesystem propios, ajusta el callback de Storage::extend() para que devuelva directamente un Illuminate\Filesystem\FilesystemAdapter.

firstOrNew / firstOrCreate / updateOrCreate de BelongsToMany

Impacto: medio En Laravel 8, el array de atributos pasado como primer argumento a estos métodos se comparaba con la tabla intermedia. En Laravel 9 se compara con la tabla del modelo relacionado.
// Busca y actualiza name contra la tabla del modelo relacionado
$user->roles()->updateOrCreate([
    'name' => 'Administrator',
]);
Además, firstOrCreate puede ahora recibir el segundo argumento $values, homogeneizando su comportamiento con el de otras relaciones.
// Fusiona created_by solo al crear
$user->roles()->firstOrCreate([
    'name' => 'Administrator',
], [
    'created_by' => $user->id,
]);

Casts personalizados y null

Impacto: medio En Laravel 9, si al objetivo del cast se le asigna null, se llama también al método set del cast personalizado. Los casts que no contemplaban null pueden lanzar excepciones tras la actualización.
public function set($model, $key, $value, $attributes)
{
    // En Laravel 9 puede pasarse null
    if ($value === null) {
        return [
            'address_line_one' => null,
            'address_line_two' => null,
        ];
    }

    if (! $value instanceof AddressModel) {
        throw new InvalidArgumentException('Debes pasar un AddressModel.');
    }

    return [
        'address_line_one' => $value->lineOne,
        'address_line_two' => $value->lineTwo,
    ];
}

Timeout por defecto del cliente HTTP

Impacto: medio El timeout por defecto del cliente HTTP pasa a ser de 30 segundos. Antes podía esperar indefinidamente.
use Illuminate\Support\Facades\Http;

// Extiende el timeout solo en las llamadas a APIs largas
$response = Http::timeout(120)->get('https://example.com/api/status');

Añadido de PHP Return Types

Impacto: medio En Laravel 9, para alinearse con los requisitos de PHP y Symfony, se han añadido tipos de retorno a varias clases core. Si heredas clases core de Laravel y sobrescribes offsetGet, offsetSet, jsonSerialize, open, read, etc., añade los mismos tipos de retorno también a tus implementaciones.
// Si sobrescribes una clase core, ajusta el tipo de retorno
public function offsetGet($key): mixed
{
    return parent::offsetGet($key);
}

Cambio de nombre de configuración schema de Postgres

Impacto: medio Si configuras el search path en la conexión Postgres, cambia la clave schema a search_path en config/database.php.
return [
    'pgsql' => [
        // Laravel 8.x
        'schema' => 'public',

        // Laravel 9.x
        'search_path' => 'public',
    ],
];

De assertDeleted a assertModelMissing

Impacto: medio Sustituye assertDeleted, que usabas para verificar el borrado de modelos, por assertModelMissing.
// Antes
$this->assertDeleted($user);

// Después
$this->assertModelMissing($user);

Movida del directorio lang

Impacto: medio En las nuevas aplicaciones de Laravel 9, la ubicación de los archivos de idioma pasa de resources/lang a lang en la raíz del proyecto. Si solo mantienes funcionando la app existente, el impacto es pequeño, pero si te alineas con el nuevo esqueleto o publicas traducciones desde un paquete, revísalo.
// Usa langPath() en lugar de una ruta fija para decidir el destino de publicación
$this->publishes([
    __DIR__.'/../lang' => app()->langPath('vendor/package-name'),
]);

Cambios en las reglas de contraseña

Impacto: medio La regla password, que validaba que coincidiera con la contraseña del usuario autenticado, se ha renombrado a current_password.
// Antes
'password' => ['required', 'password'],

// Después
'password' => ['required', 'current_password'],

Cambios en los métodos when / unless

Impacto: medio En Laravel 8, si pasabas un closure a when o unless, el propio closure se evaluaba como truthy y las bifurcaciones se ejecutaban sin querer. En Laravel 9 el closure se ejecuta y se usa su valor de retorno como condición.
$collection->when(function ($collection) {
    // El valor devuelto aquí se evalúa como expresión condicional
    return false;
}, function ($collection) {
    // Como se devolvió false, este procesamiento no se ejecuta
    $collection->merge([1, 2, 3]);
});

Tratamiento de claves de array no validadas

Impacto: medio En Laravel 9, las claves de array no validadas se excluyen siempre del array que devuelve validated(). Si quieres mantener el comportamiento de Laravel 8, llama explícitamente a includeUnvalidatedArrayKeys().
use Illuminate\Support\Facades\Validator;

public function boot()
{
    // Activar solo si quieres volver al mismo comportamiento que Laravel 8
    Validator::includeUnvalidatedArrayKeys();
}

Conclusión

En la actualización de Laravel 8 a 9, lo central es la actualización a PHP 8.0.2, la migración a Symfony Mailer y el soporte de Flysystem 3.x. Revisar primero envío de emails, storage, casts personalizados y helpers de test reduce los fallos posteriores.
CambioImpactoAcción
PHP 8.0.2 / laravel/framework:^9.0AltoActualizar composer.json y el entorno de ejecución
Migración a Symfony MailerAltoRevisar las APIs relacionadas con SwiftMailer y la configuración de mail
Flysystem 3.xAltoRevisar valores de retorno, excepciones y dependencias de driver de Storage
Métodos upsert de BelongsToManyMedioRevisar que el objetivo de búsqueda pasa a la tabla del modelo relacionado
Casts personalizados y nullMedioCorregir para que set() no falle recibiendo null
Timeout de 30 s del cliente HTTPMedioIndica explícitamente timeout() solo en las peticiones necesarias
Deprecación de assertDeletedMedioSustituir por assertModelMissing()
Movida del directorio langMedioCambiar las referencias de ruta fija por app()->langPath()
Cambio de regla passwordMedioActualizar a current_password
Exclusión de claves de array no validadasMedioUsar includeUnvalidatedArrayKeys() solo cuando sea necesario

Referencias

Última modificación el 13 de julio de 2026