Advertisement
  1. Code
  2. PHP
  3. Laravel

Deferring Tasks in Laravel Using Queues

Scroll to top

In this article, we're going to explore the Queue API in the Laravel web framework. It allows you to defer resource-intensive tasks during script execution to enhance the overall end user experience. After introducing the basic terminology, I'll demonstrate it by implementing a real-world example.

Page load time is an important aspect of any successful website as it affects both the SEO of the site and the overall user experience. More often than not, you end up looking to debug web pages with long page load times. Of course, there are different approaches you could use to rectify this issue.

Upon investigation, you often realize that certain code blocks are causing a delay in the page execution. The next thing you could try is identifying blocks that can be deferred for processing and that have no real impact on the end result of the current page. That should improve the overall web page speed as we've eliminated code blocks that were causing a delay.

Today, we're going to explore a similar concept in the context of the Laravel web framework. In fact, Laravel already provides a useful built-in API that allows us to defer the processing of tasks—the Queue API. Without wasting much of your time, I'll go ahead and discuss the basic elements of the Queue API.

Drivers, Connections, Queues, and Jobs

The basic purpose of the Queue API is to run jobs that are added in a queue. Next, the queue could belong to a specific connection, and that connection may belong to a specific queue driver configured with that connection itself. Let's briefly try to understand what I've just said.

Queue Drivers

In the same way you would have used a different driver for your database connection, you could also choose from a variety of different queue drivers. The Queue API supports different adapters like database, beanstalkd, sqs, and redis.

The queue driver is just a place that is used to store queue-related information. So if you're using a database queue driver, for example, the new job will be added in the jobs table in the database. On the other hand, if you've configured redis as the default queue driver, the job will be added to the redis server.

The Queue API also provides two special queue drivers for testing purposes—sync and null. The sync queue driver is used to execute a queue job immediately, while the null queue driver is used to skip a job so that it won't be executed at all.

Connections

When you configure the Queue API for the first time, you need to specify a default connection that should be used for default queue processing. At the very least, the connection is expected to provide the following information:

  • the queue driver that will be used
  • the queue driver's specific configuration values
  • the default queue name in which the job will be added

Queues

When you add any job into a queue, it'll be added into the default queue. That should be fine in most cases, unless you have jobs that need to be given higher priority over other jobs. In that case, you could create a queue named high and place the higher priority jobs in that particular queue.

When you run a queue worker that processes queued jobs, you could optionally pass the --queue parameter, which allows you to list queue names in the order in which they need to be processed. For example, if you specify --queue=high,default, it will first process jobs in the high queue, and once it's completed it fetches jobs in the default queue.

Jobs

A job in the Queue API is a task that's deferred from the main execution flow. For example, if you want to create a thumbnail when the user uploads an image from the front-end, you could create a new job that handles the thumbnail processing. In this way, you could defer the task of thumbnail processing from the main execution flow.

That was a basic introduction to the Queue API terminology. From the next section onwards, we'll explore how to create a custom queue job and run it by using a Laravel queue worker.

A Quick Look at the Queue Configuration File

In the previous sections, we've gone through the terminology of queues in Laravel. Let's have a quick look at the config/queue.php configuration file so that you can relate it to the concepts that we've discussed so far.

1
<?php
2
3
return [
4
5
    /*

6
    |--------------------------------------------------------------------------

7
    | Default Queue Connection Name

8
    |--------------------------------------------------------------------------

9
    |

10
    | Laravel's queue API supports an assortment of back-ends via a single

11
    | API, giving you convenient access to each back-end using the same

12
    | syntax for every one. Here you may define a default connection.

13
    |

14
    */
15
16
    'default' => env('QUEUE_CONNECTION', 'sync'),
17
18
    /*

19
    |--------------------------------------------------------------------------

20
    | Queue Connections

21
    |--------------------------------------------------------------------------

22
    |

23
    | Here you may configure the connection information for each server that

24
    | is used by your application. A default configuration has been added

25
    | for each back-end shipped with Laravel. You are free to add more.

26
    |

27
    | Drivers: "sync", "database", "beanstalkd", "sqs", "redis", "null"

28
    |

29
    */
30
31
    'connections' => [
32
33
        'sync' => [
34
            'driver' => 'sync',
35
        ],
36
37
        'database' => [
38
            'driver' => 'database',
39
            'table' => 'jobs',
40
            'queue' => 'default',
41
            'retry_after' => 90,
42
        ],
43
44
        'beanstalkd' => [
45
            'driver' => 'beanstalkd',
46
            'host' => 'localhost',
47
            'queue' => 'default',
48
            'retry_after' => 90,
49
            'block_for' => 0,
50
        ],
51
52
        'sqs' => [
53
            'driver' => 'sqs',
54
            'key' => env('AWS_ACCESS_KEY_ID'),
55
            'secret' => env('AWS_SECRET_ACCESS_KEY'),
56
            'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'),
57
            'queue' => env('SQS_QUEUE', 'your-queue-name'),
58
            'suffix' => env('SQS_SUFFIX'),
59
            'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
60
        ],
61
62
        'redis' => [
63
            'driver' => 'redis',
64
            'connection' => 'default',
65
            'queue' => env('REDIS_QUEUE', 'default'),
66
            'retry_after' => 90,
67
            'block_for' => null,
68
        ],
69
70
    ],
