Мутаторы и касты в Eloquent: как сделать правильно
Разбираем новый синтаксис аксессоров через Attribute, кастомные касты и типовые ошибки: почему JSON превращается в строку, а деньги теряют копейки.

Вы храните цену в копейках, а в шаблоне выводите рубли. Или держите настройки пользователя в JSON-колонке и хотите работать с ними как с массивом. Раньше для этого писали пару методов getPriceAttribute и setPriceAttribute, и половина команды путалась, где мутатор, а где аксессор. В Laravel 9 синтаксис поменялся: теперь один метод возвращает объект Attribute, где чтение и запись лежат рядом. Разберём, когда брать аксессор, когда каст, а когда — кастомный класс каста, и на чём чаще всего спотыкаются.
Материал написан для Laravel 10.x и 11.x (актуальный синтаксис Attribute появился в 9.x). Старый синтаксис getXxxAttribute всё ещё работает, но новые модели лучше писать на новом.
Три инструмента, которые часто путают
Eloquent даёт три способа преобразовать значение поля между базой и PHP. Их постоянно смешивают, хотя задачи у них разные:
- Аксессор/мутатор — трансформация одного атрибута при чтении и записи. Логика живёт в модели. Подходит для форматирования и производных значений.
- Каст (cast) — объявление типа поля в массиве
$casts. Встроенные касты (integer,boolean,array,datetime,encrypted) применяются автоматически ко всем экземплярам. - Кастомный класс каста — отдельный класс, реализующий
CastsAttributes. Нужен, когда логику преобразования хочется переиспользовать в нескольких моделях.
Правило, которое экономит часы споров на ревью: если преобразование нужно только этой модели — аксессор; если типизация универсальна и повторяется — каст.
Новый синтаксис аксессора через Attribute
Метод называется по имени атрибута в camelCase, возвращает объект Illuminate\Database\Eloquent\Casts\Attribute. Чтение и запись задаются замыканиями get и set.
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
class Order extends Model
{
// В базе price_cents хранится в копейках (integer)
protected function price(): Attribute
{
return Attribute::make(
// При чтении делим на 100 и отдаём рубли
get: fn ($value, array $attributes) => $attributes['price_cents'] / 100,
// При записи умножаем на 100 и кладём обратно в копейки
set: fn ($value) => ['price_cents' => (int) round($value * 100)],
);
}
}
Обратите внимание: set возвращает массив с реальным именем колонки. Это позволяет виртуальному атрибуту price писать в физическую колонку price_cents. Второй аргумент $attributes в get — это сырые значения строки из базы, без применённых кастов.
Форматирование против производных значений
Аксессор без set — это вычисляемое свойство только для чтения. Классический пример — полное имя:
protected function fullName(): Attribute
{
return Attribute::make(
get: fn ($value, array $attributes) =>
trim($attributes['first_name'] . ' ' . $attributes['last_name']),
);
}
Теперь $user->full_name работает, хотя такой колонки в базе нет. Обращение идёт по snake_case — full_name, а метод называется fullName.
Встроенные касты: где теряются данные
Массив $casts закрывает большинство рутинных задач без единой строки логики:
protected $casts = [
'is_active' => 'boolean',
'options' => 'array',
'published_at' => 'datetime',
'meta' => 'encrypted:array',
];
Тонкость с array: Eloquent считает объект «грязным» только если вы переприсвоили весь атрибут. Изменение элемента по ссылке не отслеживается.
// Так изменение НЕ сохранится:
$order->options['color'] = 'red';
$order->save();
// Так сохранится — переприсваиваем весь массив:
$options = $order->options;
$options['color'] = 'red';
$order->options = $options;
$order->save();
Начиная с Laravel 8.28 для этого есть отдельный метод — каст AsArrayObject, который позволяет менять вложенные ключи и корректно фиксирует изменения. Проверьте по документации вашей версии, прежде чем закладываться на это поведение.
Кастомный класс каста
Когда одно и то же преобразование нужно в трёх моделях, копировать аксессор — плохая идея. Класс каста реализует два метода: get (из базы в PHP) и set (из PHP в базу).
<?php
namespace App\Casts;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Database\Eloquent\Model;
class Money implements CastsAttributes
{
// Из копеек в объект-значение
public function get(Model $model, string $key, mixed $value, array $attributes): mixed
{
return $value === null ? null : $value / 100;
}
// Из рублей обратно в копейки
public function set(Model $model, string $key, mixed $value, array $attributes): mixed
{
return $value === null ? null : (int) round($value * 100);
}
}
Подключение в модели — через имя класса в $casts:
protected $casts = [
'price_cents' => \App\Casts\Money::class,
];
Сигнатуры методов get и set уточняйте в документации своей версии: список аргументов уточнялся между релизами. Если сомневаетесь — сгенерируйте заготовку командой php artisan make:cast Money, она создаст класс с правильными сигнатурами под установленную версию.
Что выбрать под конкретную задачу
| Задача | Инструмент | Почему |
|---|---|---|
| Привести поле к типу (bool, int, дата) | Встроенный каст в $casts | Одна строка, работает везде |
| Форматирование одного поля в одной модели | Аксессор через Attribute | Логика рядом с моделью, не размазана |
| Вычисляемое свойство без колонки | Аксессор только с get | Не пишет в базу |
| Одна логика преобразования в нескольких моделях | Кастомный класс каста | Переиспользуется, тестируется отдельно |
| Value Object (деньги, координаты) | Каст с CastsAttributes | Инкапсулирует несколько колонок |
Где ломается
Пять ошибок, которые встречаются в код-ревью чаще всего.
- Каст и аксессор на одном поле одновременно. Если поле объявлено в
$castsи для него есть метод-аксессор, порядок применения неочевиден и зависит от версии. Оставьте что-то одно. - Изменение вложенного ключа
array-каста. Разобрано выше: без переприсваивания или безAsArrayObjectизменение молча теряется наsave(). - Тяжёлая логика в
get. Аксессор вызывается при каждом обращении к свойству. Запрос к базе или HTTP-вызов внутриgetв цикле по коллекции превращается в проблему N+1, которую сложно заметить. - Кеширование объектов в аксессоре. По умолчанию значение аксессора не кешируется. Если
getсобирает Value Object при каждом обращении, включите->shouldCache()на объектеAttribute, иначе$order->address === $order->addressвернёт false. - Забытый
protectedвместоpublic. Методы-аксессоры в новом синтаксисе должны бытьprotected. Публичный метод Eloquent как аксессор не подхватит.
Что делать дальше
Начните с ревизии текущих моделей: везде, где значение приводится к типу вручную в get, замените на встроенный каст. Value Object вроде денег или диапазона дат вынесите в кастомный класс каста и покройте его юнит-тестом отдельно от модели — тестировать чистый класс проще, чем весь Eloquent. Если модель обращается к аксессору тяжело и часто, включите shouldCache() и замерьте количество запросов через Laravel Debugbar или DB::enableQueryLog() до и после. Полиморфные касты (Castable-интерфейс) — отдельная тема, здесь мы её не разворачивали.
Субтитры и таймкоды: Расшифровка речи и субтитры по правилам читаемости: 42 знака в строке, две строки, минимум секунда на реплику.
Спросить за 40 ₽Источники: Laravel — Eloquent: Mutators & Casting
Частые вопросы
У меня Laravel 8, метод через Attribute не работает. Что делать?
Аксессор не срабатывает, свойство возвращает сырое значение из базы. Почему?
Чем каст отличается от аксессора, если оба преобразуют значение?
Изменил элемент JSON-поля через $model->options['x'] = 1, но save() ничего не сохранил.
Value Object в аксессоре создаётся заново при каждом обращении — это дорого?
Можно ли использовать каст и аксессор на одном поле вместе?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.