NTM Solutions

Thứ Tư, 26 tháng 8, 2026

📘 Laravel 12 (2026) — BÀI 32 — LOCAL SCOPE & GLOBAL SCOPE TRONG ELOQUENT

Trong các bài trước, chúng ta thường viết Query trực tiếp trong Controller:

$users = User::where('is_active', true)
    ->where('role', 'user')
    ->latest()
    ->paginate(10);

Cách này hoàn toàn đúng.

Nhưng khi dự án lớn lên, những điều kiện truy vấn giống nhau sẽ xuất hiện ở rất nhiều Controller.

Ví dụ:

User::where('is_active', true)->get();
User::where('is_active', true)->latest()->get();
User::where('is_active', true)->paginate(10);

Lúc này chúng ta có thể đưa các điều kiện truy vấn thường dùng vào Scope.

Laravel 13 hỗ trợ hai loại chính:

  • Local Scope

  • Global Scope

Local Scope chỉ được sử dụng khi chúng ta chủ động gọi nó.

Global Scope tự động được áp dụng vào các query của Model. Laravel cũng sử dụng cơ chế Global Scope cho Soft Delete.

📑 MỤC LỤC

  1. Scope là gì?
  2. Local Scope là gì?
  3. Tạo Local Scope đầu tiên
  4. Vì sao phải dùng #[Scope]?
  5. Sử dụng Local Scope
  6. Local Scope với nhiều điều kiện
  7. Có thể kết hợp nhiều Scope
  8. Local Scope trong CRUD Users
  9. Dynamic Local Scope
  10. Dynamic Scope rất hữu ích cho Search
  11. Scope cho Post
  12. Scope cho Blog Homepage
  13. Scope cho Category
  14. Local Scope giúp Controller sạch hơn
  15. Global Scope là gì?
  16. Ví dụ Global Scope
  17. Tạo Global Scope
  18. Viết ActiveUserScope
  19. Gắn Global Scope vào User
  20. Global Scope hoạt động như thế nào?
  21. Global Scope và Soft Delete
  22. Bỏ Global Scope
  23. Bỏ tất cả Global Scope
  24. Anonymous Global Scope
  25. Bỏ Anonymous Global Scope
  26. Local Scope và Global Scope khác nhau
  27. Khi nào nên dùng Local Scope?
  28. Khi nào nên dùng Global Scope?
  29. Không nên lạm dụng Global Scope
  30. Scope trong dự án Blog CMS
  31. Kết hợp Scope với Search
  32. Scope giúp đọc code dễ hơn
  33. Một User Model hoàn chỉnh
  34. Scope không thay thế Query Builder
  35. Scope và Controller
  36. Tóm tắt Local Scope
  37. Tóm tắt Global Scope
  38. Bài tập thực hành
  39. Ghi nhớ Bài 32

1. Scope là gì?

Có thể hiểu đơn giản:

Scope
   │
   ├── Local Scope
   │       ↓
   │   Chủ động gọi
   │
   └── Global Scope
           ↓
      Tự động áp dụng

Scope giúp chúng ta đóng gói những điều kiện truy vấn thường xuyên sử dụng vào Model.

Thay vì viết:

User::where('is_active', true)->get();

nhiều lần, chúng ta có thể tạo:

User::active()->get();

Code sẽ ngắn và dễ đọc hơn.

2. Local Scope là gì?

Local Scope là một nhóm điều kiện Query được định nghĩa trong Model và chỉ được áp dụng khi chúng ta gọi Scope đó.

Ví dụ:

User::active()->get();

Ở đây:

active()
   ↓
Local Scope
   ↓
where('is_active', true)

Nếu không gọi:

User::active()

thì điều kiện đó không được áp dụng.

Laravel 13 định nghĩa Local Scope bằng attribute #[Scope].

3. Tạo Local Scope đầu tiên

Mở:

app/Models/User.php

Thêm:

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

Sau đó tạo Scope:

#[Scope]
protected function active(Builder $query): void
{
    $query->where('is_active', true);
}

Bây giờ có thể sử dụng:

$users = User::active()->get();

Laravel sẽ thêm điều kiện:

where is_active = 1

vào Query.

4. Vì sao phải dùng #[Scope]?

Đây là điểm quan trọng khi học Laravel 13.

Cách hiện đại:

#[Scope]
protected function active(Builder $query): void
{
    $query->where('is_active', true);
}

Laravel 13 sử dụng Illuminate\Database\Eloquent\Attributes\Scope để đánh dấu method là Local Scope.

Trong các phiên bản Laravel cũ, chúng ta thường gặp cú pháp:

public function scopeActive($query)
{
    return $query->where('is_active', true);
}

Cú pháp cũ có thể xuất hiện trong nhiều tutorial trên Internet, nhưng trong khóa học Laravel 13 này, chúng ta ưu tiên cú pháp hiện đại.

5. Sử dụng Local Scope

Sau khi có:

#[Scope]
protected function active(Builder $query): void
{
    $query->where('is_active', true);
}

Controller có thể viết:

$users = User::active()->get();

Thay vì:

$users = User::where('is_active', true)->get();

Hai cách cho cùng mục đích.

Nhưng Scope có ưu điểm là điều kiện:

where('is_active', true)

được đặt trong Model và có thể tái sử dụng.

6. Local Scope với nhiều điều kiện

Ví dụ chúng ta thường xuyên cần User:

role = user
is_active = true

Có thể tạo:

#[Scope]
protected function normalUsers(Builder $query): void
{
    $query
        ->where('role', 'user')
        ->where('is_active', true);
}

Sau đó:

$users = User::normalUsers()->get();

Controller không cần biết chi tiết điều kiện bên trong.

7. Có thể kết hợp nhiều Scope

Đây là điểm rất mạnh của Local Scope.

Tạo:

#[Scope]
protected function active(Builder $query): void
{
    $query->where('is_active', true);
}

và:

#[Scope]
protected function admin(Builder $query): void
{
    $query->where('role', 'admin');
}

Có thể viết:

$users = User::active()
    ->admin()
    ->get();

Laravel sẽ kết hợp các điều kiện.

Có thể hình dung:

User
 │
 ├── active()
 │      ↓
 │   is_active = 1
 │
 └── admin()
        ↓
     role = admin

Kết quả tương đương:

where is_active = 1
and role = 'admin'

Laravel cho phép chain nhiều Local Scope với nhau.

8. Local Scope trong CRUD Users

Trong dự án Blog CMS của khóa học, chúng ta có:

users
────────────────────────────
id
name
email
role
is_active
created_at
updated_at

Chúng ta có thể tạo các Scope:

active()
inactive()
admin()
editor()
user()

Sau đó Controller rất dễ đọc.

Ví dụ:

$users = User::active()
    ->latest()
    ->paginate(10);

Hoặc:

$users = User::admin()
    ->latest()
    ->paginate(10);

Hoặc:

$users = User::active()
    ->admin()
    ->latest()
    ->paginate(10);

9. Dynamic Local Scope

Không phải Scope nào cũng cố định.

Ví dụ chúng ta muốn tìm User theo Role.

Thay vì tạo:

admin()
editor()
user()

chúng ta có thể tạo một Scope nhận tham số.

#[Scope]
protected function role(Builder $query, string $role): void
{
    $query->where('role', $role);
}

Sử dụng:

User::role('admin')->get();

hoặc:

User::role('editor')->get();

hoặc:

User::role('user')->get();

Laravel gọi đây là Dynamic Scopes. Các tham số của Scope được khai báo sau $query.

10. Dynamic Scope rất hữu ích cho Search

Ví dụ:

#[Scope]
protected function role(Builder $query, string $role): void
{
    $query->where('role', $role);
}

Trong Controller:

