💡 I included the link to the example repository at the conclusion of this tutorial.
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
Step 1: Creating Migration
Let’s create the migration, we can run the following artisan command:
php artisan make:migration "create posts table"
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');
}
};
Step 2: 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
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',
];
}
Read also:
Step 3: 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,
]);
}
}
Step 4: Run The Migration and The Seeder
We can run them with one following command:
php artisan migrate --seed
Now we should see our posts
table in the database exists with the dummy data.
Step 5: Creating Global Scopes
In Laravel 9.x we can initialize the Scope class with the following artisan command:
php artisan make:scope PublishedScope
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);
}
}
Step 6: 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
}
}
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:
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.
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,
]);
});
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>
And after we run the php artisan serve
we will get the following result
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