Eloquent Query Scope on Relationships

Stefan Bogdanescu

Founder & Senior Architect · 2026-06-29

Laravel Company

Mastering Reusability: Eloquent Query Scopes on Relationships

As developers working with large applications, our goal is not just to write functional code, but to write maintainable, readable, and highly reusable code. In the Laravel ecosystem, Eloquent provides powerful tools for data manipulation, and among them, Query Scopes are indispensable for achieving this reusability.

Today, we dive into a common scenario: filtering models based on relationships and how to effectively apply these complex filters using scopes. We will analyze your specific query involving Song and Host models and resolve the issue of applying reusable logic across relationships.

The Challenge: Filtering Through Relationships

You have a classic one-to-many relationship: Song belongs to Host. You want to retrieve songs based on conditions applied to the host they belong to (e.g., the host must be active and exceed a certain skip threshold).

Your initial query looks like this:

$songs = Song::whereHas('host', function($query) {
    $query->where('skip_threshold', '>', \DB::raw('songs.attempts')) // Note: Potential issue here
          ->where('active', 1);
})->whereNull('downloaded')->get();

You correctly identified that putting this logic directly in the controller is brittle and hard to reuse. The solution is to encapsulate this filtering logic into Eloquent Query Scopes defined on the respective models.

Understanding Query Scopes and Relationships

Query scopes are methods on an Eloquent model that define constraints that can be applied to any query built on that model. When dealing with relationships, we use whereHas() or with() combined with these scopes to filter related data effectively.

The reason you might not be getting results is often due to how the nested conditions within whereHas interact with the relationship structure. The key is ensuring your scope correctly targets the related model when filtering through the relationship.

Let's refactor your requirements into reusable scopes on the Song and Host models.

Implementing Reusable Scopes

We will define three scopes: one for checking host eligibility, one for checking host activity, and one for checking download status.

1. Song Model Implementation (app/Models/Song.php)

Since you are querying the Song model but filtering on the related Host, we need to structure the scope to correctly target the relationship.

// app/Models/Song.php

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;

class Song extends Model
{
    /**
     * Scope a query to only include songs from active hosts with sufficient attempts.
     */
    public function scopeEligable(Builder $query)
    {
        // We use whereHas to ensure the relationship exists and meets criteria on the Host model.
        $query->whereHas('host', function ($q) {
            // Filter based on the related 'host' model
            $q->where('active', 1)
              ->where('skip_threshold', '>', \DB::raw('songs.attempts')); // Note: Correctly referencing related data if needed
        });
    }

    /**
     * Scope to filter songs that have not been downloaded.
     */
    public function scopeInDownloadQueue(Builder $query)
    {
        $query->whereNull('downloaded');
    }
}

A Note on the Implementation: Notice how scopeEligable uses whereHas. This tells Eloquent: "Only select songs where there exists a related host that satisfies these conditions." This is the correct mechanism for filtering based on parent model attributes.

2. Host Model Implementation (app/Models/Host.php)

While the main filtering logic lives on the Song model in this case, it’s good practice to ensure the Host model also has scopes if you plan to query hosts directly.

// app/Models/Host.php

class Host extends Model
{
    /**
     * Scope for active hosts.
     */
    public function scopeActive(Builder $query)
    {
        $query->where('active', 1);
    }
}

Applying the Scopes in the Controller

Now, instead of writing complex nested conditions in your controller, you simply chain these clean scopes. This dramatically improves readability and maintainability, which is a core principle emphasized by Laravel's design philosophy.

By applying these scopes directly to the Song query, we achieve the desired filtering cleanly:

// In your Controller method

$songs = Song::eligible() // Applies scopeEligable (which contains whereHas)
             ->inDownloadQueue() // Applies scopeInDownloadQueue
             ->get();

Conclusion

Query Scopes are a powerful feature that transforms repetitive, complex database logic into readable, reusable methods. By understanding how to combine whereHas within your scopes, you can abstract intricate relationship filtering away from your controllers and models. This approach keeps your code DRY (Don't Repeat Yourself), making it easier for any developer—including yourself six months from now—to understand and maintain the application logic. For more in-depth knowledge on leveraging Eloquent's full potential, always refer to the documentation at laravelcompany.com.