$users = User::role($request->role)
    ->latest()
    ->paginate(10);

Chúng ta có thể xây dựng:

Dashboard
      │
      ▼
Filter Role
      │
      ├── admin
      ├── editor
      └── user
             │
             ▼
        User::role()
             │
             ▼
          Database

Đây là cách rất phù hợp cho chức năng Filter Users.

11. Scope cho Post

Không chỉ User.

Ví dụ Model Post.

Bảng:

posts
────────────────────────
id
title
content
status
category_id
user_id
created_at
updated_at

Chúng ta thường xuyên cần:

published
draft
hidden

Có thể tạo:

#[Scope]
protected function published(Builder $query): void
{
    $query->where('status', 'published');
}

và:

#[Scope]
protected function draft(Builder $query): void
{
    $query->where('status', 'draft');
}

Sau đó:

Post::published()->latest()->get();

hoặc:

Post::draft()->latest()->get();

Chú ý: nếu dùng Datatables để phân trang thì không dùng được.

12. Scope cho Blog Homepage

Trang chủ Blog chỉ cần bài viết đã Published.

Thay vì:

$posts = Post::where('status', 'published')
    ->latest()
    ->paginate(10);

Có thể viết:

$posts = Post::published()
    ->latest()
    ->paginate(10);

Nhìn vào Controller, chúng ta lập tức hiểu:

Lấy những bài viết đã xuất bản.

13. Scope cho Category

Ví dụ chúng ta có:

categories
────────────────────
id
name
is_active

Tạo:

#[Scope]
protected function active(Builder $query): void
{
    $query->where('is_active', true);
}

Sau đó:

$categories = Category::active()->get();

Rất dễ đọc.

14. Local Scope giúp Controller sạch hơn

Không có Scope:

$posts = Post::where('status', 'published')
    ->whereNotNull('category_id')
    ->latest()
    ->paginate(10);

Có Scope:

$posts = Post::published()
    ->withCategory()
    ->latest()
    ->paginate(10);

Controller trở thành nơi điều phối nghiệp vụ, thay vì chứa quá nhiều điều kiện Query.

15. Global Scope là gì?̣

Khác với Local Scope.

Local Scope:

Chỉ chạy khi gọi

Global Scope:

Tự động chạy

Ví dụ:

User::all()

bình thường lấy tất cả User.

Nếu User có Global Scope:

User::all()
      │
      ▼
Global Scope
      │
      ▼
Tự động thêm điều kiện

Laravel sẽ tự động áp dụng điều kiện đó vào Query.

16. Ví dụ Global Scope

Giả sử chúng ta muốn hệ thống luôn luôn chỉ lấy User đang hoạt động.

Điều kiện:

where('is_active', true)

Nếu dùng Local Scope:

User::active()->get();

thì chúng ta phải nhớ gọi:

active()

Nhưng nếu dùng Global Scope:

User::all();

Laravel tự động thêm:

is_active = true

Đây chính là khác biệt quan trọng nhất.

17. Tạo Global Scope

Laravel 13 có thể tạo Scope class bằng Artisan:

php artisan make:scope ActiveUserScope

Laravel sẽ tạo Scope trong:

app/Models/Scopes

Đây là vị trí mặc định được Laravel sử dụng khi tạo Global Scope bằng Artisan.

18. Viết ActiveUserScope

File:

app/Models/Scopes/ActiveUserScope.php

Nội dung:

<?php

namespace App\Models\Scopes;

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

class ActiveUserScope implements Scope
{
    public function apply(
        Builder $builder,
        Model $model
    ): void {
        $builder->where('is_active', true);
    }
}

Scope class cần triển khai:

Illuminate\Database\Eloquent\Scope

và phương thức:

apply()

sẽ thêm điều kiện vào Query.

19. Gắn Global Scope vào User

Laravel 13 hỗ trợ attribute #[ScopedBy].

Trong:

