#Введение
Аксессоры, мутаторы и кастинг атрибутов позволяют преобразовывать значения атрибутов Eloquent при их получении или установке в экземплярах моделей. Например, вы можете использовать Laravel encrypter для шифрования значения при сохранении в базе данных, а затем автоматически расшифровывать атрибут при доступе к нему через модель Eloquent. Или вы можете преобразовать JSON-строку, хранящуюся в базе данных, в массив при доступе к ней через модель Eloquent.
#Аксессоры и мутаторы
#Определение аксессора
Аксессор преобразует значение атрибута Eloquent при его получении. Чтобы определить аксессор, создайте защищённый метод в вашей модели, который будет представлять доступный атрибут. Имя метода должно соответствовать "camel case" представлению реального атрибута модели / столбца базы данных, если это применимо.
В этом примере мы определим аксессор для атрибута first_name. Аксессор будет автоматически вызван Eloquent при попытке получить значение атрибута first_name. Все методы аксессоров и мутаторов должны объявлять возвращаемый тип Illuminate\Database\Eloquent\Casts\Attribute:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
/**
* Получить имя пользователя.
*/
protected function firstName(): Attribute
{
return Attribute::make(
get: fn (string $value) => ucfirst($value),
);
}
}
Все методы аксессоров возвращают экземпляр Attribute, который определяет, как атрибут будет получен и, опционально, изменён. В этом примере мы определяем только способ получения атрибута. Для этого мы передаём аргумент get в конструктор класса Attribute.
Как видите, исходное значение столбца передаётся в аксессор, что позволяет вам обработать и вернуть значение. Чтобы получить значение аксессора, достаточно обратиться к атрибуту first_name у экземпляра модели:
use App\Models\User;
$user = User::find(1);
$firstName = $user->first_name;
Если вы хотите, чтобы эти вычисляемые значения добавлялись в массивное или JSON-представление вашей модели, вам нужно будет добавить их в список добавляемых атрибутов.
#Создание объектов-значений из нескольких атрибутов
Иногда аксессор должен преобразовывать несколько атрибутов модели в один объект-значение. Для этого ваш замыкание get может принимать второй аргумент $attributes, который автоматически передаётся и содержит массив всех текущих атрибутов модели:
use App\Support\Address;
use Illuminate\Database\Eloquent\Casts\Attribute;
/**
* Взаимодействие с адресом пользователя.
*/
protected function address(): Attribute
{
return Attribute::make(
get: fn (mixed $value, array $attributes) => new Address(
$attributes['address_line_one'],
$attributes['address_line_two'],
),
);
}
#Кэширование аксессоров
При возврате объектов-значений из аксессоров любые изменения, внесённые в объект-значение, автоматически синхронизируются с моделью перед её сохранением. Это возможно, потому что Eloquent сохраняет экземпляры, возвращаемые аксессорами, чтобы возвращать один и тот же экземпляр при каждом вызове аксессора:
use App\Models\User;
$user = User::find(1);
$user->address->lineOne = 'Обновлённое значение адреса, строка 1';
$user->address->lineTwo = 'Обновлённое значение адреса, строка 2';
$user->save();
Однако иногда вы можете захотеть включить кэширование для примитивных значений, таких как строки и булевы значения, особенно если их вычисление ресурсоёмко. Для этого можно вызвать метод shouldCache при определении аксессора:
protected function hash(): Attribute
{
return Attribute::make(
get: fn (string $value) => bcrypt(gzuncompress($value)),
)->shouldCache();
}
Если вы хотите отключить поведение кэширования объектов для атрибутов, можно вызвать метод withoutObjectCaching при определении атрибута:
/**
* Взаимодействие с адресом пользователя.
*/
protected function address(): Attribute
{
return Attribute::make(
get: fn (mixed $value, array $attributes) => new Address(
$attributes['address_line_one'],
$attributes['address_line_two'],
),
)->withoutObjectCaching();
}
#Определение мутатора
Мутатор преобразует значение атрибута Eloquent при его установке. Чтобы определить мутатор, можно передать аргумент set при определении атрибута. Давайте определим мутатор для атрибута first_name. Этот мутатор будет автоматически вызван при попытке установить значение атрибута first_name в модели:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
/**
* Взаимодействие с именем пользователя.
*/
protected function firstName(): Attribute
{
return Attribute::make(
get: fn (string $value) => ucfirst($value),
set: fn (string $value) => strtolower($value),
);
}
}
Замыкание мутатора получит значение, которое устанавливается в атрибут, что позволяет вам обработать значение и вернуть изменённое значение. Чтобы использовать мутатор, достаточно установить атрибут first_name у модели Eloquent:
use App\Models\User;
$user = User::find(1);
$user->first_name = 'Sally';
В этом примере обратный вызов set будет вызван со значением Sally. Мутатор применит функцию strtolower к имени и установит полученное значение во внутренний массив $attributes модели.
#Изменение нескольких атрибутов
Иногда мутатор должен установить несколько атрибутов в базовой модели. Для этого можно вернуть массив из замыкания set. Каждый ключ в массиве должен соответствовать атрибуту / столбцу базы данных, связанному с моделью:
use App\Support\Address;
use Illuminate\Database\Eloquent\Casts\Attribute;
/**
* Взаимодействие с адресом пользователя.
*/
protected function address(): Attribute
{
return Attribute::make(
get: fn (mixed $value, array $attributes) => new Address(
$attributes['address_line_one'],
$attributes['address_line_two'],
),
set: fn (Address $value) => [
'address_line_one' => $value->lineOne,
'address_line_two' => $value->lineTwo,
],
);
}
#Кастинг атрибутов
Кастинг атрибутов предоставляет функциональность, похожую на аксессоры и мутаторы, без необходимости определять дополнительные методы в вашей модели. Вместо этого свойство $casts модели предоставляет удобный способ преобразования атрибутов в распространённые типы данных.
Свойство $casts должно быть массивом, где ключ — имя атрибута, который нужно кастить, а значение — тип, в который нужно преобразовать столбец. Поддерживаемые типы кастинга:
arrayAsStringable::classbooleancollectiondatedatetimeimmutable_dateimmutable_datetimedecimal:<precision>doubleencryptedencrypted:arrayencrypted:collectionencrypted:objectfloathashedintegerobjectrealstringtimestamp
Для демонстрации кастинга атрибутов давайте приведём атрибут is_admin, который хранится в базе как целое число (0 или 1), к булевому значению:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
/**
* Атрибуты, которые должны быть приведены к типам.
*
* @var array
*/
protected $casts = [
'is_admin' => 'boolean',
];
}
После определения кастинга атрибут is_admin всегда будет приводиться к булевому типу при доступе, даже если исходное значение в базе хранится как целое число:
$user = App\Models\User::find(1);
if ($user->is_admin) {
// ...
}
Если нужно добавить новый временный кастинг во время выполнения, можно использовать метод mergeCasts. Эти определения кастов будут добавлены к уже существующим в модели:
$user->mergeCasts([
'is_admin' => 'integer',
'options' => 'object',
]);
Атрибуты со значением null не будут приведены к типу. Кроме того, никогда не следует определять кастинг (или атрибут) с именем, совпадающим с названием отношения, или назначать кастинг для первичного ключа модели.
#Кастинг в Stringable
Вы можете использовать класс кастинга Illuminate\Database\Eloquent\Casts\AsStringable для приведения атрибута модели к fluent объекту Illuminate\Support\Stringable:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Casts\AsStringable;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
/**
* Атрибуты, которые должны быть приведены к типам.
*
* @var array
*/
protected $casts = [
'directory' => AsStringable::class,
];
}
#Кастинг в массив и JSON
Кастинг array особенно полезен при работе со столбцами, которые хранятся в виде сериализованного JSON. Например, если в базе данных есть поле типа JSON или TEXT, содержащее сериализованный JSON, добавление кастинга array к этому атрибуту автоматически десериализует его в PHP-массив при доступе через модель Eloquent:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
/**
* Атрибуты, которые должны быть приведены к типам.
*
* @var array
*/
protected $casts = [
'options' => 'array',
];
}
После определения кастинга вы можете получить доступ к атрибуту options, и он автоматически будет десериализован из JSON в PHP-массив. При установке значения атрибута options данный массив автоматически сериализуется обратно в JSON для хранения:
use App\Models\User;
$user = User::find(1);
$options = $user->options;
$options['key'] = 'value';
$user->options = $options;
$user->save();
Чтобы обновить одно поле JSON-атрибута более кратким синтаксисом, вы можете сделать атрибут массово назначаемым и использовать оператор -> при вызове метода update:
$user = User::find(1);
$user->update(['options->key' => 'value']);
#Кастинг в ArrayObject и Collection
Хотя стандартный кастинг array подходит для многих случаев, у него есть недостатки. Поскольку кастинг array возвращает примитивный тип, нельзя напрямую изменять отдельные элементы массива. Например, следующий код вызовет ошибку PHP:
$user = User::find(1);
$user->options['key'] = $value;
Чтобы решить эту проблему, Laravel предлагает кастинг AsArrayObject, который приводит JSON-атрибут к классу ArrayObject. Эта функция реализована с помощью пользовательского кастинга, который позволяет Laravel интеллектуально кэшировать и преобразовывать изменённый объект так, чтобы можно было изменять отдельные элементы без ошибок PHP. Чтобы использовать кастинг AsArrayObject, просто назначьте его атрибуту:
use Illuminate\Database\Eloquent\Casts\AsArrayObject;
/**
* Атрибуты, которые должны быть приведены к типам.
*
* @var array
*/
protected $casts = [
'options' => AsArrayObject::class,
];
Аналогично, Laravel предлагает кастинг AsCollection, который приводит JSON-атрибут к экземпляру Laravel Collection:
use Illuminate\Database\Eloquent\Casts\AsCollection;
/**
* Атрибуты, которые должны быть приведены к типам.
*
* @var array
*/
protected $casts = [
'options' => AsCollection::class,
];
Если вы хотите, чтобы кастинг AsCollection создавал экземпляр пользовательского класса коллекции вместо базового класса Laravel, вы можете указать имя класса коллекции в качестве аргумента кастинга:
use App\Collections\OptionCollection;
use Illuminate\Database\Eloquent\Casts\AsCollection;
/**
* Атрибуты, которые должны быть приведены к типам.
*
* @var array
*/
protected $casts = [
'options' => AsCollection::class.':'.OptionCollection::class,
];
#Кастинг дат
По умолчанию Eloquent приводит столбцы created_at и updated_at к экземплярам Carbon, который расширяет класс PHP DateTime и предоставляет множество полезных методов. Вы можете кастить дополнительные атрибуты дат, определяя дополнительные касты дат в массиве $casts вашей модели. Обычно даты следует кастить с помощью типов datetime или immutable_datetime.
При определении кастинга date или datetime вы можете также указать формат даты. Этот формат будет использоваться при сериализации модели в массив или JSON:
/**
* Атрибуты, которые должны быть приведены к типам.
*
* @var array
*/
protected $casts = [
'created_at' => 'datetime:Y-m-d',
];
Когда столбец кастится как дата, вы можете установить соответствующий атрибут модели в UNIX timestamp, строку даты (Y-m-d), строку даты и времени или экземпляр DateTime / Carbon. Значение даты будет корректно преобразовано и сохранено в базе данных.
Вы можете настроить формат сериализации по умолчанию для всех дат модели, определив метод serializeDate в вашей модели. Этот метод не влияет на формат даты при сохранении в базе данных:
/**
* Подготовить дату для сериализации в массив / JSON.
*/
protected function serializeDate(DateTimeInterface $date): string
{
return $date->format('Y-m-d');
}
Чтобы указать формат, который должен использоваться при сохранении дат модели в базе данных, определите свойство $dateFormat в вашей модели:
/**
* Формат хранения столбцов даты модели.
*
* @var string
*/
protected $dateFormat = 'U';
#Кастинг дат, сериализация и часовые пояса
По умолчанию касты date и datetime сериализуют даты в строку UTC ISO-8601 (YYYY-MM-DDTHH:MM:SS.uuuuuuZ), независимо от часового пояса, указанного в конфигурации timezone вашего приложения. Настоятельно рекомендуется всегда использовать этот формат сериализации и хранить даты приложения в часовом поясе UTC, не изменяя значение timezone в конфигурации приложения с его значения по умолчанию UTC. Последовательное использование часового пояса UTC во всём приложении обеспечит максимальную совместимость с другими библиотеками для работы с датами на PHP и JavaScript.
Если к касту date или datetime применён пользовательский формат, например datetime:Y-m-d H:i:s, то при сериализации даты будет использоваться внутренний часовой пояс экземпляра Carbon. Обычно это часовой пояс, указанный в конфигурации timezone вашего приложения.
#Кастинг Enum
Eloquent также позволяет кастить значения атрибутов к PHP Enum. Для этого укажите атрибут и Enum, к которому нужно кастить, в массиве $casts вашей модели:
use App\Enums\ServerStatus;
/**
* Атрибуты, которые должны быть приведены к типам.
*
* @var array
*/
protected $casts = [
'status' => ServerStatus::class,
];
После определения кастинга атрибут будет автоматически приводиться к Enum и обратно при взаимодействии с ним:
if ($server->status == ServerStatus::Provisioned) {
$server->status = ServerStatus::Ready;
$server->save();
}
#Кастинг массивов Enum
Иногда нужно хранить массив значений Enum в одном столбце модели. Для этого можно использовать касты AsEnumArrayObject или AsEnumCollection, предоставляемые Laravel:
use App\Enums\ServerStatus;
use Illuminate\Database\Eloquent\Casts\AsEnumCollection;
/**
* Атрибуты, которые должны быть приведены к типам.
*
* @var array
*/
protected $casts = [
'statuses' => AsEnumCollection::class.':'.ServerStatus::class,
];
#Зашифрованный кастинг
Кастинг encrypted шифрует значение атрибута модели с помощью встроенных возможностей шифрования Laravel. Кроме того, касты encrypted:array, encrypted:collection, encrypted:object, AsEncryptedArrayObject и AsEncryptedCollection работают аналогично своим незашифрованным аналогам, но, как и ожидалось, значение при хранении в базе данных шифруется.
Поскольку итоговая длина зашифрованного текста непредсказуема и больше, чем у исходного текста, убедитесь, что соответствующий столбец базы данных имеет тип TEXT или больше. Кроме того, так как значения шифруются в базе, вы не сможете выполнять запросы или поиск по зашифрованным атрибутам.
#Ротация ключей
Как известно, Laravel шифрует строки с использованием значения key из конфигурации app вашего приложения. Обычно это значение соответствует переменной окружения APP_KEY. Если вам нужно сменить ключ шифрования приложения, необходимо вручную повторно зашифровать ваши зашифрованные атрибуты с новым ключом.
#Кастинг во время запроса
Иногда нужно применять касты во время выполнения запроса, например, при выборе сырых значений из таблицы. Рассмотрим следующий запрос:
use App\Models\Post;
use App\Models\User;
$users = User::select([
'users.*',
'last_posted_at' => Post::selectRaw('MAX(created_at)')
->whereColumn('user_id', 'users.id')
])->get();
Атрибут last_posted_at в результатах этого запроса будет простой строкой. Было бы удобно применить к нему кастинг datetime во время выполнения запроса. К счастью, это можно сделать с помощью метода withCasts:
$users = User::select([
'users.*',
'last_posted_at' => Post::selectRaw('MAX(created_at)')
->whereColumn('user_id', 'users.id')
])->withCasts([
'last_posted_at' => 'datetime'
])->get();
#Пользовательские касты
Laravel предоставляет множество встроенных полезных типов кастов, однако иногда может потребоваться определить собственные типы кастов. Чтобы создать каст, выполните Artisan-команду make:cast. Новый класс кастинга будет помещён в директорию app/Casts:
php artisan make:cast Json
Все пользовательские классы кастов реализуют интерфейс CastsAttributes. Классы, реализующие этот интерфейс, должны определить методы get и set. Метод get отвечает за преобразование сырого значения из базы в кастовое значение, а метод set должен преобразовывать кастовое значение в сырое, пригодное для хранения в базе. В качестве примера мы переопределим встроенный каст json как пользовательский:
<?php
namespace App\Casts;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Database\Eloquent\Model;
class Json implements CastsAttributes
{
/**
* Привести заданное значение.
*
* @param array<string, mixed> $attributes
* @return array<string, mixed>
*/
public function get(Model $model, string $key, mixed $value, array $attributes): array
{
return json_decode($value, true);
}
/**
* Подготовить заданное значение для хранения.
*
* @param array<string, mixed> $attributes
*/
public function set(Model $model, string $key, mixed $value, array $attributes): string
{
return json_encode($value);
}
}
После определения пользовательского кастинга вы можете прикрепить его к атрибуту модели, используя имя класса:
<?php
namespace App\Models;
use App\Casts\Json;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
/**
* Атрибуты, которые должны быть приведены к типам.
*
* @var array
*/
protected $casts = [
'options' => Json::class,
];
}
#Кастинг в объекты-значения
Вы не ограничены кастингом в примитивные типы. Вы также можете кастить значения в объекты. Определение пользовательских кастов, которые кастят значения в объекты, очень похоже на кастинг в примитивы, но метод set должен возвращать массив пар ключ / значение, которые будут использоваться для установки сырых значений в модель.
В качестве примера мы определим пользовательский класс кастинга, который объединяет несколько значений модели в один объект-значение Address. Предположим, что объект Address имеет два публичных свойства: lineOne и lineTwo:
<?php
namespace App\Casts;
use App\ValueObjects\Address as AddressValueObject;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Database\Eloquent\Model;
use InvalidArgumentException;
class Address implements CastsAttributes
{
/**
* Привести заданное значение.
*
* @param array<string, mixed> $attributes
*/
public function get(Model $model, string $key, mixed $value, array $attributes): AddressValueObject
{
return new AddressValueObject(
$attributes['address_line_one'],
$attributes['address_line_two']
);
}
/**
* Подготовить заданное значение для хранения.
*
* @param array<string, mixed> $attributes
* @return array<string, string>
*/
public function set(Model $model, string $key, mixed $value, array $attributes): array
{
if (! $value instanceof AddressValueObject) {
throw new InvalidArgumentException('Переданное значение не является экземпляром Address.');
}
return [
'address_line_one' => $value->lineOne,
'address_line_two' => $value->lineTwo,
];
}
}
При кастинге в объекты-значения любые изменения, внесённые в объект-значение, автоматически синхронизируются с моделью перед её сохранением:
use App\Models\User;
$user = User::find(1);
$user->address->lineOne = 'Обновлённое значение адреса';
$user->save();
Если вы планируете сериализовать ваши модели Eloquent, содержащие объекты-значения, в JSON или массивы, следует реализовать интерфейсы Illuminate\Contracts\Support\Arrayable и JsonSerializable в объекте-значении.
#Кэширование объектов-значений
При разрешении атрибутов, кастящихся в объекты-значения, они кэшируются Eloquent. Поэтому при повторном доступе к атрибуту возвращается тот же экземпляр объекта.
Если вы хотите отключить поведение кэширования объектов в пользовательских классах кастов, можете объявить публичное свойство withoutObjectCaching в вашем классе кастинга:
class Address implements CastsAttributes
{
public bool $withoutObjectCaching = true;
// ...
}
#Сериализация массива / JSON
Когда модель Eloquent преобразуется в массив или JSON с помощью методов toArray и toJson, ваши объекты-значения из пользовательских кастов обычно сериализуются, если они реализуют интерфейсы Illuminate\Contracts\Support\Arrayable и JsonSerializable. Однако при использовании объектов-значений из сторонних библиотек вы можете не иметь возможности добавить эти интерфейсы в объект.
Поэтому вы можете указать, что ваш пользовательский класс кастинга будет отвечать за сериализацию объекта-значения. Для этого ваш класс кастинга должен реализовывать интерфейс Illuminate\Contracts\Database\Eloquent\SerializesCastableAttributes. Этот интерфейс требует наличия метода serialize, который должен возвращать сериализованное представление объекта-значения:
/**
* Получить сериализованное представление значения.
*
* @param array<string, mixed> $attributes
*/
public function serialize(Model $model, string $key, mixed $value, array $attributes): string
{
return (string) $value;
}
#Входящий кастинг
Иногда нужно написать пользовательский класс кастинга, который преобразует только значения, устанавливаемые в модель, и не выполняет никаких операций при получении атрибутов из модели.
Входящие касты должны реализовывать интерфейс CastsInboundAttributes, который требует определения только метода set. Команда Artisan make:cast может быть вызвана с опцией --inbound для генерации класса входящего кастинга:
php artisan make:cast Hash --inbound
Классический пример входящего кастинга — "хеширование". Например, можно определить каст, который хеширует входящие значения с помощью заданного алгоритма:
<?php
namespace App\Casts;
use Illuminate\Contracts\Database\Eloquent\CastsInboundAttributes;
use Illuminate\Database\Eloquent\Model;
class Hash implements CastsInboundAttributes
{
/**
* Создать новый экземпляр класса кастинга.
*/
public function __construct(
protected string|null $algorithm = null,
) {}
/**
* Подготовить заданное значение для хранения.
*
* @param array<string, mixed> $attributes
*/
public function set(Model $model, string $key, mixed $value, array $attributes): string
{
return is_null($this->algorithm)
? bcrypt($value)
: hash($this->algorithm, $value);
}
}
#Параметры кастов
При прикреплении пользовательского кастинга к модели параметры кастинга можно указать, отделив их от имени класса символом : и разделив запятыми несколько параметров. Параметры будут переданы в конструктор класса кастинга:
/**
* Атрибуты, которые должны быть приведены к типам.
*
* @var array
*/
protected $casts = [
'secret' => Hash::class.':sha256',
];
#Castables
Вы можете позволить вашим объектам-значениям определять собственные пользовательские классы кастинга. Вместо того чтобы прикреплять класс кастинга к модели, вы можете прикрепить класс объекта-значения, реализующий интерфейс Illuminate\Contracts\Database\Eloquent\Castable:
use App\ValueObjects\Address;
protected $casts = [
'address' => Address::class,
];
Объекты, реализующие интерфейс Castable, должны определить метод castUsing, который возвращает имя класса кастера, отвечающего за кастинг из и в класс Castable:
<?php
namespace App\ValueObjects;
use Illuminate\Contracts\Database\Eloquent\Castable;
use App\Casts\Address as AddressCast;
class Address implements Castable
{
/**
* Получить имя класса кастера для кастинга из / в этот тип.
*
* @param array<string, mixed> $arguments
*/
public static function castUsing(array $arguments): string
{
return AddressCast::class;
}
}
При использовании классов Castable вы всё равно можете передавать аргументы в определении $casts. Аргументы будут переданы в метод castUsing:
use App\ValueObjects\Address;
protected $casts = [
'address' => Address::class.':argument',
];
#Castables и анонимные классы кастинга
Объединив "castables" с PHP анонимными классами, вы можете определить объект-значение и его логику кастинга как единый кастабельный объект. Для этого верните анонимный класс из метода castUsing вашего объекта-значения. Анонимный класс должен реализовывать интерфейс CastsAttributes:
<?php
namespace App\ValueObjects;
use Illuminate\Contracts\Database\Eloquent\Castable;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
class Address implements Castable
{
// ...
/**
* Получить класс кастера для кастинга из / в этот тип.
*
* @param array<string, mixed> $arguments
*/
public static function castUsing(array $arguments): CastsAttributes
{
return new class implements CastsAttributes
{
public function get(Model $model, string $key, mixed $value, array $attributes): Address
{
return new Address(
$attributes['address_line_one'],
$attributes['address_line_two']
);
}
public function set(Model $model, string $key, mixed $value, array $attributes): array
{
return [
'address_line_one' => $value->lineOne,
'address_line_two' => $value->lineTwo,
];
}
};
}
}