April 13, 2021

Injecting Additional Data into Laravel Queued Jobs

laravel tips

Earlier today we shipped an enhancement to Laravel Forge that stores the ID of the user who initiated an event.

At first glance, you may be wondering why this is a big deal and you're probably asking yourself why I'm writing a blog post about it. All good thoughts. Let's dive into it!

In Forge, almost everything you can do to a server is handled within a queued job. Additionally, some of these jobs will dispatch other jobs via the sync connection. Forge has a ServerRepository class, most methods in the class are responsible for dispatching the relevant job, for example:

public function addJob(Server $server, $command, $user, $frequency, $minute, $hour, $day, $month, $weekday)
{
    $cron = $this->formatCronExpression(
        // ...
    );

    $job = $server->jobs()->create([
        // ...
    ]);

    Queue::push('Forge\Jobs\AddJob', [
        'server_id' => $server->id,
        'job_id' => $job->id,
    ]);

    return $job;
}

The most obvious way to achieve this is to go through every single method and add a User $initiator parameter, then pass that value in every place it's called. In the case of Forge, that would be very tedious, time consuming and not a great solution.

If, like me, you've read Mohamed Said's Laravel Queues in Action book, then you already know how powerful the Queue system is.

The Queue class contains a method named createPayloadUsing which allows you to register a callback that is executed when creating job payloads. This is exactly what we're after!

Queue::createPayloadUsing(function ($connection, $queue, $payload) {
    // $payload contains all of the job information, including the supplied data.
    $jobData = $payload['data'];

    if (! isset($jobData['initiated_by'])) {
        $jobData = array_merge($payload['data'], array_filter([
            'initiated_by' => request()->user()->id ?? null,
        ]));
    }

    return ['data' => $jobData];
});

This code is placed in the boot method of the AppServiceProvider.

We check whether the job's payload contains a key called initiated_by, if not then we add it to the array with the value being the current request's user's id.

Every single job initiated by Forge automatically knows who initiated it, and we can use this value when we're recording the event in the database.