Make your Laravel Jobs multi-tenancy friendly
Tags: laravel, jobs, multitenancy
Published: 2018-10-19 21:12:48

---
title: Make your Laravel Jobs multi-tenancy friendly
published: true
tags: laravel, jobs, multi tenancy
canonical_url: https://davidllop.com/make-your-laravel-jobs-multi-tenancy-friendly
---

Our team is working hard on a project which can connect to different databases depending on the client.
Here is the wikipedia link: [multi-tenancy](https://en.wikipedia.org/wiki/Multitenancy):

> The term "software multitenancy" refers to a software architecture in which a single instance of 
software runs on a server and serves multiple tenants.

So the point here is that we have a single installation of our app and depending on the user logged in, it will show the respective data.

When you send a job to a queue, Laravel serializes the class to save it using your chosen driver until a Worker can fire it.
When the job is fired, it tries to restore the models you passed in the constructor from the database using 
the `SerializesModel` trait.

This works totally fine, but sometimes you get a non-trivial requirement, and this time it was our turn.

We don't have a connection defined for each client in `config/databases.php` configuration file. Instead, we have 
the database name in a `clients` table with the other information, and each `User` has a `client_id` to create the relation. 
We use a middleware at the beginning of each request to set the connection attributes in the configuration. 

The problem began when we started working with jobs: due to the fact that there's no client logged in, there's no way to determine
which connection must be set up. After an intense research, and digging through the framework's code, we figured out a 
solution which fitted our needs.

I'm going to show some examples with the `database` driver, since it's the one we chose for our app. But I'm sure you can
apply the same modifications to your driver of choice.

Our goal here is to set up each job with the connection it needs, so the job must know which client it belongs to.

This step is easy: since we use the database driver, we just need to run this command:
```
php artisan queue:table
```

Then we modify the migration file and we add our 'client_id' before migrating

```php
Schema::create('jobs', function (Blueprint $table) {
    $table->bigIncrements('id');
    $table->string('queue')->index();
    $table->integer('client_id')->unsigned()->nullable();
    ...
});
```

But how will the framework know the way to correctly populate this new field whenever it dispatches a new job? This is where the tricky part starts:

In our `config/app.php`, we need to specify our custom `QueueServiceProvider` instead of `Illuminate`'s:

```php
'providers' => [
    ...
    Illuminate\Pipeline\PipelineServiceProvider::class,
    App\Queue\QueueServiceProvider::class,
    Illuminate\Redis\RedisServiceProvider::class,
    ...
```

> From now on, we will organize the extended classes of Illuminate's Queue in the `App\Queues` namespace

This provider is very simple, it just needs to extends the original class. 

```php
namespace App\Queue;

use App\Queue\Connectors\DatabaseConnector;

class QueueServiceProvider extends \Illuminate\Queue\QueueServiceProvider
{
    protected function registerDatabaseConnector($manager)
    {
        $manager->addConnector('database', function () {
            return new DatabaseConnector($this->app['db']);
        });
    }
}
``` 

We need to override `DatabaseConnector` in order to use our own, which overrides `connect` to use our `DatabaseQueue`


```php
namespace App\Queue\Connectors;

use App\Queue\DatabaseQueue;

class DatabaseConnector extends \Illuminate\Queue\Connectors\DatabaseConnector
{

    public function connect(array $config)
    {
        return new DatabaseQueue(
            $this->connections->connection($config['connection'] ?? null),
            $config['table'],
            $config['queue'],
            $config['retry_after'] ?? 60
        );
    }
}
```

Now we need to override the method `buildDatabaseRecord` to check if we have a client and, if we do, 
fill up that field on the `jobs` table.

```php
namespace App\Queue;

use App\Queue\Jobs\DatabaseJob;

class DatabaseQueue extends \Illuminate\Queue\DatabaseQueue
{

    protected function buildDatabaseRecord($queue, $payload, $availableAt, $attempts = 0)
    {
        $queueRecord = [
            'queue' => $queue,
            'attempts' => $attempts,
            'reserved_at' => null,
            'available_at' => $availableAt,
            'created_at' => $this->currentTime(),
            'payload' => $payload,
        ];

        if (client()) {
            $queueRecord['client_id'] = client()->id;
        }

        return $queueRecord;
    }
}
```

Okay, so now our jobs have a `client_id` assigned, the only remaining thing is... 
How do we set up the client connection before a job is fired? Yes, you guessed it, 
we will need to extend another Illuminate class for that

```php
namespace App\Queue\Jobs;

use App\Models\Client;
use App\Helpers\ClientConnector;

class DatabaseJob extends \Illuminate\Queue\Jobs\DatabaseJob
{
    public function fire()
    {
        if ($this->job->client_id) {
            $client = Client::findOrFail($this->job->client_id);

            ClientConnector::connectByClient($client);
        }

        parent::fire();
    }
}
```

Lastly, we need to come back to our own `DatabaseQueue` and tell it to use our `DatabaseJob` instead
of Illuminate's Queue default by overriding the method `marshalJob`

```php
protected function marshalJob($queue, $job)
{
    $job = $this->markJobAsReserved($job);

    return new DatabaseJob(
        $this->container, $this, $job, $this->connectionName, $queue
    );
}
```