71
72
    /*

73
    |--------------------------------------------------------------------------

74
    | Failed Queue Jobs

75
    |--------------------------------------------------------------------------

76
    |

77
    | These options configure the behavior of failed queue job logging so you

78
    | can control which database and table are used to store the jobs that

79
    | have failed. You may change them to any database / table you wish.

80
    |

81
    */
82
83
    'failed' => [
84
        'driver' => env('QUEUE_FAILED_DRIVER', 'database'),
85
        'database' => env('DB_CONNECTION', 'mysql'),
86
        'table' => 'failed_jobs',
87
    ],
88
89
];

As you can see, you can choose from several different sets of connections. For each connection, you can configure the connection-specific information, queue driver, and default queue name. More importantly, you can also set the default queue connection which you want to use under the default key.

So that's the basics of the queue configuration file.

Create Your First Queue Job

By now, you should feel confident about queue jobs. From this section onwards, we're going to implement a real-world example that demonstrates the concept of queue jobs in Laravel.

You'll often need to create different thumbnail versions of an image uploaded by a user. In most cases, the developer tries to process it in real time so that different versions of images are created right away when the user uploads an image.

It seems to be a reasonable approach if you're going to create a couple of versions and it doesn't take too much time in the first place. On the other hand, if you're dealing with an application that requires heavy processing and thus eats up more resources, real-time processing could end up in a bad user experience.

The obvious option that first pops up in your mind is to defer processing of the thumbnail generation as late as possible. The simplest approach you could implement in this specific scenario is to set a cron job that triggers processing at regular intervals, and you should be fine.

A much better approach, on the other hand, is to defer and push the task into a queue, and let the queue worker process it when it gets a chance to do so. In a production environment, the queue worker is a daemon script that's always running and processing tasks in a queue. The obvious benefit of this approach is a much better end user experience, and you don't have to wait for the cron run as the job will be processed as soon as possible.

I guess that's enough theory to get started with an actual implementation.

Create the jobs Table

In our case, we're going to use the database queue driver, and it requires us to create the jobs table in the database. The jobs table holds all the jobs that need to be processed in the next queue worker run.

Before we go ahead and create the jobs table, let's change the default queue configuration from sync to database in the .env file.

1
...
2
...
3
QUEUE_CONNECTION=database
4
...
5
...

In fact, Laravel already provides an artisan command that helps us to create the jobs table. Run the following command in the root of your Laravel application, and it should create the necessary database migration that creates the jobs table.

1
$php artisan queue:table

The migration file which is generated at database/migrations/YYYY_MM_DD_HHMMSS_create_jobs_table.php should look like this:

1
<?php
2
3
use Illuminate\Database\Migrations\Migration;
4
use Illuminate\Database\Schema\Blueprint;
5
use Illuminate\Support\Facades\Schema;
6
7
class CreateJobsTable extends Migration
8
{
9
    /**

10
     * Run the migrations.

11
     *

12
     * @return void

13
     */
14
    public function up()
15
    {
16
        Schema::create('jobs', function (Blueprint $table) {
17
            $table->bigIncrements('id');
18
            $table->string('queue')->index();
19
            $table->longText('payload');
20
            $table->unsignedTinyInteger('attempts');
21
            $table->unsignedInteger('reserved_at')->nullable();
22
            $table->unsignedInteger('available_at');
23
            $table->unsignedInteger('created_at');
24
        });
25
    }
26
27
    /**

28
     * Reverse the migrations.

29
     *

30
     * @return void

31
     */
32
    public function down()
33
    {
34
        Schema::dropIfExists('jobs');
35
    }
36
}

Next, let's run the migrate command so that it actually creates the jobs table in a database.

1
$php artisan migrate

That's it as far as the jobs migration is concerned.

Create the Image Model

