Eloquent:突变器和转换

Introduction

访问器、修改器和属性转换允许您在检索或设置模型实例时转换 Eloquent 属性值。例如,您可能想使用Laravel 加密器 加密存储在数据库中的值,然后当您在 Eloquent 模型上访问它时自动解密该属性。或者,您可能希望在通过 Eloquent 模型访问时将存储在数据库中的 JSON 字符串转换为数组。

访问器和修改器

定义访问器

访问器在访问时转换 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
{
    /**
     * Get the user's first name.
     */
    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;

Note
如果您希望将这些计算值添加到模型的数组/JSON 表示中,你需要附加它们.

从多个属性构建值对象

有时您的访问器可能需要将多个模型属性转换为单个“值对象”。为此,您的get 闭包可以接受第二个参数$attributes,它将自动提供给闭包,并将包含模型所有当前属性的数组:

use App\Support\Address;
use Illuminate\Database\Eloquent\Casts\Attribute;

/**
 * Interact with the user's address.
 */
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 = '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 (string $value) => bcrypt(gzuncompress($value)),
    )->shouldCache();
}

如果你想禁用属性的对象缓存行为,你可以调用withoutObjectCaching 定义属性时的方法:

/**
 * Interact with the user's address.
 */
protected function address(): Attribute
{
    return Attribute::make(
        get: fn (mixed $value, array $attributes) => new Address(
            $attributes['address_line_one'],
            $attributes['address_line_two'],
        ),
    )->withoutObjectCaching();
}

定义一个增变器

