Laravel chaining multiple jobs with an example
Tags: laravel, tutorial, php, laravelqueue
Published: 2022-09-23 03:13:03




![Laravel queue jobs](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/evvgq4pnolo0yi2d1za5.gif)
In this article, I will show you how to perform Job chaining in laravel. For this example, I am uploading an excel file with thousands of rows. Laravel queue will allow importing an excel in the background without making the user wait on the page until the importing has been finished. Once the importing excel finishes, we will send an email to the user to inform him that the file was imported successfully. Job chaining helps to process multiple jobs sequentially. In our example importing an excel is one job and sending a notification email is another job. However, your case might be different. You may want to register a user and send a welcome message once they successfully registered, or you may want to process the user's orders and send an invoice to the user's email. In all these cases, you want your application to run quickly. This can be achieved with a Laravel queue that allows us to run tasks asynchronously. 

For this, I am using
Laravel 9: https://laravel.com
Laravel-Excel package: https://laravel-excel.com
Mailtrap to receive the email: https://mailtrap.io

Let's start:
Part1:
Install excel package: `composer require maatwebsite/excel`
add the ServiceProvider in `config/app.php`

```php
'providers' => [
    Maatwebsite\Excel\ExcelServiceProvider::class,
]
```
add the Facade in `config/app.php`

```php
'aliases' => [
    'Excel' => Maatwebsite\Excel\Facades\Excel::class,
]
```
Part2: Create a migration file for order
My excel has orders information so I want to create an `orders` table and model 

