Deep Dive into Laravel Query Scopes: Simplifying Query Logic Without Repositories

In the Laravel community, one of the most discussed topics revolves around whether the Repository Pattern is necessary, given that Laravel's Eloquent ORM already provides powerful abstractions. Many developers implement repositories to further separate query logic from business logic, but in many cases, using Eloquent’s Query Scopes can be more efficient and maintainable, avoiding unnecessary complexity.

In this blog post, we will explore the value of Query Scopes in Laravel, explain why repositories might be redundant, and walk through practical examples to show how you can structure your Laravel application with clean and reusable query logic using Query Scopes.

Why Query Scopes are Better Than Repositories

Eloquent ORM, the heart of Laravel's data layer, is designed to abstract the complexities of database interactions, making repositories an additional and often unnecessary layer of abstraction. Query Scopes offer a cleaner, more concise way to encapsulate query logic directly in your model without needing the overhead of separate repository classes.

Repositories can lead to more boilerplate code and make the codebase harder to navigate. With Query Scopes, you can avoid this complexity by handling common query logic directly within the Eloquent model. This allows your services to remain clean and focused on business logic.

Let's dive deeper into the role of Query Scopes and when to use them.

What are Query Scopes?

Query Scopes in Laravel allow developers to encapsulate repetitive or commonly used query logic into a method that can be reused throughout the application. There are two types of Query Scopes in Laravel:

  • Local Scopes: Methods prefixed with scope, which can be chained to a query builder in an elegant and reusable way.
  • Global Scopes: Scopes that are automatically applied to all queries involving the model unless explicitly removed.

For this blog, we’ll focus on Local Scopes, which help to keep query logic reusable and clean.

Example 1: Active Users Query Scope

Consider a scenario where we need to retrieve only active users from the database. Instead of rewriting the same where clause in every part of your application, we can encapsulate this logic in a Local Scope.

Defining the Query Scope

In the User model, we can define a scope like this:

class User extends Model
{
    // Query scope for filtering active users
    public function scopeActive($query)
    {
        return $query->where('status', 'active');
    }
}

With this Query Scope, you no longer need to write ->where('status', 'active') each time you need active users. Instead, you can simply use User::active() in any query involving the User model.

Using the Query Scope in a Service

Let’s look at how to use this Query Scope in a service that manages user operations:

class UserService
{
    protected $user;

    public function __construct(User $user)
    {
        $this->user = $user;
    }

    public function getActiveUsersWithPagination($page, $limit)
    {
        // Default pagination values if none are provided
        $limit = $limit ?: 15; // Default to 15 items per page
        $page = $page ?: 1; // Default to first page

        // Use the active query scope and paginate the results
        return $this->user->active()->paginate($limit, ['*'], 'page', $page);
    }
}

By using the active() scope, we keep our service logic focused on business rules, while the query logic remains encapsulated in the model. This keeps the service clean and reusable.

Controller to Handle Requests

In the controller, we can now call the service to retrieve and paginate active users:

class UserController extends Controller
{
    protected $userService;

    public function __construct(UserService $userService)
    {
        $this->userService = $userService;
    }

    public function index(Request $request)
    {
        // Retrieve page and limit from query parameters with default values
        $page = $request->query('page', 1);
        $limit = $request->query('limit', 15);

        // Use the service to get active users with pagination
        $users = $this->userService->getActiveUsersWithPagination($page, $limit);

        return view('users.index', compact('users'));
    }
}

This approach results in a clean and efficient flow from controller to service to model, with clear separation of concerns.

Example 2: Retrieving Posts from Active Users

Let’s take another example: retrieving posts only from active users. This time, we’ll use the whereHas query method in a scope to filter posts based on the activity status of the users.

Defining the Query Scope

In the Post model, we can define the following scope:

class Post extends Model
{
    public function user()
    {
        return $this->belongsTo(User::class);
    }

    // Query scope to get posts where the user is active
    public function scopeByActiveUser($query)
    {
        return $query->whereHas('user', function ($q) {
            $q->where('status', 'active');
        });
    }
}

This scope ensures that only posts from active users are retrieved, without the need for additional queries in your services or controllers.

Service for Fetching Posts

Now, let’s create a service that retrieves posts by active users:

class PostService
{
    protected $post;

    public function __construct(Post $post)
    {
        $this->post = $post;
    }

    public function getPostsByActiveUsers($page, $limit)
    {
        return $this->post->byActiveUser()->paginate($limit, ['*'], 'page', $page);
    }
}

Again, the service logic remains clean and focused on business rules while the query is handled by the model.

Example 3: Using Raw Queries in Scopes

Sometimes, you might need to use raw SQL queries within your Laravel application. For example, let’s say we want to retrieve users who have logged in within the last 30 days. We can use a raw query inside a scope to achieve this.

Defining the Query Scope

Here’s how to define a scope for users who have logged in within the last 30 days:

class User extends Model
{
    // Query scope to filter users who have logged in within the last 30 days
    public function scopeRecentlyLoggedIn($query)
    {
        return $query->whereRaw('last_login_at >= NOW() - INTERVAL 30 DAY');
    }
}

This query uses raw SQL to filter users based on their last login date.

Service for Recently Logged-In Users

Here’s how we can use this scope in a service:

class UserService
{
    protected $user;

    public function __construct(User $user)
    {
        $this->user = $user;
    }

    public function getRecentlyLoggedInUsers($page, $limit)
    {
        return $this->user->recentlyLoggedIn()->paginate($limit, ['*'], 'page', $page);
    }
}

The recentlyLoggedIn scope keeps the query logic encapsulated within the model, while the service handles the business logic of paginating and retrieving the data.

The Power of Query Scopes in Laravel

By leveraging Query Scopes in Laravel, you can encapsulate your query logic within the model, where it belongs. This helps keep your service layer focused on business logic and your controller thin and maintainable. The encapsulation of query logic ensures that the code is reusable and that you aren’t duplicating queries across your codebase.

Moreover, Query Scopes keep your code DRY and flexible. If you need to modify how a particular query works, you only need to change the scope within the model, and all instances of that query will be updated.

When Should You Use Query Scopes?

Query Scopes are ideal for encapsulating:

  • Common filters: Such as filtering for active users or featured products.
  • Complex queries: When using relationships or whereHas conditions.
  • Raw SQL queries: When you need to include raw SQL logic but want to keep your service and controller clean.

Conclusion

Laravel’s Eloquent ORM is already a powerful abstraction layer that reduces the need for a repository in most cases. Query Scopes offer an elegant way to encapsulate query logic within your models, keeping your services and controllers clean and focused on business rules. By using Query Scopes, you can avoid the redundancy of repositories, maintain a clear separation of concerns, and keep your codebase efficient and easy to maintain.

The next time you find yourself considering adding repositories to your Laravel project, ask yourself whether a Query Scope might be the simpler, more efficient solution. In most cases, Query Scopes will keep your application clean, maintainable, and focused on what truly matters: delivering business value.