Toidicode.com

Toidicode.com

BASIC TO ADVANCE

Bài 19: Accessor, Mutator và Cast trong Laravel 13

Sau bài trước, cột status của task vẫn là chuỗi 'todo', 'doing', 'done' và due_date cũng chỉ là chuỗi ngày. 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ề accessor, mutator và cast trong Laravel 13, các cách biến đổi giá trị khi đọc, ghi attribute của model.

Các ví dụ chạy trong php artisan tinker, các bạn chạy php artisan migrate:fresh --seed trước để về dữ liệu mẫu (Bài 13).

1. Cast trong Eloquent.

Cast là khai báo kiểu cho một attribute: Eloquent tự chuyển giá trị từ database sang kiểu đó khi đọc và chuyển ngược lại khi lưu.

Các bạn khai báo cast trong method casts() của model, key là tên cột, value là kiểu muốn ép. Mình cast hai cột ngày của task:

- app/Models/Task.php (trích)

protected function casts(): array
{
    return [
        'due_date' => 'date',
        'completed_at' => 'datetime',
    ];
}

Lúc này hai cột trả về object Carbon (giống created_at) thay vì chuỗi:

$task = Task::find(1);
$task->due_date; // Illuminate\Support\Carbon, giờ là 00:00:00
$task->due_date->format('d/m/Y'); // 16/11/2026 (2 ngày sau lúc seed)

$task->due_date = '2026-12-01'; // gán chuỗi, Carbon hay timestamp đều được

Một số kiểu cast hay dùng:

  • boolean, integer, float, string: ép kiểu nguyên thủy, VD cột lưu 0/1 đọc ra true/false.
  • date, datetime: trả về Carbon. immutable_date, immutable_datetime trả về CarbonImmutable, các method như addDay() tạo object mới thay vì sửa chính attribute.
  • array (hay json): cột JSON/TEXT đọc ra mảng PHP, gán mảng thì tự json_encode khi lưu.
  • encrypted: mã hóa bằng APP_KEY khi lưu, giải mã khi đọc. Cột cần kiểu TEXT và không where theo giá trị được.

Giá trị null không bị cast. Danh sách đầy đủ các bạn xem tại đây.

Custom cast.

Khi cần kiểu riêng, các bạn tạo cast bằng php artisan make:cast TenCast. Class trong app/Casts implement CastsAttributes với hai method: get() chuyển giá trị thô thành giá trị dùng trong code, set() làm ngược lại. Sau đó khai báo 'cot' => TenCast::class trong casts().

2. Enum cast.

Ba trạng thái của task rất hợp với một backed enum (PHP 8.1+). Laravel có sẵn command tạo enum:

php artisan make:enum Enums/TaskStatus --string

- app/Enums/TaskStatus.php

<?php

namespace App\Enums;

enum TaskStatus: string
{
    case Todo = 'todo';
    case Doing = 'doing';
    case Done = 'done';

    // tên trạng thái hiển thị cho người dùng
    public function label(): string
    {
        return match ($this) {
            self::Todo => 'Cần làm',
            self::Doing => 'Đang làm',
            self::Done => 'Hoàn thành',
        };
    }
}

Rồi thêm 'status' => TaskStatus::class vào casts() của Task (nhớ use App\Enums\TaskStatus;). Database vẫn lưu chuỗi, còn trong code status là case của enum. TaskStatus là class mới nên các bạn chạy lại composer dump-autoload để tinker gọi tắt được (Bài 15):

$task = Task::find(1);
$task->status; // App\Enums\TaskStatus::Doing
$task->status->label(); // Đang làm
$task->status === TaskStatus::Doing; // true

$task->status = TaskStatus::Done; // gán case
$task->status = 'done'; // gán chuỗi cũng được, Eloquent gọi TaskStatus::from('done')
$task->status = 'pending'; // ValueError: "pending" is not a valid backing value for enum App\Enums\TaskStatus

TaskStatus::tryFrom('pending'); // null, tryFrom() không ném lỗi

Scope status() của Bài 16 cũng đổi tham số thành TaskStatus $status, where() nhận thẳng case enum và tự lấy giá trị chuỗi: Task::status(TaskStatus::Todo)->count(); // 4.

Validate input theo enum thì dùng Rule::enum() thay cho Rule::in() của Bài 10:

use App\Enums\TaskStatus;
use Illuminate\Validation\Rule;

$request->validate([
    'status' => ['required', Rule::enum(TaskStatus::class)],
]);

// status=pending
// The selected status is invalid.

3. Accessor và mutator.

Accessor biến đổi giá trị khi đọc attribute, mutator biến đổi khi gán. Cả hai khai báo bằng một method protected trả về Attribute, tên method là tên attribute dạng camelCase:

use Illuminate\Database\Eloquent\Casts\Attribute;

protected function tenAttribute(): Attribute
{
    return Attribute::make(
        get: fn (mixed $value, array $attributes) => ...,
        set: fn (mixed $value) => ...,
    ); // chỉ cần một trong hai thì bỏ tham số kia
}

Trong đó:

  • get nhận giá trị gốc của cột và mảng tất cả attribute, trả về giá trị khi đọc.
  • set nhận giá trị được gán, trả về giá trị sẽ lưu (hoặc mảng ['cot' => giá trị] để ghi nhiều cột).

