IronMQ and Laravel: Implementation
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: https://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.
- Go to http://953ffbb.ngrok.com (It should be working)
- In Iron MQ Dashboard -> Project -> Queues -> Your Queue -> Push Queues : You should see http://953ffbb.ngrok.com/queue/receive under subscribers.
- 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).
- 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”.
- 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!