Mutator 在设置时转换 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
{
    /**
     * Interact with the user's first name.
     */
    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;

/**
 * Interact with the user's address.
 */
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 property 提供了一种将属性转换为通用数据类型的便捷方法。

$casts property 应该是一个数组,其中键是要转换的属性的名称,值是您希望将列转换为的类型。支持的转换类型是:

  • 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
{
    /**
     * The attributes that should be cast.
     *
     * @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',
]);

Warning
属性是null 不会被施放。此外,您永远不应定义与关系同名的强制转换(或属性)。

可串铸

您可以使用Illuminate\Database\Eloquent\Casts\AsStringable cast class 将模型属性转换为流利Illuminate\Support\Stringable 目的:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Casts\AsStringable;
use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    /**
     * The attributes that should be cast.
     *
     * @var array
     */
    protected $casts = [
        'directory' => AsStringable::class,
    ];
}

数组和 JSON 转换

array cast 在处理存储为序列化 JSON 的列时特别有用。例如,如果您的数据库有一个JSON 或者TEXT 包含序列化 JSON 的字段类型,添加array 当您在 Eloquent 模型上访问该属性时,转换为该属性会自动将该属性反序列化为 PHP 数组:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    /**
     * The attributes that should be cast.
     *
     * @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 cast 对于许多应用程序来说已经足够了,它确实有一些缺点。自从array cast 返回原始类型,无法直接改变数组的偏移量。例如,以下代码将触发 PHP 错误:

$user = User::find(1);

$user->options['key'] = $value;

为了解决这个问题,Laravel 提供了一个AsArrayObject cast 将您的 JSON 属性转换为ArrayObject 班级。这个功能是使用 Laravel 的实现的定制演员 实现,它允许 Laravel 智能地缓存和转换变异对象,以便可以修改单个偏移量而不会触发 PHP 错误。使用AsArrayObject 演员,只需将其分配给一个属性:

use Illuminate\Database\Eloquent\Casts\AsArrayObject;

/**
 * The attributes that should be cast.
 *
 * @var array
 */
protected $casts = [
    'options' => AsArrayObject::class,
];

同样,Laravel 提供了一个AsCollection cast 将你的 JSON 属性转换为 LaravelCollection 实例:

use Illuminate\Database\Eloquent\Casts\AsCollection;

/**
 * The attributes that should be cast.
 *
 * @var array
 */
protected $casts = [
    'options' => AsCollection::class,
];

日期铸造

默认情况下,Eloquent 会投射created_atupdated_at 实例的列Carbon, 它扩展了 PHPDateTime 类并提供各种有用的方法。您可以通过在模型的$casts 属性数组。通常,日期应该使用datetime 或者immutable_datetime 演员类型。

当定义一个date 或者datetime cast,您还可以指定日期的格式。当模型被序列化为数组或 JSON:

/**
 * The attributes that should be cast.
 *
 * @var array
 */
protected $casts = [
    'created_at' => 'datetime:Y-m-d',
];

将列转换为日期时,可以将相应的模型属性值设置为 UNIX 时间戳、日期字符串 (Y-m-d)、日期时间字符串或DateTime /Carbon 实例。日期值将被正确转换并存储在您的数据库中。

您可以通过定义一个serializeDate 模型上的方法。此方法不会影响您的日期在数据库中存储的格式:

/**
 * Prepare a date for array / JSON serialization.
 */
protected function serializeDate(DateTimeInterface $date): string
{
    return $date->format('Y-m-d');
}

要指定在数据库中实际存储模型日期时应使用的格式,您应该定义一个$dateFormat您模型上的属性:

/**
 * The storage format of the model's date columns.
 *
 * @var string
 */
protected $dateFormat = 'U';

日期转换、序列化和时区

默认情况下,datedatetime 强制转换会将日期序列化为 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 配置选项。

枚举铸造

Warning
枚举转换仅适用于 PHP 8.1+。

Eloquent 还允许您将属性值转换为 PHPEnums.为此,您可以在模型的$casts 属性数组:

use App\Enums\ServerStatus;

/**
 * The attributes that should be cast.
 *
 * @var array
 */
protected $casts = [
    'status' => ServerStatus::class,
];

一旦您在模型上定义了转换,当您与该属性交互时,指定的属性将自动转换为枚举或从枚举转换:

if ($server->status == ServerStatus::Provisioned) {
    $server->status = ServerStatus::Ready;

    $server->save();
}

铸造枚举数组

有时您可能需要您的模型在单个列中存储枚举值数组。为此,您可以使用AsEnumArrayObject 或者AsEnumCollection Laravel 提供的转换:

use App\Enums\ServerStatus;
use Illuminate\Database\Eloquent\Casts\AsEnumCollection;

/**
 * The attributes that should be cast.
 *
 * @var array
 */
protected $casts = [
    'statuses' => AsEnumCollection::class.':'.ServerStatus::class,
];

加密铸造

encrypted cast 将使用 Laravel 的内置加密模型的属性值encryption 特征。除此之外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 界面。实现此接口的类必须定义一个getset 方法。这get 方法负责将数据库中的原始值转换为转换值,而set 方法应该将转换值转换为可以存储在数据库中的原始值。作为示例,我们将重新实现内置的json 将类型转换为自定义转换类型:

<?php

namespace App\Casts;

use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Database\Eloquent\Model;

class Json implements CastsAttributes
{
    /**
     * Cast the given value.
     *
     * @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);
    }

    /**
     * Prepare the given value for storage.
     *
     * @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
{
    /**
     * The attributes that should be cast.
     *
     * @var array
     */
    protected $casts = [
        'options' => Json::class,
    ];
}

值对象转换

您不限于将值转换为基本类型。您还可以将值转换为对象。定义将值转换为对象的自定义转换与转换为基本类型非常相似;但是,那set 方法应该返回一个键/值对数组,用于在模型上设置原始的、可存储的值。

例如,我们将定义一个自定义类型转换类,将多个模型值转换为一个Address值对象。我们将假设Address 值有两个公共属性:lineOnelineTwo:

<?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
{
    /**
     * Cast the given value.
     *
     * @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']
        );
    }

    /**
     * Prepare the given value for storage.
     *
     * @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('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();

Note
如果你计划将包含值对象的 Eloquent 模型序列化为 JSON 或数组,你应该实现Illuminate\Contracts\Support\ArrayableJsonSerializable 值对象上的接口。

数组/JSON 序列化

当使用 Eloquent 模型转换为数组或 JSON 时toArraytoJson 方法,您的自定义转换值对象通常会被序列化,只要它们实现了Illuminate\Contracts\Support\ArrayableJsonSerializable 接口。但是,在使用第三方库提供的值对象时,你可能没有能力为对象添加这些接口。

因此,您可以指定您的自定义转换类将负责序列化值对象。为此,您的自定义演员类应该实现Illuminate\Contracts\Database\Eloquent\SerializesCastableAttributes 界面。这个接口声明你的类应该包含一个serialize 应该返回值对象的序列化形式的方法:

/**
 * Get the serialized representation of the value.
 *
 * @param  array<string, mixed>  $attributes
 */
public function serialize(Model $model, string $key, mixed $value, array $attributes): string
{
    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;
use Illuminate\Database\Eloquent\Model;

class Hash implements CastsInboundAttributes
{
    /**
     * Create a new cast class instance.
     */
    public function __construct(
        protected string $algorithm = null,
    ) {}

    /**
     * Prepare the given value for storage.
     *
     * @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);
    }
}

投射参数

将自定义转换附加到模型时,可以通过使用将它们与类名分开来指定转换参数: 字符和逗号分隔的多个参数。参数将传递给 cast 类的构造函数:

/**
 * The attributes that should be cast.
 *
 * @var array
 */
protected $casts = [
    'secret' => Hash::class.':sha256',
];

Castables

您可能希望允许您的应用程序的值对象定义它们自己的自定义类型转换类。您可以选择附加一个实现Illuminate\Contracts\Database\Eloquent\Castable 界面:

use App\Models\Address;

protected $casts = [
    'address' => Address::class,
];

实现的对象Castable 接口必须定义一个castUsing 返回自定义施法器类的类名的方法,该类负责与Castable 班级:

<?php

namespace App\Models;

use Illuminate\Contracts\Database\Eloquent\Castable;
use App\Casts\Address as AddressCast;

class Address implements Castable
{
    /**
     * Get the name of the caster class to use when casting from / to this cast target.
     *
     * @param  array<string, mixed>  $arguments
     */
    public static function castUsing(array $arguments): string
    {
        return AddressCast::class;
    }
}

使用时Castable 类,你仍然可以在$casts 定义。参数将传递给castUsing 方法:

use App\Models\Address;

protected $casts = [
    'address' => Address::class.':argument',
];

Castables 和匿名 Cast 类

通过将“castables”与 PHP 相结合匿名类,您可以将值对象及其转换逻辑定义为单个可转换对象。为此,从您的值对象的返回一个匿名类castUsing 方法。匿名类应该实现CastsAttributes 界面:

<?php

namespace App\Models;

use Illuminate\Contracts\Database\Eloquent\Castable;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Database\Eloquent\Model;

class Address implements Castable
{
    // ...

    /**
     * Get the caster class to use when casting from / to this cast target.
     *
     * @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,
                ];
            }
        };
    }
}
豫ICP备18041297号-2