Introduction
Laravel 9 was released on February 8, 2022. This guide walks you through the upgrade process from Laravel 8.x to 9.x and explains the major breaking changes.The estimated time required for the upgrade is approximately 30 minutes. However, the actual time may vary depending on your application’s usage of email sending, file storage, custom casts, and framework core class overrides.
Using Laravel Shift for Automatic Upgrades
You can also automate the upgrade process using Laravel Shift. Shift helps with updatingcomposer.json and configuration files, making it a convenient starting point for reviewing differences.
Breaking Changes Overview
High-Impact Changes
- Dependency updates
- Migration to Flysystem 3.x
- Migration to Symfony Mailer
Medium-Impact Changes
BelongsToManyfirstOrNew/firstOrCreate/updateOrCreatemethods- Custom Casts and
nullhandling - HTTP client default timeout
- PHP Return Types additions
- PostgreSQL
schemaconfiguration rename assertDeletedmethod deprecationlangdirectory relocation- Password rule changes
when/unlessmethod behavior changes- Unvalidated array keys in validation
Upgrade Steps
Updating Dependencies
Impact Level: High Laravel 9 requires PHP 8.0.2 or higher. First, review the dependencies in yourcomposer.json:
- Replace
facade/ignitionwithspatie/laravel-ignition:^1.0 - Update
pusher/pusher-php-serverto^5.0if using Pusher - Verify third-party packages support Laravel 9
- Check Vonage notification channel upgrade guide if applicable
PHP Version Requirements
Impact Level: High Laravel 9 requires PHP 8.0.2 or higher. Ensure your CI/CD pipeline, local development environment, and production servers all run the same PHP version before upgrading.Migration to Symfony Mailer
Impact Level: High A major change in Laravel 9 is the migration from SwiftMailer (maintenance ended December 2021) to Symfony Mailer. Applications using only standardMail::to()->send() will see minimal impact, but those directly accessing SwiftMailer’s low-level APIs will need adjustments.
Driver Dependencies
Transitioning from withSwiftMessage to withSymfonyMessage
send, html, raw, and plain methods on Illuminate\Mail\Mailer now return Illuminate\Mail\SentMessage instead of void. Additionally, the message property of the MessageSent event now contains Symfony\Component\Mime\Email instead of Swift_Message.
Reviewing SMTP Configuration
Symfony Mailer removes the SMTPstream option, with supported configurations moving to the top level:
auth_mode setting no longer requires explicit configuration. It’s safer to validate email addresses before sending rather than collecting invalid addresses afterward.
Migration to Flysystem 3.x
Impact Level: High Laravel 9 updates the internal implementation of theStorage facade from Flysystem 1.x to 3.x. File operation methods maintain backward compatibility as much as possible, but there are differences in exceptions, return values, and adapter registration.
Installing Additional Drivers
Major Storage Behavior Changes
put/write/writeStreamoverwrite existing files by default- Write failures return
falseinstead of throwing exceptions - Reading non-existent files returns
nullinstead of throwing exceptions - Deleting non-existent files returns
true - The cached adapter was removed; remove
cachekeys fromdiskconfigurations
throw option:
Storage::extend() callback to directly return Illuminate\Filesystem\FilesystemAdapter.
BelongsToMany firstOrNew / firstOrCreate / updateOrCreate
Impact Level: Medium
In Laravel 8, the first argument’s attributes were compared against the pivot table. In Laravel 9, they’re compared against the related model’s table:
firstOrCreate now accepts a second $values argument, aligning its behavior with other relationships:
Custom Casts and null
Impact Level: Medium
In Laravel 9, the custom cast’s set method is called even when null is assigned to a cast attribute. Custom casts that don’t account for null may throw exceptions after upgrading:
HTTP Client Default Timeout
Impact Level: Medium The HTTP client’s default timeout is now 30 seconds. Previously, it could wait indefinitely:PHP Return Types
Impact Level: Medium Laravel 9 adds return type hints to core classes to match PHP and Symfony requirements. If your application extends Laravel core classes and overrides methods likeoffsetGet, offsetSet, jsonSerialize, open, or read, add matching return types:
PostgreSQL schema Configuration Rename
Impact Level: Medium
If you’re using PostgreSQL with a search path configuration, update your config/database.php key from schema to search_path:
assertDeleted to assertModelMissing
Impact Level: Medium
Replace the deprecated assertDeleted method with assertModelMissing in your tests:
lang Directory Relocation
Impact Level: Medium
In new Laravel 9 applications, language files are located at the project root in lang instead of resources/lang. While existing applications continue to work, consider aligning with the new structure, especially when publishing package language files:
Password Rule Rename
Impact Level: Medium Thepassword validation rule that checks against the currently authenticated user’s password has been renamed to current_password:
when / unless Method Changes
Impact Level: Medium
In Laravel 8, passing a closure to when or unless would evaluate the closure itself as truthy, potentially causing unintended behavior. In Laravel 9, the closure is executed and its return value is used as the condition:
Unvalidated Array Keys Exclusion
Impact Level: Medium In Laravel 9,validated() always excludes unvalidated array keys from the returned array. To maintain Laravel 8’s behavior, explicitly call includeUnvalidatedArrayKeys():
Summary
Upgrading from Laravel 8 to 9 focuses on updating to PHP 8.0.2, migrating to Symfony Mailer, and handling Flysystem 3.x changes. Pre-upgrade audits of email sending, file storage, custom casts, and test helpers reduce post-upgrade issues.| Breaking Change | Impact | Action |
|---|---|---|
PHP 8.0.2 / laravel/framework:^9.0 | High | Update composer.json and runtime environment |
| Symfony Mailer migration | High | Review SwiftMailer APIs and mail configuration |
| Flysystem 3.x | High | Test Storage return values, exceptions, and driver dependencies |
BelongsToMany upsert methods | Medium | Verify search targets the related model table |
Custom Casts and null | Medium | Handle null in set() method |
| HTTP client 30-second timeout | Medium | Explicitly set timeout() for long-running requests |
assertDeleted deprecation | Medium | Use assertModelMissing() instead |
lang directory relocation | Medium | Use app()->langPath() instead of hardcoded paths |
password rule rename | Medium | Update to current_password |
| Unvalidated array keys exclusion | Medium | Use includeUnvalidatedArrayKeys() only if needed |
References
- Official Upgrade Guide (English)
- laravel/docs 9.x
upgrade.md - laravel/laravel Repository Differences (8.x → 9.x)
- Laravel Shift — Community service for automated upgrades
- Symfony Mailer Documentation
- Flysystem 3 Documentation