Свойство $casts в Laravel — это одна из тех незаметных полезных функций, которая определяет то, как вы работаете с данными, не привлекая к себе лишнего внимания. Добавьте встроенный тип, такой как boolean или datetime, в массив, и Eloquent автоматически преобразует сырые строки из базы данных в более удобный формат еще до того, как значение попадет в ваши контроллеры или представления. Это делает ваш код чистым. Но как только вы попытаетесь вызвать собственный хелпер прямо внутри этого массива, фреймворк даст отпор. Запись вида 'username' => 'encrypt_data()' не сработает. Laravel ожидает либо ключевое слово встроенного приведения типов, либо класс, реализующий интерфейс CastsAttributes. Это требование обусловлено тем, что слой приведения типов работает глубоко внутри цикла гидратации и сериализации Eloquent; ему нужен предсказуемый объект с четким контрактом, а не произвольная строка с функцией, которой может не оказаться в рантайме.

Почему функции-хелперы не работают в массиве $casts

Когда Eloquent загружает модель, он сканирует массив $casts, чтобы решить, как преобразовать каждую колонку. Фреймворк распознает определенные примитивы — string, integer, array, encrypted и так далее — а также полные имена классов, реализующих CastsAttributes. Он не выполняет строку как код. Поэтому 'encrypt_data()' воспринимается как неизвестный тип приведения, что вызывает ошибку. Даже если бы Laravel парсил её, не было бы надежного способа передать экземпляр модели, ключ атрибута, текущее значение и связанные атрибуты в вашу функцию в правильном порядке. Интерфейс существует именно для того, чтобы стандартизировать это взаимодействие между вашей пользовательской логикой и внутренними механизмами Eloquent.

У вас есть два надежных способа преодолеть этот разрыв. Один ориентирован на долгосрочное повторное использование. Другой — на скорость, когда вам нужно быстрое решение внутри одной модели.

Метод 1: Создание пользовательского класса приведения типов

Если одно и то же преобразование применяется к нескольким полям или используется в разных моделях, создание отдельного класса приведения типов будет более правильным решением. Он живет в отдельном файле, его можно тестировать изолированно, и это избавляет ваши модели от повторяющегося шаблонного кода.

Начните с создания файла app/Casts/CustomEncrypt.php. Пространство имен должно соответствовать вашей настройке автозагрузки, обычно это 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: Использование аксессоров и мутаторов

Иногда не хочется создавать новый файл для преобразования, которое требуется только в одном месте. Класс 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 требуют такого же подхода, вам придется копировать эти замыкания в разные методы или модели. Этот «шум» накапливается. Кроме того, это мешает повторно использовать логику в юнит-тестах без загрузки всей модели.

Выбор между двумя вариантами

Используйте пользовательский класс cast, когда вам важны повторное использование, тестирование и лаконичность моделей. Это сигнализирует другим разработчикам, что данная трансформация является полноценной концепцией вашего приложения, а не разовым костылем.

Используйте аксессор, когда логика действительно локализована, экспериментальна или вряд ли переживет текущий спринт. Это позволяет двигаться быстро, не разбрасывая мелкие файлы по всей кодовой базе. Просто будьте готовы отрефакторить её в класс cast, как только тот же паттерн появится во второй или третий раз.

Практические детали, которые стоит учитывать

Пользовательские cast-классы мощны, но они привносят поведение, которое может застать вас врасплох, если вы не будете внимательны. Во-первых, помните, что метод get получает всё, что вернула база данных, включая null. Если decrypt_data не обрабатывает null на входе, добавьте проверку:

public function get(Model $model, string $key, mixed $value, array $attributes): mixed
{
    return is_null($value) ? null : decrypt_data($value);
}

Во-вторых, приведение типов происходит во время сериализации в массив и JSON. Когда вы возвращаете модель как API-ресурс или вызываете toArray(), логика get в касте всё равно применяется. Обычно это именно то, что вам нужно, но об этом стоит помнить, если вы раскрываете расшифрованные значения и вам необходимо наложить сверху дополнительные уровни контроля видимости.

В-третьих, и это самое важное: приведение типов происходит после получения данных. Вы не можете выполнять запросы по преобразованному значению. Запрос вроде User::where('username', 'john_doe')->first() отправляет строку john_doe напрямую в базу данных. Она никогда не проходит через вашу логику расшифровки. Если столбец зашифрован в хранилище (at rest), такой запрос ничего не найдет, если только вы не будете искать по самому зашифрованному тексту (ciphertext). Планируйте паттерны доступа к базе данных соответствующим образом, так как касты не являются заменой нативным функциям базы данных или индексированным столбцам с открытым текстом.

Пользовательские классы cast также являются отличным местом для небольших фрагментов конфигурации. Если вам нужны аргументы конструктора — например, передача режима шифрования или строки формата — Laravel поддерживает их через массив $casts, используя выражение вида 'field' => CustomEncrypt::class . ':arg', хотя это уже выходит за рамки базовой настройки, описанной здесь.

Главный вывод

Запрет на использование вспомогательных функций напрямую в $casts — это не произвольная бюрократия. Он подталкивает вас к написанию явного, тестируемого и пригодного для повторного использования кода. Пользовательские классы cast превращают разрозненную инлайновую логику в надежные компоненты, которыми можно делиться между моделями без дублирования. Аксессоры оставляют возможность для быстрых локальных исправлений, когда создание отдельного файла кажется избыточным. Освойте оба подхода, выбирайте их в зависимости от масштаба задачи, и ваш слой Eloquent будет оставаться читаемым даже после значительного роста приложения.