Skip to main content

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

Automated upgrades with Laravel Shift

You can automate the upgrade with Laravel Shift. 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.
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:

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

From withSwiftMessage to withSymfonyMessage

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

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.
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.
firstOrCreate also now accepts a second $values argument, matching the behavior of other relations.

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.

HTTP client default timeout

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

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.

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.

From assertDeleted to assertModelMissing

Impact: medium Replace assertDeleted, previously used to confirm model deletion, with assertModelMissing.

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.

Password rule change

Impact: medium The password rule, which validated the current user’s password, has been renamed to 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.

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().

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.

References

Last modified on July 13, 2026