Laravel: What does every trait in a Job class?

Because queuing is one thing, but actually understanding the queue is other.

Italo Baeza Cabrera
5 min readOct 14, 2019

Queueing jobs is probably the best way to avoid letting the user spend several seconds staring at a “Loading…” screen, or worse, closing the browser and calling it a day. Put your logic there, push it with the needed data, and you’re good to go. Additionally, see the end results using Horizon.

The thing is, the “Job” class that will be put in the queue is more than just a class. There are four traits that come with a Job when you generate one via the Artisan command, along with one interface. Let’s see them.

The ShouldQueue interface

This itself is pretty much self-explanatory. When the class implements this interface, the framework will automatically push the Job to the queue, instead of executing it in the current application lifecycle where it was dispatched.

That’s it. The part of the application that checks if the Job to dispatch should be queued for later is here.

Basically it checks if the class has implemented the interface, and if it did, then it goes to a pipeline that will save it for later after the application lifecycle ends.

The advantages of this are clear: if the logic will be costly enough to hang the request lifecycle, or you need some kind of serial processing with one unique worker dedicated to that queue, or you don’t have control on how a logic will take, this is the interface to put.

If you don’t use this trait, the Bus Dispatcher of the framework will understand that this logic should run immediately.

The “Dispatchable” trait

This trait is just a helper to allow dispatching your Job without needing to instance it.

For example, you can use this code to push it to the queue:

// Let's grab a random Podcast
$podcast = Podcast::inRandomOrder()->first();
// Dispatch to the queue
ProcessPodcast::dispatch($podcast);
// Process it in the current lifecycle
ProcessPodcast::dispatchNow($podcast);
// Run other Jobs if this is successful
ProcessPodcast::withChain([
NotifyPodcastProcessed::class,
NotifyPodcastSubscribers::class,
PublishNewPodcast::class,
])->dispatch($podcast);

These methods will take the arguments and pass it to the constructor of the class. In the last examples, the ProcessPodcast job must receive a Podcast instance otherwise it won’t work.

This trait is also responsible for letting you use the withChain() method to chain jobs.

The “InteractsWithQueue” trait

This one is also optional, but also quite useful. This provides methods to let the job interact with itself inside a worker process.

For example, in the handle() method of the job you can check how many attempts have been done to process itself, release it and run it later, mark itself as failed or even delete itself from the queue. This is just a silly example:

public function handle()
{
// If this podcast fails to transcode, then we will notify
// the publisher that we will try again later if there is
// any attempts left. Otherwise, throw the exception.
try {
PodcastTranscoder::transcode($this->podcast);
} catch (TranscoderException $exception) {
if ($this->attempts() < $this->tries) {
$this->podcast->publisher->notify(
new Retrying($this->podcast)
);
}
throw $exception;
}
// ...
}

The Queueable trait

This trait, which is also optional, allows you to set the connection, queue and delayment of the job.

For example, you can create a new instance of the Job and manage these properties before dispatching it.

$job = (new ProcessPodcast($podcast))
->onConnection('redis')
->onQueue('podcasts')
->delay(now()->addMinutes(10));
// If the User authenticated is a premium user, we will
// elevate the job to the premium concurrent queue and
// remove the delay to process it as fast as possible.
if ($request->user()->isPremium()) {
$job->onQueue('podcast.premium')
->delay(null);
}
dispatch($job);

When you dispatch a Job that should be queued, you usually end up with a PendingDispatch instance. This is just a class that holds the real Job instance inside. A quick check on the class and you will see that when using onConnection(), onQueue(), and other methods, you are just calling the same underlying job methods.

The SerializedModels trait

This one is magic, and probably the main culprit why your jobs fail when you pass a deleted model to the job that will be processed later.

When the Job is being serialized to be saved into the queue, it will check if there are Eloquent Models or Eloquent Models Collections (like one-to-many relation) inside the properties of the job. If they exist, it will strip them down to only its values needed to be retrieved later from the database, like the ID and the connection. This slims down the serialized string greatly, specially if you’re passing something like Collection of 100 Eloquent Models.

When the Job is unserialized to be processed, the job will automatically restore the models by just retrieving them again from their sources.

Dealing with the Queue on deleted models

In this part is when your Job fails. For example, let’s say your User decided to delete his account. The Model gets deleted from the database, but before going out, we will put is email in a “blacklist” for 30 days so someone cannot use the same email, and queue a goodbye email. We pass the User model, and done.

/**
* Deletes the User Account
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function deleteAccount(Request $request)
{
$request->user()->delete();
dispatch(new GoodbyeUser($request->user()));

return response()->view('user.goodbye');
}

Wake up next morning, the job failed: when the job was unserialized, it couldn’t find the Model, so it returns a ModelNotFoundException. Damn!

This is why, when you pass a Model that is bound to be deleted (or was deleted), is just far better to pass the properties needed to the Job to work, which in this case is just the Email and the User’s name.

/**
* Deletes the User Account
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function deleteAccount(Request $request)
{
$user = $request->user();
$user->delete(); dispatch(new GoodbyeUser($user->email, $user->name);
}

Based on Laravel Queue: Job Class demystified and How Laravel’s SerializesModels Trait Could Save Your Bacon.

--

--

Italo Baeza Cabrera
Italo Baeza Cabrera

Written by Italo Baeza Cabrera

Graphic Designer graduate. Full Stack Web Developer. Retired Tech & Gaming Editor. https://italobc.com

No responses yet