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

# Upgrading from Laravel 8 to 9

> Upgrade steps and major changes for moving from Laravel 8.x to 9.x.

## Introduction

Laravel 9 was released on February 8, 2022. This guide covers the upgrade path from Laravel 8.x to 9.x and organizes the impactful changes.

<Info>
  Estimated time to upgrade is **about 30 minutes**. The workload can grow depending on your mail sending, file storage, custom casts, and how many core framework classes you override.
</Info>

### Automated upgrades with Laravel Shift

You can automate the upgrade with [Laravel Shift](https://laravelshift.com/). Shift helps update `composer.json` and configuration files, making it a useful starting point for reviewing diffs.

***

## Changes by impact level

### Impact: high

* Dependency updates
* Migration to Flysystem 3.x
* Migration to Symfony Mailer

### Impact: medium

* `firstOrNew` / `firstOrCreate` / `updateOrCreate` on `BelongsToMany`
* Custom casts and `null` behavior
* HTTP client default timeout
* PHP return types added
* Postgres `schema` config key renamed
* The `assertDeleted` method is deprecated
* Moved `lang` directory
* Password rule changes
* Behavior changes for `when` / `unless`
* Handling of unvalidated array keys

***

## Upgrade steps

### Update dependencies

**Impact: high**

Laravel 9 requires **PHP 8.0.2 or later**. Review your `composer.json` dependencies first.

```json theme={null}
{
  "require": {
    "php": "^8.0.2",
    "laravel/framework": "^9.0",
    "spatie/laravel-ignition": "^1.0"
  },
  "require-dev": {
    "nunomaduro/collision": "^6.1"
  }
}
```

Additionally, depending on your app, you may need to:

* Remove `facade/ignition` and replace it with `spatie/laravel-ignition:^1.0`
* Update `pusher/pusher-php-server` to `^5.0` if used
* Verify that third-party packages you use have Laravel 9 support
* Consult individual upgrade guides for Vonage notification channels if used

Then install dependencies:

```shell theme={null}
composer update
```

***

## PHP version requirement

**Impact: high**

Laravel 9 requires PHP 8.0.2 or later. Align the PHP version across CI, local, and production environments before starting the upgrade.

***

### Migration to Symfony Mailer

**Impact: high**

One of the big changes in Laravel 9 is the switch from SwiftMailer (whose maintenance ended in December 2021) to Symfony Mailer. Apps that only use `Mail::to()->send()` are mostly unaffected, but apps calling low-level SwiftMailer APIs need review.

#### Driver dependencies

```shell theme={null}
# Only add when using Mailgun
composer require symfony/mailgun-mailer symfony/http-client

# For Postmark, remove the SwiftMailer package and replace
composer remove wildbit/swiftmailer-postmark
composer require symfony/postmark-mailer symfony/http-client
```

#### From `withSwiftMessage` to `withSymfonyMessage`

```php theme={null}
// Laravel 8.x: SwiftMailer-based
$this->withSwiftMessage(function ($message) {
    $message->getHeaders()->addTextHeader('Custom-Header', 'Header Value');
});

// Laravel 9.x: Symfony Mailer-based
use Symfony\Component\Mime\Email;

$this->withSymfonyMessage(function (Email $message) {
    $message->getHeaders()->addTextHeader('Custom-Header', 'Header Value');
});
```

`Illuminate\Mail\Mailer`'s `send`, `html`, `raw`, and `plain` methods now return `Illuminate\Mail\SentMessage` instead of `void`. Also, the `message` property on the `MessageSent` event now holds a `Symfony\Component\Mime\Email` rather than a `Swift_Message`.

#### Review SMTP settings

Symfony Mailer removes the SMTP `stream` option; supported settings move to the top level.

```php theme={null}
return [
    'mailers' => [
        'smtp' => [
            // Laravel 8.x settings
            'stream' => [
                'ssl' => [
                    'verify_peer' => false,
                ],
            ],

            // Laravel 9.x settings
            'verify_peer' => false,
        ],
    ],
];
```

Explicit `auth_mode` configuration is also no longer needed. Instead of recovering after sending to bad addresses, it's safer to validate them up front.

***

### Migration to Flysystem 3.x

**Impact: high**

Laravel 9 updates the `Storage` facade internals from Flysystem 1.x to 3.x. File operations preserve compatibility as much as possible, but there are differences around exceptions, return values, and adapter registration.

#### Additional driver installs

```shell theme={null}
# Amazon S3 driver
composer require -W league/flysystem-aws-s3-v3 "^3.0"

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

# SFTP driver
composer require league/flysystem-sftp-v3 "^3.0"
```

#### Key behavior changes in `Storage`

* `put` / `write` / `writeStream` overwrite existing files by default
* Write failures return `false` rather than throwing
* Reading a missing file returns `null` rather than throwing
* `delete` on a missing file returns `true`
* The cached adapter has been removed—you can delete the `cache` key from `disk` config

To restore the previous "throw on write failure" behavior, set the `throw` option.

```php theme={null}
return [
    'disks' => [
        'public' => [
            'driver' => 'local',
            // Enable only if you want exceptions on write failures
            'throw' => true,
        ],
    ],
];
```

If you register custom filesystem drivers, adjust `Storage::extend()` callbacks so they return an `Illuminate\Filesystem\FilesystemAdapter` directly.

***

### `firstOrNew` / `firstOrCreate` / `updateOrCreate` on `BelongsToMany`

**Impact: medium**

In Laravel 8, the attributes array passed to these methods was compared against the pivot table. In Laravel 9, it's compared against the related model's table.

```php theme={null}
// Search and update by name on the related model's table
$user->roles()->updateOrCreate([
    'name' => 'Administrator',
]);
```

`firstOrCreate` also now accepts a second `$values` argument, matching the behavior of other relations.

```php theme={null}
// Merge created_by only on new creation
$user->roles()->firstOrCreate([
    'name' => 'Administrator',
], [
    'created_by' => $user->id,
]);
```

***

### Custom casts and `null`

**Impact: medium**

In Laravel 9, the `set` method on a custom cast is called even when `null` is assigned. Casts that don't consider `null` may throw after upgrading.

```php theme={null}
public function set($model, $key, $value, $attributes)
{
    // In Laravel 9, null may be passed
    if ($value === null) {
        return [
            'address_line_one' => null,
            'address_line_two' => null,
        ];
    }

    if (! $value instanceof AddressModel) {
        throw new InvalidArgumentException('An AddressModel must be provided.');
    }

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

***

### HTTP client default timeout

**Impact: medium**

The default HTTP client timeout is now 30 seconds. Previously, requests could wait indefinitely.

```php theme={null}
use Illuminate\Support\Facades\Http;

// Extend the timeout for long-running API calls individually
$response = Http::timeout(120)->get('https://example.com/api/status');
```

***

### PHP return types added

**Impact: medium**

Laravel 9 adds return types to various core classes to comply with PHP core and Symfony requirements. If you extend Laravel core classes and override methods like `offsetGet`, `offsetSet`, `jsonSerialize`, `open`, or `read`, add matching return types to your overrides.

```php theme={null}
// Match return types when overriding core classes
public function offsetGet($key): mixed
{
    return parent::offsetGet($key);
}
```

***

### Postgres `schema` config key renamed

**Impact: medium**

If your Postgres connection sets a search path, rename the key in `config/database.php` from `schema` to `search_path`.

```php theme={null}
return [
    'pgsql' => [
        // Laravel 8.x
        'schema' => 'public',

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

***

### From `assertDeleted` to `assertModelMissing`

**Impact: medium**

Replace `assertDeleted`, previously used to confirm model deletion, with `assertModelMissing`.

```php theme={null}
// Before
$this->assertDeleted($user);

// After
$this->assertModelMissing($user);
```

***

### Moved `lang` directory

**Impact: medium**

In new Laravel 9 apps, language files live in `lang` at the project root instead of `resources/lang`. Existing apps mostly continue to work, but if you align to the new skeleton or your package publishes translation files, review the layout.

```php theme={null}
// Use langPath() to determine the publish destination instead of a fixed path
$this->publishes([
    __DIR__.'/../lang' => app()->langPath('vendor/package-name'),
]);
```

***

### Password rule change

**Impact: medium**

The `password` rule, which validated the current user's password, has been renamed to `current_password`.

```php theme={null}
// Before
'password' => ['required', 'password'],

// After
'password' => ['required', 'current_password'],
```

***

### Behavior changes for `when` / `unless`

**Impact: medium**

In Laravel 8, passing a closure to `when` or `unless` made the closure itself truthy, unintentionally triggering the branch. In Laravel 9, the closure is executed and its return value is used as the condition.

```php theme={null}
$collection->when(function ($collection) {
    // The returned value is what's used as the condition
    return false;
}, function ($collection) {
    // Not executed because false was returned
    $collection->merge([1, 2, 3]);
});
```

***

### Handling of unvalidated array keys

**Impact: medium**

In Laravel 9, the array returned from `validated()` always excludes unvalidated array keys. To preserve Laravel 8's behavior, explicitly call `includeUnvalidatedArrayKeys()`.

```php theme={null}
use Illuminate\Support\Facades\Validator;

public function boot()
{
    // Enable only if you need Laravel 8 behavior
    Validator::includeUnvalidatedArrayKeys();
}
```

***

## Summary

The upgrade from Laravel 8 to 9 centers on PHP 8.0.2, the switch to Symfony Mailer, and Flysystem 3.x compatibility. Reviewing mail sending, storage, custom casts, and test helpers first reduces post-upgrade issues.

| Change                               | Impact | Action                                                              |
| ------------------------------------ | ------ | ------------------------------------------------------------------- |
| PHP 8.0.2 / `laravel/framework:^9.0` | High   | Update `composer.json` and runtime                                  |
| Move to Symfony Mailer               | High   | Review SwiftMailer APIs and mail config                             |
| Flysystem 3.x                        | High   | Review `Storage` return values, exceptions, and driver dependencies |
| `BelongsToMany` upsert methods       | Medium | Note the search target is now the related model's table             |
| Custom casts and `null`              | Medium | Fix `set()` to handle `null`                                        |
| HTTP client 30s timeout              | Medium | Set `timeout()` explicitly where needed                             |
| `assertDeleted` deprecated           | Medium | Replace with `assertModelMissing()`                                 |
| Moved `lang` directory               | Medium | Replace fixed paths with `app()->langPath()`                        |
| Password rule change                 | Medium | Use `current_password`                                              |
| Excluding unvalidated array keys     | Medium | Use `includeUnvalidatedArrayKeys()` only when needed                |

***

## References

* [Official upgrade guide (English)](https://laravel.com/docs/9.x/upgrade)
* [laravel/docs 9.x `upgrade.md`](https://github.com/laravel/docs/blob/9.x/upgrade.md)
* [laravel/laravel diff (8.x → 9.x)](https://github.com/laravel/laravel/compare/8.x...9.x)
* [Laravel Shift](https://laravelshift.com) — community service to automate upgrades
* [Symfony Mailer docs](https://symfony.com/doc/6.0/mailer.html)
* [Flysystem 3 docs](https://flysystem.thephpleague.com/v3/docs/)


## Related topics

- [Package Static Analysis (PHPStan / Larastan)](/en/advanced/package-static-analysis.md)
- [Upgrading from Laravel 9 to 10](/en/blog/upgrade-9-to-10.md)
- [Upgrading from Laravel 10 to 11](/en/blog/upgrade-10-to-11.md)
- [Laravel Horizon](/en/horizon.md)
- [Laravel Socialite (Social Authentication)](/en/socialite.md)
