Laravel: 3 ways of Processing a Job for a deleted Model

“Two books on a desk near a MacBook with lines of code on its screen” by Émile Perron on Unsplash

One the capabilities of the framework is pushing jobs to queue to be processed later, rather than in the same request cycle. This makes the application faster, because the request isn’t blocked until the end of the “Job” you want to do, like calculating the meaning of life or whatever.

Let’s say, for example, we want to send an Greeting Email to an User when its registered successfully, using our painfully slow email server that takes 2.4 eons to send a fuc… a simple email. Very well, then:

When we push the User model to the our ProcessNewUser Job, it will greet the user, among other needs for our sample application.

Pretty much straightforward. We notify the user with an email, and we do some other additional random things; it’s only to show that Jobs are very flexible in terms of what you can do and you know some processes may take a while to complete.

Everything fine, until we want to execute a job on a deleted model.

Trying to pull our a rabbit from a hat, without the hat

For our next Job, we will want to send an email to the user when he decides to leave, and delete some other information related to it.

Pretty much like we would hope it to work, a custom Job would be in charge of all. So on our hypothetically UnregisterController.php we will delete the user, and push the job passing the User Model we are about to delete. This is only an example controller, so take it lightly. Yes, it should validate a request and authorize the deletion but it’s for show only.

Now that we have the job called, and the user deleted, we proceed to write what we need to do when the user stops existing. We feel pretty sure that it will work, because we are putting in queue a model before it is deleted. It should exist when the Job is pushed, right?

WRONG.

The Queued Job won’t find the User model because it will be deleted when the job executes later, even if it’s pushed to the queue before the deletion.

The async nature of the Queue Job doesn’t account if the model you are passing through the dispatch() method exists. It will try to find it regardless, fail miserably, and throw a nice Exception in your failed_jobs table. And this is the expected behavior.

What happens when we dispatch using a model? The Job will be saved into the queue, but if you’re passing an Eloquent Model, it will be referenced from the Database by its id (or primary key) instead of being saved entirely into the payload.

If you think about it, this approach is smart, because ensures the queue payload doesn’t get too big if, for example, your are pushing a model with huge data attached onto beanstalkd or Redis. It also allows for changes before the job is executed, and avoids other problems with serialization.

There are three ways to deal with this kind of “Job with an nonexistent model”. None of them are too difficult, though, but not a walk in the park either.

A) From Model to Array to Model again

This is the most easy approach. In your Job, instead of passing a model instance, like we did with our User Model, we will pass the model as an array. You can find the caveats of this serialization method on the Laravel Docs, but for this example, we need only three pieces of information for this Job to work.

The Job dispatch method won’t hard-reference this data into the payload, because it’s not a model to query in the database. So instead, it will be saved entirely in the payload, that the Job will read once executed. If you are using in-memory queues like beanstalkd or Redis, you may want to keep this information at minimum.

When the Job awakes and executes afterwards, we will “remake” the model from the array that it will get from the saved payload.

With this approach we can use almost all methods related to the model, like getters and mutators, notifications, and querying relations — as long as these are not deleted. In that case, you should push the array with the related models that were deleted too.

B) Play with SoftDeletes and the primary key

The second approach is little less elegant: we soft-delete the user model in the database. We dispatch the Job using only the ID (or primary key), we retrieve the soft-deleted model and proceed to execute the rest of the Job.

In our FarewellUser Job, we will retrieve the user from behind the curtain, as Laravel says, using the withTrashed() method for soft-deleted models.

After all that, we can ensure to delete completely the User model using the method$this->user->forceDelete() at the end of the job, if you want, or just make a Scheduled Job to clean soft-deleted models in your database gracefully. Did someone said Quicksand?

C) Copy it to the Trash bin

This is kind of tedious, but it’s a solution that won’t affect your application in big ways. We simple copy the model to another inert model, and we then dispatch the Job using the new inert model instead of the original that we deleted.

The new inert model will be called UserDeleted, but you are free to name it like TrashBin or whatever. You don’t have to reinvent the wheel or code endlessly, a nice basic model will do for simplicity. Okay, okay, nothing stops you from calling the table directly without the Eloquent Model overhead, but let’s use the former so it can be referenced by the Job.

Our users_deleted table will contain an id and payload column. The former will allow the Job to find the row in the table, and we will use the latter to save the deleted User model.

We now dispatch the Job using the UserDeleted Model instead of the User model.

And we tell our Job to receive our UserDeleted model.

This will mean to rewrite your FarewellUser Job to match the payload contents as an array. Obviously it will lack native relations but you can workaround that replicating the primary key in a new column, or just remaking the User model again like we did in the first solution:

Not the most nice of all three, but this makes deleting the User model transparent to the application: leaves the origin table (and probably its index) alone, the data persists unmodifiable on another table, and once the Job is done we can end with a nice $this->user->delete() or schedule a cleaner.

If you have another workaround, hit the comments.

Graphic Designer graduate. Full Stack Web Developer. Retired Tech & Gaming Editor.