Mastering Laravel Eloquent: WhereHas and MorphTo Filtering
Master Laravel Eloquent relationship filtering using whereHas and morphTo. Learn how to query nested polymorphic relationships with precision and performance.
During a recent refactor of a multi-tenant content management system, I hit a wall while trying to filter comments based on the type of model they were attached to. We had a polymorphic commentable relationship, and the business logic required fetching only comments associated with "Published" articles. Getting this right without executing a mountain of N+1 queries—or worse, writing raw SQL—required a deep dive into combining whereHas and morphTo constraints.
If you are already familiar with the basics of Eloquent basics: models, relationships, and your first queries, you know that standard filtering is simple. However, things get messy the moment you introduce polymorphism.
Understanding the WhereHas Query in Polymorphic Contexts
The whereHas method is your primary tool for filtering a model based on the existence of a relationship. When you're working with a morphTo relationship, the standard syntax often feels counter-intuitive because you aren't just filtering by a table—you're filtering by a specific type of record.
We first tried a naive approach using a standard whereHas. It failed because the commentable relationship doesn't point to a single table; it points to whatever model the ID belongs to. Here is how we correctly constrained the query to ensure we only get comments for published articles:
PHPComment::whereHasMorph('commentable', [Article::class], function ($query) { $query->where('status', 'published'); })->get();
This approach is clean and readable. By using whereHasMorph, you explicitly tell Laravel which model types to check, preventing the query builder from trying to join tables that don't exist in that context.
Filtering Nested Eloquent Queries with Precision
Often, you aren't just filtering the parent; you're filtering deep into the nested relationships. Imagine needing to find all users who have written comments on articles that belong to a specific category.
Before I landed on the solution below, I spent about two hours fighting with manual joins. I eventually realized that Mastering Laravel whereHas: Filter Models by Relationship Presence is only half the battle; when you nest these, you have to keep the closure scope tight.
PHPUser::whereHas('comments', function ($query) { $query->whereHasMorph('commentable', [Article::class], function ($subQuery) { $subQuery->whereHas('category', function ($catQuery) { $catQuery->where('slug', 'tech'); }); }); })->get();
Why this works
- Scope Isolation: Each
whereHasorwhereHasMorphclosure opens a new scope. - Type Safety:
whereHasMorphensures we only execute thecategoryfilter on theArticletable, avoiding "column not found" errors on other morphable models likeVideoorPost. - Performance: Eloquent translates this into a series of
EXISTSclauses. While complex, it is generally much faster than loading all models into memory and filtering them with PHP collections.
Comparison: Handling Polymorphic vs Standard Relationships
When deciding how to structure your queries, keep this distinction in mind.
| Feature | Standard whereHas | whereHasMorph |
|---|---|---|
| Relationship Type | hasMany, belongsTo | morphTo, morphMany |
| Target | Single table | Multiple potential tables |
| Complexity | Low | Moderate |
| Use Case | Simple filtering | Dynamic model types |
A Note on Performance
While whereHas is incredibly powerful, it isn't a silver bullet. If you find your query execution time creeping above 300ms, you likely need to look at your database indexes. Database Indexing Strategies: Optimizing Laravel Query Performance covers how to index your morphable_id and morphable_type columns. Without a composite index on those two columns, your database will perform a full table scan every time you filter, regardless of how clean your Eloquent code looks.
If you find yourself repeating these complex filters, don't keep them in your controller. I always move these into Mastering Laravel Eloquent Scopes: Writing Reusable Query Constraints to keep the codebase maintainable.
FAQ
Q: Can I use whereHasMorph with multiple models?
A: Yes. You can pass an array of models, like [Article::class, Video::class]. Just ensure that the constraints inside the closure apply to columns present in both tables, or use a conditional check.
Q: Does whereHasMorph work with belongsTo polymorphic relations?
A: It does. It is specifically designed to handle the morphTo relationship where the inverse is a morphMany or morphOne.
Q: Is there a performance penalty for nesting too many whereHas calls?
A: Yes. Every whereHas adds an EXISTS subquery. If you nest 4-5 levels deep, your SQL becomes quite heavy. If performance degrades, consider denormalizing your data or using a specialized search index like Meilisearch or Elasticsearch.
I’m still experimenting with caching these complex relationship queries. Sometimes, the overhead of building the query is negligible compared to the execution time, but for high-traffic dashboards, caching the result set for a few minutes is often worth the trade-off. Don't over-engineer it until you have a benchmark that proves you need to.