When I started working with Laravel on real projects which introduced juggling multiple stakeholders, admin panels, and third-party integrations, I noticed my controllers and models slowly turning into a mess of repeated queries and bloated conditionals. Eloquent is powerful, but it’s easy to use it in ways that just barely work instead of ways that actually scale.
Here are five Eloquent tricks that made the biggest difference in cleaning up my code where each one solves a problem I ran into more than once.
1. Query Scopes Instead of Repeated where() Chains
If you find yourself writing the same where() conditions in multiple places, that’s a sign you need a scope.
Before:
$activeCustomers = Customer::where('status', 'active')
->where('deleted_at', null)
->get();
After:
// In the Customer model
public function scopeActive($query)
{
return $query->where('status', 'active')
->whereNull('deleted_at');
}
// Anywhere else in the app
$activeCustomers = Customer::active()->get();
Scopes turn business logic into readable, reusable one-liners. When the definition of “active” changes, you update it in exactly one place instead of hunting through the codebase.
You can also build dynamic scopes that accept parameters:
public function scopeFromRegion($query, $region)
{
return $query->where('region', $region);
}
// Usage
$customers = Customer::fromRegion('east-kalimantan')->get();
2. whenLoaded() to Avoid Accidental N+1 Queries in API Responses
The problem: you conditionally eager-load a relationship, but your resource always tries to access it which in turn triggers a lazy load (and an N+1 query) whenever it wasn’t loaded.
The fix:
public function toArray($request)
{
return [
'id' => $this->id,
'title' => $this->title,
'category' => $this->whenLoaded('category'),
'author' => $this->whenLoaded('author', function () {
return new AuthorResource($this->author);
}),
];
}
whenLoaded() only includes the relationship in the response if it was actually eager-loaded upstream. No relationship in the payload, no surprise query, no N+1 problem creeping into production.
3. Accessors and Mutators for Data That Needs Shaping
Anytime I catch myself formatting or transforming the same field in multiple views or controllers, I move that logic into the model.
Before (scattered everywhere):
$fullName = $user->first_name . ' ' . $user->last_name;
After (Laravel 9+ attribute syntax):
use Illuminate\Database\Eloquent\Casts\Attribute;
protected function fullName(): Attribute
{
return Attribute::make(
get: fn () => "{$this->first_name} {$this->last_name}",
);
}
// Usage anywhere
$user->full_name;
This is especially handy for things like formatting shipping costs from an API integration, normalizing phone numbers, or masking sensitive fields which are logics that belongs on the model, not scattered across every controller that touches it.
4. chunk() and lazy() for Large Datasets
Pulling thousands of records with ->get() and looping over them in PHP is a fast way to run out of memory.
Before (memory-heavy):
$orders = Order::where('status', 'pending')->get();
foreach ($orders as $order) {
// process
}
After:
Order::where('status', 'pending')->chunk(200, function ($orders) {
foreach ($orders as $order) {
// process
}
});
Or, if you don’t need control over batch size and just want a memory-efficient iterator:
foreach (Order::where('status', 'pending')->lazy() as $order) {
// process
}
chunk() pulls records in batches, and lazy() uses a cursor under the hood, both keep memory flat no matter how large the table gets.
5. Eager Loading with Specific Columns to Cut Down Payload Size
Eager loading with with() solves N+1 queries, but people often forget you can also limit which columns come back which matters a lot once your models have dozens of fields.
Before:
$posts = Post::with('author')->get();
After:
$posts = Post::with('author:id,name,email')->get();
Just make sure the foreign key (id here) is included, or the relationship won’t resolve correctly. This trick alone noticeably shrunk response payloads and query time on listing pages where I only needed an author’s name and email, not every column on the users table.
Summary
None of these tricks are exactly new, they’re all first-class Eloquent features. The real win came from recognizing the patterns that kept repeating across different projects (dashboards, APIs, bulk processing) and reaching for the right tool instead of patching around the symptom each time.
If you’re maintaining a growing Laravel codebase, I’d start with scopes and whenLoaded() which tend to catch the most common messes with the least amount of refactoring.