Thuộc tính $casts của Laravel là một trong những tiện ích âm thầm giúp định hình cách bạn xử lý dữ liệu mà không gây quá nhiều sự chú ý. Chỉ cần đưa một kiểu dữ liệu có sẵn như boolean hoặc datetime vào mảng, Eloquent sẽ tự động chuyển đổi các chuỗi thô từ cơ sở dữ liệu thành thứ gì đó thân thiện hơn trước khi giá trị đó chạm tới controller hoặc view của bạn. Nó giúp mã nguồn của bạn gọn gàng hơn. Nhưng ngay khi bạn cố gắng gọi trực tiếp một hàm helper của riêng mình bên trong mảng đó, framework sẽ không chấp nhận. Viết một thứ gì đó như 'username' => 'encrypt_data()' sẽ không hoạt động. Laravel mong đợi một từ khóa cast gốc hoặc một lớp thực thi interface CastsAttributes. Yêu cầu đó tồn tại vì lớp casting chạy sâu bên trong chu kỳ hydration và serialization của Eloquent; nó cần một đối tượng có thể dự đoán được với một hợp đồng (contract) rõ ràng thay vì một chuỗi hàm tùy ý mà có thể không tồn tại tại thời điểm runtime.
Tại sao các hàm Helper lại thất bại trong mảng $casts
Khi Eloquent tải một model, nó sẽ quét mảng $casts để quyết định cách biến đổi từng cột. Framework nhận diện các kiểu dữ liệu nguyên thủy cụ thể—string, integer, array, encrypted, v.v.—và nó cũng nhận diện các tên lớp đầy đủ (fully-qualified class names) có thực thi CastsAttributes. Nó không thực thi chuỗi đó như một đoạn mã. Vì vậy, 'encrypt_data()' bị coi là một kiểu cast không xác định, điều này sẽ gây ra lỗi. Ngay cả khi Laravel có phân tích cú pháp chuỗi đó, cũng sẽ không có cách nào đáng tin cậy để truyền instance của model, khóa thuộc tính (attribute key), giá trị hiện tại và các thuộc tính xung quanh vào hàm của bạn theo đúng thứ tự. Interface này tồn tại chính xác để tiêu chuẩn hóa sự kết nối đó giữa logic tùy chỉnh của bạn và các thành phần nội bộ của Eloquent.
Bạn có hai cách chắc chắn để thu hẹp khoảng cách này. Một cách ưu tiên việc tái sử dụng lâu dài. Cách còn lại ưu tiên tốc độ khi bạn chỉ cần một bản vá nhanh trong một model duy nhất.
Cách 1: Viết một lớp Custom Cast
Nếu cùng một kiểu biến đổi được áp dụng cho nhiều trường hoặc trải rộng trên nhiều model, một lớp cast chuyên dụng sẽ là lựa chọn sạch sẽ hơn. Nó nằm trong tệp riêng, có thể được unit-test một cách độc lập và giúp model của bạn không bị dính các đoạn mã lặp lại (boilerplate).
Bắt đầu bằng cách tạo app/Casts/CustomEncrypt.php. Namespace nên khớp với thiết lập autoloading của bạn, thông thường là App\Casts. Lớp này phải thực thi Illuminate\Contracts\Database\Eloquent\CastsAttributes, điều này bắt buộc bạn phải định nghĩa hai phương thức: get và set.
<?php
namespace App\Casts;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Database\Eloquent\Model;
class CustomEncrypt implements CastsAttributes
{
public function get(Model $model, string $key, mixed $value, array $attributes): mixed
{
return decrypt_data($value);
}
public function set(Model $model, string $key, mixed $value, array $attributes): mixed
{
return encrypt_data($value);
}
}
Các chữ ký phương thức (signatures) rất quan trọng. Eloquent truyền bốn đối số vào mỗi phương thức. $model là instance đang được đổ dữ liệu hoặc được lưu, cho phép bạn kiểm tra các trường khác nếu logic của bạn phụ thuộc vào chúng. $key là tên cột hiện đang được cast. $value là chuỗi thô hoặc null lấy từ cơ sở dữ liệu khi thực hiện get, hoặc giá trị do người dùng cung cấp khi thực hiện set. $attributes là toàn bộ mảng thô của các cột cho hàng đó. Bạn không cần phải sử dụng cả bốn đối số mọi lúc, nhưng interface yêu cầu chúng.
Trong phương thức get ở trên, decrypt_data($value) sẽ chạy sau khi Eloquent lấy hàng từ cơ sở dữ liệu và trước khi giá trị đó được đưa vào model của bạn. Trong phương thức set, encrypt_data($value) sẽ chạy trước lệnh INSERT hoặc UPDATE, đảm bảo cơ sở dữ liệu không bao giờ nhìn thấy văn bản thuần (plaintext).
Để kết nối nó, hãy tham chiếu đến lớp bên trong mảng $casts của model:
protected $casts = [
'username' => CustomEncrypt::class,
'password' => CustomEncrypt::class,
];
Vì bạn đang sử dụng hằng số của lớp (class constant), trình tự nạp tự động (autoloader) sẽ xử lý phần còn lại. Nếu sau này ứng dụng của bạn cần thay đổi cơ chế mã hóa, bạn chỉ cần chỉnh sửa một tệp duy nhất và mọi trường được ánh xạ sẽ thay đổi hành vi ngay lập tức. Sự tập trung đó rất khó có thể đánh bại khi bạn đang quản lý dữ liệu nhạy cảm trên nhiều bảng khác nhau.
Cách 2: Sử dụng Accessor và Mutator
Đôi khi bạn không muốn tạo một tệp mới cho một kiểu biến đổi chỉ có ý nghĩa ở một nơi duy nhất. Lớp Attribute của Laravel cho phép bạn định nghĩa logic get và set trực tiếp trên model bằng cú pháp closure của PHP 8+.
use Illuminate\Database\Eloquent\Casts\Attribute;
protected function username(): Attribute
{
return Attribute::make(
get: fn ($value) => decrypt_data($value),
set: fn ($value) => encrypt_data($value),
);
}
Tại đây, tên phương thức phải khớp với cột hoặc thuộc tính mà bạn đang nhắm tới. Khi Eloquent đọc username từ cơ sở dữ liệu, nó sẽ truyền giá trị thô qua closure get. Khi bạn gán một giá trị mới cho thuộc tính username của model, closure set sẽ mã hóa nó trước khi Eloquent xây dựng câu truy vấn.
Cách tiếp cận này tỏa sáng trong các bản prototype hoặc các model cũ (legacy models) nơi mà việc tạo cả một thư mục chứa các lớp cast có vẻ là quá mức cần thiết. Nhược điểm là sự lặp lại. Nếu sau này bạn quyết định rằng email, phone, và backup_code cũng cần được xử lý tương tự, bạn sẽ kết thúc bằng việc sao chép các closure đó qua nhiều phương thức hoặc model khác nhau. Sự rườm rà đó sẽ tích tụ dần. Nó cũng ngăn cản bạn tái sử dụng logic trong một unit test mà không cần phải khởi chạy (boot) toàn bộ model.
Lựa chọn giữa hai phương pháp
Hãy sử dụng một lớp custom cast khi bạn quan tâm đến khả năng tái sử dụng, kiểm thử và việc giữ cho các model luôn gọn nhẹ. Điều này báo hiệu cho các lập trình viên khác rằng quá trình chuyển đổi này là một khái niệm chính thống trong ứng dụng của bạn, chứ không phải là một giải pháp tạm thời.
Hãy sử dụng accessor khi logic thực sự mang tính cục bộ, mang tính thử nghiệm, hoặc khó có khả năng tồn tại lâu hơn sprint hiện tại. Nó cho phép bạn phát triển nhanh mà không làm rải rác các tệp nhỏ khắp mã nguồn. Chỉ cần sẵn sàng tái cấu trúc thành một lớp cast khi cùng một mô hình xuất hiện lần thứ hai hoặc thứ ba.
Các chi tiết thực tế cần lưu ý
Custom cast rất mạnh mẽ, nhưng chúng mang lại những hành vi có thể khiến bạn bất ngờ nếu không để ý. Đầu tiên, hãy nhớ rằng get nhận bất cứ thứ gì cơ sở dữ liệu trả về, bao gồm cả null. Nếu decrypt_data không chấp nhận đầu vào là null, hãy thực hiện kiểm tra để phòng tránh:
public function get(Model $model, string $key, mixed $value, array $attributes): mixed
{
return is_null($value) ? null : decrypt_data($value);
}
Thứ hai, các cast sẽ chạy trong quá trình tuần tự hóa mảng và JSON. Khi bạn trả về một model dưới dạng API resource hoặc gọi toArray(), logic get của cast vẫn được áp dụng. Đó thường là điều bạn muốn, nhưng cũng đáng để lưu ý nếu bạn đang hiển thị các giá trị đã giải mã và cần thêm các lớp kiểm soát hiển thị bổ sung.
Thứ ba, và quan trọng nhất, việc casting diễn ra sau khi truy xuất dữ liệu. Bạn không thể truy vấn dựa trên giá trị đã được chuyển đổi. Một truy vấn như User::where('username', 'john_doe')->first() sẽ gửi trực tiếp chuỗi john_doe đến cơ sở dữ liệu. Nó không bao giờ đi qua logic giải mã của bạn. Nếu cột đó được mã hóa khi lưu trữ, truy vấn đó sẽ không tìm thấy gì trừ khi bạn tìm kiếm dựa trên chính bản mã (ciphertext) đã được mã hóa. Hãy lập kế hoạch cho các mô hình truy cập cơ sở dữ liệu của bạn một cách phù hợp, vì cast không phải là sự thay thế cho các hàm cơ sở dữ liệu gốc hoặc các cột văn bản thuần túy đã được đánh chỉ mục.
Các lớp custom cast cũng là nơi tuyệt vời để chứa các cấu hình nhỏ. Nếu bạn cần các đối số của hàm khởi tạo—chẳng hạn như truyền một chế độ mã hóa hoặc một chuỗi định dạng—Laravel hỗ trợ chúng thông qua mảng $casts bằng một biểu thức như 'field' => CustomEncrypt::class . ':arg', mặc dù điều đó vượt quá mức thiết lập cơ bản được mô tả ở đây.
Bài học cốt lõi
Việc hạn chế đưa các hàm helper trực tiếp vào $casts không phải là một sự quan liêu tùy tiện. Nó thúc đẩy bạn hướng tới mã nguồn rõ ràng, có thể kiểm thử và có thể tái sử dụng. Các lớp custom cast biến các logic nội dòng rải rác thành các thành phần đáng tin cậy mà bạn có thể chia sẻ giữa các model mà không bị trùng lặp. Accessor giúp mở ra khả năng cho các bản sửa lỗi nhanh chóng, mang tính cục bộ khi việc tạo một tệp riêng biệt cảm thấy quá rườm rà. Hãy làm chủ cả hai, lựa chọn dựa trên phạm vi của vấn đề, và lớp Eloquent của bạn sẽ luôn dễ đọc ngay cả khi ứng dụng phát triển lớn mạnh.
