From Laravel 8 to Laravel 10 – A migration guide and new features introduction

From Laravel 8 to Laravel 10 – A migration guide and new features introduction

Why should you migrate from Laravel 8?

Laravel’s new version is now released yearly. Changes such as deprecated features and new features keep coming up. Therefore, the more versions ahead from the latest version you are, the more “painful” and time-consuming it is to upgrade compared with upgrading regularly. The main reason for this is that you let your codebase grow too large (classes, methods, and functions become more and more dependent on the others), and it is much harder to make any breaking change. Another thing you should be aware of is that since Laravel 10 was released, Laravel 8 is neither in maintenance nor has support. Therefore, the sooner your project gets upgraded, the better things will be.

In this blog, we will show you how to upgrade your project from Laravel 8 to Laravel 10. This guide is about manual upgradation. Therefore, if you want an automated solution, you should consider using the Laravel Shift (this is not a free service).

Laravel 10’s new features introduction:

1. Laravel Pennant:

This is a new package that ships with Laravel 10. It provides a feature flag function to your application. When you flag a feature, that feature will be considered available or unavailable. Let’s say your application has a “meeting booking” feature, and this feature is only available for users whose role is “boss”. You might manage that feature availability like the below:

For more information, please check the Laravel Pennant documentation.

2. Process Interaction:

Laravel 10.x introduces a beautiful abstraction layer for starting and interacting with external processes via a new Process facade. You can execute commands in your application or fake for convenient testing, etc.:

3. Test Profiling:

The Artisan test command has received a new –profile option that allows you to easily identify the fastest or slowest tests in your application:

php artisan test --profile

For convenience, the slowest tests will be displayed directly within the CLI output:

4. Pest Scaffolding:

Pest is a PHP testing framework provided by  PHPUnit. And Laravel has now supported testing with Pest. New Laravel projects may now be created with Pest test scaffolding by default. To opt-in to this feature, provide the –pest flag when creating a new application via the Laravel installer:

laravel new example-application --pest

Laravel 9’s new features introduction:

1. Simplified Accessors and Mutators:

“Accessors and Mutators” are also called “getters and setters”. In Laravel, traditionally, you add a prefix to the attribute name like the one below to declare a “getter or setter”. The getXAttribute() and setXAttribute() API for declaring accessors and mutators is not going away; However, there’s a new, simplified approach that will be recommended going forward:

2. Enum Attribute Casting:

Now that PHP 8 offers enum class support. Therefore, Laravel 9 has been updated to include Eloquent attribute casting to and from an enum object. For example, you can now declare and use an enum like the below:

3. Implicit Route Bindings With Enums:

This feature allows you to bind a request’s parameter with an enum. Let’s say you declare an enum, and then you are binding it like below. With this binding, any request that passes a value that is not a valid enum will lead to an HTTP 404 response. This is convenient since you no longer have to check it in controllers:

4. Forced Scoping Of Route Bindings:

In previous releases of Laravel, you can scope the second Eloquent model in a route definition such that it must be a child of the previous Eloquent model. For example, consider this route definition that retrieves a blog post by slug for a specific user:

When you declare the route as above, Laravel will try to determine the foreign key base on the predefined set of rules called conventions. But first, you must define the custom key binding for :slug; otherwise, Laravel will not acknowledge that there is a second model binding required.

However, in the newer version, you only have to invoke the scopeBindings(), and it works the same:

5. Controller Route Groups:

You can now group routes by a controller like the one below:

6. Full-Text Indexes / Where Clauses:

You could use new methods like whereFullText(), orWhereFullText() to perform full-text search query:

7. Laravel Scout Database Engine:

The main purpose of Laravel Scout is to simplify the full-text search process of your project. It is a driver-based solution so that you can work with many different engines, such as Elasticsearch, Algolia, MeiliSearch, and MySQL. PostgreSQL,… using the Scout API.

The idea is to make a search() method available to use for your Eloquent model. Also, it automatically performs indexing when the data is written. Keep in mind that the search() method is not supported complex queries like Elasticsearch DSL; therefore, Scout is only suitable for small or medium projects. Have a look at the below snippet:

8. Rendering Inline Blade Templates:

Instead of defining your template in a .blade.php file, you can simply render it within a call to Blade::render(). This is very useful when you need to make an AJAX call to retrieve a partial view for your SPA application.

9. Slot Name Shortcut:

In previous releases of Laravel, slot names were provided using a name attribute on the x-slot tag. However, beginning in Laravel 9.x, you may specify the slot’s name using a convenient, shorter syntax:

10. Improved Validation Of Nested Array Data:

Sometimes you may need to access the value for a given nested array element when assigning validation rules to the attribute. You may now accomplish this using the Rule::forEach() method.

