Eloquent Model trong Laravel 13 cho người mới bắt đầu
Hướng dẫn dùng **Model** (Eloquent ORM) từ số 0, qua ví dụ một model `Post` (bài viết blog). Bài này độc lập — bạn chỉ cần Laravel 13 + một bảng `posts` trong database là làm theo được.
1. Model là gì?
Model là một class PHP đại diện cho một bảng trong database. Mỗi dòng trong bảng = một object của model. Thay vì viết SQL tay, bạn thao tác bằng code PHP:
$post = Post::find(1); // SELECT * FROM posts WHERE id = 1
echo $post->title; // đọc cột title
$post->title = 'Mới'; // đổi giá trị
$post->save(); // UPDATE posts SET title = 'Mới' WHERE id = 1
Cơ chế này gọi là ORM (Object-Relational Mapping). Trong Laravel nó tên là Eloquent.
⚠ Quan trọng: Model không tạo ra bảng. Bảng do migration tạo. Model chỉ ánh xạ tới bảng đã tồn tại.
2. Tạo model
Dùng lệnh Artisan:
php artisan make:model Post
Muốn tạo kèm luôn migration (-m), factory (-f), seeder (-s), controller
(-c):
php artisan make:model Post -mfsc
File tạo ra: app/Models/Post.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
//
}
Chỉ cần extends Model là đã có toàn bộ sức mạnh Eloquent.
3. Quy ước ngầm (convention) — hiểu để đỡ cấu hình
Laravel "đoán" nhiều thứ theo quy ước. Model Post mặc định:
| Thứ | Giá trị mặc định | Ghi chú |
|---|---|---|
| Tên bảng | posts |
số nhiều, snake_case của tên class |
| Khóa chính | id |
kiểu bigint tự tăng |
| Timestamps | created_at, updated_at |
Eloquent tự quản |
Nếu bảng của bạn tên khác, khai báo tường minh:
class Post extends Model
{
protected $table = 'blog_posts'; // bảng không phải "posts"
protected $primaryKey = 'post_id'; // khóa chính không phải "id"
public $timestamps = false; // bảng không có created_at/updated_at
}
4. Mass Assignment — $fillable (BẮT BUỘC hiểu)
Khi bạn tạo/sửa nhiều cột cùng lúc từ một mảng (thường là dữ liệu form):
Post::create($request->all()); // gán hàng loạt
Laravel chặn việc này để tránh lỗ hổng bảo mật (kẻ xấu gửi thêm field lạ như
is_admin=1). Bạn phải khai báo cột nào được phép gán hàng loạt:
class Post extends Model
{
protected $fillable = ['title', 'slug', 'body', 'published'];
}
Nếu quên, bạn sẽ gặp lỗi MassAssignmentException.
$fillable= danh sách trắng (chỉ cột này được gán) — khuyên dùng.$guarded= danh sách đen (chặn cột này, còn lại cho phép) — ngược lại.
💡 Nguyên tắc: những cột người dùng KHÔNG được tự set (vd
views,role,user_id) thì đừng cho vào$fillable. Set chúng bằng code riêng.
5. Ép kiểu dữ liệu — casts()
Database lưu mọi thứ dạng chuỗi/số. casts() giúp Eloquent trả về đúng kiểu
PHP:
class Post extends Model
{
protected function casts(): array
{
return [
'published' => 'boolean', // 0/1 -> true/false
'published_at' => 'datetime', // chuỗi -> Carbon object
'meta' => 'array', // JSON -> mảng PHP (và ngược lại)
'views' => 'integer',
];
}
}
Kết quả:
$post = Post::find(1);
$post->published; // true (bool), không phải "1" (string)
$post->published_at->diffForHumans(); // "2 ngày trước" — vì là Carbon
$post->meta['seo_title']; // truy cập như mảng
Laravel 11+ dùng method
casts(). Code cũ dùngprotected $casts = [...](property) — vẫn chạy, nhưng method là cách mới, khuyến nghị.
6. Ẩn cột khi xuất JSON — $hidden
Khi model được trả về API (return $post;), Laravel tự chuyển sang JSON. Ẩn cột
nhạy cảm:
protected $hidden = ['password', 'remember_token'];
7. CRUD — Tạo / Đọc / Sửa / Xóa (phần dùng nhiều nhất)
7.1. CREATE — tạo dòng mới
Cách 1 — create() (gán hàng loạt, cần $fillable):
$post = Post::create([
'title' => 'Bài đầu tiên',
'slug' => 'bai-dau-tien',
'body' => 'Nội dung...',
'published' => true,
]);
// $post->id đã có ngay sau khi tạo
Cách 2 — tạo object rồi save():
$post = new Post();
$post->title = 'Bài thứ hai';
$post->slug = 'bai-thu-hai';
$post->body = 'Nội dung...';
$post->save(); // INSERT
firstOrCreate — tìm, không có thì tạo (tránh trùng):
// Tìm theo slug; nếu chưa có thì tạo với dữ liệu ở mảng thứ 2
$post = Post::firstOrCreate(
['slug' => 'bai-dau-tien'], // điều kiện tìm
['title' => 'Bài đầu tiên', 'body' => '...'] // dữ liệu khi tạo mới
);
updateOrCreate — có thì cập nhật, không có thì tạo:
$post = Post::updateOrCreate(
['slug' => 'bai-dau-tien'], // tìm theo
['title' => 'Tiêu đề đã sửa'] // giá trị cập nhật/tạo
);
7.2. READ — truy vấn dữ liệu
Lấy tất cả:
$posts = Post::all(); // Collection tất cả bài — CẨN THẬN nếu bảng lớn
Tìm theo khóa chính:
$post = Post::find(1); // trả null nếu không thấy
$post = Post::findOrFail(1); // ném lỗi 404 nếu không thấy (hay dùng ở controller)
$posts = Post::find([1, 2, 3]); // nhiều id -> Collection
Lọc theo điều kiện — where():
$posts = Post::where('published', true)->get();
// Nhiều điều kiện (AND)
$posts = Post::where('published', true)
->where('views', '>', 100)
->get();
// Toán tử tùy ý
Post::where('title', 'like', '%laravel%')->get();
Post::whereIn('id', [1, 2, 3])->get();
Post::whereNull('published_at')->get();
Post::whereBetween('views', [10, 100])->get();
Lấy 1 dòng:
$post = Post::where('slug', 'bai-dau-tien')->first(); // dòng đầu hoặc null
$post = Post::where('slug', 'bai-dau-tien')->firstOrFail(); // hoặc lỗi 404
Sắp xếp & giới hạn:
Post::orderBy('created_at', 'desc')->get(); // cũ -> mới ngược lại
Post::latest()->get(); // = orderBy('created_at','desc'), mới nhất trước
Post::oldest()->get(); // cũ nhất trước
Post::latest()->limit(5)->get(); // 5 bài mới nhất
Lấy riêng vài cột (nhẹ hơn):
Post::select('id', 'title')->get();
Post::pluck('title'); // Collection chỉ chứa title
Post::pluck('title', 'id'); // [id => title]
Post::where('id', 1)->value('title'); // đúng 1 giá trị của 1 cột
Phân trang — cực hay dùng:
$posts = Post::latest()->paginate(10); // 10 bài / trang, tự đọc ?page=
Trong Blade: {{ $posts->links() }} để render nút chuyển trang.
7.3. UPDATE — cập nhật
Sửa 1 object đã lấy:
$post = Post::findOrFail(1);
$post->title = 'Tiêu đề mới';
$post->save(); // chỉ UPDATE các cột thay đổi; tự cập nhật updated_at
Hoặc update() (gán mảng, cần $fillable):
$post->update(['title' => 'Tiêu đề mới', 'published' => true]);
Cập nhật hàng loạt (nhiều dòng cùng lúc):
Post::where('published', false)->update(['published' => true]);
// UPDATE posts SET published = 1 WHERE published = 0
Tăng/giảm số — increment() / decrement():
$post->increment('views'); // views = views + 1 (atomic, an toàn race)
$post->increment('views', 5); // + 5
$post->decrement('stock', 2); // - 2
💡
increment()/decrement()chạy thẳng ở DB (SET views = views + 1) nên an toàn khi nhiều request cùng lúc — khác với đọc rồi$post->views + 1(dễ đếm sai). Lưu ý: 2 hàm này cũng tự cập nhậtupdated_at. Muốn không đụng timestamp:Post::withoutTimestamps(fn () => $post->increment('views'));
7.4. DELETE — xóa
$post = Post::find(1);
$post->delete(); // xóa 1 dòng
Post::destroy(1); // xóa theo id
Post::destroy([1, 2, 3]); // xóa nhiều id
Post::where('published', false)->delete(); // xóa hàng loạt theo điều kiện
Soft Delete (xóa mềm — chỉ đánh dấu, không mất hẳn):
use Illuminate\Database\Eloquent\SoftDeletes;
class Post extends Model
{
use SoftDeletes; // cần cột deleted_at trong bảng
}
Khi đó ->delete() chỉ set deleted_at = now(). Khôi phục bằng ->restore(),
xóa vĩnh viễn bằng ->forceDelete(), lấy cả bản đã xóa bằng ->withTrashed().
8. Aggregate — thống kê nhanh
Post::count(); // đếm tổng số bài
Post::where('published', true)->count();
Post::sum('views'); // tổng lượt xem
Post::avg('views'); // trung bình
Post::max('views'); // nhiều nhất
Post::where('slug', 'x')->exists(); // true/false — có tồn tại không
Post::where('slug', 'x')->doesntExist();
9. Quan hệ (Relationships) — sức mạnh thật sự của Eloquent
Giả sử: một User có nhiều Post, mỗi Post thuộc về một User
(bảng posts có cột user_id).
Trong Post — thuộc về user:
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Post extends Model
{
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}
Trong User — có nhiều post:
use Illuminate\Database\Eloquent\Relations\HasMany;
class User extends Model
{
public function posts(): HasMany
{
return $this->hasMany(Post::class);
}
}
Cách dùng:
$post = Post::find(1);
echo $post->user->name; // tên tác giả (truy cập như thuộc tính)
$user = User::find(1);
foreach ($user->posts as $post) { // tất cả bài của user
echo $post->title;
}
// Đếm bài của user không cần load hết
$user->posts()->count();
// Tạo bài gắn thẳng vào user (tự set user_id)
$user->posts()->create(['title' => 'Bài mới', 'body' => '...']);
9.1. Eager Loading — tránh lỗi N+1 (RẤT quan trọng)
Sai (mỗi vòng lặp query 1 lần → chậm):
$posts = Post::all();
foreach ($posts as $post) {
echo $post->user->name; // +1 query mỗi bài!
}
Đúng — dùng with() nạp trước:
$posts = Post::with('user')->get(); // chỉ 2 query cho mọi bài
foreach ($posts as $post) {
echo $post->user->name; // không query thêm
}
Lọc theo quan hệ:
Post::has('comments')->get(); // bài CÓ comment
Post::whereHas('user', fn ($q) =>
$q->where('role', 'admin'))->get(); // bài của admin
Post::withCount('comments')->get(); // kèm $post->comments_count
10. Accessor & Mutator — biến đổi giá trị khi đọc/ghi
Accessor (biến đổi khi đọc) và Mutator (biến đổi khi ghi), gộp trong
một Attribute:
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Support\Str;
class Post extends Model
{
// Tự tạo slug từ title khi GHI title
protected function title(): Attribute
{
return Attribute::make(
set: fn (string $value) => [
'title' => $value,
'slug' => Str::slug($value),
],
);
}
// Thuộc tính "ảo" excerpt khi ĐỌC (không có cột trong DB)
protected function excerpt(): Attribute
{
return Attribute::make(
get: fn () => Str::limit(strip_tags($this->body), 100),
);
}
}
Dùng:
$post->title = 'Xin Chào Laravel'; // slug tự thành "xin-chao-laravel"
echo $post->excerpt; // 100 ký tự đầu, dù DB không có cột này
11. Query Scope — đóng gói điều kiện hay dùng lại
Thay vì lặp where('published', true) khắp nơi:
use Illuminate\Database\Eloquent\Builder;
class Post extends Model
{
// Local scope: tên method bắt đầu bằng "scope"
public function scopePublished(Builder $query): void
{
$query->where('published', true);
}
public function scopePopular(Builder $query, int $min = 100): void
{
$query->where('views', '>=', $min);
}
}
Gọi (bỏ tiền tố scope, viết thường chữ đầu):
Post::published()->get();
Post::published()->popular(500)->latest()->get(); // xâu chuỗi thoải mái
12. Collection — kết quả trả về không phải mảng thường
get(), all(), quan hệ hasMany... trả về Collection (mảng "xịn" nhiều
tiện ích):
$posts = Post::all();
$posts->count(); // số phần tử
$posts->first(); // phần tử đầu
$posts->where('published', true); // lọc trong bộ nhớ
$posts->pluck('title'); // lấy list title
$posts->sortByDesc('views'); // sắp xếp
$posts->map(fn ($p) => $p->title); // biến đổi
$posts->groupBy('user_id'); // gom nhóm
$titles = $posts->pluck('title')->implode(', '); // nối chuỗi
13. Bảng "cheat sheet" các hàm dùng nhiều nhất
| Việc cần làm | Câu lệnh |
|---|---|
| Tạo mới | Post::create([...]) |
| Tìm theo id | Post::find($id) / findOrFail($id) |
| Lọc | Post::where('col', $val)->get() |
| Lấy 1 dòng | ->first() / ->firstOrFail() |
| Mới nhất | Post::latest()->get() |
| Phân trang | Post::paginate(10) |
| Cập nhật | $post->update([...]) |
| Tăng số | $post->increment('views') |
| Xóa | $post->delete() / Post::destroy($id) |
| Đếm | Post::count() |
| Có tồn tại? | Post::where(...)->exists() |
| Nạp quan hệ | Post::with('user')->get() |
14. Sai lầm thường gặp của người mới
- Quên
$fillable→MassAssignmentException. Thêm cột vào$fillable(trừ cột nhạy cảm). - Lỗi N+1 → dùng
with()khi lặp qua quan hệ. Post::all()trên bảng lớn → load hết RAM. Dùngpaginate()hoặcchunk().- Nhầm Model tạo bảng → không. Bảng do migration tạo; chạy
php artisan migrate. - Cho cột đếm (
views) vào$fillable→ nguy cơ mass-assignment. Tăng bằngincrement()thay vì để người dùng gán. - Đọc rồi cộng thủ công (
$post->views + 1; save()) khi nhiều request → đếm sai. Dùngincrement()(atomic).
Kết
Eloquent giúp bạn làm việc với database bằng PHP thuần, đọc như tiếng Anh. Ba thứ cần nắm chắc trước:
$fillable— an toàn mass assignment.- CRUD cơ bản —
create,find/where/get,update/increment,delete. - Quan hệ +
with()— nối bảng và tránh N+1.
Nắm 3 phần này là đã tự tin làm 80% công việc thực tế với model trong Laravel 13.