Продвинутый model attribute casting в Laravel
По долгу службы приходится знать про каждый винтик в Laravel, но бывают моменты, когда появляется новый проект и ты в него погружаешься с головой, а через пол года — год узнаешь, что ты сильно отстал от жизни, в Laravel вышло 10 релизов и ты пропустил все самое интересное. Пришлось даже начать следить за их репо, чтобы получать уведомления о всех релизах и вдумчиво вчитываться в то, что они там прикрутили. И вот только сегодня я решил рассказать об одной из фич, которая мне очень понравилась и которую я не могу пройти стороной, а именно возможность продвинутого кастинга атрибутов модели.
Раньше мы могли кастить только в этот набор типов
$primitiveCastTypes = [
'array',
'bool',
'boolean',
'collection',
'custom_datetime',
'date',
'datetime',
'decimal',
'double',
'float',
'int',
'integer',
'json',
'object',
'real',
'string',
'timestamp',
]
а для чего-то большего приходилось использовать мутаторы, что заставляло использовать магию прям в модели и иметь методы с непонятным названием, которые мозолили глаз. Сейчас же все изменилось!
Итак, начнём
Представим, что у нас интернет магазин, а в любом интернет магазине есть товары, которые имеют стоимость, в нашем случае в различной валюте.
Создадим миграцию
Schema::create('product', function (Blueprint $table) {
...
$table->decimal('amount');
$table->string('currency', 3);
...
});
Мы имеет два поля в модели, одно хранит стоимость, второе хранит код валюты. Т.е. у нас два атрибута в модели и оперировать ими уже не удобно. Первое что мы можем сделать, это подключить библиотеку, например Brick\Money
Ну и пример создания объекта из документации
use Brick\Money\Money;
$money = Money::of(50, 'USD');
Старый способ работы с ними выглядеть как то так
Использовать можно, но модели пухнут. Теперь рассмотрим один из новых вариантов. Для начала создадим новый вспомогательный класс MoneyCast
, который должен реализовывать интерфейс Illuminate\Contracts\Database\Eloquent\CastsAttributes
И укажем что атрибут money должен обрабатываться этим классом в момент получения и присвоения значения
protected $casts = [
'money' => MoneyCast::class,
];
Этот вариант очень хорошо подходит в том, случае, когда мы хотим атрибуты делать объектами-значениями (ValueObjects).
Новая ситуация
В нашем интернет-магазине есть клиенты и у каждого клиента есть адрес
Вот для него схема
Schema::create('client', function (Blueprint $table) {
...
$table->string('city');
$table->string('street');
...
});
и ValueObject
class Address
{
... public function __construct($city, $street, ...) {
$this->city = $city;
$this->street = $street;
}
}
И, с этим ValueObject мы уже не можем использовать тот интерфейс, т.к. мы должны указать в модели, что кастинг атрибута address
должен производиться этим VO, а в нем есть конструктор, который ожидает передачу города, улицы, ….
protected $casts = [
'address' => Address::class,
];
По мне, так еще один недостаток в том, чтобы использовать CastsAttributes
в своих VO — это возможный конфликт названий методов get и set, которые нужно будет втащить вместе с интерфейсом. На этот случай разработчики предусмотрели план действий, а именно интерфейс Illuminate\Contracts\Database\Eloquent\Castable
И напоследок расскажу еще про один интерфейс Illuminate\Contracts\Database\Eloquent\CastsInboundAttributes
, который работает только в одну сторону, а именно только в момент присвоения и, соответственно содержит он только метод set
Вспомним про наш интернет магазин. У нашего товара, помимо стоимости есть также slug, который должен сгенерироваться при изменении title
protected $casts = [
'title' => SlugifyTitleCast::class,
];
А теперь немного магии. В некоторых случая мы хотим изменить разделитель —
на _
, нет проблем, мы можем это сделать просто указав через двоеточие значение, которое будет передано в виде параметра в конструктор.
protected $casts = [
'title' => SlugifyTitleCast::class.':_',
];
Ну вот собственно и все. Буду рад обратной связи и пожеланиям о том, о чем хотели бы узнать в следующий раз.
P.S. Как говорится, не забывайте подписываться, ставить лайки и оставлять комментарии под видео.