PHP Laravel

Robust Job Handling in Laravel: Avoiding ModelNotFoundExceptions with Tenant Context

Introduction

In multi-tenant Laravel applications, queued jobs that interact with tenant-specific data can sometimes fail due to issues with model serialization and tenant context. Specifically, jobs that serialize Eloquent models might attempt to deserialize them outside of the correct tenant's database connection, leading to ModelNotFoundException errors. This post explores a solution to this problem within the context of the Reimpact/platform project.

The Problem: Tenant Context and Model Serialization

Consider a scenario where a StoreMassiveUpload job needs to process a MassiveUpload model. The naive approach might involve serializing the entire MassiveUpload model instance when dispatching the job. However, in a multi-tenant environment, each tenant has its own database schema. When the job is processed, the SerializesModels trait attempts to deserialize the MassiveUpload model. If the tenant context is not properly set during deserialization, the job will query the wrong database table (e.g., public.massive_uploads instead of tenant.massive_uploads), resulting in a ModelNotFoundException.

The Solution: Scalar Data and Explicit Tenant Context

To avoid this issue, we can modify the job to accept only the scalar data needed to retrieve the model within the correct tenant context. Instead of serializing the entire MassiveUpload model, we pass the ID and any other necessary scalar data (e.g., an array of upload details) to the job.

Here's an illustrative example:

// Instead of:
// MassiveUploadJob::dispatch($massiveUpload);

// Do:
MassiveUploadJob::dispatch($massiveUpload->id, $uploadData);


class MassiveUploadJob implements ShouldQueue
{
    public function __construct(public int $massiveUploadId, public array $uploadData)
    {
    }

    public function handle()
    {
        TenantContext::run(function () {
            $massiveUpload = MassiveUpload::find($this->massiveUploadId);

            // Process the upload using $massiveUpload and $this->uploadData
        });
    }
}

By passing only the ID and retrieving the model within TenantContext::run(), we ensure that the database query is executed against the correct tenant's schema. This prevents the ModelNotFoundException.

Making Tenant Table Names Accessible

In some cases, jobs may need to reference the tenant's table name directly, for instance, when using insertOrIgnore. To facilitate this, the getTenantTableName() method in the HasTenantTable trait can be made public.

// Example usage within a job
$tableName = (new MassiveUpload())->getTenantTableName();
DB::table($tableName)->insertOrIgnore([/* data */]);

Key Takeaways

When working with queued jobs in multi-tenant Laravel applications, be mindful of how Eloquent models are serialized and deserialized. Passing scalar data and explicitly setting the tenant context within the job's handle method can prevent common ModelNotFoundException errors. Exposing the getTenantTableName function can also be helpful.

Robust Job Handling in Laravel: Avoiding ModelNotFoundExceptions with Tenant Context
GERARDO RUIZ

GERARDO RUIZ

Author

Share: