Get "PHP 8 in a Nuthshell" (Now with PHP 8.4)
Amit Merchant

Amit Merchant

A blog on PHP, JavaScript, and more

Re-using query constraints in Laravel Eloquent - Global vs. Local scopes

Laravel’s Eloquent ORM comes with this unique set of features called “Scopes” which can be used to re-use some of the query constraints onto all the queries of a certain model. This can be useful to minimize the code while writing the query and is an easy way to make sure every query for a given model receives certain constraints.

There are basically two types of scopes available in Laravel: “Global Scopes” and “Local Scopes”. Let’s now understand what are these scopes and how to use them.

Global Scopes

Global Scopes are useful when you’re certain about the set of query constriants that you think would be applicable to all the queries on which these scopes will be assigned.

To define a global scope, all you need to do is to define a class that implements the Illuminate\Database\Eloquent\Scope interface.This interface requires you to implement one method: apply. Inside this apply method, you can add where constraint which will be applied to the query. You can define all your scopes under namespace App\Scopes like this.


<?php

namespace App\Scopes;

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

class PublishedScope 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('published', 1);
    }
}

Applying Global Scopes to models

In order to assign a certain global scope to a model, you need to override the boot method of the particular model and then use the addGlobalScope method.

<?php

namespace App;

use App\Scopes\PublishedScope;
use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    /**
     * The "booting" method of the model.
     *
     * @return void
     */
    protected static function boot()
    {
        parent::boot();

        static::addGlobalScope(new PublishedScope);
    }
}

Now, as the PublishedScope scope is assigned to the Post model, whenever a query will be issued for this model, it will be attached with the constraint assigned in the scope. For instance Post::all() will generate the following query:

select * from `posts` where `published` = 1

Anonymous Global Scopes

You can also utilize the global scopes by assigning them in form of Closures/Anonymous functions. This is particularly useful for simple scopes that do not require a separate class. Here’s how you can do that.

<?php

namespace App;

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

class Post extends Model
{
    /**
     * The "booting" method of the model.
     *
     * @return void
     */
    protected static function boot()
    {
        parent::boot();

        static::addGlobalScope('published', function (Builder $builder) {
            $builder->where('published', 1);
        });
    }
}

Here in this case, the first parameter of the addGlobalScope method is the name of the Closure which we’ve passed in the second parameter which can be used to remove the scope while issuing the query. I’ll discuss this in the next section.

Removing Global Scopes

If you would like to remove a global scope for a given query, you may use the withoutGlobalScope method. The method accepts the class name of the global scope as its only argument:

Post::withoutGlobalScope(PublishedScope::class)->get();

If you want to remove all of the global scopes, you can just use the withoutGlobalScope without any parameter

Post::withoutGlobalScope()->get();

Or if there are multiple global scopes and you want to remove some of the scopes you can do this by assining the array of the selected global scopes.

// Remove some of the global scopes...
User::withoutGlobalScopes([
    FirstScope::class, SecondScope::class
])->get();

Local Scopes

The other type of scopes that are available in Eloquent is “Local Scopes”. These are especially useful when you want to utilize certain contraints as smaller chunks which can be utilized on the model queries.

For instance, for the earlier example of Post model, if you want to fetch all the posts that have been “starred” extensively, you can do this by defining a local scope. To define a local scope, prefix an Eloquent model method with scope. Scopes should always return a query builder instance:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    /**
     * Scope a query to only include most starred posts.
     *
     * @param  \Illuminate\Database\Eloquent\Builder  $query
     * @return \Illuminate\Database\Eloquent\Builder
     */
    public function scopeStarred($query)
    {
        return $query->where('starred', '>', 30);
    }

    /**
     * Scope a query to only include published posts.
     *
     * @param  \Illuminate\Database\Eloquent\Builder  $query
     * @return \Illuminate\Database\Eloquent\Builder
     */
    public function scopePublished($query)
    {
        return $query->where('published', 1);
    }
}

If you want to pass the dynamic parameter to scopes in order to make the local scopes dynamic, you can do this by just adding your additional parameters to your scope. Scope parameters should be defined after the $query parameter:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    /**
     * Scope a query to only include most starred posts.
     *
     * @param  \Illuminate\Database\Eloquent\Builder  $query
     * @return \Illuminate\Database\Eloquent\Builder
     */
    public function scopeOfCategory($query, $category)
    {
        return $query->where('category', $category);
    }
}

Now, you may pass the parameters when calling the scope:

$posts = App\Post::ofCategory('php')->get();

Utilizing A Local Scope

To use the defined local scope on the model query, all you need to do is to call the scope methods on the querying model without using the prefix scope.

$posts = App\Post::starred()->get();

You can also chain the scope methods in order to aggregate different constraints like this.

$posts = App\Post::starred()->published()->get();

If you want to use or query on model using scopes, you can do this by using a “higher order” orWhere method that allows you to fluently chain the scopes.

$posts = App\Post::starred()->orWhere->published()->get();

Conclusion

So the bottomline here is, use “Global Scopes” when you want to apply a monolith of query constraints on the model queries and if you want to utilize smaller chunks of query constraints which can do a specific operation, use “Local Scopes” in that case.

Learn the fundamentals of PHP 8 (including 8.1, 8.2, 8.3, and 8.4), the latest version of PHP, and how to use it today with my new book PHP 8 in a Nutshell. It's a no-fluff and easy-to-digest guide to the latest features and nitty-gritty details of PHP 8. So, if you're looking for a quick and easy way to PHP 8, this is the book for you.

Like this article?

Buy me a coffee

👋 Hi there! I'm Amit. I write articles about all things web development. You can become a sponsor on my blog to help me continue my writing journey and get your brand in front of thousands of eyes.

Comments?