Toidicode.com

Toidicode.com

BASIC TO ADVANCE

Bài 15: Eloquent Model trong Laravel 13

Ở bài trước chúng ta đã truy vấn database bằng DB::table(). Tiếp tục với series, bài này mình sẽ giới thiệu với mọi người về Eloquent Model trong Laravel 13, rồi chuyển trang /projects của TaskFlow từ session sang database.

1. Eloquent Model là gì?

Eloquent là ORM (object-relational mapper) của Laravel: mỗi bảng trong database có một model tương ứng, dùng để đọc, thêm, sửa, xóa bản ghi của bảng đó bằng object PHP.

Model nằm trong thư mục app/Models và kế thừa class Illuminate\Database\Eloquent\Model. Các bạn tạo model với lệnh make:model:

php artisan make:model Project -f
#  INFO  Model [app/Models/Project.php] created successfully.
#  INFO  Factory [database/factories/ProjectFactory.php] created successfully.

php artisan make:model Task -f

Trong đó:

  • -f (--factory) tạo kèm factory (Bài 13).
  • -m tạo kèm migration. Bảng đã có từ Bài 12 nên mình không dùng, với model mới thì hay gặp nhất là -mf.
  • Ngoài ra còn -s (seeder), -c (controller), -a (tất cả).

Model mới tạo chỉ có trait HasFactory, không cần khai báo bảng hay cột nhờ các quy ước.

2. Quy ước của model.

  • Tên bảng là tên class viết dạng snake_case số nhiều: Project ứng với bảng projects, TaskComment ứng với task_comments.
  • Khóa chính là cột id tự tăng.
  • Timestamps: bảng có hai cột created_at, updated_at, Eloquent tự điền khi thêm và sửa bản ghi.

Bảng nào không theo quy ước thì Laravel 13 cho khai báo bằng PHP attribute #[Table] ngay trên class:

use Illuminate\Database\Eloquent\Attributes\Table;

#[Table('du_an', key: 'ma_du_an', timestamps: false)]
class Project extends Model
{
    // ...
}

