Unrivaled Creations Stories

Internet Website Design and Bespoke Software Application Development with PHP and the Laravel Stack.

2 Ways to Code Laravel Query Scopes, Part II

1 year ago · 7 MIN READ
#development  #laravel 

Laravel query scopes

This is Part II of a two-part series about Laravel query scopes which will cover global query scopes. Be sure to visit Part I of this series to learn about local query scopes.

Laravel Eloquent Query Scopes: Part II

Part I: Quick Review

In Part I of this series, I explained that a query scope takes a database query and limits its scope in some way; and that a local query scope does so locally (applied only when the scope is explicitly called). Now, let’s explore global query scopes.

Global Query Scopes

Global query scopes limit all queries in the model (that is, the database). Once you define a global query scope in your model class, its effect is silently applied every time you query the database with that specific model. Applying global query scopes means that database code will not have to be re-entered for every query into that database model. Because of this, you’re less likely to forget to apply that database code somewhere (in other words, it prevents bugs in your code) and you will not have to add that database code every time your database is accessed (in other words, it makes your code clean and succinct).

You can, however, locally override a global query scope; so its effect can be suppressed for edge cases.

Example: You are writing an online takeout ordering app for FooBar Bakery. You want the app to automatically exclude stale donuts (those which are 3-days old or older) without adding SQL code to each of your queries for that.

The most important thing about this example is the part about not adding SQL code to all of your queries. In years past, I used to keep an SQL suffix such as AND active=true handy to access records in a database table. Re-typing this suffix (e.g., adding a so-called .= $querySuffix variable) to every single query in the code means extra work, ugly-looking code and a high likelihood that a mistake will be made (like forgetting to add it to a query or two here and there). Since I want that code applied to every query, a global query scope entirely eliminates the need to add such instructions to every single query.

$donuts = new App\Donut;
$freshDonuts = $donuts->get();

As with our example shown in Part I of this series, the code above is elegant and expressive. You cannot forget to specify “donuts baked within the past 3 days” because the global query scope automatically does that for you, every time. You cannot mis-type your query (e.g., accidentally using baked_at <= instead of baked_at >=), so you make fewer mistakes with logic or have typo-induced bugs introduced into your code.

Here’s what the example does.

This code first creates a new App\Donut database model class instance and then creates an object (called $freshDonuts) with an inventory of donuts (using the get() method). Since the global query scope limits the App\Donut model to baked_at >= /* 3 days ago */ (see below), you can always count on the $donuts instance of this model to only give you bakery-fresh donuts, every query! Your client’s customers will love you.

Implementing a Global Query Scope

As you may recall from Part I of this series, you merely had to modify your model class file to add local query scope features to the class. Global query scopes are a little more involved because it requires that you create the necessary files on your own (as of Laravel 5.4).

Method 1: Global Query Scopes using Using a Class File

This is the preferred method for creating Laravel Eloquent global query scopes because it keeps the code out of the base model class file. This keeps your model class file clean, especially for larger projects (code can get get pretty gross when model class files start to get too busy). It might be a little silly in this case, but the example is meant to show you how it works and how to do it, more than to produce correctness.

Let’s take a look.

(1.) Go to the app/ directory of your Laravel installation.

(2.) Create a directory called Scopes (app/Scopes) there.

(3.) Create a class file called StaleScope.php in the app/Scopes directory.

(4.) Copy and paste the following contents into the app/Scopes/StaleScope.php class file:


# app/Scopes/StaleScope.php:

namespace App\Scopes;

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

# NOT required by Laravel Eloquent global query scopes, but this example does need this:
use Carbon\Carbon;

class StaleScope implements Scope
{
    /**
     * Apply the scope to a given Eloquent query builder.
     *
     * @param  \Illuminate\Database\Eloquent\Builder  $builder
     * @param  \Illuminate\Database\Eloquent\Model  $model
     * @return void
     */
    public function apply(Builder $builder, Model $model)
    {
        $builder->where('baked_at', '>=', Carbon::now()->subDays(3));
    }
}

(5.) Modify the Donut.php model file in the app/ directory to use the scope class’ namespace and the new StaleScope class:


# app/Donut.php:

use App\Scopes\StaleScope;

(6.) Add the following boot() method to that same Donut.php file to apply the global query scope to your model:


# app/Donut.php:

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

        static::addGlobalScope(new StaleScope);
    }

Naming Requirements

  • By convention, Laravel class namespaces match their directory structure. This is why in Step 2., above, you created both a namespace app/Scopes and a matching directory, app/Scopes. The global query scope class files, such as app/Scopes/StaleScope.php, will all go here.
  • By convention, the class file name in Steps 3. & 4., above (StaleScope.php), without its .php filename extension, matches the file’s class name itself (StaleScope).
  • Model names match their file names in a similar fashion (Donut.php will contain the Donut class).

Method 2: Global Query Scopes using Anonymous Model Class Functions

This method is much simpler than using class files, but it’s only appropriate for very simple Laravel apps. This is because model files get cluttered and bloated the more you add things to it. Smaller apps might not need any query scopes, or perhaps just a few. Larger apps could have many, so it justifies putting them into a separate file. But for now, since there are only a few (in this case, just one) query scope to define, it’s perfectly acceptable to put it right in the model class file directly.

Here’s how to implement a global query scope using an anonymous function in the model class file:

(1.) The addGlobalScope(...) closure references the Builder object which resides in the Illuminate\Database\Eloquent\Builder namespace; so make sure you add its reference to the app/Donut.php class file:

# app/Donut.php:

# `php artisan make:model Donut` automatically adds this `use` statement for you:
use Illuminate\Database\Eloquent\Model;

# But you must add this `use` to your code in order for the `addGlobalScope(...)` closure to work:
use Illuminate\Database\Eloquent\Builder;

# NOT required by Laravel Eloquent global query scopes, but this example does need this:
use Carbon\Carbon;

(2.) add the addGlobalScope(...) closure to the app/Donut.php model class file within its boot() method to define and apply the global query scope:

# app/Donut.php:

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

        static::addGlobalScope('stale', function (Builder $builder) {
            $builder->where('baked_at', '>=', Carbon::now()->subDays(3));
        });
    }

Try It Yourself!

Give global query scopes a spin! All of the source code you see in this example is available for viewing or downloading from the Laravel Query Scopes Example Github repository.

(1.) Install Laravel and create a scratch project to experiment with. (I’ve created a 5-minute Laravel Setup guide for Apple Mac users that might be helpful if you’ve never done it before or don’t already have Laravel ready to go on your system.)

laravel new donut

(2.) Clone the Github repository, pulling down the global-class branch (instead of the master branch, which demonstrates the local query scopes):

git clone -b global-class https://github.com/unrivaledcreations/laravel-query-scopes-example.git

(3.) Copy the relevant repository files into their proper locations for your new Laravel app:

cp -r laravel-query-scopes-example/app/* donut/app
cp -r laravel-query-scopes-example/database/* donut/database

(4.) Initialize the database tables:

cd donut
composer update
composer dump-autoload
php artisan migrate --seed

If you need to reload the donuts table to create new baked_at dates (for demo purposes), replace the artisan command with:

php artisan migrate:refresh --seed

(5.) Use php artisan tinker to play with the query scopes (using the example from this article):

php artisan tinker
>>> $donuts = new App\Donut;
=> App\Donut {#678}
>>> $freshDonuts = $donuts->get();
=> Illuminate\Database\Eloquent\Collection {#696...results are shown...}
>>>

You may optionally implement routes and views to take your exploration to a more practical level, but tinker is very nice for learning.

The GitHub laravel-query-scopes-example repository contains four git branches. Each branch allows you to examine working Laravel Eloquent query scope code:

Branch Purpose
master Demonstrates local query scopes as described in Part I of this series.
both Shows local query scopes side-by-side with the class file global query scope.
global-anonymous Shows a global query scope implemented as an anonymous function in the Donut.php model file.
global-class Shows only a class file global query scope (without local query scope functions in the Donut.php model file).

Use the command, git checkout master to load the master branch; or change master to whichever branch you want to play with in your editor. The git checkout {branch} command will change the files in your working directory (laravel-query-scopes-example/ in this example) to automatically match the described query scope example.

Summary

Laravel’s Eloquent query scopes give you powerful tools to create readable, maintainable code for accessing data. This article explored global query scopes implemented as either a class file-based implementation (which is preferred way) or as an implementation using anonymous model class functions. Code written with global query scopes ensure clean, readable code. Programmers coming from other languages or frameworks might be familiar with this concept because they have similar constructs (such as the default_scope method in Ruby on Rails). Newer developers should take the time to carefully study query scopes (both local query scopes, as described in Part I of this series, as well as the global query scopes described here).

Need Help?

Comment down below if you have any questions about query scopes or need help setting up the examples in this article. I will be happy to help you as best I can.

···

Michael Hall

Hi! I am a digital product designer and website/web application developer always seeking a better version of myself. Follow my journey as I share my story (and expertise) through the mutable, ever-changing, ever-growing world of design, web development and technology.
comments powered by Disqus


Copyright © 2017 by Michael Hall. All rights reserved. · Sign In