IronMQ and Laravel: Implementation

This entry is part 2 of 3 in the series IronMQ and Laravel

IronMQ and Laravel

Welcome back to the IronMQ and Laravel series – this is the second and final part in which we finalize our background-job enabled web app.

There are several tutorials out there related to queues already – for example: http://vimeo.com/64703617 where Taylor teaches you how to write to a file using queues. In this tutorial, we will try something different.

We will make a jobs table which has job_id, and the status of the job. When you put a job on the queue, the job status will be queued and when we receive the job, we set the status to running. Accordingly, after finishing it we will mark it finished.

Later, we will resize images.

Jobs Table

Step 1

Let’s create a jobs table:

php artisan migrate:make create_jobs_table

Go to app/database/migrations/xxxxx_create_jobs_table.php and add the following (This code is extracted from app/database/migrations/xxxxx_create_jobs_table.php, so edit your file accordingly):

public function up()
{
    Schema::create('jobs', function($table)
{
    $table->increments('id');
    $table->string('job_id');
    $table->enum('status', array('queued', 'running','finished'))->default('queued');
    $table->timestamps();
    });
}

public function down()
{
    Schema::drop('jobs');
}

Here we created a table with the columns job_id, status and timestamps. Run php artisan migrate to create the table.

Step 2

Create a file job.php in app/models with the following content:

<?php

class Job extends Eloquent {

    protected $table = 'jobs';

    protected $fillable = array('job_id', 'status');

}

Make a JobController using the following command:

php artisan controller:make JobController

and add the following in the app/routes.php file:

Route::resource('job','JobController');

Go to JobController populate the index method.

public function index()
{
    $jobs = Job::all();

    return View::make('job.index')->with('jobs',$jobs);
}

Before we go ahead we need to set up our views. First, let’s create a views/master.blade.php which will serve as a template.

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Laravel PHP Framework</title>
    <link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css">
</head>
<body>

    <div class="container">
        <div class="row">
            <div class="col-md-12 well">
                Let's Learn Queues
            </div>

            @yield('content')
        </div>
    </div>

</body>
</html>

Let’s make a view for our jobs. Put the following content in app/views/job/index.blade.php

@extends('master')

@section('content')
<div class="col-md-12">
    <table class="table">
        <thead>
            <tr>
                <th>#</th>
                <th>Job ID</th>
                <th>Status</th>
            </tr>
        </thead>

        <tbody>
            @foreach($jobs as $job)
                <tr>
                    <td>{{ $job->id }}</td>
                <td>{{ $job->job_id }}</td>
                <td>{{ $job->status }}</td>
                </tr>
            @endforeach
        </tbody>

    </table>
</div>
@stop

As we have no jobs now, we won’t see anything if we go to http://localhost:8000/job.

Now let’s create a new job (where we write a string to a file) and then update the status of the job to ‘finished’.

public function create()
{
    $job_id = Queue::push('JobController@run',array('string' => 'Hello World '));

    Job::create(array('job_id' => $job_id));

    return Redirect::route('job.index');
}

Note that above we pushed a method ‘run’ onto the queue with some data. So we need to create a method ‘run’ in JobController.

public function run($job,$data)
{

    $job_id = $job->getJobId(); // Get job id

    $ejob = Job::where('job_id',$job_id)->first(); // Find the job in database

    $ejob->status = 'running'; //Set job status to running

    $ejob->save();

    File::append('public/queue.txt',$data['string'].$job_id.PHP_EOL); //Add content to file

    $ejob->status = 'finished'; //Set job status to finished

    $ejob->save();

    return true;
}

At this point you can test this. Let’s give it a go with the url generated by ngrok. Please note that your url will be different from mine.

  1. Go to http://953ffbb.ngrok.com (It should be working)
  2. In Iron MQ Dashboard -> Project -> Queues -> Your Queue -> Push Queues : You should see http://953ffbb.ngrok.com/queue/receive under subscribers.
  3. Go to http://953ffbb.ngrok.com/job : It should show a list of jobs (It’s possible there are no jobs as we didn’t create any).
  4. Create a job: http://953ffbb.ngrok.com/job/create. This will create a job and come back to http://953ffbb.ngrok.com/job where you see your job with job_id and status “queued”.
  5. Refresh the page: http://953ffbb.ngrok.com/job and you can see that the status is changed to ‘finished’ and find the file public/queue.txt with some text.

That’s it! We pushed a job into the queue, Iron sent the job to the subscriber and we completed our job (wrote data to file and changed the status).


Resize Photos

Let’s upload photos and resize them using Intervention.

Make a PhotoController to handle operations on photos:

php artisan controller:make PhotoController

Add a route for photos in app/routes.php

Route::resource('photo','PhotoController');

Let’s make a table for photos:

php artisan migrate:make create_photos_table

Open the file app/database/migrations/xxxxxx_create_photos_table.php and the following content:

public function up()
{
    Schema::create('photos', function($table)
{
    $table->increments('id');

        $table->string('path');

        $table->timestamps();
    });
}

public function down()
{
    Schema::drop('photos');
}

And then run php artisan migrate to create the table.