app/Models/User.php

thêm:

use App\Models\Scopes\ActiveUserScope;
use Illuminate\Database\Eloquent\Attributes\ScopedBy;

Sau đó:

#[ScopedBy([ActiveUserScope::class])]
class User extends Authenticatable
{
    //
}

Bây giờ:

User::all();

sẽ tự động áp dụng:

is_active = true

Laravel 13 hỗ trợ ScopedBy để khai báo Global Scope trực tiếp trên Model.

20. Global Scope hoạt động như thế nào?

Giả sử database:

users
────────────────────────────
id | name | is_active
────────────────────────────
1  | A    | 1
2  | B    | 0
3  | C    | 1
4  | D    | 0

Khi:

User::all();

Global Scope sẽ tự động thêm:

is_active = true

Kết quả:

1 | A | 1
3 | C | 1

User:

2 | B | 0
4 | D | 0

không xuất hiện.

21. Global Scope và Soft Delete

Đây là ví dụ rất dễ hiểu vì chúng ta vừa học Soft Delete ở Bài 30.

Khi Model sử dụng:

use SoftDeletes;

Laravel không đơn giản chỉ thay đổi cách delete() hoạt động.

Laravel còn sử dụng Global Scope để mặc định loại các record đã Soft Delete khỏi kết quả Query.

Ví dụ:

users
────────────────────────────
id | name | deleted_at
────────────────────────────
1  | A    | NULL
2  | B    | 2026-08-20
3  | C    | NULL

Khi:

User::all();

kết quả mặc định:

A
C

User B không xuất hiện.

22. Bỏ Global Scope

Đôi khi chúng ta cần lấy cả dữ liệu bị Global Scope loại bỏ.

Laravel cung cấp:

withoutGlobalScope()

Ví dụ:

User::withoutGlobalScope(
    ActiveUserScope::class
)->get();

Lúc này Global Scope:

ActiveUserScope

sẽ không được áp dụng cho Query đó.

23. Bỏ tất cả Global Scope

Nếu muốn bỏ toàn bộ Global Scope:

User::withoutGlobalScopes()->get();

Có thể hiểu:

User::all()
     │
     ▼
Global Scopes
     │
     ▼
Áp dụng

User::withoutGlobalScopes()
     │
     ▼
Bỏ Global Scopes

Laravel 13 cũng hỗ trợ withoutGlobalScopes([...]) để chỉ bỏ một số Scope cụ thể.

24. Anonymous Global Scope

Nếu Scope rất đơn giản, không nhất thiết phải tạo một class riêng.

Có thể khai báo trực tiếp trong Model:

protected static function booted(): void
{
    static::addGlobalScope('active', function (Builder $builder) {
        $builder->where('is_active', true);
    });
}

Khi đó:

User::all();

vẫn tự động thêm:

is_active = true

Laravel hỗ trợ Anonymous Global Scope thông qua Closure.

25. Bỏ Anonymous Global Scope

Vì chúng ta đặt tên:

active

nên có thể bỏ Scope bằng:

User::withoutGlobalScope('active')->get();

Tên:

active

phải giống tên đã đăng ký trong:

addGlobalScope()

26. Local Scope và Global Scope khác nhau

Đặc điểmLocal ScopeGlobal Scope
Kích hoạtChủ động gọiTự động
Phạm viQuery cụ thểMọi Query của Model
Mục đíchTái sử dụng điều kiệnLuôn áp dụng một quy tắc
Có thể bỏKhông cần bỏCó thể withoutGlobalScope()
Ví dụactive()Soft Delete

Có thể nhớ:

LOCAL
↓
Tôi muốn thì tôi gọi

GLOBAL
↓
Laravel tự động gọi

27. Khi nào nên dùng Local Scope?

Local Scope phù hợp khi điều kiện:

  • Được sử dụng nhiều lần.

  • Không phải Query nào cũng cần.

  • Có ý nghĩa rõ ràng.

  • Giúp Controller dễ đọc.

