I’ve been playing around with complex queue items using Laravel Eloquent model classes not saved in the database. Before being serialized for a queue job, there are two situations where a model instance can get into this state.

  • A Model@make() / Model@setRelation() structure built up before Model@save() is called.
  • After Model@delete() is called.

The idea is fill the gap where Laravel doesn’t consider these objects eligible for queue processing. Please note this is really just a coding exercise. The former case is very rare. For post-delete queue actions, a SoftDeletes framework-ready solution is shown at the bottom of this post.

Queued jobs can throw ModelNotFoundException

The base Illuminate\Database\Eloquent\Model class implements contract QueueableEntity. This acts as a flag when it comes time to serialize objects for a queue. By default, the framework serializes models as just the class name and active record key. e.g.,

Database row(s) can change after the item is queued so by the time a worker picks it up, it’s almost guaranteed to have the latest application snapshot of that model’s state.

For an unsaved payment method:

unserialize() of the null model identifier is followed by an App\PaymentMethod database query that will throw ModelNotFoundException since there’s nothing to be found for a null key.

Sending mailable for a permanently deleted model

So let’s go through a potential solution for queued models already deleted.

Permanently deleting someone’s tokenized card from their user account, the controller method (ignoring request authorization) may look like this.

The PaymentMethod model class will implement a QueueablePayload contract. Queued jobs, mailables, notifications, etc. will handle that contract by using a custom trait.

shouldQueueAsPayload() would be declared in a base App\Model class to indicate when the database primary key can’t be referenced. In Laravel 5.4, Eloquent sets the exists property to true after Model@create() and false after Model@delete() is called so I just leveraged that. (Note this behaviour changes in Laravel 5.5!)

The new QueueablePayload contract (also used as a serialization flag) is mostly covered by Eloquent’s Model class.

Instead of the mailable class pulling in framework trait Illuminate\Queue\SerializesModels, an app-namespace App\Queue\SerializesModels is used to serialize the key/value paired payload and eager-loaded relations.

The meat of this implementation will override SerializesModels methods getSerializedPropertyValue() and getRestoredPropertyValue() to put the raw serialized model on the queue. If related eager-loaded models are still saved, only their ID is put in the payload string.

The Illuminate\Contracts\Database\ModelIdentifier class replacement put through PHP serialize() is just three properties.

So despite a payment_methods row no longer existing, this email template can still be sent (with an eager-loaded user!)

There Be Danger

  • If eager-loaded relations are also unsaved, the raw PHP serialized string could get huge fast
  • With the QueueablePayload contract applied to all model classes, you run the risk of accidentally sending customer communications about stuff not yet saved.

Actual Solutions For Real World Humans

For the above example, the Laravel OOTB solution is simple:

  • Make the App\PaymentMethod model class use the SoftDeletes trait, adding a payment_methods.deleted_at nullable database column.
  • Viola, you don’t have to worry about the mailable throwing a ModelNotFoundException.
  • Permanently nuke yesterday’s deleted payment methods by scheduling a command to run everyday at 1AM. It will just execute a forceDelete() query.