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 updatecomposer.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/updateOrCreateonBelongsToMany- Custom casts and
nullbehavior - HTTP client default timeout
- PHP return types added
- Postgres
schemaconfig key renamed - The
assertDeletedmethod is deprecated - Moved
langdirectory - 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 yourcomposer.json dependencies first.
- Remove
facade/ignitionand replace it withspatie/laravel-ignition:^1.0 - Update
pusher/pusher-php-serverto^5.0if used - Verify that third-party packages you use have Laravel 9 support
- Consult individual upgrade guides for Vonage notification channels if used
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 useMail::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 SMTPstream option; supported settings move to the top level.
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 theStorage 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/writeStreamoverwrite existing files by default- Write failures return
falserather than throwing - Reading a missing file returns
nullrather than throwing deleteon a missing file returnstrue- The cached adapter has been removed—you can delete the
cachekey fromdiskconfig
throw option.
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 likeoffsetGet, 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 Thepassword 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 fromvalidated() 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
- Official upgrade guide (English)
- laravel/docs 9.x
upgrade.md - laravel/laravel diff (8.x → 9.x)
- Laravel Shift — community service to automate upgrades
- Symfony Mailer docs
- Flysystem 3 docs