Next, let's create the Image model that will be used to manage images uploaded by the end user. The image model also requires an associated database table, so we'll use the --migrate option while creating the Image model.

1
$php artisan make:model Image --migration

The above command should create the Image model class and an associated database migration as well.

The Image model class should look like this:

1
<?php
2
3
namespace App;
4
5
use Illuminate\Database\Eloquent\Model;
6
7
class Image extends Model
8
{
9
    //

10
}

And the database migration file should be created at database/migrations/YYYY_MM_DD_HHMMSS_create_images_table.php. We also want to store the original path of the image uploaded by the end user. Let's revise the code of the Image database migration file to look like the following.

1
<?php
2
// database/migrations/YYYY_MM_DD_HHMMSS_create_images_table.php

3
use Illuminate\Support\Facades\Schema;
4
use Illuminate\Database\Schema\Blueprint;
5
use Illuminate\Database\Migrations\Migration;
6
 
7
class CreateImagesTable extends Migration
8
{
9
    /**

10
     * Run the migrations.

11
     *

12
     * @return void

13
     */
14
    public function up()
15
    {
16
        Schema::create('images', function (Blueprint $table) {
17
            $table->increments('id');
18
            $table->timestamps();
19
            $table->string('org_path');
20
        });
21
    }
22
 
23
    /**

24
     * Reverse the migrations.

25
     *

26
     * @return void

27
     */
28
    public function down()
29
    {
30
        Schema::dropIfExists('images');
31
    }
32
}

As you can see, we've added the $table->string('org_path') column to store the path of the original image. Next, you just need to run the migrate command to actually create that table in the database.

1
php artisan migrate

And that's it as far as the Image model is concerned.

Create a Laravel Job

Next, let's create an actual queue job that's responsible for processing image thumbnails. For the thumbnail processing, we're going to use a very popular image processing library—Intervention Image.

To install the Intervention Image library, go ahead and run the following command at the root of your application.

1
php composer.phar require intervention/image

Now, it's time to create the Job class, and we'll use an artisan command to do that.

1
php artisan make:job ProcessImageThumbnails

That should create the Job class template at app/Jobs/ProcessImageThumbnails.php. Let's replace the contents of that file with the following.

1
<?php
2
// app/Jobs/ProcessImageThumbnails.php

3
namespace App\Jobs;
4
 
5
use App\Image as ImageModel;
6
use Illuminate\Bus\Queueable;
7
use Illuminate\Queue\SerializesModels;
8
use Illuminate\Queue\InteractsWithQueue;
9
use Illuminate\Contracts\Queue\ShouldQueue;
10
use Illuminate\Foundation\Bus\Dispatchable;
11
use Illuminate\Support\Facades\DB;
12
 
13
class ProcessImageThumbnails implements ShouldQueue
14
{
15
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
16
 
17
    protected $image;
18
 
19
    /**

20
     * Create a new job instance.

21
     *

22
     * @return void

23
     */
24
    public function __construct(ImageModel $image)
25
    {
26
        $this->image = $image;
27
    }
28
 
29
    /**

30
     * Execute the job.

31
     *

32
     * @return void

33
     */
34
    public function handle()
35
    {
36
        // access the model in the queue for processing

37
        $image = $this->image;
38
        $full_image_path = public_path($image->org_path);
39
        $resized_image_path = public_path('thumbs' . DIRECTORY_SEPARATOR .  $image->org_path);
40
 
41
        // create image thumbs from the original image

42
        $img = \Image::make($full_image_path)->resize(300, 200);
43
        $img->save($resized_image_path);
44
    }
45
}

When the queue worker starts processing any job, it looks for the handle method. So it's the handle method that holds the main logic of your job.

In our case, we need to create a thumbnail of an image uploaded by the user. The code of the handle method is pretty straightforward—we retrieve an image from the ImageModel model and create a thumbnail using the Intervention Image library. Of course, we need to pass the corresponding Image model when we dispatch our job, and we'll see it in a moment.

How to Test a Job

To test our newly created job, we'll create a simple upload form that allows the user to upload an image. Of course, we won't create image thumbnails right away; we'll defer that task so that it can be processed by the queue worker.

Let's create a controller file at app/Http/Controllers/ImageController.php as shown below.

1
<?php
2
namespace App\Http\Controllers;
3
 
4
use App\Image;
5
use App\Jobs\ProcessImageThumbnails;
6
use Illuminate\Http\Request;
7
use Illuminate\Support\Facades\Redirect;
8
use App\Http\Controllers\Controller;
9
use Validator;
10
 
