Certainly! Let’s break down your scenario and solution.
Problem Restated:
You want to apply a global scope for multi-tenancy, and you’d like to set the tenant relation on each retrieved model (using setRelation('tenant', $tenant)), to avoid eager loading, by using an afterQuery callback.
However, you’ve observed that afterQuery doesn’t work consistently inside a global scope, potentially due to internal clone calls in Eloquent’s applyScopes().
Solution
Why afterQuery is Unreliable in a Global Scope
You're right: Eloquent's core internally clones query builders and models while applying global scopes. That cloning can break the chain so that the afterQuery() configured in your custom builder or scope isn’t always carried over. This is expected due to how the framework manages query state.
Thus, the inconsistent execution of afterQuery() within a global scope is not a bug, but a consequence of the underlying query cloning behavior.
Alternative Approaches
1. Set the Relation in the retrieved Model Event
You can hook into the model's retrieved event. This ensures that when any model is hydrated from the database, you inject the correct relation.
In your model:
protected static function booted()
{
static::retrieved(function ($model) {
$tenant = Tenant::current();
if ($tenant && $model->tenant_id === $tenant->id) {
$model->setRelation('tenant', $tenant);
}
});
}
This will ensure that every time the model is instantiated, it will have the correct tenant relation set, without the need to eager load.
2. Use a Custom Collection
Alternatively, you can override the newCollection method in your model to set the relation on all models at once when a collection is hydrated:
public function newCollection(array $models = [])
{
$tenant = Tenant::current();
if ($tenant) {
collect($models)->each(function ($model) use ($tenant) {
if ($model->tenant_id === $tenant->id) {
$model->setRelation('tenant', $tenant);
}
});
}
return new Collection($models);
}
3. Manually Map After Retrieval
If you only care about specific queries, you can wrap your query in a collection map instead of relying on a global scope:
$models = Model::all()->map(function ($model) use ($tenant) {
if ($model->tenant_id === $tenant->id) {
$model->setRelation('tenant', $tenant);
}
return $model;
});
Summary Table
| Approach | Global/Affects All? | Applies to |
|---|---|---|
Model retrieved event |
Yes | All retrievals |
Override newCollection |
Yes | All collections |
Manual map() after query |
No | Single query |
Final Recommendation
The retrieved event is generally the cleanest, Laravel-supported solution for your need:
protected static function booted()
{
static::retrieved(function ($model) {
$tenant = Tenant::current();
if ($tenant && $model->tenant_id === $tenant->id) {
$model->setRelation('tenant', $tenant);
}
});
}
This avoids both the performance impact of eager loading and the unreliable application of afterQuery in a global scope.
References:
Let me know if you need more details or a more advanced customization!