Аксессоры, мутаторы и приведение атрибутов к типам позволяют преобразовывать значения атрибутов Eloquent, когда вы извлекаете экземпляр модели или присваиваете их экземпляру модели. Например, вы можете использовать шифровальщик Laravel, чтобы зашифровать значение при его сохранении в базу данных, а затем автоматически расшифровать атрибут при доступе к нему в модели Eloquent. Или вы можете преобразовать строку JSON, которая хранится в вашей базе данных, в массив при доступе к ней через вашу модель Eloquent.
Аксессор преобразует значение атрибута экземпляра Eloquent при обращении к нему. Чтобы определить метод доступа, создайте в модели защищенный метод для представления доступного атрибута. Это имя метода должно соответствовать атрибуту модели / столбца базы данных в «верблюжьем регистре» , когда это применимо.
В этом примере мы определим аксессор для атрибута 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
{
/**
* Взаимодействие с именем пользователя.
*
* @return \Illuminate\Database\Eloquent\Casts\Attribute
*/
protected function firstName(): Attribute
{
return Attribute::make(
get: fn ($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;
/**
* Взаимодействие с адресом пользователя.
*
* @return \Illuminate\Database\Eloquent\Casts\Attribute
*/
protected function address(): Attribute
{
return Attribute::make(
get: fn ($value, $attributes) => new Address(
$attributes['address_line_one'],
$attributes['address_line_two'],
),
);
}
При возврате объектов-значений из аксессоров любые изменения, внесенные в объект-значение, будут автоматически синхронизированы с моделью перед ее сохранением. Это возможно, потому что Eloquent сохраняет экземпляры, возвращаемые аксессорами, поэтому он может возвращать один и тот же экземпляр каждый раз, когда вызывается аксессор:
use App\Models\User;
$user = User::find(1);
$user->address->lineOne = 'Updated Address Line 1 Value';
$user->address->lineTwo = 'Updated Address Line 2 Value';
$user->save();
Иногда требуется включить кэширование для примитивных значений, таких как строки и логические значения, особенно если они требуют больших вычислительных ресурсов. Для этого вы можете вызвать метод shouldCache
при определении вашего аксессора:
protected function hash(): Attribute
{
return Attribute::make(
get: fn ($value) => bcrypt(gzuncompress($value)),
)->shouldCache();
}
Если вы хотите избежать кэширования объектов в атрибутах, то вы можете вызвать метод withoutObjectCaching
при определении атрибута:
/**
* Взаимодействие с адресом пользователя.
*
* @return \Illuminate\Database\Eloquent\Casts\Attribute
*/
protected function address(): Attribute
{
return Attribute::make(
get: fn ($value, $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
{
/**
* Взаимодействие с именем пользователя.
*
* @return \Illuminate\Database\Eloquent\Casts\Attribute
*/
protected function firstName(): Attribute
{
return Attribute::make(
get: fn ($value) => ucfirst($value),
set: fn ($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;
/**
* Взаимодействие с адресом пользователя.
*
* @return \Illuminate\Database\Eloquent\Casts\Attribute
*/
protected function address(): Attribute
{
return Attribute::make(
get: fn ($value, $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
должно быть массивом, где ключ – это имя преобразуемого атрибута, а значение – это тип, к которому вы хотите привести столбец. Поддерживаемые типы преобразования:
array
AsStringable::class
boolean
collection
date
datetime
immutable_date
immutable_datetime
decimal:<precision>
double
encrypted
encrypted:array
encrypted:collection
encrypted:object
float
integer
object
real
string
timestamp
Чтобы продемонстрировать преобразование атрибутов, давайте преобразуем атрибут 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
, не будут преобразованы. Кроме того, вы никогда не должны определять типизацию (или атрибут), имя которого совпадает с именем отношения.
Вы можете использовать класс Illuminate\Database\Eloquent\Casts\AsStringable
для приведения атрибута модели к объекту класса 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,
];
}
Преобразование в array
особенно полезно при работе со столбцами, которые хранятся как сериализованный JSON. Например, если ваша база данных имеет поле типа JSON
или TEXT
, содержащее сериализованный JSON, то добавленная типизация array
этому атрибуту автоматически десериализует атрибут модели Eloquent в массив PHP при обращении к нему:
<?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']);
Хотя типизации стандартного array
достаточно для многих приложений, но у него есть некоторые недостатки. Поскольку типизация array
возвращает примитивный тип, невозможно напрямую изменить смещение массива. Например, следующий код вызовет ошибку PHP:
$user = User::find(1);
$user->options['key'] = $value;
Чтобы решить эту проблему, Laravel предлагает типизацию AsArrayObject
, которая преобразует ваш атрибут JSON в класс ArrayObject. Эта функция реализована с использованием реализации пользовательской типизации Laravel, которая позволяет 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,
];
По умолчанию Eloquent преобразует столбцы created_at
и updated_at
в экземпляры Carbon, расширяющего класс DateTime PHP и предоставляющего набор полезных методов. Вы можете типизировать дополнительные атрибуты даты, определив дополнительные преобразования даты в массиве свойств $casts
вашей модели. Обычно даты следует типизировать с использованием типов datetime
или immutable_datetime
.
При определении типизации date
или datetime
вы также можете указать формат даты. Этот формат будет использоваться, когда модель сериализуется в массив или JSON:
/**
* Атрибуты, которые должны быть типизированы.
*
* @var array
*/
protected $casts = [
'created_at' => 'datetime:Y-m-d',
];
Когда столбец типизирован как дата, вы можете установить соответствующее значение атрибута модели в виде временной метки форматов UNIX, строки даты (Y-m-d
), строки даты-времени или экземпляров DateTime
/ Carbon
. Значение даты будет правильно преобразовано и сохранено в вашей базе данных.
Вы можете настроить формат сериализации по умолчанию для всех дат вашей модели, переопределив метод serializeDate
вашей модели. Этот метод не влияет на форматирование дат для их сохранения в базе данных:
/**
* Подготовить дату для сериализации массива / JSON.
*
* @param \DateTimeInterface $date
* @return string
*/
protected function serializeDate(DateTimeInterface $date)
{
return $date->format('Y-m-d');
}
Чтобы указать формат, который следует использовать при фактическом сохранении дат модели в вашей базе данных, вы должны определить свойство $dateFormat
вашей модели:
/**
* Формат хранения столбцов даты модели.
*
* @var string
*/
protected $dateFormat = 'U';
По умолчанию типизации date
и datetime
будут сериализовать даты в строковый формат UTC ISO-8601 (1986-05-28T21:05:54.000000Z
) даты, независимо от часового пояса, указанного в параметре timezone
конфигурации вашего приложения. Вам настоятельно рекомендуется всегда использовать этот формат сериализации, а также сохранять даты вашего приложения в часовом поясе UTC, не изменяя параметр конфигурации вашего приложения timezone
по умолчанию, имеющий значение UTC
. Последовательное использование часового пояса UTC во всем приложении обеспечит максимальный уровень взаимодействия с другими библиотеками обработки даты, написанными на PHP и JavaScript.
Если типизации date
или datetime
дополнены пользовательским форматом, например, datetime:Y-m-d H:i:s
, то во время сериализации даты будет использован внутренний часовой пояс экземпляра Carbon. Обычно это часовой пояс, указанный в параметре timezone
конфигурации вашего приложения.
Предупреждение
Перечисляемые типы доступны только в PHP 8.1+.
Eloquent также позволяет вам преобразовывать значения ваших атрибутов в типизированные перечисления PHP. Для этого вы можете указать атрибут, который вы хотите типизировать, и соответствующий класс перечисления в массиве $casts
вашей модели:
use App\Enums\ServerStatus;
/**
* Атрибуты, которые должны быть типизированы.
*
* @var array
*/
protected $casts = [
'status' => ServerStatus::class,
];
После того, как вы определили типизацию в своей модели, указанный атрибут будет автоматически преобразован в перечисляемый тип и из него при взаимодействии с атрибутом:
if ($server->status == ServerStatus::Provisioned) {
$server->status = ServerStatus::Ready;
$server->save();
}
Типизация 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 есть множество встроенных полезных преобразователей; однако иногда требуется определить свои собственные. Чтобы создать класс типизации (типизатор), выполните команду 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;
class Json implements CastsAttributes
{
/**
* Преобразовать значение к пользовательскому типу.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @param string $key
* @param mixed $value
* @param array $attributes
* @return array
*/
public function get($model, $key, $value, $attributes)
{
return json_decode($value, true);
}
/**
* Подготовить переданное значение к сохранению.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @param string $key
* @param array $value
* @param array $attributes
* @return string
*/
public function set($model, $key, $value, $attributes)
{
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 InvalidArgumentException;
class Address implements CastsAttributes
{
/**
* Преобразовать значение к пользовательскому типу.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @param string $key
* @param mixed $value
* @param array $attributes
* @return \App\ValueObjects\Address
*/
public function get($model, $key, $value, $attributes)
{
return new AddressValueObject(
$attributes['address_line_one'],
$attributes['address_line_two']
);
}
/**
* Подготовить переданное значение к сохранению.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @param string $key
* @param \App\ValueObjects\Address $value
* @param array $attributes
* @return array
*/
public function set($model, $key, $value, $attributes)
{
if (! $value instanceof AddressValueObject) {
throw new InvalidArgumentException('The given value is not an Address instance.');
}
return [
'address_line_one' => $value->lineOne,
'address_line_two' => $value->lineTwo,
];
}
}
При приведении к объектам-значениям любые изменения, внесенные в объект-значения, будут автоматически синхронизированы с моделью до ее сохранения:
use App\Models\User;
$user = User::find(1);
$user->address->lineOne = 'Updated Address Value';
$user->save();
Примечание
Если вы планируете сериализовать свои модели Eloquent, содержащие объекты-значения, в JSON или массивы, вам следует реализовать интерфейсыIlluminate\Contracts\Support\Arrayable
иJsonSerializable
для объекта-значения.
Когда модель Eloquent преобразуется в массив или JSON с использованием методов toArray
и toJson
, ваши пользовательские типизаторы объекты-значения обычно будут сериализованы, в частности, пока они (типизаторы) реализуют интерфейсы Illuminate\Contracts\Support\Arrayable
и JsonSerializable
. Однако при использовании объектов-значений, предоставляемых сторонними библиотеками, у вас может не быть возможности добавить эти интерфейсы к объекту.
Поэтому вы можете указать, что ваш собственный класс типизатора будет отвечать за сериализацию объекта-значения. Для этого ваш собственный класс типизатора должно реализовывать интерфейс Illuminate\Contracts\Database\Eloquent\SerializesCastableAttributes
. В этом интерфейсе указано, что ваш класс должен содержать метод serialize
, возвращающий сериализованную форму вашего объекта значения:
/**
* Получить сериализованное представление значения.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @param string $key
* @param mixed $value
* @param array $attributes
* @return mixed
*/
public function serialize($model, string $key, $value, array $attributes)
{
return (string) $value;
}
Иногда требуется написать свой типизатор, который только преобразует указанные значения атрибутов модели, и не выполняет никаких операций при обращении к этим атрибутам.
Пользовательские типизаторы только для входящих значений должны реализовывать интерфейс CastsInboundAttributes
, требующий определение метода set
. Для этого можно вызвать команду make:cast
Artisan с параметром --inbound
:
php artisan make:cast Hash --inbound
Классическим примером только входящей типизации является «хеширование». Например, мы можем определить типизатор, который хэширует входящие значения с помощью определенного алгоритма:
<?php
namespace App\Casts;
use Illuminate\Contracts\Database\Eloquent\CastsInboundAttributes;
class Hash implements CastsInboundAttributes
{
/**
* Алгоритм хеширования.
*
* @var string
*/
protected $algorithm;
/**
* Создать новый экземпляр класса типизации.
*
* @param string|null $algorithm
* @return void
*/
public function __construct($algorithm = null)
{
$this->algorithm = $algorithm;
}
/**
* Подготовить переданное значение к сохранению.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @param string $key
* @param array $value
* @param array $attributes
* @return string
*/
public function set($model, $key, $value, $attributes)
{
return is_null($this->algorithm)
? bcrypt($value)
: hash($this->algorithm, $value);
}
}
При добавлении пользовательского типизатора к модели, параметры типизатора задаются отделением их от имени класса с помощью символа :
и разделением нескольких параметров запятыми. Параметры будут переданы в конструктор класса типизатора:
/**
* Атрибуты, которые должны быть типизированы.
*
* @var array
*/
protected $casts = [
'secret' => Hash::class.':sha256',
];
Вы можете разрешить объектам-значениям вашего приложения определять свои собственные классы типизаторы. Вместо указания пользовательской типизации в модели, вы можете альтернативно указать класс, который реализует интерфейс Illuminate\Contracts\Database\Eloquent\Castable
:
use App\Models\Address;
protected $casts = [
'address' => Address::class,
];
Объекты, реализующие интерфейс Castable
, должны определять метод castUsing
, который возвращает имя пользовательского класса типизатора, отвечающего за двустороннее преобразование:
<?php
namespace App\Models;
use Illuminate\Contracts\Database\Eloquent\Castable;
use App\Casts\Address as AddressCast;
class Address implements Castable
{
/**
* Получить имя класса типизатора для использования двустороннего преобразования.
*
* @param array $arguments
* @return string
*/
public static function castUsing(array $arguments)
{
return AddressCast::class;
}
}
При использовании классов Castable
вы все равно можете указывать аргументы в свойстве $casts
. Аргументы будут переданы методу castUsing
:
use App\Models\Address;
protected $casts = [
'address' => Address::class.':argument',
];
Комбинируя castable
и анонимными классами PHP, вы можете определить объект-значение и его логику преобразования как единый типизируемый объект. Для этого верните анонимный класс из метода castUsing
вашего объекта-значения. Анонимный класс должен реализовывать интерфейс CastsAttributes
:
<?php
namespace App\Models;
use Illuminate\Contracts\Database\Eloquent\Castable;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
class Address implements Castable
{
// ...
/**
* Получить имя класса типизатора для использования двустороннего преобразования.
*
* @param array $arguments
* @return object|string
*/
public static function castUsing(array $arguments)
{
return new class implements CastsAttributes
{
public function get($model, $key, $value, $attributes)
{
return new Address(
$attributes['address_line_one'],
$attributes['address_line_two']
);
}
public function set($model, $key, $value, $attributes)
{
return [
'address_line_one' => $value->lineOne,
'address_line_two' => $value->lineTwo,
];
}
};
}
}