DEV Community

Fajar Windhu Zulfikar
Fajar Windhu Zulfikar

Posted on • Originally published at fajarwz.com on

Create Global Query With Laravel Global Scope

Global illustration

In backend development, sometimes we want the data to be retrieved with default query. For example, when we want to get all users data, we want our app retrieve only active users by default. All queries like, getting users only from Indonesia, getting users only above 18, getting users only female, we want all of it -- by default -- retrieve only active users, excluding inactive users (because we have two type of users, active and inactive).

In Laravel there is a feature called query scope. Query scope have two types, Local Scope and Global Scope. I already covered Laravel Local Scopes here, so this time I just want to share about Laravel Global Scopes.

One example of Global Scope in Laravel can be seen in Laravel's Soft Delete. When we use soft delete in Laravel, Laravel automatically retrieve only non-soft deleted data unless we chain our query with withTrashed() function.

This is indeed very useful and Laravel let us create our own global scope for us to use in our app. So let's get started to create our own global scopes.

Global scope implemented the same from Laravel version 7.x to 9.x.

Example Case Intro

For example we want to create a Global Scope to make our post data query published posts by default. So we need a table named posts that contains title, content, and is_draft columns.

Here we will use Laravel 9.x. So first create an app with Laravel 9.x and create a database for it.

Tutorial

Creating Migration

Let's create the migration, we can run the following artisan command:

php artisan make:migration "create posts table"
Enter fullscreen mode Exit fullscreen mode

After that write the content of the migration like this:

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('posts', function (Blueprint $table) {
            $table->id();
            $table->string('title');
            $table->text('content');
            $table->tinyInteger('is_draft')->unsigned(); // 1 for draft and 0 for published
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('posts');
    }
};

Enter fullscreen mode Exit fullscreen mode

Initialize Post Model

Now create a Post model with the $fillable data. We can create the model with the following artisan command

php artisan make:model Post
Enter fullscreen mode Exit fullscreen mode

After that a model named Post created in app\Models\Post.php. Fill the necessary field for the fillable.

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use App\Models\Scopes\PublishedScope;

class Post extends Model
{
    use HasFactory;

    protected $fillable = [
        'title',
        'content',
        'is_draft',
    ];
}
Enter fullscreen mode Exit fullscreen mode

Read also:

Creating Database Seeder

We now will create a seeder for posts table. I will create just 3 records but it is up to you to add as much as you want. The seeder contain 2 published posts and 1 draft post.

Here I will just create the seeder inside DatabaseSeeder.php. Of course you can create it in a dedicated seeder file if you want.

<?php

namespace Database\Seeders;

// use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use App\Models\Post;

class DatabaseSeeder extends Seeder
{
    /**
     * Seed the application's database.
     *
     * @return void
     */
    public function run()
    {
        Post::create([
            'title' => 'Laravel CRUD Tutorial',
            'content' => 'This is the content of Laravel CRUD Tutorial',
            'is_draft' => 0,
        ]);
        Post::create([
            'title' => 'VueJS CRUD Tutorial',
            'content' => 'This is the content of VueJS CRUD Tutorial',
            'is_draft' => 0,
        ]);
        Post::create([
            'title' => 'React CRUD Tutorial',
            'content' => 'This is the content of React CRUD Tutorial',
            'is_draft' => 1,
        ]);
    }
}

Enter fullscreen mode Exit fullscreen mode

Run The Migration and The Seeder

We can run them with one following command:

php artisan migrate --seed
Enter fullscreen mode Exit fullscreen mode

Now we should see our posts table in the database exists with the dummy data.

Creating Global Scopes

In Laravel 9.x we can initialize the Scope class with the following artisan command:

php artisan make:scope PublishedScope
Enter fullscreen mode Exit fullscreen mode

After the command successful, we will have a new file named PublishedScope in App\Models\Scopes folder.

The class implements Illuminate\Database\Eloquent\Scope interface and requires it to implements one method named apply. There we can use our constraints we needed.

// app\Models\Scopes\PublishedScope.php

<?php

namespace App\Models\Scopes;

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

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('is_draft', 0);
    }
}
Enter fullscreen mode Exit fullscreen mode

Applying Global Scopes

To apply a global scope to a model, we need to override the model's booted method and use addGlobalScope() method. The addGlobalScope method accepts an argument: our scope instance.

We will update our Post model like so:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use App\Models\Scopes\PublishedScope; // use the Scope

class Post extends Model
{
    use HasFactory;

    protected $fillable = [
        'title',
        'content',
        'is_draft',
    ];

    protected static function booted()
    {
        static::addGlobalScope(new PublishedScope); // assign the Scope here
    }
}

Enter fullscreen mode Exit fullscreen mode

That's it, we successfully implemented our global scope.

Test the Scope in Artisan Tinker

Run a Query with Global Scope

We can always test it inside php artisan tinker. Here is how I test it inside tinker.

Everytime we run a query it will always implement our applied global scope unless we chain our query with withoutGlobalScope() or withoutGlobalScopes().

When I run \App\Models\Post::all() tinker with return the following response:

Testing in Tinker

It shows only the published or is_draft = 0 data.

Run a Query without Global Scope

And yes we can run our query without the model's global scope, we can use withoutGlobalScope(MyScope::class) to remove only a scope and withoutGlobalScopes() (notice the s postfix) to remove all scopes or withoutGlobalScopes([FirstScope::class, SecondScope::class]) to remove multiple global scopes.

Testing in Tinker without Global Scope

Read also:

Test the Scope in Web

I will implement the scope also to the web so I write the web.php like so:

<?php

