Laravel: Use scope from related model to filter results

Stefan Bogdanescu

Founder & Senior Architect · 2026-06-29

Laravel Company

Laravel: Use Scopes from Related Models to Filter Results Efficiently

TLTR: When filtering parent models based on conditions in related child models, avoid complex eager loading with nested scopes. Instead, leverage Eloquent's whereHas method to perform efficient database joins and filtering directly at the source.


As developers working with relational data in Laravel, we frequently encounter scenarios where we need to filter a primary model (like User) based on conditions applied to its related models (like UserSubscriber). When dealing with one-to-one or one-to-many relationships, optimizing these queries is crucial for performance. This post dives into a common sticking point: how to effectively use scopes defined on related models to filter parent results without resorting to inefficient post-collection filtering.

The Setup: Models and Relationships

Let's establish the context using our provided model structure. We have a User who can have an optional UserSubscriber.

User Model

class User extends \Sentinel\Models\User
{
    public function subscriber()
    {        
        return $this->hasOne('App\Models\UserSubscriber', 'user_id');
    }       
}

UserSubscriber Model with Scope

The UserSubscriber model contains the scope we want to utilize:

class UserSubscriber extends Model
{  
    public function user()
    {        
        return $this->belongsTo('App\Models\User', 'user_id');
    }        

    /**
     * Scope to filter subscribers by language.
     */
    public function scopeLangSubscribers($query, $langCode)
    {
        $query->where('languages_subscribed', 'LIKE', '%' . $langCode . '%');
    }    
}

The Pitfall: Why Nested Eager Loading Fails

The goal is to fetch all users who have at least one subscriber matching a certain language scope. A common initial attempt involves eager loading the relationship and applying the scope within the closure:

$users = User::with(['subscriber' => function ($query) use ($lang) {
    $query->langSubscribers($lang);
}])->get();

As you correctly observed, this approach often fails to yield the desired result. When using nested scopes within an eager load context, Eloquent primarily focuses on loading the related data alongside the parent model. If a user has no matching subscriber record (or if the scope logic doesn't perfectly map back to the parent query in a way that guarantees existence), the resulting collection contains all users, even those where the relationship is null, leading to unnecessary overhead.

Attempting to fix this by post-filtering:

$users = $users->filter(function($item) {
    return $item->subscriber !== null;
});

This solution is computationally expensive. It forces Laravel to retrieve a potentially massive collection of User models from the database only to discard many records in PHP memory, completely defeating the purpose of letting the database handle the heavy lifting. This pattern should be avoided when dealing with large datasets, especially when following best practices promoted by frameworks like Laravel.

The Solution: Leveraging whereHas for Efficient Filtering

The most idiomatic and performant way to filter a parent model based on the existence of related records is by using the whereHas method. This method translates directly into an efficient SQL JOIN or EXISTS clause, pushing the filtering logic down to the database engine where it can execute it quickly.

To use scopes within whereHas, we simply call the scope on the relationship being checked:

$lang = 'en';

$users = User::whereHas('subscriber', function ($query) use ($lang) {
    // Apply the scope directly to the related model query
    $query->langSubscribers($lang);
})->get();

How This Works

  1. User::whereHas(...): This tells Eloquent to only return User models that satisfy the condition defined in the closure, based on the relationship specified (subscriber).
  2. 'subscriber': Specifies the relationship we are checking against.
  3. function ($query) use ($lang) { ... }: This closure receives a query builder instance specifically for the related UserSubscriber model.
  4. $query->langSubscribers($lang): Inside this closure, we invoke our custom scope. This translates into a clause like WHERE EXISTS (SELECT 1 FROM user_subscribers WHERE user_id = users.id AND languages_subscribed LIKE '%en%').

This single query executes against the database, ensuring that only users who possess a related subscriber matching the language criteria are retrieved. This is significantly more efficient than loading everything and filtering it in PHP memory. For complex querying involving multiple relationships or nested scopes, mastering techniques like this is essential for building scalable applications, just as we strive to do when developing robust APIs on platforms like Laravel Company.

Conclusion

When you need to filter parent records based on conditions in related models, always prioritize database-level operations over application-level filtering. While nested eager loading is great for retrieving related data, using whereHas combined with custom scopes allows you to delegate the entire filtering process to the SQL engine. By adopting this approach, you ensure your Laravel applications remain fast, scalable, and maintainable.