VD1: Accessor is_overdue cho biết task quá hạn chưa. Bảng không có cột này, accessor tính từ các attribute đã cast:

- app/Models/Task.php (trích)

protected function isOverdue(): Attribute
{
    return Attribute::make(
        get: fn (): bool => $this->due_date?->lt(today()) && $this->status !== TaskStatus::Done,
    );
}
$task = Task::find(3); // todo, hạn 7 ngày nữa
$task->is_overdue; // false
$task->due_date = today()->subDay();
$task->is_overdue; // true
$task->status = TaskStatus::Done;
$task->is_overdue; // false

VD2: Mutator cho tên tag, luôn lưu chữ thường và bỏ khoảng trắng thừa để " Gấp " không thành tag mới bên cạnh "gấp":

- app/Models/Tag.php (trích, thêm use Illuminate\Support\Str;)

protected function name(): Attribute
{
    return Attribute::make(
        set: fn (string $value) => Str::lower(trim($value)),
    );
}
$tag = Tag::create(['name' => '  Review ']); // create() cũng đi qua mutator
$tag->name; // review

Lưu ý: Mutator chỉ chạy khi gán attribute, còn where('name', ' Gấp ') vẫn so sánh nguyên chuỗi, nên giá trị dùng để query các bạn phải tự chuẩn hóa.

4. Serialize model.

toArray() chuyển model (kèm các relationship đã load) thành mảng, toJson() thành chuỗi JSON. Model return từ route, controller cũng tự thành JSON (Bài 24 sẽ dùng API Resource cho việc này). Khi serialize, cast vẫn có tác dụng: enum thành giá trị chuỗi, ngày thành chuỗi ISO-8601 giờ UTC.

Accessor không có cột như is_overdue mặc định không có trong kết quả, muốn có thì khai báo #[Appends]:

use Illuminate\Database\Eloquent\Attributes\Appends;

#[Appends(['is_overdue'])]
class Task extends Model
Task::find(1)->setVisible(['title', 'status', 'due_date', 'is_overdue'])->toJson(JSON_UNESCAPED_UNICODE);
// {"title":"Viết bài Seeder và Factory","status":"doing","due_date":"2026-11-16T00:00:00.000000Z","is_overdue":false}

array_keys(User::find(1)->toArray());
// ['id', 'name', 'email', 'email_verified_at', 'created_at', 'updated_at']

Trong đó:

  • User có sẵn #[Hidden(['password', 'remember_token'])] nên hai cột này không bao giờ lọt ra JSON. Ngược lại #[Visible([...])] chỉ cho các cột trong danh sách (hoặc thuộc tính $hidden, $visible như bản cũ).
  • setVisible(), makeHidden(), makeVisible() đổi tạm trên một object, attribute của #[Appends] cũng tuân theo các danh sách này.

5. Áp dụng vào TaskFlow.

Bộ lọc ở show() của ProjectController dùng tryFrom() thay cho mảng chuỗi (thêm use App\Enums\TaskStatus; đầu file):

// tryFrom() trả về null khi giá trị không phải một case, ?status=abc thì bỏ qua
$status = TaskStatus::tryFrom((string) $request->query('status'));

$tasks = $project->tasks()
    ->with(['assignee', 'tags'])
    ->when($status, fn ($query) => $query->status($status))
    ->paginate(3)
    ->withQueryString();

View lấy link lọc từ TaskStatus::cases(), mỗi task hiện tên trạng thái và hạn, quá hạn thì tô đỏ:

- resources/views/projects/show.blade.php (trích)

<a href="{{ route('projects.show', ['project' => $project, 'tab' => 'tasks']) }}"
    @class(['text-blue-600', 'font-bold' => $status === null])>Tất cả</a>
@foreach (App\Enums\TaskStatus::cases() as $case)
    <a href="{{ route('projects.show', ['project' => $project, 'tab' => 'tasks', 'status' => $case]) }}"
        @class(['text-blue-600', 'font-bold' => $status === $case])>{{ $case->label() }}</a>
@endforeach

{{-- trong mỗi <li> của danh sách task --}}
<span class="text-sm">{{ $task->status->label() }}</span>
@if ($task->due_date)
    <span @class(['text-sm', 'text-red-600' => $task->is_overdue])>hạn {{ $task->due_date->format('d/m/Y') }}</span>
@endif

Kết quả: /projects/1?tab=tasks có các link "Tất cả Cần làm Đang làm Hoàn thành" (route() nhận thẳng case enum), task đầu tiên hiện "Viết bài Seeder và Factory (Nguyễn Văn An) Đang làm hạn 16/11/2026 viết bài gấp". Dữ liệu mẫu chưa có task quá hạn, các bạn đổi due_date một task về hôm qua trong tinker để thấy hạn tô đỏ.

6. Lời kết.

Như vậy chúng ta đã tìm hiểu xong accessor, mutator và cast trong Laravel 13: cast ngày và enum, Attribute::make với get, set, serialize model với #[Hidden], #[Appends]. 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ề filesystem và upload file trong Laravel 13 để đính kèm file cho task.

Đă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