Ví dụ:

active()
published()
draft()
admin()
editor()
popular()
recent()
role()

Đây là trường hợp sử dụng phổ biến nhất.

28. Khi nào nên dùng Global Scope?

Global Scope phù hợp với những quy tắc mà Model gần như luôn luôn phải tuân thủ.

Ví dụ:

Chỉ lấy dữ liệu active
Chỉ lấy dữ liệu thuộc tenant hiện tại
Chỉ lấy dữ liệu của một cửa hàng
Soft Delete

Nhưng cần cẩn thận.

Global Scope có thể khiến Developer mới khó hiểu:

User::all();

nhưng database không trả về tất cả record.

Lý do là phía sau Query đang có Global Scope.

29. Không nên lạm dụng Global Scope

Đây là nguyên tắc rất quan trọng.

Nếu chúng ta tạo quá nhiều Global Scope:

User
 ├── ActiveScope
 ├── RoleScope
 ├── CountryScope
 ├── DepartmentScope
 ├── PermissionScope
 └── ...

một Query đơn giản:

User::all();

có thể trở nên rất khó đoán.

Vì vậy:

Local Scope nên là lựa chọn mặc định khi chỉ muốn tái sử dụng Query.

Global Scope chỉ nên dùng khi quy tắc đó thực sự cần áp dụng mặc định.

30. Scope trong dự án Blog CMS

Với dự án chúng ta đang xây dựng, có thể tổ chức như sau:

User
├── active()
├── role()
└── admin()

Post
├── published()
├── draft()
├── hidden()
└── category()

Category
└── active()

Ví dụ trang quản trị:

$users = User::active()
    ->latest()
    ->paginate(10);

Trang Blog:

$posts = Post::published()
    ->latest()
    ->paginate(10);

Trang Draft:

$posts = Post::draft()
    ->latest()
    ->paginate(10);

Code rất dễ đọc.

31. Kết hợp Scope với Search

Ví dụ trang Users có:

Search
Role
Status
Pagination

Chúng ta có thể xây dựng:

$users = User::query()
    ->active()
    ->role($request->role)
    ->latest()
    ->paginate(10);

Nếu cần thêm Search:

$users = User::query()
    ->active()
    ->role($request->role)
    ->search($request->search)
    ->latest()
    ->paginate(10);

Khi đó Model trở thành nơi chứa những Query có thể tái sử dụng.

32. Scope giúp đọc code dễ hơn

So sánh:

$posts = Post::where('status', 'published')
    ->whereNotNull('category_id')
    ->where('is_active', true)
    ->orderByDesc('created_at')
    ->paginate(10);

với:

$posts = Post::published()
    ->withCategory()
    ->active()
    ->latest()
    ->paginate(10);

Cách thứ hai gần giống ngôn ngữ tự nhiên hơn:

Lấy Post
→ đã Published
→ có Category
→ đang Active
→ mới nhất
→ phân trang

Đó chính là giá trị lớn của Scope.

33. Một User Model hoàn chỉnh

Ví dụ:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{
    #[Scope]
    protected function active(Builder $query): void
    {
        $query->where('is_active', true);
    }

    #[Scope]
    protected function role(
        Builder $query,
        string $role
    ): void {
        $query->where('role', $role);
    }

    #[Scope]
    protected function admin(Builder $query): void
    {
        $query->where('role', 'admin');
    }
}

Controller:

$users = User::active()
    ->role('editor')
    ->latest()
    ->paginate(10);

Hoặc:

$admins = User::active()
    ->admin()
    ->latest()
    ->paginate(10);

34. Scope không thay thế Query Builder

Scope chỉ là cách đóng gói Query thường dùng.

Chúng ta vẫn có thể sử dụng:

where()
orderBy()
latest()
with()
paginate()

Ví dụ:

Post::published()
    ->where('category_id', 5)
    ->with('user')
    ->latest()
    ->paginate(10);