Lúc này model dùng bảng du_an, khóa chính ma_du_an và không tự điền timestamps (chỉ tắt timestamps thì dùng #[WithoutTimestamps]).

3. Mass assignment: fillable và guarded.

Truyền cả một mảng vào model, như Project::create($data), gọi là mass assignment (gán hàng loạt). Nếu $data lấy thẳng từ request, người dùng có thể gửi thêm field user_id để ghi vào cột không được phép, nên mặc định model chặn mass assignment:

Project::create(['name' => 'Học Eloquent']);
// MassAssignmentException: Add [name] to fillable property to allow mass assignment on [App\Models\Project].

Các cột được phép gán hàng loạt khai báo bằng attribute #[Fillable] (model User có sẵn cũng vậy):

- app/Models/Project.php

use Illuminate\Database\Eloquent\Attributes\Fillable;

// user_id không cho gán hàng loạt, chủ project sẽ gán khi có đăng nhập (Bài 21)
#[Fillable(['name', 'description'])]
class Project extends Model
{
    /** @use HasFactory<ProjectFactory> */
    use HasFactory;
}

Model Task tương tự, gồm mọi cột trừ id, completed_at và timestamps. Cột không có trong danh sách bị bỏ qua, không báo lỗi (cả với new Project([...])):

$project = new Project(['name' => 'Học Laravel 13', 'user_id' => 3]);
$project->user_id; // null

Ngược lại, #[Guarded(['id', 'user_id'])] liệt kê các cột bị chặn, còn lại đều được gán. Mình khuyên dùng #[Fillable], kết hợp với validated() của Bài 10 để chỉ đưa vào model các field đã có rule.

4. Thêm, đọc, sửa, xóa với Eloquent.

Model cũng là một Query Builder nên các method where(), orderBy(), pluck()... của Bài 14 đều dùng được. Mình chạy trong php artisan tinker với dữ liệu mẫu.

Lưu ý: Tinker chỉ gọi tắt được Project (thay cho App\Models\Project) với class có trong classmap của Composer. Laravel bật optimize-autoloader nên classmap chỉ có các class lúc cài đặt, model mới báo Class "Project" not found: các bạn chạy composer dump-autoload, và chạy lại mỗi khi thêm class mới.

Đọc dữ liệu.

count(Project::all()); // 3

$project = Project::find(1);
$project->name; // Website toidicode.com

Project::where('user_id', 1)->pluck('name'); // ['Website toidicode.com', 'TaskFlow API']
Task::where('status', 'todo')->count(); // 4

Project::find(99);       // null
Project::findOrFail(99); // ModelNotFoundException: No query results for model [App\Models\Project] 99

Kết quả là object Project, các cột đọc như property. all(), get() trả về collection các model (Bài 16). Trong controller, findOrFail() không tìm thấy sẽ tự trả về 404.

Thêm mới.

$project = new Project;
$project->name = 'Học Eloquent';
$project->save();
$project->id; // 4

$project = Project::create(['name' => 'Học Laravel 13', 'description' => 'Series trên toidicode.com']);
$project->id; // 5

save() lưu object vào database, create() tạo và lưu trong một lệnh.

Sửa và xóa.

$project = Project::find(4);
$project->description = 'Bài 15 của series';
$project->save();
$project->update(['name' => 'Học Eloquent Model']); // true

// sửa nhiều bản ghi, trả về số bản ghi bị ảnh hưởng
Task::where('project_id', 3)->update(['assignee_id' => 1]); // 3

Project::find(4)->delete(); // true
Project::destroy(5);        // 1, xóa theo khóa chính, nhiều id: destroy([1, 2])
Task::where('status', 'done')->delete(); // 3

firstOrCreate và updateOrCreate.

firstOrCreate() tìm theo mảng thứ nhất, không có thì tạo mới từ cả hai mảng. updateOrCreate() tìm theo mảng thứ nhất rồi cập nhật bằng mảng thứ hai, không có thì tạo mới:

Project::firstOrCreate(['name' => 'TaskFlow API'])->id; // 2, đã có nên không tạo
Project::updateOrCreate(['name' => 'Học Vue'], ['description' => 'Series sắp tới'])->id; // 6, tạo mới

5. Dùng model trong TaskFlow.

Factory và seeder.

Có model rồi, mình viết hai factory. Một cột có thể là closure nhận các giá trị đã sinh:

- database/factories/TaskFactory.php

public function definition(): array
{
    return [
        // không truyền project_id thì tạo kèm một project mới
        'project_id' => Project::factory(),
        'title' => fake()->sentence(4),
        'status' => 'todo',
        'due_date' => fake()->optional()->dateTimeBetween('now', '+30 days')?->format('Y-m-d'),
        // chỉ task đã xong mới có thời điểm hoàn thành
        'completed_at' => fn (array $attributes) => $attributes['status'] === 'done' ? now() : null,
    ];
}

ProjectFactory sinh tên dạng "Dự án Hải Phòng". Các seeder của Bài 13 chuyển sang factory, dữ liệu giữ nguyên: ProjectSeeder gọi Project::factory()->createMany([...]) với 3 project, TaskSeeder gọi Task::factory()->create([...]) cho từng task, bỏ id, timestamps và completed_at. Factory không bị #[Fillable] chặn nên vẫn gán được user_id. Chạy lại seeder, dữ liệu đã sửa ở phần 4 cũng được xóa:

php artisan migrate:fresh --seed

Route model binding và controller.

Như đã hẹn ở Bài 4, route đổi {id} thành {project}, bỏ parameters() để resource dùng tên mặc định:

- routes/web.php

Route::middleware(['throttle:projects', 'log.requests'])
    ->whereNumber('project')
    ->missing(fn () => abort(404, 'Không tìm thấy project.'))
    ->group(function () {
        Route::get('/projects/{project}/tasks', ListProjectTasks::class)
            ->name('projects.tasks.index');

        Route::resource('projects', ProjectController::class);
    });

missing() chạy khi không tìm thấy project, thiếu nó trang 404 của Bài 11 sẽ hiện câu tiếng Anh. ProjectController dùng thẳng model thay cho ProjectRepository:

- app/Http/Controllers/ProjectController.php

use App\Models\Project;
use App\Models\Task;

// bỏ constructor nhận ProjectRepository

public function store(StoreProjectRequest $request): RedirectResponse
{
    // validated() chỉ chứa name, description nên lưu thẳng vào model
    $project = Project::create($request->validated());

    // Log::info('Đã tạo project', ...) như Bài 11, đọc $project->id, $project->name

    return to_route('projects.show', $project)
        ->with('status', 'Đã tạo project "'.$project->name.'".');
}

public function show(Request $request, Project $project): View
{
    // chưa có relationship (Bài 17) nên lấy task theo project_id
    $tasks = Task::where('project_id', $project->id)->get();

    return view('projects.show', compact('project', 'tasks'))
        ->with('tab', $request->query('tab', 'overview'));
}

Trong đó:

  • index() lấy danh sách bằng Project::all(). show() không cần abort_if() nữa, route model binding đã trả về 404 khi không tìm thấy.
  • Truyền model cho to_route(), route() thì Laravel tự lấy id: ['id' => $project['id']] trong các view đổi thành $project (kèm tab thì ['project' => $project, 'tab' => 'tasks']). Các view đổi $project['name'] thành $project->name. Trong show.blade.php, $project['tasks'] đổi thành $tasks (mỗi phần tử là Task, in $task->title), count($project['tasks']) thành count($tasks).
  • ListProjectTasks và component ProjectCard cũng nhận Project $project, lấy task bằng Task::where('project_id', $project->id).

Cuối cùng, xóa ProjectRepository. Middleware LimitProjects (Bài 9) đổi từ đếm session('projects') sang Project::count() >= (int) $max (thêm use App\Models\Project;), giới hạn nâng lên limit.projects:20 vì database đã có 3 project.

6. Lời kết.

Như vậy chúng ta đã tìm hiểu xong Eloquent Model trong Laravel 13. Các bạn xem thêm tại tài liệu chính thức. Bài sau mình sẽ giới thiệu về query với Eloquent ORM: scope, soft delete, phân trang và collection.

Đăng ký nhận tin.

Chúng tôi chỉ gửi tối đa 2 lần trên 1 tháng. Tuyên bố không spam mail!

Vũ Thanh Tài

About author
The best way to learn is to share
Xem tất cả bài đăng

0 Comments

Bài viết chưa có ai bình luận, hãy là người đầu tiên đi bạn!

Bình luận

Captcha