11. Laravel Breeze API & Next.js:

The Laravel Breeze starter kit has received an “API” scaffolding mode and complimentary Next.js frontend implementation. This starter kit scaffolding may be used to jump-start your Laravel applications that are serving as a backend, Laravel Sanctum authenticated API for a JavaScript frontend.

12. Bundling with Vite:

Besides new features, bundling your project assets in Laravel is now migrated from using Mix webpack to Vite. You can check it at the official docs of Laravel here.

13. Improved Ignition Exception Page:

The exception debug page has been redesigned from the ground up. The new, improved will help you debug easier and more efficiently:


Vulnerabilities in security that are covered by the newer version:

Blocking of the upload of .phar content:

When you upload executable PHP files, they are blocked by Laravel for security purposes, including '.php', '.php3', '.php4', '.php5', '.php7', '.php8', '.phtml' files. And Laravel has now included the missing extension '.phar'.

SQL Injection in SQL Server:

Laravel has now covered a vulnerability that opens for SQL Injection attacks when passing user input directly to the limit() and offset() functions of SQL Server (Other database drivers such as MySQL and Postgres are not affected by this vulnerability).

SQL Injection – Binding query parameter using an array:

The below code has a vulnerability that could lead to a SQL Injection attack. Because the binding value of is_admin would-be 1 instead 0. This happens due to the false binding when passing an array $value = [1,1] to the where() method.

User::where('id', [1,1])->where('is_admin', 0)->first();
// sql: select * from `users` where `id` = 1 and `is_admin` = 1

But this has now been covered in the update. The query string will now be like this:

// sql: select * from `users` where `id` = 1 and `is_admin` = 0

In the above section, we have talked about Laravel 9 and Laravel 10 new features, and the security vulnerabilities are covered. Next, let us show you how to upgrade your Laravel project from version 8 to 10.

Migrate your project from Laravel 8 to Laravel 10:

Upgrade to PHP 8.1.0 & Composer 2.2.0:

Since Laravel 10 requires at least PHP 8.1.0, you must upgrade to PHP 8.1.0. Also, you must upgrade Composer to 2.2.0 or greater. This is a very crucial step. You must complete it before proceeding to the next section.

Make changes to your dependencies in composer.json:

a. Packages version upgrade:

Below are packages that need to update to newer versions. Follow the instruction to update them.