11
class ImageController extends Controller
12
{
13
    /**

14
     * Show Upload Form

15
     *

16
     * @param  Request  $request

17
     * @return Response

18
     */
19
    public function index(Request $request)
20
    {
21
        return view('upload_form');
22
    }
23
 
24
    /**

25
     * Upload Image

26
     *

27
     * @param  Request  $request

28
     * @return Response

29
     */
30
    public function upload(Request $request)
31
    {
32
        // upload image

33
        $this->validate($request, [
34
          'demo_image' => 'required|image|mimes:jpeg,png,jpg,gif,svg|max:2048',
35
        ]);
36
        $image = $request->file('demo_image');
37
        $input['demo_image'] = time().'.'.$image->getClientOriginalExtension();
38
        $destinationPath = public_path('/images');
39
        $image->move($destinationPath, $input['demo_image']);
40
 
41
        // make db entry of that image

42
        $image = new Image;
43
        $image->org_path = 'images' . DIRECTORY_SEPARATOR . $input['demo_image'];
44
        $image->save();
45
 
46
        // defer the processing of the image thumbnails

47
        ProcessImageThumbnails::dispatch($image);
48
 
49
        return Redirect::to('image/index')->with('message', 'Image uploaded successfully!');
50
    }
51
}

Let's create an associated view file at resources/views/upload_form.blade.php.

1
<!DOCTYPE html>
2
<html lang="{{ config('app.locale') }}">
3
    <head>
4
        <meta charset="utf-8">
5
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
6
        <meta name="viewport" content="width=device-width, initial-scale=1">
7
        <meta name="csrf-token" content="{{ csrf_token() }}" />
8
        <title>Laravel</title>
9
 
10
        <!-- Fonts -->
11
        <link href="https://fonts.googleapis.com/css?family=Raleway:100,600" rel="stylesheet" type="text/css">
12
 
13
        <!-- Styles -->
14
        <style>
15
            html, body {
16
                background-color: #fff;
17
                color: #636b6f;
18
                font-family: 'Raleway', sans-serif;
19
                font-weight: 100;
20
                height: 100vh;
21
                margin: 0;
22
            }
23
 
24
            .full-height {
25
                height: 100vh;
26
            }
27
 
28
            .flex-center {
29
                align-items: center;
30
                display: flex;
31
                justify-content: center;
32
            }
33
 
34
            .position-ref {
35
                position: relative;
36
            }
37
 
38
            .top-right {
39
                position: absolute;
40
                right: 10px;
41
                top: 18px;
42
            }
43
 
44
            .content {
45
                text-align: center;
46
            }
47
 
48
            .title {
49
                font-size: 84px;
50
            }
51
 
52
            .links > a {
53
                color: #636b6f;
54
                padding: 0 25px;
55
                font-size: 12px;
56
                font-weight: 600;
57
                letter-spacing: .1rem;
58
                text-decoration: none;
59
                text-transform: uppercase;
60
            }
61
 
62
            .m-b-md {
63
                margin-bottom: 30px;
64
            }
65
             
66
            .alert {
67
                color: red;
68
                font-weight: bold;
69
                margin: 10px;
70
            }
71
            .success {
72
                color: blue;
73
                font-weight: bold;
74
                margin: 10px;
75
            }
76
        </style>
77
    </head>
78
    <body>
79
        <div class="flex-center position-ref full-height">
80
            @if (Route::has('login'))
81
                <div class="top-right links">
82
                    @if (Auth::check())
83
                        <a href="{{ url('/home') }}">Home</a>
84
                    @else
85
                        <a href="{{ url('/login') }}">Login</a>
86
                        <a href="{{ url('/register') }}">Register</a>
87
                    @endif
88
                </div>
89
            @endif
90
 
91
            <div class="content">
92
                <div class="m-b-md">
93
                    <h1 class="title">Demo Upload Form</h1>
94
                     
95
                    @if ($errors->any())
96
                        <div class="alert alert-danger">
97
                            <ul>
98
                                @foreach ($errors->all() as $error)
99
                                    <li>{{ $error }}</li>
100
                                @endforeach
101
                            </ul>
102
                        </div>
103
                    @endif
104
                     
105
                    @if (session('message'))
106
                        <div class="success">
107
                            {{ session('message') }}
108
                        </div>
109
                    @endif
110
                     
111
                    <form method="post" action="{{ url('/image/upload') }}" enctype="multipart/form-data">
112
                      <div>
113
                        <input type="file" name="demo_image" />
114
                      </div>
115
                      <br/>
116
                      <div>
117
                        <input type="hidden" name="_token" value="{{ csrf_token() }}">
