> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# Actualización de Laravel 8 a 9

> Explicación de los pasos de actualización de Laravel 8.x a 9.x y de los principales cambios.

## 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.

<Info>
  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.
</Info>

### Actualización automatizada con Laravel Shift

Puedes automatizar la actualización con [Laravel Shift](https://laravelshift.com/). 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`.

```json theme={null}
{
  "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.

```shell theme={null}
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

```shell theme={null}
# 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`

```php theme={null}
// 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.

```php theme={null}
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

```shell theme={null}
# 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`.

```php theme={null}
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.

```php theme={null}
// 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.

```php theme={null}
// 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.

```php theme={null}
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.

```php theme={null}
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.

```php theme={null}
// 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`.

```php theme={null}
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`.

```php theme={null}
// 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.

```php theme={null}
// 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`.

```php theme={null}
// 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.

```php theme={null}
$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()`.

```php theme={null}
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.

| Cambio                                    | Impacto | Acción                                                                        |
| ----------------------------------------- | ------- | ----------------------------------------------------------------------------- |
| PHP 8.0.2 / `laravel/framework:^9.0`      | Alto    | Actualizar `composer.json` y el entorno de ejecución                          |
| Migración a Symfony Mailer                | Alto    | Revisar las APIs relacionadas con SwiftMailer y la configuración de mail      |
| Flysystem 3.x                             | Alto    | Revisar valores de retorno, excepciones y dependencias de driver de `Storage` |
| Métodos upsert de `BelongsToMany`         | Medio   | Revisar que el objetivo de búsqueda pasa a la tabla del modelo relacionado    |
| Casts personalizados y `null`             | Medio   | Corregir para que `set()` no falle recibiendo `null`                          |
| Timeout de 30 s del cliente HTTP          | Medio   | Indica explícitamente `timeout()` solo en las peticiones necesarias           |
| Deprecación de `assertDeleted`            | Medio   | Sustituir por `assertModelMissing()`                                          |
| Movida del directorio `lang`              | Medio   | Cambiar las referencias de ruta fija por `app()->langPath()`                  |
| Cambio de regla `password`                | Medio   | Actualizar a `current_password`                                               |
| Exclusión de claves de array no validadas | Medio   | Usar `includeUnvalidatedArrayKeys()` solo cuando sea necesario                |

***

## Referencias

* [Guía oficial de actualización (inglés)](https://laravel.com/docs/9.x/upgrade)
* [`upgrade.md` de laravel/docs 9.x](https://github.com/laravel/docs/blob/9.x/upgrade.md)
* [Diff del repositorio laravel/laravel (8.x → 9.x)](https://github.com/laravel/laravel/compare/8.x...9.x)
* [Laravel Shift](https://laravelshift.com): servicio comunitario que automatiza la actualización
* [Documentación de Symfony Mailer](https://symfony.com/doc/6.0/mailer.html)
* [Documentación de Flysystem 3](https://flysystem.thephpleague.com/v3/docs/)


## Related topics

- [Actualización de Laravel 9 a 10](/es/blog/upgrade-9-to-10.md)
- [Actualización de Laravel 10 a 11](/es/blog/upgrade-10-to-11.md)
- [Actualización de Laravel — Mayo de 2026](/es/blog/changelog/202605.md)
- [Actualización de Laravel — Marzo de 2026](/es/blog/changelog/202603.md)
- [Actualización de Laravel — Abril de 2026](/es/blog/changelog/202604.md)