`php artisan make:model Order -m`
```php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('orders', function (Blueprint $table) {
            $table->id();
            $table->string('order');
            $table->string('order_date');
            $table->string('order_qty');
            $table->string('sales');
            $table->string('ship_model');
            $table->string('profit');
            $table->string('unit_price');
            $table->string('customer_name');
            $table->string('customer_segment');
            $table->string('product_category');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('orders');
    }
};
```
run `php artisan queue:table` to create jobs table. A jobs table hold the information about queue, payload, number of attempts etc of unprocessed jobs. Any information about failed jobs will be stored in the `failed_jobs` table.
and finally migrate these files by running command `php artisan migrate`
Part3: Let's work on `.env  `file
change `QUEUE_CONNECTION=sync` to `QUEUE_CONNECTION=database`
also, login to [mailtrap](https://mailtrap.io/) and get your username and password 
and the setting will be as below:

```php
MAIL_MAILER=smtp
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=your_username_from_mailtrap
MAIL_PASSWORD=your_password_from_mailtrap
MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_NAME="${APP_NAME}
```

Part 4: Now let's make a `OrdersImport` class
`php artisan make:import OrdersImport --model=Order`
The file can be found in `app/Imports`

![excel header](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/teivsbogqibzajdzgouc.png)
As you can see my excel headings are not well formatted, there is space between two texts. Laravel excel package can handle this very easily with `Maatwebsite\Excel\Concerns\WithHeadingRow;`
 Check this:[Click here](https://docs.laravel-excel.com/3.1/imports/heading-row.html#heading-row)

 So after implementing `WithHeadingRow`, my Excel `Order Id` has changed to `order_id`, `Order Date ` has changed to `order_date`, and so on. You can check this by doing `dd($row)` below.

```php
<?php

namespace App\Imports;

use App\Models\Order;
use Maatwebsite\Excel\Concerns\ToModel;
use Maatwebsite\Excel\Concerns\WithHeadingRow;

class OrdersImport implements ToModel,WithHeadingRow
{
    /**
    * @param array $row
    *
    * @return \Illuminate\Database\Eloquent\Model|null
    */
    public function model(array $row): Order
    {
        return new Order([
            'order'     => $row['order_id'],
            'order_date'    => $row['order_date'], 
            'order_qty' => $row['order_quantity'],
            'sales' => $row['sales'],
            'ship_model' => $row['ship_mode'],
            'profit' => $row['profit'],
            'unit_price' => $row['unit_price'],
            'customer_name' => $row['customer_name'],
            'customer_segment' => $row['customer_segment'],
            'product_category' => $row['product_category'],
        ]);
    }
}
```
Part 5: let's make a route 

```php
Route::get('upload', [UploadController::class,'index']);
Route::post('upload', [UploadController::class,'store'])->name('store');
```
The `index` method will render the view which has an upload form and `store` method will handle upload.
Part6: let's make `UploadController` with command `php artisan make:controller UploadController`

Part7: let's make an upload form in `resources/views` with the name `index.blade.php` 

```php
@extends('layouts.app')
@section('content')
    <div class="container">
        <div class="row justify-content-center">
            <div class="col-md-8">
                <div class="card">
                    <div class="card-header">{{ ('Upload') }}</div>
                    <div class="card-body">
                        <form action="{{ route('store') }}" method="post" enctype="multipart/form-data">
                            @csrf
                            <input type="file" name="order_file" class="form-control" required>
                            <button class="btn btn-primary" id="btn" type="submit">Upload </button>
                        </form> 
                    </div>
                </div>
            </div>
        </div>
    </div>
@endsection
```
Part8: Now we will make two methods in `UploadController.php` `index` method will return a view that has an upload form and the `store` method will have a logic to store the excel data in the database and send the user an email once the task is finished. However, we will use job classes to perform these actions.

```php
public function index()
{
    return view('index');
}

public function store()
{
}
```
Part9: Now let's make a two Job class with the name `ProcessUpload` and `SendEmail`.These two files will be located in the `app/Jobs` directory of your project normally containing only a `handle` method that is invoked when the job is processed by the queue. Every job class implements the `ShouldQueue` interface and comes with constructor and handle() methods.
`php artisan make:job ProcessUpload
 php artisan make:job SendEmail`

`ProcessUpload.php `

```php
<?php

namespace App\Jobs;

use App\Imports\OrdersImport;
use Illuminate\Bus\Queueable;
use Maatwebsite\Excel\Facades\Excel;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Contracts\Queue\ShouldBeUnique;

class ProcessUpload implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    /**
     * Create a new job instance.
     *
     * @return void
     */
    public function __construct()
    {
    }

    /**
     * Execute the job.
     *
     * @return void
     */
    public function handle()
    {
        // we will write our logic to import a file 
    }
}
```
Part 10: Now let's work on the `store` method of `UploadController.php `. Here we will store the excel file in the storage directory and dispatch the path and email to `ProcessUpload.php` and `SendEmail.php` respectively. We will use `Job Chaining`, which allows us to perform multiple jobs to group together and processed them sequentially. To achieve this we have to make use of the `Bus` facade and call the `chain` method. 
Now, our `store` method looks like this:
```php
public function store(Request $request): string
{
    $file = $request->file('order_file')->store('temp'); 
    $path = storage_path('app'). '/' .$file;  
    $email = 'ab@gmail.com'; // or auth()->user()->email
    Bus::chain([
    new ProcessUpload($path),
    new SendEmail($email)
    ])->dispatch();

    return 'Your file is being uploaded. We will email you once it is completed';
}
```

Here we just passed some data to `ProcessUpload.php` and `SendEmail.php` and return a message to notify the user, remember to use `use Illuminate\Support\Facades\Bus;`

Now our `UploadController.php` looks like this
```php
<?php

namespace App\Http\Controllers;

use App\Jobs\SendEmail;
use Illuminate\View\View;
use App\Jobs\ProcessUpload;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Bus;

class UploadController extends Controller
{
    public function index(): View
    {
        return view('index');
    }

    public function store(Request $request): string
    {
        $file = $request->file('order_file')->store('temp');
        $path = storage_path('app') . '/' . $file;
        $email = 'ab@gmail.com'; //auth()->user()->email
        Bus::chain([
            new ProcessUpload($path),
            new SendEmail($email)
        ])->dispatch();

        return 'Your file is being uploaded. We will email you once it is completed';
    }
}

```
Note:
 >Jobs can also be arranged in chains or batches. Both allow multiple jobs to be grouped together. The main difference between a `batch` and a `chain` is that jobs in a `batch` are processed simultaneously, Whereas jobs in a `chain` are processed sequentially. By making use of the `chain` method if one job fails to process the whole sequence will fail to process, which means if importing an excel file fails, sending a notification email will never be proceeded. So based on the type of work you want to perform, either you can use `batch()` or `chain()`. One advantage of using `batch` is that it helps to track the progress of your job. All the information like the total number of jobs, number of pending jobs, number of failed jobs etc will be stored in a `job_batches` table

Now let's work on `ProcessUpload.php` and `SendEmail.php`
first `ProcessUpload.php `

```php
<?php

namespace App\Jobs;

use App\Imports\OrdersImport;
use Illuminate\Bus\Queueable;
use Maatwebsite\Excel\Facades\Excel;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Contracts\Queue\ShouldBeUnique;

class ProcessUpload implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    /**
     * Create a new job instance.
     *
     * @return void
     */
    public string $data;
    public function __construct($data)
    {
        $this->data  = $data;
    }

    /**
     * Execute the job.
     *
     * @return void
     */
    public function handle(): void
    {
        Excel::import(new OrdersImport, $this->data);    
    }
}
```
As you can see in the handle method, we have received the file name as `$this->data`,and we used the `Excel` facade provided by the package to send those excel data to `OrdersImport`, and the rest `OrdersImport` will handle. `OrdersImport` will insert data into the database. 

Remember to use `Maatwebsite\Excel\Facades\Excel;` as above 

Part11: Now before we work on `SendEmail.php`, let's make Mailables with the name `NotificationEmail.php` and 
These classes will be stored in the `app/Mail` directory.

`php artisan make:mail NotificationEmail`

This is a very simple class that looks like this 
```php
<?php

namespace App\Mail;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;

class NotificationEmail extends Mailable
{
    use Queueable, SerializesModels;

    /**
     * Create a new message instance.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }

    /**
     * Build the message.
     *
     * @return $this
     */
    public function build()
    {
        return $this->view('notification.mail');
    }
}
```
Now let's make `mail.blade.php` inside `resources/views/notification` folder 

`mail.blade.php `

```html
<!DOCTYPE html>
<html>
<head>
	<title>Success Email</title>
</head>
<body>
<p>Congratulation! Your file was successfully imported.😃</p>
</body>
</html>
```
The message is simple. We just want to send the above message to user email when file imports finish .The final part is to work on `SendEmail.php`

Let's work on `SendEmail.php `

```php
<?php

namespace App\Jobs;

use Illuminate\Bus\Queueable;
use App\Mail\NotificationEmail;
use Illuminate\Support\Facades\Mail;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Contracts\Queue\ShouldBeUnique;

class SendEmail implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    /**
     * Create a new job instance.
     *
     * @return void
     */
    public string $email;
    public function __construct($email)
    {
        $this->email = $email; 
    }

    /**
     * Execute the job.
     *
     * @return void
     */
    public function handle(): void
    {
        Mail::to($this->email)->send(new NotificationEmail());
    }
}
```
To send a message, we use [Mail facade](https://laravel.com/docs/9.x/mail#sending-mail). We used the  `Mail` facade and specified the recipient's email. Finally, we are sending `mail` view to user which is specified in `NotificationEmail` class

Now final part is to run the queue and upload the excel file.
To run the queue worker `php artisan queue:work` inside your project directory. A queue worker is a regular process that runs in the background and begin processing the unprocessed job. 
Now. visit `/upload` and upload a excel file

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j6l2c6yce8hjsojiquz6.png)
Now you should see this in your terminal

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/g7ultvko6q4szc69f4gl.png)
This means both tasks, importing an excel file and sending an email was successful.
Now let's check `mailtrap` for the email, and there should be the email sent to you.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mc3vmek0mxistlc3me36.png)

**Delete files from Storage**

Did you notice we have stored all the excel files in the storage folder of our app? There is no reason to keep these files there forever as we no longer need them once we import all their data into the database.Therefore, we have to delete it from the storage. For this let's make another job with the name `DeleteFile`.
`php artisan make:job DeleteFile`
 Now, add this in UploadController within the store method as below:

```php
 public function store(Request $request): string
 {
      $file = $request->file('order_file')->store('temp');
      $path = storage_path('app') . '/' . $file;
      $email = 'ab@gmail.com'; //auth()->user()->email
      Bus::chain([
          new ProcessUpload($path),
          new SendEmail($email),
          new DeleteFile($file)// new class added
      ])->dispatch();

      return 'Your file is being uploaded. We will email you once it is completed';
  }
```
We passed `$file` in `DeleteFile` class, if you `dd($file)`, you will get something like this`temp/M3aj6Ee29CdgmrW9USwUezmEHpBmlV0DkXP8P0ce.xlsx` where **temp** is the folder inside **storage/app** directory that store all the uploaded excel files.Now, we have to write the logic in `DeleteFile.php` in `handle` method to delete the file from storage.
So, our `DeleteFile.php` looks like this:

```php
<?php

namespace App\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Storage;

class DeleteFile implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    /**
     * Create a new job instance.
     *
     * @return void
     */
    public string $file;
    public function __construct($file)
    {
        $this->file = $file;
    }

    /**
     * Execute the job.
     *
     * @return void
     */
    public function handle()
    {
        unlink(storage_path('app/'. $this->file));
    }
}

```
Restart queue: `php artisan queue:work`
![laravel queue](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6jd2z7gex5goytx6u0x8.png)
The [unlink()](https://www.php.net/manual/en/function.unlink.php) function is an inbuilt function in PHP which is used to delete files


**Dealing With Failed Jobs**

Sometime things do not work as expected. There may be a chance that jobs fail. The good thing is that Laravel provides a way to retry failed jobs. Laravel includes a convenient way to specify the maximum number of times a job should be attempted.
[Check this](https://laravel.com/docs/8.x/queues#dealing-with-failed-jobs)
In a job class, we can declare the `public $tries` property and specify the number of times you want Laravel to retry the process once it fails. You can also calculate the number of seconds to wait before retrying the job. This can be defined in the `$backoff` array. Let's take the example of `SendEmail.php` and let's throw the plain exception within the `handle` method and also declare `$tries` and `$backoff `property.
`SendEmail.php`

```php
<?php

namespace App\Jobs;

use Illuminate\Bus\Queueable;
use App\Mail\NotificationEmail;
use Illuminate\Support\Facades\Mail;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;

class SendEmail implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    /**
     * Create a new job instance.
     *
     * @return void
     */
    public string $email;
    public $tries = 3;
    public $backoff = [10, 30, 60];
    
    public function __construct($email)
    {
        $this->email = $email;
    }

    /**
     * Execute the job.
     *
     * @return void
     */
    public function handle(): void
    {
        throw new Exception();
       // Mail::to($this->email)->send(new NotificationEmail());
    }
}

```
It's a good idea to include `$tries` and `$backoff` property in every job class.In the above example, I have commented on the Mail sending feature and declared a plain exception just to test the retry features of Laravel.Now restart the queue: `php artisan queue:work`  and upload the file.
Now you should see `SendEmail` job was processed `three` times because we have mentioned `public $tries = 3` and 
when the first job failed, Laravel tried after 10 seconds, when the second job failed, Laravel tried in 30 seconds, and when the third job failed Laravel tried in 60 seconds. If it still fails after the third attempt, failed jobs will be stored in the `failed_jobs` table.

![laravel queue](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uh44vnxs4l10cyttzkco.png)
I hope this was helpful 
Thank you :)