Optimizing Laravel Projects with Events and Queued Events

Chris Nwamba

It is amazing how supporting features in Laravel turn out to be as important as the "important" features. When creating an app, our basic flow is structure the models, create routes, handle them in a controller, and return nicely styled views.

But what do we do when our code base starts growing unexpectedly and things start falling apart. Situations where we need to implement a particular logic more often, handle heavy HTTP requests, or deal with some other heavy tasks will arise when our projects start growing. Trust me on this one, controllers cannot take it alone, they will need support.

Laravel has some nice features to make life easier for both the end users and developers. Features like Queues and Events. Events , just like the name says, provide an observer pattern for implementing logic by listening for something else to happen. That sounds a little vague upfront, but the idea will be clear by the end of this article.

We will discuss (with a practical example) how Events:

  1. Support the DRY rule
  2. Make our code easier to review and understand
  3. Lead to better performance with queued events.

The Conventional Controller

To better appreciate what Events and Queued Events do, we will look at a typical controller and analyze the limitations. To do so, create a Laravel project setup your .env to use Mailtrap for Emails and database driver for Queues:

QUEUE_DRIVER=database

MAIL_DRIVER=smtp
MAIL_HOST=mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=MAILTRAP-INBOX-USERNAME
MAIL_PASSWORD=MAILTRAP-INBOX-PASSWORD
MAIL_ENCRYPTION=null
MAIL_FROM=YOUR@EMAIL.COM
MAIL_NAME=YOUR-NAME

Migrations and Models

We need just two migrations, one for the articles table and the other for the queues:

php artisan make:migration create_articles_table --create=articles

php artisan queue:table

php artisan migrate

For this tutorial, we need just two models: a User model and an Article model. The User model has already been created by Laravel so all we need do is create one for the article:

php artisan make:model Article

Database Seeders

We will be sending emails to sample users, so it would be nice to seed the users database, which is created by Laravel. Create a new seed file at database/seeds named UsersTableSeeder by running:

php artisan make:seeder UsersTableSeeder

And add the following in the run() method as seen below:

public function run()
    {
        factory(App\User::class, 10)->create();
    }

Laravel looks for seeds in database/seeds/DatabaseSeeder.php so we can't just run the seed command now. Let us call the UsersTableSeeder in it's run():

public function run()
    {
        Model::unguard();

        $this->call(UsersTableSeeder::class);

        Model::reguard();
    }

We can now run the seed command

php artisan db:seed

Controller and Routes

We'll create two routes for this tutorial: one that presents us with a button to create an Article and another to actually create an article:

Route::get('/',  'ArticleController@index');
Route::get('/create',  'ArticleController@create');

We have created two routes pointing to a controller and controller actions that do not exists. To create the controller, run:

php artisan make:controller ArticleController

Create an index() action method for the home page that just returns a view:

public function index()
    {
        return view("article.index");
    }

The index() action method is returning a view called index in articles folder of the views directory generated by Laravel. Create the file with this small piece:


<div class="container">
    <div class="main">
        <a href="/create" class="btn btn-default">Create Article</a>
    </div>
</div>

The second action method, create() will handle the logic for creating a new article:

  public function create()
    {
        $article_title = "Hi random title with " . str_random(10);
        $users = User::all();

        $article = new Article;
        $article->title = $article_title;
        $article->save();

        foreach($users as $user){
            Mail::raw("Checkout Scotch's new article titled: " . $article_title, function ($message) use ($user) {

                $message->from('chris@scotch.io', 'Chris Sevilleja');

                $message->to($user->email);

            });
        }
    }

The problem

Everything runs fine with this controller, so where is the problem? Although this code seems lean, what happens when we want to share the article we create on Facebook, Twitter and Google Plus? As we add more logic, our code will get twisted and confusing to our team members.

There is also a very good chance that we will go against the DRY principle as we will most likely want to use certain logic in different places.

Furthermore, the request takes huge amount of time to complete on a local PC. Over 30 seconds to dispatch just 10 mails as seen below.

[2016-02-24 15:15:36] local.INFO: Request fired  
[2016-02-24 15:16:10] local.INFO: Request ended 

With Events and Queued Events, we can make a better project with a huge performance boost.

Introducing Events

It is time to add events to our simple app. Events are made up of two classes, the event class and the listener class. The event class is what is fired and the listener class is called once an event is fired.

Generating Events

Creating events in Laravel is simple. Laravel generates an EventServiceProvider for us to register our events at app/Providers. In the file, replace the listen with the following:

 protected $listen = [
        'App\Events\ArticleWasPublished' => [
            'App\Listeners\SendNewsletter',
        ],
    ];

ArticleWasPublished is the event we want to fire after creating an article and SendNewsletter is the listener that will be called when the event is fired.

We generate the event classes by running:

php artisan event:generate

Firing Events

To make use of our events, we need to adjust the create action method to fire the event after an article has been created:

public function create()
    {
        Log::info("Request fired");
        $article_title = "Hi random title with " . str_random(10);

        $article = new Article;
        $article->title = $article_title;
        $article->save();

        Event::fire(new ArticleWasPublished($article_title));
        Log::info("Request ended");
    }

The mail sending logic has been moved out and we are just firing the event with the Event facade and passing the article title to it:

Event::fire(new ArticleWasPublished($article_title));

We are injecting the article title into the event so we need to edit the ArticleWasPublished class as well:

 public $article;

    public function __construct($article)
    {
        $this->article = $article;
    }

Listening for Events

The SendNewsletter listener is what will actually implement the mail sending logic via its handle() method:

public function handle(ArticleWasPublished $event)
    {
        $users = User::all();
        foreach($users as $user){
            Mail::raw("Checkout Scotch's new article titled: " . $event->article, function ($message) use ($user) {

                $message->from('chris@scotch.io', 'Chris Sevilleja');

                $message->to($user->email);

            });
        }
    }

The ArticleWasPublished is type-hinted to this method and we can access the article title we passed in the ArticleWasPublished constructor from it.

Now we have neater code and we can create multiple listeners, such as share on social media, notify users, etc., for when an article is created. And our code remains neat and simple.

Queued Events

One of the issues I pointed out was that the it takes over 30 seconds to send 10 emails. What will happen if we are on production and have a million emails to send to our one million readers? Will it take 3 million seconds?

That is definitely crazy, but implementing queues will allow the request to complete seconds and send the emails in the background.

Adding Queues to Event Listeners

Laravel made adding queues to events dead simple. All that is needed to do is to make the listener class implement the ShouldQueue interface:

class SendNewsletter implements ShouldQueue
{
...

We can now listen to queues before sending the request using:

php artisan queue:listen database

This request from my own end takes only one second compared to the former experiment:

[2016-02-24 15:22:21] local.INFO: Request fired  
[2016-02-24 15:22:22] local.INFO: Request ended  

Conclusion

The demo is available as usual. There are different branches which show the different stages of the application: before we added events and after we added events and queued events. It pays to use events in projects, as it will not only give you a good experience as a developer but also give your users a good experience because of the performance improvements.

Chris Nwamba

44 posts

JavaScript Preacher. Building the web with the JS community.