The magic of query scopes in Laravel

bertheyman

Bert Heyman

Posted on June 3, 2018

The magic of query scopes in Laravel

Let's say you're building a site where people can enter book suggestions for others. As a teaser, the homepage will contain 5 book suggestions. The query could be as simple as:

// A simple select
Book::limit(5)->get();
Enter fullscreen mode Exit fullscreen mode

Now, you might have selected some low quality book suggestions, or the horcrux book with knowledge that better stays hidden forever. You decide that book suggestions, in order to be published on the homepage, should

  • be set to visible
  • created during the last month
  • have at least 10 upvotes
// Some quality control
Book::where('visible', 1)
    ->where('created_at', '>', Carbon::now()->subMonth())
    ->whereHas('votes', '>=', 10)
    ->limit(5)
    ->get();
Enter fullscreen mode Exit fullscreen mode

This gives you some perfect book suggestions, so you're satisfied and set aside some time to to read some of them. But after a while, you end up needing a lot more checks to improve the quality of the suggestions. This might make the query a bit messy or harder to read.

At this point, you might consider spitting your query using query scopes for. By breaking your query into building blocks, it becomes easier to read and reuse.

A refactor with query scopes

Have a look at the following refactor with query scopes from Laravel:

// App\Http\Controllers\BookController.php

// This syntax is very readable and easy to reuse
// Every building block is defined on your model
Book::popular()
    ->createdAfter(Carbon::now()->subMonth())
    ->limit(5)
    ->get();

// App\Book.php (model)

protected static function boot()
{
    parent::boot();

    // A global scope is applied to all queries on this model
    // -> No need to specify visibility restraints on every query
    static::addGlobalScope('visible', function (Builder $builder) {
        $builder->where('visible', 1);
    });
    // Bonus: if multiple models are hideable, this behaviour might
    // belong in a specific scope for easy reuse
}

// These are local scopes: ->popular() is added to the query to apply this where statement
public function scopePopular($query)
{
    // By defining the conditions to be a popular book,
    // it's easy to change them later on for all queries at once
    return $query->whereHas('votes', '>=', 10);
}

public function scopeCreatedAfter($query, $date)
{
    // A scope can be dynamic and accept parameters
    return $query->where('created_at', '>', $date)
}

Enter fullscreen mode Exit fullscreen mode

It's not always useful to use scopes, but I've found them particulary useful when feeling the need for smaller, reusable blocks in my queries or when struggling with readability.

I hope you've learned something if you didn't use query scopes before.

How have you wrestled with (more complex) queries?
Feel free to leave a comment to tell about:

  • Any useful tips for beginners missing in this article?
  • What you'd do differently in the query from the example?
  • Patterns you use for organising queries in larger projects. Repositories, query scopes, a mix or something completely different?

Further reading: Laravel docs on query scopes (check if that Laravel version is still the most recent one)
Follow up article: Breaking model functionality down in reusable components

💖 💪 🙅 🚩
bertheyman
Bert Heyman

Posted on June 3, 2018

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related