Мутаторы и касты в Eloquent: чек-лист перед продакшеном
Касты и аксессоры в Eloquent удобны, пока не всплывают лишние запросы, сломанная сериализация и данные, которые попадают в базу не в том виде. Разбираем, что проверить до релиза.

Касты и мутаторы в Eloquent выглядят безобидно: одна строка в модели превращает JSON-колонку в массив, timestamp — в Carbon, а сырую строку — в объект. Проблемы начинаются на бою. Аксессор дёргает связь на каждой итерации в цикле — и вместо одного запроса в БД летят сотни. Кастомный каст молча превращает null в пустой объект и ломает валидацию. Поле с секретом попадает в лог, потому что о нём забыли в $hidden. Это чек-лист того, что стоит проверить в моделях перед тем, как выкатывать релиз. Ориентируюсь на Laravel 10.x и 11.x — синтаксис новых аксессоров (метод, возвращающий Attribute) стабилен с версии 9.
Два поколения синтаксиса в одном проекте
С Laravel 9 старые методы getXxxAttribute() и setXxxAttribute() считаются устаревшим стилем, но продолжают работать. Новый способ — один метод, возвращающий объект Illuminate\Database\Eloquent\Casts\Attribute с геттером и сеттером. В реальном проекте обычно живут оба, и первое, что стоит проверить перед продом, — что вы не смешали их для одного и того же поля.
use Illuminate\Database\Eloquent\Casts\Attribute;
class User extends Model
{
// Новый синтаксис (Laravel 9+): один метод на аксессор и мутатор
protected function fullName(): Attribute
{
return Attribute::make(
get: fn (mixed $value, array $attributes) =>
trim($attributes['first_name'] . ' ' . $attributes['last_name']),
);
}
}
Если для одного поля объявлены и старый getFullNameAttribute(), и новый fullName(), поведение зависит от того, как Eloquent разрешает аксессор внутри — не полагайтесь на это. Оставьте один вариант.
Каст против аксессора: что где применять
Оба механизма преобразуют значение при чтении и записи, но у них разные зоны ответственности. Касты ($casts) — про тип хранимого поля: колонка в базе есть, надо привести её к int, bool, array, datetime или к своему классу. Аксессоры — про вычисляемые значения, которых в таблице нет, или про поля, требующие нетривиальной логики на чтении.
| Признак | Каст ($casts) | Аксессор/мутатор |
|---|---|---|
| Есть колонка в таблице | Обязательно | Не обязательно |
| Меняет тип, а не логику | Да | Скорее нет |
| Вычисляет из нескольких полей | Нет | Да |
| Переиспользуется в разных моделях | Через custom cast | Только копипастой |
| Попадает в toArray()/toJson() | Автоматически | Только через $appends |
Практическое правило: если поле физически хранится и вам нужен другой PHP-тип — это каст. Если значение производное (полное имя, отформатированная цена, флаг «активен ли по датам») — это аксессор. Кастомный каст оправдан, когда одинаковое преобразование нужно нескольким моделям: логика живёт в одном классе, а не размазана по аксессорам.
Где это ломается на бою
Аксессор с запросом внутри цикла
Самая дорогая ошибка. Аксессор обращается к связи, а модель перебирается в цикле — получаем классический N+1, только спрятанный за красивым свойством.
// Опасно: каждое обращение к $order->manager_name дёргает связь
protected function managerName(): Attribute
{
return Attribute::make(
get: fn () => $this->manager->name, // ленивая загрузка на каждой строке
);
}
В шаблоне @foreach ($orders as $order) {{ $order->manager_name }} @endforeach на 300 заказов это 300 отдельных запросов. Решение — грузить связь заранее через with('manager') и держать в голове, что аксессор не бесплатен. Перед продом прогоните страницу через Laravel Debugbar или Telescope и посмотрите число запросов на список.
Кеширование аксессоров и объекты
С Laravel 9 аксессоры, возвращающие объекты, кешируются в рамках жизни модели. Если ваш аксессор возвращает объект-значение (например, свой Money или Address), Eloquent запомнит его и вернёт тот же инстанс при повторном чтении. Для скалярных значений кеширование по умолчанию отключено — включается вызовом shouldCache() на объекте Attribute. Проблема возникает, когда аксессор возвращает объект, а вы меняете его наружу, ожидая, что при следующем чтении он «пересоберётся». Он не пересоберётся.
Аксессор, возвращающий объект, — это не функция, которую зовут каждый раз. Это значение, которое Eloquent посчитает один раз и запомнит. Если логика зависит от изменяемого состояния, кеш вас укусит.
Каст в массив и молчаливая перезапись
Каст array или AsArrayObject удобен для JSON-колонок, но у него есть ловушка: чтобы Eloquent зафиксировал изменение вложенного элемента, присваивать нужно всё поле целиком, а не мутировать вложенный ключ. Со старым кастом 'settings' => 'array' это частый источник «почему не сохранилось».
// НЕ сработает с обычным 'array'-кастом: изменение не отследится
$user->settings['theme'] = 'dark';
$user->save(); // theme останется старым
// Сработает: присваиваем поле целиком
$settings = $user->settings;
$settings['theme'] = 'dark';
$user->settings = $settings;
$user->save();
Каст AsArrayObject (доступен с Laravel 8.28) отслеживает мутации вложенных ключей — если такое поведение нужно, используйте его вместо array. Точную доступность класта под вашу версию сверьте в разделе документации Eloquent про мутаторы и касты.
Сериализация: что уходит в API
Касты автоматически попадают в toArray() и toJson(), аксессоры — нет, пока вы не добавите их в $appends. Обратная сторона: datetime-касты сериализуются в ISO 8601 через serializeDate(), и если фронт ждёт другой формат, вы получите рассинхрон без единой ошибки. Проверьте, что уходит в JSON, на реальном ответе, а не в предположениях.
Чек-лист перед продакшеном
- Один синтаксис на поле. Убедитесь, что для одного атрибута не объявлены одновременно старый
getXxxAttribute()и новый метод-Attribute. - Нет запросов в аксессорах на списках. Прогоните страницы со списками через Debugbar/Telescope. Аксессор, дёргающий связь, — грузите связь через
with(). - Секреты в $hidden. Пароли, токены, ключи — в
$hidden, чтобы они не утекли вtoArray(), логи и ответы API. - $appends осознанно. Каждый аксессор в
$appendsсчитается при каждой сериализации. Тяжёлый аксессор в$appendsзамедляет любой JSON-ответ со списком. - null-безопасность кастов. Проверьте, что кастомные касты и аксессоры не падают и не подставляют мусор на
null. Отдельно — новые записи, где поле ещё не заполнено. - Мутации массивов. Для JSON-колонок либо присваивайте поле целиком, либо используйте
AsArrayObject. Не рассчитывайте на отслеживание вложенных изменений в обычномarray. - Формат дат в API. Сверьте формат сериализации
datetime-полей с тем, что ждёт клиент. - Массовое присвоение. Мутатор не отменяет
$fillable/$guarded. Поле, приходящее из формы, должно быть в$fillable.
Кастомный каст: минимальный безопасный шаблон
Если преобразование повторяется в нескольких моделях, выносите его в класс, реализующий CastsAttributes. Важно обработать null в обе стороны — это самое частое место, где касты падают в проде.
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Database\Eloquent\Model;
class MoneyCast implements CastsAttributes
{
// Из БД (копейки) в объект приложения
public function get(Model $model, string $key, mixed $value, array $attributes): ?int
{
return $value === null ? null : (int) $value;
}
// Из приложения обратно в БД
public function set(Model $model, string $key, mixed $value, array $attributes): ?int
{
if ($value === null) {
return null;
}
return (int) round($value * 100); // рубли в копейки
}
}
Подключение — protected $casts = ['price' => MoneyCast::class];. В Laravel 11 $casts можно объявлять и методом casts(), возвращающим массив, — оба варианта рабочие, метод удобнее, когда касту нужны параметры.
Что делать дальше
После правок пройдитесь по трём вещам в бою. Первое — число запросов на страницах-списках: любой аксессор со связью виден в Telescope сразу. Второе — размер JSON-ответов: если в $appends заехал тяжёлый аксессор, время ответа на список вырастет линейно от числа записей. Третье — что реально хранится в JSON-колонках после сохранения: сравните значение до и после save() на тестовой записи, чтобы поймать молчаливую потерю изменений. Дальше стоит почитать раздел документации Eloquent про мутаторы, касты и сериализацию — там же описаны встроенные касты вроде encrypted и AsCollection, которые часто закрывают задачу без своего класса.
Тендерный специалист 44-ФЗ / 223-ФЗ: Разбор PDF-документации закупки, поиск «заточек» под поставщика, чек-лист заявки.
Спросить за 20 ₽Источники: Laravel — Eloquent: Mutators & Casting, Laravel — Eloquent: Serialization
Частые вопросы
У меня Laravel 8, а в статье синтаксис с методом Attribute. Это сработает?
Почему изменение вложенного ключа в JSON-колонке не сохраняется?
Аксессор вызывается каждый раз при чтении свойства?
Мутатор защищает поле от массового присвоения?
Как поймать N+1 из-за аксессора перед релизом?
Каст encrypted безопасно использовать для секретов?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.