When I started working with Laravel, I wrote queries like I was still in pure SQL mode.
They looked something like this:
$projects = DB::table('projects')
->join('tasks', 'projects.id', '=', 'tasks.project_id')
->join('comments', 'tasks.id', '=', 'comments.task_id')
->select(
'projects.*',
DB::raw('COUNT(DISTINCT tasks.id) as total_tasks'),
DB::raw('COUNT(DISTINCT comments.id) as total_comments')
)
->where('projects.status', 'active')
->groupBy('projects.id')
->get();It worked — but maintaining these queries became painful. Every time the database changed, I had to edit multiple joins, aliases, and counts.
That’s when I realized Laravel had a cleaner, more expressive way: Eloquent relationships with closures and constraints.
The Turning Point
One day, I asked myself:
“Why am I manually joining tables that already have relationships defined in the models?”
That thought changed everything.
I started defining relationships properly in my models:
// app/Models/Project.php
class Project extends Model
{
public function tasks()
{
return $this->hasMany(Task::class);
}
public function comments()
{
return $this->hasManyThrough(Comment::class, Task::class);
}
}Now, instead of chaining multiple joins, I could simply write:
$projects = Project::query()
->withCount('tasks')
->withCount([
'comments as approved_comments_count' => function ($query) {
$query->where('status', 'approved');
}
])
->where('status', 'active')
->orderBy('approved_comments_count', 'desc')
->get();Why Closures Changed Everything
That small function ($query) — the closure — became my new best friend.
It allowed me to filter related data directly within relationships without polluting the main query. I could apply any condition inside it — date ranges, user roles, approval status, anything.
For example, to count only completed tasks and sum their total hours:
$projects = Project::withCount([
'tasks as completed_tasks_count' => function ($q) {
$q->where('status', 'completed');
}
])->withSum([
'tasks as total_hours' => function ($q) {
$q->where('status', 'completed');
}
], 'hours')
->get();This felt cleaner, faster, and easier to read.
Realizing the Power of Constraints
Closures aren’t just for counts — they’re also powerful filters for whereHas queries.
Let’s say I want only active projects with tasks assigned to verified users:
$projects = Project::whereHas('tasks', function ($taskQuery) {
$taskQuery->whereHas('assignedUser', function ($userQuery) {
$userQuery->where('verified', true);
});
})->get();This would have been a nightmare with raw SQL joins — but with Eloquent, it reads almost like English.
What I Gained by Letting Go of Manual Joins
1. Readability
I stopped scanning lines of joins and conditions. Now, withCount('tasks') tells me exactly what’s happening.
2. Maintainability
When I renamed a column or table, I only needed to update it in one place — the model.
3. Reusability
The same relationships worked in analytics, filters, dashboards — everywhere.
4. Performance
Laravel’s eager loading optimized everything behind the scenes. No more N+1 queries or redundant joins.
5. Confidence
My queries stopped breaking every time I added a new relationship.
The Lesson I Learned
I realized that writing clever SQL doesn’t always mean writing clean code. Closures and constraints gave me the best of both worlds — power and clarity.
Now, every time I start a new Laravel project, I think in terms of relationships, not joins.
My Advice to Developers
If you’re still manually joining tables in Laravel:
Define your relationships first.
Use withCount, withSum, and whereHas.
Experiment with closures to filter related data.
You’ll not only write better code — you’ll actually enjoy reading it later.
Final Thought
Switching from raw SQL joins to Laravel closures didn’t just improve my code — it made me a more expressive developer.
Less syntax. More meaning. That’s what clean Laravel feels like.
¡Sé el primero en compartir tus pensamientos!