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
- Scope là gì?
- Local Scope là gì?
- Tạo Local Scope đầu tiên
- Vì sao phải dùng
#[Scope]? - Sử dụng Local Scope
- Local Scope với nhiều điều kiện
- Có thể kết hợp nhiều Scope
- Local Scope trong CRUD Users
- Dynamic Local Scope
- Dynamic Scope rất hữu ích cho Search
- Scope cho Post
- Scope cho Blog Homepage
- Scope cho Category
- Local Scope giúp Controller sạch hơn
- Global Scope là gì?
- Ví dụ Global Scope
- Tạo Global Scope
- Viết ActiveUserScope
- Gắn Global Scope vào User
- Global Scope hoạt động như thế nào?
- Global Scope và Soft Delete
- Bỏ Global Scope
- Bỏ tất cả Global Scope
- Anonymous Global Scope
- Bỏ Anonymous Global Scope
- Local Scope và Global Scope khác nhau
- Khi nào nên dùng Local Scope?
- Khi nào nên dùng Global Scope?
- Không nên lạm dụng Global Scope
- Scope trong dự án Blog CMS
- Kết hợp Scope với Search
- Scope giúp đọc code dễ hơn
- Một User Model hoàn chỉnh
- Scope không thay thế Query Builder
- Scope và Controller
- Tóm tắt Local Scope
- Tóm tắt Global Scope
- Bài tập thực hành
- 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ểm | Local Scope | Global Scope |
|---|---|---|
| Kích hoạt | Chủ động gọi | Tự động |
| Phạm vi | Query cụ thể | Mọi Query của Model |
| Mục đích | Tái sử dụng điều kiện | Luô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 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