Post-Process Query Results Elegantly with Laravel's afterQuery Method

Need a cleaner way to manipulate Eloquent collections after queries? Laravel's new afterQuery() hook provides an elegant solution that keeps your code DRY and maintainable.
When working with Eloquent models, we often need to perform operations on our results after retrieving them from the database. Traditionally, this meant writing additional code after executing the query or creating complex local scopes. With Laravel's new afterQuery() method, you can now define these post-processing steps right alongside your query, creating a more cohesive and maintainable codebase.
Let's see how it works:
$query->afterQuery(function ($models) {
// Make changes to the queried models ...
});
The afterQuery()
method accepts a closure that receives the collection of models returned by the query. This allows you to perform transformations, add computed properties, or apply other modifications directly to the results before they're returned to the calling code.
Real-World Example
Let's look at a practical example to understand how afterQuery() can improve your code. Imagine we have a product catalog where users can favorite items. We want to add an 'is_favorite' attribute to products when querying them.
Here's the traditional approach without afterQuery():
// Before
public function scopeWithIsFavoriteOf($query, ?User $user = null) : void
{
if ($user === null) {
return $query;
}
$query->addSelect([
// 'is_favorite' => some query ...
]);
}
$products = Product::withIsFavoriteOf(auth()->user())->get();
if (auth()->user() === null) {
$products->each->setAttribute('is_favorite', false);
}
Notice how we have to check for a null user in two places: first in the scope definition and again after executing the query. This creates a fragile relationship where the calling code must know to perform additional processing if no user is authenticated.
Now, let's improve this using the afterQuery() hook:
// After
public function scopeWithIsFavoriteOf($query, ?User $user = null) : void
{
if ($user === null) {
$query->afterQuery(fn ($products) => $products->each->setAttribute('is_favorite', false));
return;
}
$query->addSelect([
// 'is_favorite' => some query ...
]);
}
Product::withIsFavoriteOf(auth()->user())->get();
With this approach, the responsibility for handling the null user case is entirely contained within the scope. The calling code simply uses the scope without needing to know about the conditional logic, making for cleaner and more maintainable controller code.
If this guide was helpful to you, subscribe to my daily newsletter and give me a follow on X/Twitter (https://x.com/harrisrafto), Bluesky (https://bsky.app/profile/harrisrafto.eu), and YouTube (https://www.youtube.com/@harrisrafto). It helps a lot!