use Illuminate\Support\Facades\Route;
use App\Models\Post;
use App\Models\Scopes\PublishedScope;

/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/

Route::get('/', function () {
    $posts = Post::all();
    $allposts = Post::withoutGlobalScope(PublishedScope::class)->get();

    return view('welcome', [
        'posts' => $posts,
        'allposts' => $allposts,
    ]);
});
Enter fullscreen mode Exit fullscreen mode

And I modify the welcome.blade.php like so:

<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">

        <title>Laravel</title>

        <!-- Fonts -->
        <link href="https://fonts.bunny.net/css2?family=Nunito:wght@400;600;700&display=swap" rel="stylesheet">

        <!-- Styles -->
        <style>
            /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}a{background-color:transparent}[hidden]{display:none}html{font-family:system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;line-height:1.5}*,:after,:before{box-sizing:border-box;border:0 solid #e2e8f0}a{color:inherit;text-decoration:inherit}svg,video{display:block;vertical-align:middle}video{max-width:100%;height:auto}.bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity))}.bg-gray-100{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity))}.border-gray-200{--tw-border-opacity: 1;border-color:rgb(229 231 235 / var(--tw-border-opacity))}.border-t{border-top-width:1px}.flex{display:flex}.grid{display:grid}.hidden{display:none}.items-center{align-items:center}.justify-center{justify-content:center}.font-semibold{font-weight:600}.h-5{height:1.25rem}.h-8{height:2rem}.h-16{height:4rem}.text-sm{font-size:.875rem}.text-lg{font-size:1.125rem}.leading-7{line-height:1.75rem}.mx-auto{margin-left:auto;margin-right:auto}.ml-1{margin-left:.25rem}.mt-2{margin-top:.5rem}.mr-2{margin-right:.5rem}.ml-2{margin-left:.5rem}.mt-4{margin-top:1rem}.ml-4{margin-left:1rem}.mt-8{margin-top:2rem}.ml-12{margin-left:3rem}.-mt-px{margin-top:-1px}.max-w-6xl{max-width:72rem}.min-h-screen{min-height:100vh}.overflow-hidden{overflow:hidden}.p-6{padding:1.5rem}.py-4{padding-top:1rem;padding-bottom:1rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.pt-8{padding-top:2rem}.fixed{position:fixed}.relative{position:relative}.top-0{top:0}.right-0{right:0}.shadow{--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / .1), 0 1px 2px -1px rgb(0 0 0 / .1);--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.text-center{text-align:center}.text-gray-200{--tw-text-opacity: 1;color:rgb(229 231 235 / var(--tw-text-opacity))}.text-gray-300{--tw-text-opacity: 1;color:rgb(209 213 219 / var(--tw-text-opacity))}.text-gray-400{--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity))}.text-gray-600{--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity))}.text-gray-700{--tw-text-opacity: 1;color:rgb(55 65 81 / var(--tw-text-opacity))}.text-gray-900{--tw-text-opacity: 1;color:rgb(17 24 39 / var(--tw-text-opacity))}.underline{text-decoration:underline}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.w-5{width:1.25rem}.w-8{width:2rem}.w-auto{width:auto}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}@media (min-width:640px){.sm\:rounded-lg{border-radius:.5rem}.sm\:block{display:block}.sm\:items-center{align-items:center}.sm\:justify-start{justify-content:flex-start}.sm\:justify-between{justify-content:space-between}.sm\:h-20{height:5rem}.sm\:ml-0{margin-left:0}.sm\:px-6{padding-left:1.5rem;padding-right:1.5rem}.sm\:pt-0{padding-top:0}.sm\:text-left{text-align:left}.sm\:text-right{text-align:right}}@media (min-width:768px){.md\:border-t-0{border-top-width:0}.md\:border-l{border-left-width:1px}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (min-width:1024px){.lg\:px-8{padding-left:2rem;padding-right:2rem}}@media (prefers-color-scheme:dark){.dark\:bg-gray-800{--tw-bg-opacity: 1;background-color:rgb(31 41 55 / var(--tw-bg-opacity))}.dark\:bg-gray-900{--tw-bg-opacity: 1;background-color:rgb(17 24 39 / var(--tw-bg-opacity))}.dark\:border-gray-700{--tw-border-opacity: 1;border-color:rgb(55 65 81 / var(--tw-border-opacity))}.dark\:text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}.dark\:text-gray-400{--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity))}.dark\:text-gray-500{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity))}}
        </style>

        <style>
            body {
                font-family: 'Nunito', sans-serif;
            }
        </style>
    </head>
    <body class="text-center">
        <h1>Published</h1>
        @foreach ($posts as $post)
            <h2>{{ $post->title }}</h2>
            <p>{{ $post->content }}</p>
            <p>{{ $post->is_draft ? 'DRAFT' : 'PUBLISHED' }}</p>
        @endforeach

        <br>

        <h1>All Posts</h1>
        @foreach ($allposts as $post)
            <h2>{{ $post->title }}</h2>
            <p>{{ $post->content }}</p>
            <p>{{ $post->is_draft ? 'DRAFT' : 'PUBLISHED' }}</p>
        @endforeach
    </body>
</html>
Enter fullscreen mode Exit fullscreen mode

And after we run the php artisan serve we will get the following result

Test in web

Conclusions

And that's it, we have successfully implemented our own global scope and tested it. Now we know how to create and apply a global scope, run a query with the global scope and run a query without it.

A repo for this example case can be found here fajarwz/blog-laravel-global-scope.

Reference

Eloquent: Getting Started - Laravel Scopes - Laravel

Read Also

Create Reusable Query With Laravel Query Scope

Top comments (0)