Властивість $casts у Laravel — це одна з тих непомітних зручностей, яка визначає те, як ви працюєте з даними, не привертаючи зайвої уваги. Додайте в масив вбудований тип, наприклад boolean або datetime, і Eloquent автоматично перетворить сирі рядки з бази даних на щось зручніше ще до того, як значення потрапить у ваші контролери чи представлення (views). Це тримає ваш код у порядку. Але щойно ви спробуєте викликати власну допоміжну функцію безпосередньо всередині цього масиву, фреймворк чинитиме опір. Запис на кшталт 'username' => 'encrypt_data()' не працюватиме. Laravel очікує або рідне ключове слово для приведення типів, або клас, що реалізує інтерфейс CastsAttributes. Ця вимога існує тому, що рівень приведення типів працює глибоко всередині циклу гідратації та серіалізації Eloquent; йому потрібен передбачуваний об'єкт із чітким контрактом, а не довільний рядок із функцією, яка може не існувати під час виконання.
Чому допоміжні функції не працюють у масиві $casts
Коли Eloquent завантажує модель, він сканує масив $casts, щоб вирішити, як трансформувати кожну колонку. Фреймворк розпізнає специфічні примітиви — string, integer, array, encrypted тощо — і повні імена класів, що реалізують CastsAttributes. Він не виконує рядок як код. Тому 'encrypt_data()' сприймається як невідомий тип приведення, що викликає помилку. Навіть якби Laravel міг його розпарсити, не було б надійного способу передати екземпляр моделі, ключ атрибута, поточне значення та сусідні атрибути у вашу функцію в правильному порядку. Інтерфейс існує саме для того, щоб стандартизувати цей «рукостискання» між вашою власною логікою та внутрішніми механізмами Eloquent.
У вас є два надійні способи подолати цей розрив. Один сприяє довгостроковому повторному використанню. Інший — швидкості, коли вам потрібне лише швидке виправлення в межах однієї моделі.
Метод 1: Створення власного класу приведення типів (Custom Cast Class)
Якщо одна й та сама трансформація застосовується до кількох полів або розподілена між кількома моделями, створення спеціального класу приведення типів буде чистішим рішенням. Він живе у власному файлі, його можна протестувати ізольовано, і він позбавляє ваші моделі повторюваного шаблонного коду (boilerplate).
Почніть зі створення app/Casts/CustomEncrypt.php. Простір імен (namespace) має відповідати вашому налаштуванню автозавантаження, зазвичай це App\Casts. Клас повинен реалізовувати Illuminate\Contracts\Database\Eloquent\CastsAttributes, що змушує вас визначити два методи: get та 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);
}
}
Сигнатури мають значення. Eloquent передає чотири аргументи в кожен метод. $model — це екземпляр, який заповнюється або зберігається, що дозволяє вам перевіряти інші поля, якщо ваша логіка залежить від них. $key — це назва колонки, яка зараз приводиться до типу. $value — це сирий рядок або null, що надходить із бази даних під час get, або значення, надане користувачем, під час set. $attributes — це весь сирий масив колонок для цього рядка. Вам не обов'язково використовувати всі чотири щоразу, але інтерфейс вимагає їх наявності.
У методі get вище decrypt_data($value) виконується після того, як Eloquent отримує рядок із бази даних, і перед тим, як значення потрапить у вашу модель. У методі set функція encrypt_data($value) виконується перед INSERT або UPDATE, гарантуючи, що база даних ніколи не побачить відкритий текст.
Щоб підключити це, посилайтеся на клас у масиві $casts вашої моделі:
protected $casts = [
'username' => CustomEncrypt::class,
'password' => CustomEncrypt::class,
];
Оскільки ви використовуєте константу класу, автозавантажувач зробить усе інше. Якщо вашій програмі згодом знадобиться змінити схему шифрування, ви відредагуєте лише один файл, і поведінка кожного відповідного поля зміниться миттєво. Така централізація є незамінною, коли ви керуєте конфіденційними даними в багатьох таблицях.
Метод 2: Використання аксесора та мутатора (Accessor and Mutator)
Іноді ви не хочете створювати новий файл для трансформації, яка важлива лише в одному місці. Клас Attribute у Laravel дозволяє визначати логіку get та set безпосередньо в моделі, використовуючи синтаксис замикань (closures) 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),
);
}
Тут назва методу має збігатися з колонкою або атрибутом, з яким ви працюєте. Коли Eloquent зчитує username з бази даних, він пропускає сире значення через замикання get. Коли ви призначаєте нове значення властивості username моделі, замикання set шифрує його перед тим, як Eloquent побудує запит.
Цей підхід чудово підходить для прототипів або застарілих (legacy) моделей, де створення цілого каталогу класів приведення типів здається надмірним. Недоліком є повторюваність. Якщо пізніше ви вирішите, що email, phone та backup_code потребують такої ж обробки, вам доведеться копіювати ці замикання в кілька методів або моделей. Цей шум накопичується. Це також заважає повторно використовувати логіку в юніт-тестах без завантаження всієї моделі.
Choosing Between the Two
Use a custom cast class when you care about reuse, testing, and keeping models slim. It signals to other developers that this transformation is a first-class concept in your application, not a one-off hack.
Use an accessor when the logic is truly localized, experimental, or unlikely to outlive the current sprint. It lets you move fast without scattering small files across the codebase. Just be ready to refactor into a cast class once the same pattern appears a second or third time.
Practical Details to Keep in Mind
Custom casts are powerful, but they introduce behavior that can surprise you if you are not looking. First, remember that get receives whatever the database returned, including null. If decrypt_data does not tolerate null input, guard against it:
public function get(Model $model, string $key, mixed $value, array $attributes): mixed
{
return is_null($value) ? null : decrypt_data($value);
}
Second, casts run during array and JSON serialization. When you return a model as an API resource or call toArray(), the cast get logic still applies. That is usually what you want, but it is worth remembering if you are exposing decrypted values and need to layer additional visibility controls on top.
Third, and most importantly, casting happens after retrieval. You cannot query against the transformed value. A query like User::where('username', 'john_doe')->first() sends the string john_doe straight to the database. It never passes through your decrypt logic. If the column is encrypted at rest, that query will find nothing unless you search against the encrypted ciphertext itself. Plan your database access patterns accordingly, because casts are not a substitute for native database functions or indexed plaintext columns.
Custom cast classes also make great homes for small bits of configuration. If you need constructor arguments—perhaps passing a cipher mode or a format string—Laravel supports them through the $casts array using an expression like 'field' => CustomEncrypt::class . ':arg', though that is a step beyond the basic setup described here.
The Real Takeaway
The restriction against dropping helper functions directly into $casts is not arbitrary bureaucracy. It nudges you toward code that is explicit, testable, and reusable. Custom cast classes turn scattered inline logic into dependable components you can share across models without duplication. Accessors keep the door open for quick, localized fixes when a separate file feels like ceremony. Master both, choose based on the scope of the problem, and your Eloquent layer will stay readable long after the application grows.
