Подписывайтесь на наш Telegram канал и будьте в курсе всех событий.
Примите наш вызов и улучшите свои навыки!
Примите наш вызов и улучшите свои навыки!

Eloquent · Мутаторы и типизация

Вы просматриваете документ для прошлой версии.
Рассмотрите возможность обновления вашего проекта до актуальной версии 10.x. Почему это важно?

Введение

Аксессоры, мутаторы и приведение атрибутов к типам позволяют преобразовывать значения атрибутов Eloquent, когда вы извлекаете экземпляр модели или присваиваете их экземпляру модели. Например, вы можете использовать шифровальщик Laravel, чтобы зашифровать значение при его сохранении в базу данных, а затем автоматически расшифровать атрибут при доступе к нему в модели Eloquent. Или вы можете преобразовать строку JSON, которая хранится в вашей базе данных, в массив при доступе к ней через вашу модель Eloquent.

Аксессоры и мутаторы (Accessors and Mutators)

Определение аксессора (Accessor)

Аксессор преобразует значение атрибута экземпляра Eloquent при обращении к нему. Чтобы определить метод доступа, создайте метод get{Attribute}Attribute в вашей модели, где {Attribute} – это имя столбца, к которому вы хотите получить доступ, в «верхнем» регистре.

В этом примере мы определим аксессор для атрибута first_name. Аксессор будет автоматически вызван Eloquent при попытке получить значение атрибута first_name:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    /**
     * Получить имя пользователя.
     *
     * @param  string  $value
     * @return string
     */
    public function getFirstNameAttribute($value)
    {
        return ucfirst($value);
    }
}

Как видите, исходное значение столбца передается аксессору, что позволяет вам манипулировать и возвращать значение. Чтобы получить доступ к значению аксессора, вы можете просто получить доступ к атрибуту first_name экземпляра модели:

use App\Models\User;

$user = User::find(1);

$firstName = $user->first_name;

Вы не ограничены взаимодействием с одним атрибутом в вашем аксессоре. Вы также можете использовать аксессор для возврата новых вычисленных значений из существующих атрибутов:

/**
 * Получить полное имя пользователя.
 *
 * @return string
 */
public function getFullNameAttribute()
{
    return "{$this->first_name} {$this->last_name}";
}

Если вы хотите, чтобы эти вычисленные значения были добавлены к представлениям массива / JSON вашей модели, вам нужно будет добавить их.

Определение мутатора (Mutator)

Мутатор преобразует значение атрибута в момент их присвоения экземпляру Eloquent. Чтобы определить мутатор, определите метод set{Attribute}Attribute в вашей модели, где {Attribute} – это имя столбца, к которому вы хотите получить доступ, в «верхнем» регистре.

Определим мутатор для атрибута first_name. Этот мутатор будет автоматически вызываться, когда мы попытаемся присвоить значение атрибута first_name модели:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    /**
     * Присвоить имя пользователю.
     *
     * @param  string  $value
     * @return void
     */
    public function setFirstNameAttribute($value)
    {
        $this->attributes['first_name'] = strtolower($value);
    }
}

Мутатор получит значение, заданное для атрибута, что позволит вам манипулировать этим значением и устанавливать желаемое значение во внутреннем свойстве $attributes модели Eloquent. Чтобы использовать наш мутатор, нам нужно только установить атрибут first_name для модели Eloquent:

use App\Models\User;

$user = User::find(1);

$user->first_name = 'Sally';

В этом примере метод setFirstNameAttribute будет вызываться со значением Sally. Затем, мутатор применит к имени функцию strtolower и установит полученное значение во внутреннем массиве $attributes.

Приведение атрибутов к типам

Приведение атрибутов обеспечивает функциональность, аналогичную аксессорам и мутаторам, но без необходимости определения каких-либо дополнительных методов вашей модели. Вместо этого свойство $casts вашей модели представляет удобный способ преобразования атрибутов в распространенные типы данных.

Свойство $casts должно быть массивом, где ключ – это имя преобразуемого атрибута, а значение – это тип, к которому вы хотите привести столбец. Поддерживаемые типы преобразования:

  • array
  • AsStringable::class
  • boolean
  • collection
  • date
  • datetime
  • immutable_date
  • immutable_datetime
  • decimal:<digits>
  • 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 для приведения атрибута модели к объекту строки 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 этому атрибуту автоматически десериализует атрибут модели 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']);

Типизация ArrayObject и Collection

Хотя типизации стандартного 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 и предоставляющего набор полезных методов. Вы можете типизировать дополнительные атрибуты даты, определив дополнительные преобразования даты в массиве свойств вашей модели $cast. Обычно даты следует приводить с использованием типизации 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.

Типизация "Enum"

Приведение Enum доступно только для 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"

Приведение encrypted зашифрует значение атрибута модели, используя встроенные в Laravel функции шифрования. Кроме того, преобразование encrypted:array, encrypted:collection, encrypted:object, AsEncryptedArrayObject и AsEncryptedCollection работает так же, как и их незашифрованные копии; однако, как и следовало ожидать, базовое значение зашифровано при хранении в вашей базе данных.

Поскольку окончательная длина зашифрованного текста непредсказуема и больше, чем его копия в виде обычного текста, убедитесь, что связанный столбец базы данных имеет тип TEXT или больше. Кроме того, поскольку значения зашифрованы в базе данных, вы не сможете запрашивать или искать зашифрованные значения атрибутов.

Типизация во время запроса

Иногда может потребоваться применить типизацию при выполнении запроса, например, при выборе сырого значения из таблицы. Например, рассмотрим следующий запрос:

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 есть множество встроенных полезных преобразователей; однако иногда требуется определить свои собственные. Вы можете добиться этого, определив класс, реализующий интерфейс 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\Models\Address as AddressModel;
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\Models\Address
     */
    public function get($model, $key, $value, $attributes)
    {
        return new AddressModel(
            $attributes['address_line_one'],
            $attributes['address_line_two']
        );
    }

    /**
     * Подготовить переданное значение к сохранению.
     *
     * @param  \Illuminate\Database\Eloquent\Model  $model
     * @param  string  $key
     * @param  \App\Models\Address  $value
     * @param  array  $attributes
     * @return array
     */
    public function set($model, $key, $value, $attributes)
    {
        if (! $value instanceof AddressModel) {
            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 для объекта-значения.

Сериализация в массив и JSON

Когда модель 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.

<?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',
];

Интерфейс Castable

Вы можете разрешить объектам-значениям вашего приложения определять свои собственные классы типизаторы. Вместо указания пользовательской типизации в модели, вы можете альтернативно указать класс, который реализует интерфейс 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 и анонимные классы типизаторов

Комбинируя 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,
                ];
            }
        };
    }
}