Laravel: Understanding Queues, Events and Notifications in a multi-tenancy multi-database environ

I’m building a project based on Samuel Štancl‘s amazing multi-tenancy project, available for free on github or with various paid options.

I’m using the paid for Multi-Tenancy Saas boilerplate, which I would thoroughly recommend if you’re looking for a quick start.

Most of your app will be completely unaware that it is running in a multi-database environment and so you just concentrate on developing your functionality and leave the multi-tenancy bit to Stancl.

Every now and again, though, you will have to understand the multi-tenancy internals and apply those to some aspects of your apps.

As I come across those in my build, I will document them here because I find the provided documentation lacks the basic introductions that would help less experienced developers get up and running.  You also see this in the discord channel that often-times people just can’t find that basic intro – so here goes …

btw – if you find this helpful, think about dropping me a line or popping a link onto Twitter – thanks!

So, as a relative noob to Laravel (i’ve been using other PHP frameworks for the past 11 years ! ) I find that Laravel is so rich in detail it is quite over-whelming and sometimes confusing.  When do you use events, notifications, observers and so on?  I will cover all this later but for now I want to start with queues.

Queues

Is it me, or is that a difficult word to type?

Anyway, one of the key concepts of Stancl’s boilerplate (I think) is that the default queue runs on the central domain.  However you might be setting up your tenants, beit on;

  • sub-domains (tenant.yourdomain,com),
  • custom domains (my-own-tenant-domain.com) or
  • a tenant path (yourdomain.com/tenant)

… your central domain is where your landing page(s) is/are and the sign-up functionality – so the default queue runs against this domain.

Sure, you could kick off queues in the background for each domain and you probably will in development using something like:

artisan queue:work --tenant=xyz

but you won’t likely want to do that on your production server as it would mean setting up the initialisation of each queue for each tenant, probably manually.  It would be better to push everything onto the default queue and then tell each job which domain it should run on.  This is better for server load, as well, as you have fewer queues running and therefore not over-burden the server.

If you have a higher queue volume, then you could always create different central queues for different kind of jobs, welcome emails, background notifications, batch jobs etc…

So notice the key element here is

tell each job which domain it should run on.

Using telescope to look at your central queue – if you don’t have telescope installed then go do that now and then come back here – you will see that some jobs will have tags that determine which domain that job should run in.

Notice the tags at the bottom where the tenant id is specified

The Saas boilerplate has all this built-in by default and using the github version, you can set this all up by ploughing through the documentation…

So everything you queue up from within your tenant enabled app, will apply the tenant tag to the queue and the queue will run that job on the tentant db – clever huh!

However, if you want to queue up a console job, for example, it will have been initiated outside of the tenant initialised app, so you will need to tell the queue which tenant db to run this job against.

Overnight Batch jobs

Let’s take a specific example; I have a todo list app and each task has a ‘must do by’ date.  Each tenant can say they want reminding x days before the ‘must do by’ date, so I want to run an over-night job every night that creates a reminder for any task that is within x days of the ‘must do by’ date.

So to create a scheduled job, we add a line to the console/Kernel.php class within the schedule method:

$schedule->job( new JobClassName )->at( time-to-run );

Well, thats pretty easy!

Uh..oh – failed!

Coz, we didn’t tell the schedule which tenant to run on …

Here we need to rewind a little.  When you want to run a job, you create a job Class and pop the code you want to run in the handle method of that class …

artisan make:job RaiseTaskReminders

This creates a stub that looks like this:

<?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;

class RaiseTaskReminders 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 can then add the code that we need to run within the handle() method … but we need to make it tenant aware.

Lets go back to the console/kernel.php and rather than just calling our schedule->job, we will add some tenant processing:

foreach( Tenant::all() as $tenant )
{
    $schedule->job( new RaiseTaskReminders($tenant) )->dailyAt('06:'.rand(01,59));
}

So, for each tenant, we setup a schedule.  Here, I have randomize the time but you can change that as you need to … Notice though, that I am passing the tenant object to the constructor of the RaiseTaskReminders class, so now we need to update that class to be tenant aware.

<?php

namespace App\Jobs;

use App\Models\Task;
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;

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

    
    /** @var Tenant */
    protected $tenant;

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

    /**
     * Execute the job.
     *
     * @return void
     */
    public function handle()
    {
        $this->tenant->run(function ($tenant) {
            // put our code in here like ...   
            Task::query->where('must_do_by_date','<',now()->subDays(setting('must_do_by_delay')))->all()->each(
                ....
            );
        }
    }
}

Now, in our dev environement we can run

artisan schedule:listen

 Now your queues should be running properly …

Next, I will write about Observers and Events in order to create some Notifications …

Coming soon!

 

Let’s Start a Project!

Contact Me