Let’s create a model for Photos at app/models/photo.php

<?php

class Photo extends Eloquent {

    protected $table = 'photos';

    protected $fillable = array('path');

}

Add the index method in PhotoController

public function index()
{
    $photos = Photo::all();

    return View::make('photo.index')->with('photos',$photos);
}

Add the view for photos index in app/views/photo/index.blade.php

@extends('master')

@section('content')
<div class="col-md-12">
    <h2>Photos</h2>
    <a href="{{ route('photo.create') }}" class="btn btn-primary">Upload New Photo</a> <br><br>
    <table class="table">
        <thead>
            <tr>
                <th>#</th>
                <th>Path</th>
            </tr>
        </thead>

        <tbody>
            @foreach($photos as $photo)
                <tr>
                    <td>{{ $photo->id }}</td>
                <td>{{ $photo->path }}</td>

                    <td>
                        <a href="{{ route('photo.edit', $photo->id) }}" class="btn btn-primary btn-xs"> Resize </a>
                    </td>
                </tr>
            @endforeach
        </tbody>

    </table>
</div>
@stop

You can now visit: http://localhost:8000/photo and check the page (but there won’t be any photos)

We need a page to upload photos, so let’s make a view and hook in a few methods to achieve that.

Put this content in app/views/photo/create.blade.php

@extends('master')

@section('content')
<div class="col-md-12">
    <h2>Upload New Photo</h2>

    {{ Form::open(array('route' => array('photo.store'),'files' => true)) }}

    <div class="form-group">
        {{ Form::file('photo') }}
    </div>

    {{ Form::submit('Upload',array('class' => 'btn btn-blue btn-primary')) }}

    {{ Form::close() }}

</div>
@stop

Make changes in app/controllers/PhotoController.php

public function create()
{
    return View::make('photo.create');
}

public function store()
{
    $file = Input::file('photo'); // Get the file

    $extension = $file->getClientOriginalExtension(); //Get the extension

    $filename = time().'.'.$extension; //Prepare filename

    $upload_success = $file->move('public/uploads', $filename); //Move file

    Photo::create(array('path' => 'uploads/'.$filename )); //Store info in DB

    return Redirect::route('photo.index');
}

Go to http://localhost:8000/photo and click on ‘Upload New Photo’ and upload the photo. You can go public/uploads and find your photo. At the same time you can find the photos list along with a resize button. Now, we need a form where we can mention width and height to resize.

Put this content in app/views/photo/edit.blade.php

@extends('master')

@section('content')
<div class="col-md-12">
    <h2>Resize Photo</h2>

    {{ Form::open(array('route' => array('photo.update',$photo->id), 'method' => 'PUT')) }}

    <div class="form-group">
        {{ Form::label('width', 'Width') }}

        {{ Form::text('width') }}
    </div>

    <div class="form-group">
        {{ Form::label('height', 'Height') }}

        {{ Form::text('height') }}
    </div>

    {{ Form::submit('Submit',array('class' => 'btn btn-blue btn-primary')) }}

    {{ Form::close() }}

</div>
@stop

The following three methods will be doing our work to resize images:

    public function edit($id)
    {
        $photo = Photo::find($id);

        return View::make('photo.edit')->with('photo', $photo);
    }

    public function update($id)
    {
        $data['photo_id'] = $id;
        $data['width'] = Input::get('width');
        $data['height'] = Input::get('height');

        $job_id = Queue::push('PhotoController@resize', $data); //Put a job in queue to resize

        Job::create(array('job_id' => $job_id));

        return Redirect::route('photo.index');
    }

    public function resize($job, $data)
    {

        $job_id = $job->getJobId(); // Get job id

        $ejob = Job::where('job_id', $job_id)->first(); // Find the job in database

        $ejob->status = 'running'; //Set job status to running

        $ejob->save();

        $photo = Photo::find($data['photo_id']);

        $filename = $data['width'] . '_' . $data['height'] . '_' . basename($photo->path);

        Image::open('public/' . $photo->path)
            ->resize($data['width'], $data['height'], true)
            ->save('public/uploads/' . $filename);

        Photo::create(array('path' => 'uploads/' . $filename));

        $ejob->status = 'finished'; //Set job status to finished

        $ejob->save();

        return true;
    }

Now if you click on resize and submit the form, a job will be queued. If you refresh the photos page, you will find the new photo.

Conclusion

In this article series we implemented IronMQ with Laravel to add background image processing capabilities to our web app. See how simple it was? Did you have any problems? Want to see more? Let us know!

IronMQ and Laravel

<< IronMQ and Laravel: SetupIronMQ and Laravel: Delays and Retries >>

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

  • Laurie Knight

    Thanks! As a complete newb to Iron MQ I found this tutorial very useful, I didn’t go through the entire tutorial, instead adapted the latter part to my own problem – and managed to move the actual sending of email out from a bunch of places inline into a queue… Removing the actual mail send from the scripts sped things up considerably! (Oh and that ngrok is incredibly useful!! Thanks for the heads up on that)

    • rajivseelam

      Glad to be helpful :) Part 3 is on it’s way, do read that as I cover much needed scenarios.