118
                        <input type="submit" value="Upload Image"/>
119
                      </div>
120
                    </form>
121
                </div>
122
            </div>
123
        </div>
124
    </body>
125
</html>

Finally, let's add routes for the index and upload actions in the routes/web.php file.

1
Route::get('image/index', 'ImageController@index');
2
Route::post('image/upload', 'ImageController@upload');

In the ImageController controller, the index method is used to render an upload form.

1
public function index(Request $request)
2
{
3
    return view('upload_form');
4
}

When the user submits a form, the upload method is invoked.

1
public function upload(Request $request)
2
{
3
    // upload image

4
    $this->validate($request, [
5
        'demo_image' => 'required|image|mimes:jpeg,png,jpg,gif,svg|max:2048',
6
    ]);
7
    $image = $request->file('demo_image');
8
    $input['demo_image'] = time().'.'.$image->getClientOriginalExtension();
9
    $destinationPath = public_path('/images');
10
    $image->move($destinationPath, $input['demo_image']);
11
 
12
    // make db entry of that image

13
    $image = new Image;
14
    $image->org_path = 'images' . DIRECTORY_SEPARATOR . $input['demo_image'];
15
    $image->save();
16
 
17
    // defer the processing of the image thumbnails

18
    ProcessImageThumbnails::dispatch($image);
19
 
20
    return Redirect::to('image/index')->with('message', 'Image uploaded successfully!');
21
}

At the beginning of the upload method, you'll notice the usual file upload code that moves the uploaded file to the public/images directory. Next, we insert a database record using the App/Image model.

Finally, we use the ProcessImageThumbnails job to defer the thumbnail processing task. It's important to note that it's the dispatch method that's used to defer a task. At the end, the user is redirected to the upload page with a success message.

At this point in time, the job is added to the jobs table for processing. Let's confirm it by issuing the following query.

1
mysql> select * FROM jobs;
2
|  1 | default | {"displayName":"App\\Jobs\\ProcessImageThumbnails","job":"Illuminate\\Queue\\CallQueuedHandler@call","maxTries":null,"timeout":null,"data":{"commandName":"App\\Jobs\\ProcessImageThumbnails","command":"O:31:\"App\\Jobs\\ProcessImageThumbnails\":5:{s:8:\"\u0000*\u0000image\";O:45:\"Illuminate\\Contracts\\Database\\ModelIdentifier\":2:{s:5:\"class\";s:9:\"App\\Image\";s:2:\"id\";i:2;}s:6:\"\u0000*\u0000job\";N;s:10:\"connection\";N;s:5:\"queue\";N;s:5:\"delay\";N;}"}} |        0 |        NULL |   1510219099 | 1510219099 |

You must be wondering, what does it take to process a job then? Don't worry—that's what we're going to discuss in the very next section.

Queue Worker

The job of the Laravel queue worker is to process jobs that are queued up for processing. In fact, there's an artisan command that helps us to start the queue worker process.

1
$php artisan queue:work

As soon as you run that command, it processes pending jobs. In our case, it should process the ProcessImageThumbnails job that was queued up when the user uploaded an image earlier.

1
$php artisan queue:work
2
[YYYY-MM-DD HHMMSS] Processing: App\Jobs\ProcessImageThumbnails
3
[YYYY-MM-DD HHMMSS] Processed:  App\Jobs\ProcessImageThumbnails

You would have noticed that when you start a queue worker, it keeps running until you kill it manually or close the terminal. In fact, it's waiting for the next job to be processed. As soon as there's a new job in the queue, it'll be processed right away if the queue worker is running.

Of course, we can't keep it running that way, so we need to find a way for the queue worker to run permanently in the background.

To our rescue, there are several process management tools out there you could choose from. To name a few, here's a list:

  • Circus
  • daemontools
  • Monit
  • Supervisor
  • Upstart

You should choose a tool that you're comfortable with to manage the Laravel queue worker. Basically, we want to make sure that the queue worker should run indefinitely so that it processes queued jobs right away.

So that's the Queue API at your disposal. You can use it in your day-to-day development to defer time-consuming tasks for the betterment of the end user experience.

Conclusion

In this article, we discussed the Queue API in Laravel, which is really helpful should you wish to defer processing of resource-consuming tasks.

We started with a basic introduction to the Queue API, which involved a discussion of connections, queues, and jobs. In the second half of the article, we created a custom queue job that demonstrated how you could use the Queue API in the real world.

For those of you who are either just getting started with Laravel or looking to expand your knowledge, site, or application with extensions, we have a variety of things you can study at Envato Market.

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.