Scope và Query Builder có thể kết hợp với nhau.

35. Scope và Controller

Một nguyên tắc tốt:

MODEL
↓
Quy tắc truy vấn có thể tái sử dụng

CONTROLLER
↓
Điều phối Request → Query → View

Không nên biến Controller thành một nơi chứa hàng trăm dòng:

where()
where()
where()
where()
where()

nếu những điều kiện đó được sử dụng lặp lại nhiều lần.

Đưa chúng vào Scope sẽ giúp source dễ bảo trì hơn.

36. Tóm tắt Local Scope

Local Scope:

#[Scope]
protected function active(Builder $query): void
{
    $query->where('is_active', true);
}

Sử dụng:

User::active()->get();

Dynamic Scope:

#[Scope]
protected function role(
    Builder $query,
    string $role
): void {
    $query->where('role', $role);
}

Sử dụng:

User::role('admin')->get();

37. Tóm tắt Global Scope

Tạo:

php artisan make:scope ActiveUserScope

Scope:

class ActiveUserScope implements Scope
{
    public function apply(
        Builder $builder,
        Model $model
    ): void {
        $builder->where('is_active', true);
    }
}

Gắn vào Model (ngoài class):

#[ScopedBy([ActiveUserScope::class])]
class User extends Authenticatable
{
    //
}

Bây giờ:

User::all();

tự động áp dụng Scope.

Muốn bỏ:

User::withoutGlobalScope(
    ActiveUserScope::class
)->get();

38. Bài tập thực hành

Bài tập 1 — Active User

Tạo Local Scope:

active()

để lấy User:

is_active = 1

Sau đó sử dụng:

User::active()->get();

Bài tập 2 — Role

Tạo Dynamic Scope:

role()

Sử dụng:

User::role('admin')->get();

và:

User::role('editor')->get();

Bài tập 3 — Published Post

Trong Post Model tạo:

published()

để lấy:

status = published

Sau đó:

Post::published()
    ->latest()
    ->paginate(10);

Bài tập 4 — Draft Post

Tạo:

draft()

để lấy:

status = draft

Sử dụng:

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

Bài tập 5 — Kết hợp Scope

Viết Query:

User
→ active
→ role = editor
→ mới nhất
→ paginate 10

Kết quả mong muốn:

User::active()
    ->role('editor')
    ->latest()
    ->paginate(10);

🎯 GHI NHỚ BÀI 32

SCOPE
│
├── LOCAL SCOPE
│     │
│     ├── #[Scope]
│     ├── Chủ động gọi
│     ├── Có thể chain
│     └── Có thể nhận tham số
│
└── GLOBAL SCOPE
      │
      ├── Tự động áp dụng
      ├── #[ScopedBy]
      ├── Có thể dùng Scope class
      └── Có thể bỏ bằng withoutGlobalScope()

Cách nhớ đơn giản nhất:

Local Scope
→ "Tôi gọi thì nó chạy."

Global Scope
→ "Nó tự chạy."

Accessor
→ "Đọc dữ liệu thì xử lý."

Mutator
→ "Ghi dữ liệu thì xử lý."

Scope
→ "Query thường dùng thì đóng gói."

Sau Bài 32, chúng ta đã có thể đưa những điều kiện Query lặp lại ra khỏi Controller và tổ chức chúng ngay trong Eloquent Model.

Đây là nền tảng rất quan trọng trước khi bước sang:

📘 Bài 33 — Observer

Ở bài tiếp theo, chúng ta sẽ tìm hiểu cách Laravel tự động thực hiện một hành động khi Model xảy ra các sự kiện như:

creating
created
updating
updated
deleting
deleted
restored

và xây dựng UserObserver / PostObserver cho dự án Blog CMS.

x1
quay về MỤC LỤC

Không có nhận xét nào:

Đăng nhận xét

Facebook Youtube RSS