In your composer.json, change the version of these items as the following:

  "require": {
    "php": "^8.1",
    "laravel/framework": "^10.0",
    "laravel/sanctum": "^3.2",
    "doctrine/dbal": "^3.0",
    "laravel/passport": "^11.0"
  "require-dev": {
    "nunomaduro/collision": "^6.1",
    "spatie/laravel-ignition": "^2.0"

If your project uses the Broadcasting feature, you must also change this item version:

  "require": {
    "pusher/pusher-php-server": "^5.0"

If you use the S3, FTP, or SFTP drivers through Storage, you must also change this item version:

  "require": {
    "league/flysystem-aws-s3-v3": "^3.0",
    "league/flysystem-ftp": "^3.0",
    "league/flysystem-sftp-v3": "^3.0"

Furthermore, if you wish to use PHPUnit 10, you should delete the processUncoveredFiles="true" attribute from the <coverage> section of your application’s phpunit.xml configuration file. Then, update the following dependencies in your application’s composer.json file:

  "require-dev": {
    "nunomaduro/collision": "^7.0",
    "phpunit/phpunit": "^10.0"

b. Removed packages:

These packages are no longer required or used, so you should remove it.

Trusted Proxies:

In your app/Http/Middleware/TrustProxies.php file, update

use Fideloper\Proxy\TrustProxies as Middleware;

to this

use Illuminate\Http\Middleware\TrustProxies as Middleware;

Next, in app/Http/Middleware/TrustProxies.php, you should update the $headers property definition:

// Before...
protected $headers = Request::HEADER_X_FORWARDED_ALL;
// After...
protected $headers =

Finally, you can remove the fideloper/proxy from your application:

  "require": {
    // Remove this package
    "fideloper/proxy": "^4.4"


This package is no longer maintained. And in Laravel 10, it is removed. First, in the app\Http\Kernel.php, you must change the namespace of HandleCors middleware.




Then, remove it from composer.json:

  "require": {
    // Remove this package
    "fruitcake/laravel-cors": "^2.0"

c. Packages Replacement:

Switch these items with their replacement in composer.json.

  "require-dev": {
    // Replace this
    "facade/ignition": "^2.5"
    // With this
    "spatie/laravel-ignition": "^1.0"

For the Laravel SMS Notifications feature, you must be aware that Nexmo is now acquired by Vonage. Therefore, make changes to your composer.json:

  "require": {
    // Replace this
    "laravel/nexmo-notification-channel": "^2.0"
    // With this
    "laravel/vonage-notification-channel": "^3.0"

d. Swift Mailer is replaced with Symfony Mailer:

One of the largest changes in Laravel 9.x is the transition from SwiftMailer, which is no longer maintained as of December 2021, to Symfony Mailer. To upgrade, run the following command:

composer remove wildbit/swiftmailer-postmark
composer require symfony/mailgun-mailer symfony/postmark-mailer symfony/http-client

Run “composer update”:

After making all changes to the composer.json, run the “composer update” command. Be aware that all the above packages are just for Laravel basis, so you must also upgrade your project’s other dependencies (if required). If you don’t upgrade them, they will conflict with Laravel dependencies.

When you have successfully run the command, next, you need to refactor your source code to remove deprecated features so that your project can normally function. Finally, you might want to consider using new features or applying features that changed in the new version of Laravel.

Configuration changes:

In your config/database.php, change the ‘schema’ to ‘search_path’ for Postgres database configuration:

'connections' => [
  'pgsql' => [
    // replace this
    'schema' => 'public',
    // by this
    'search_path' => 'public',

If you are using SFTP through Laravel Storage, in your config/filesystems.php:

'sftp' => [
  // Replace this         
  'password' => env('SFTP_PASSPHRASE'),
  // By this
  'passphrase' => env('SFTP_PASSPHRASE'),

Defining stream options for the SMTP is no longer supported. Instead, you must define the relevant options directly within the configuration if they are supported. For example, to disable TLS peer verification, you can follow the below configuration. Also, notice that auth_mode is commented out since this is no longer available in Laravel’s new version.

'smtp' => [
  // 'auth_mode' => null,

  // Laravel 8.x...
  'stream' => [
      'ssl' => [
          'verify_peer' => false,
  // Laravel 9.x...
  'verify_peer' => false,

Directories structure changes:

In new Laravel applications, the resources/lang directory is now located in the root project directory (lang). If you’ve been hardcode the lang directory, you must update it. You should use app()->langPath() instead of a hard-coded path.

Refactor your code:

There are breaking changes and deprecated features that are required to make changes in the source code when upgrading to Laravel 10. You don’t have to change the code for every feature list below because it depends on whether the feature is used by your project.

Removed functions:

These functions exist in Laravel 8 but are no longer available in Laravel’s newer version. So you need to search for all of them in your source code, then fix it manually.

EnumeratesValues Trait: the reduceWithKeys() method is removed, so you might use reduce() method instead. And the reduceMany() method has been renamed to reduceSpread().

Illuminate\Database\Schema\Builder::registerCustomDoctrineType() method has been removed. You may use the registerDoctrineType() method on the DB facade instead or register custom Doctrine types in the config/database.php configuration file.

Testing: All assertDeleted() method calls should be updated to assertModelMissing().

Queue: the Bus::dispatchNow() and dispatch_now() methods have been removed. Instead, your application should use the Bus::dispatchSync() and dispatch_sync() methods, respectively.

The Redirect::home() method has been removed. Instead, your application should redirect to an explicitly named route: return Redirect::route('home').

[Deprecated] – Blade – Lazy Collections & The $loop Variable:

    use App\Models\Car;
    $cars = Car::cursor();  // cars lazy collection
    foreach($cars as $c) {
        echo $loop->iteration;

In the above snippet, you are trying to use $loop var in a LazyCollection. This is no longer suporrted. This doesn’t mean that the $loop var is removed, but you should not use it with LazyCollection because it causes the entire Collection to be loaded into memory (that is not how LazyCollection is supposed to be).

[Removed] – Storage:

Storage – Flysystem no longer supports cached-adapters feature. You should remove it using the composer command:

composer require league/flysystem-cached-adapter

[Removed] Get DB Expression string value:

In the previous version, you might do this to get the Expression string:

$expression = DB::raw('select * from news');
$expStr = (string)$expression;

But that is no longer supported, instead:

$expStr = $expression->getValue(DB::connection()->getQueryGrammar());

[Removed] Eloquent model’s $date property:

The Eloquent model’s deprecated $dates property has been removed.

protected $dates = [

Your application should now use the $casts property:

protected $casts = [
    'deployed_at' => 'datetime',

[Removed] Testing – Service Mocking:

The deprecated MocksApplicationServices trait has been removed from the framework. This trait provided testing methods such as expectsEvents(), expectsJobs(), and expectsNotifications(). If your application uses these methods, we recommend transitioning to Event::fake, Bus::fake, and Notification::fake, respectively. You can learn more about mocking via fakes in the corresponding documentation for the component you are attempting to fake.

[Changed] Validation – the ‘password’ Rule:

The password rule validates that the given input value matches the authenticated user’s current password and has been renamed to current_password.

[Changed] Blade – Avoid overwriting Vue snippet:

Laravel 9 comes with new blade directives @diabled, @checked, and @selected that will overwrite your Vue directives. Therefore, to escape it, you must replace them with escaped ones @@disabled, @@checked, @@selected.

[Changed] – Collections:

You can now pass a closure as the first argument into Collection::when(), unless(). In the previous version, the first argument is always treated as a value and will not be executed. Now you can do this.

$collection->when(function ($collection) {
  // This closure is executed...
  return false;
}, function ($collection) {
  // Not executed since first closure returned "false"...
  $collection->merge([1, 2, 3]);

[Changed] – Eloquent – custom cast with a null value:

In Laravel 8, the mutator set() method won’t be executed when $value = null. But in Laravel 9, it will be executed. Due to this change, the result of the below snippet is totally different:

// In App\Casts\FilenameWithTimestamp.php
public function set($model, $key, $value, $attributes) {
  return time() . '_' . $value;

// In App\Models\File.php
protected $casts = [
  'filename' => FilenameWithTimestamp::class

// Somewhere in your program
// With Laravel 8, the result of the echo statement will be: null
// With Laravel 9, the result of the echo statement will be: "20230322_"
$file = File::first();
$file->filename = null;
echo $file->filename;

As you can see, it makes no sense the filename got prefixed with a timestamp even when it was set to null. So, in Laravel 9, you must handle “the null case” for all of your custom casts. E.g.:

// In App\Casts\FilenameWithTimestamp.php
public function set($model, $key, $value, $attributes) {
  if (empty($value)) {
    return '';
  return time() . '_' . $value;

Now it works as expected.

[Changed] Storage – Throw exception behavior:

Previously, Laravel will throw exceptions for failure operations such as reading or deleting unexisting files. But in the newer version, Laravel simply returns the appropriate result, such as false, null, or true,…
This change could make an impact on your program if you’ve been using the try-catch block to handle the failures. Therefore, you either config to the default behavior (like below) or just refactor your code to check for failures (e.g., using if-else block)

'public' => [
  'driver' => 'local',
  // ...
  'throw' => true,

[Changed] Storage – Custom Filesystems:

Slight changes have been made to the steps required to register custom filesystem drivers. Therefore, if you were defining your own custom filesystem drivers or using packages that define custom drivers, you should update your code and dependencies.

For example, in Laravel 8.x, a custom filesystem driver might be registered like so:

use Illuminate\Support\Facades\Storage;
use League\Flysystem\Filesystem;
use Spatie\Dropbox\Client as DropboxClient;
use Spatie\FlysystemDropbox\DropboxAdapter;

Storage::extend('dropbox', function ($app, $config) {
    $client = new DropboxClient(

    return new Filesystem(new DropboxAdapter($client));

However, in Laravel 9.x, the callback given to the Storage::extend method should return an instance of Illuminate\Filesystem\FilesystemAdapter directly:

use Illuminate\Filesystem\FilesystemAdapter;
use Illuminate\Support\Facades\Storage;
use League\Flysystem\Filesystem;
use Spatie\Dropbox\Client as DropboxClient;
use Spatie\FlysystemDropbox\DropboxAdapter;

Storage::extend('dropbox', function ($app, $config) {
    $adapter = new DropboxAdapter(
        new DropboxClient($config['authorization_token'])

    return new FilesystemAdapter(
        new Filesystem($adapter, $config),

[Changed] Migrate from Swift Mailer to Symfony Mailer:

In Lavarel 8 and the previous version, Laravel used the Swift Mailer library to send outgoing emails. However, that library is no longer maintained and has been succeeded by Symfony Mailer, which made this transition one of the largest changes. Therefore, you must refactor your code base as below.

Renamed “Swift” Methods: various SwiftMailer-related methods, some of which were undocumented, have been renamed to their Symfony Mailer counterparts. Use the advanced search of your IDE and perform the mass replacement for these methods with the following:





Mailer::setSymfonyTransport(TransportInterface $transport);


MessageSent Event Changes:

Illuminate\Mail\Events\MessageSent event has changed to include an instance of type Symfony\Component\Mime\Email instead of type Swift_Message. This instance contains data about the message before it is sent. At the same time, a new property called sent has been added too. This new property contains data about the message after it is sent, for example, the MessageID.

Failed Recipients:

It is no longer possible to retrieve a list of failed recipients after sending a message. Instead, a Symfony\Component\Mailer\Exception\TransportExceptionInterface exception will be thrown if a message fails to send.

Comprehensive Exam:

We have created the exam to check your comprehension of this article.
Let’s try it!!