Back to Blog
PHPJune 28, 20264 min read

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.

laraveleloquentphpdatabaseoptimization

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:

PHP
Comment::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.

PHP
User::whereHas('comments', function ($query) {
    $query->whereHasMorph('commentable', [Article::class], function ($subQuery) {
        $subQuery->whereHas('category', function ($catQuery) {
            $catQuery->where('slug', 'tech');
        });
    });
})->get();

Why this works

  1. Scope Isolation: Each whereHas or whereHasMorph closure opens a new scope.
  2. Type Safety: whereHasMorph ensures we only execute the category filter on the Article table, avoiding "column not found" errors on other morphable models like Video or Post.
  3. Performance: Eloquent translates this into a series of EXISTS clauses. 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.

FeatureStandard whereHaswhereHasMorph
Relationship TypehasMany, belongsTomorphTo, morphMany
TargetSingle tableMultiple potential tables
ComplexityLowModerate
Use CaseSimple filteringDynamic 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.

Similar Posts