Skip to content

Eloquent: Mutators & Casting

介绍

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

访问器与修改器

定义访问器

访问器在访问 Eloquent 属性值时对其进行转换。要定义访问器,请在模型上创建一个受保护的方法来表示 accessible 属性。此方法名称应对应于真实基础模型属性/数据库列的“驼峰式大小写”表示形式(如果适用)。

在此示例中,我们将为 first_name 属性定义一个访问器。当尝试检索 first_name 属性的值时,Eloquent 将自动调用该访问器。所有 属性 访问器 / 修改器 方法都必须声明一个返回 type-hint : Illuminate\Database\Eloquent\Casts\Attribute

php
    <?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 属性:

php
    use App\Models\User;

    $user = User::find(1);

    $firstName = $user->first_name;

NOTE

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

从多个属性构建 Value 对象

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

php
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'],
        ),
    );
}

访问器缓存

当从访问器返回 value 对象时,对 value 对象所做的任何更改都会在保存模型之前自动同步回模型。这是可能的,因为 Eloquent 保留了访问器返回的实例,因此每次调用访问器时它都可以返回相同的实例:

php
    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 方法:

php
protected function hash(): Attribute
{
    return Attribute::make(
        get: fn (string $value) => bcrypt(gzuncompress($value)),
    )->shouldCache();
}

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

php
/**
 * 与用户的地址交互。
 */
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

设置后,mutator 会变换 Eloquent 属性值。要定义 mutator,你可以在定义 attribute 时提供 set 参数。让我们为 first_name 属性定义一个 mutator。当我们尝试在模型上设置 first_name 属性的值时,这个 mutator 将被自动调用:

php
    <?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),
            );
        }
    }

mutator 闭包将接收在 attribute 上设置的值,允许你操作该值并返回作的值。要使用我们的 mutator,我们只需要在 Eloquent 模型上设置 first_name 属性:

php
    use App\Models\User;

    $user = User::find(1);

    $user->first_name = 'Sally';

在此示例中,将使用值 Sally 调用 set 回调。然后,mutator 会将 strtolower 函数应用于名称,并在模型的内部 $attributes 数组中设置其结果值。

更改多个属性

有时你的 mutator 可能需要在底层模型上设置多个属性。为此,您可以从 set 闭包返回一个数组。数组中的每个键都应与与模型关联的底层属性/数据库列相对应:

php
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

Attribute casting 提供了类似于 accessor 和 mutator 的功能,而无需在模型上定义任何其他方法。相反,模型的 casts 方法提供了一种将属性转换为常见数据类型的便捷方法。

casts 方法应返回一个数组,其中 key 是要强制转换的属性的名称,value 是您希望将列强制转换到的类型。支持的强制转换类型包括:

  • array
  • AsStringable::class
  • boolean
  • collection
  • date
  • datetime
  • immutable_date
  • immutable_datetime
  • decimal:<precision>
  • double
  • encrypted
  • encrypted:array
  • encrypted:collection
  • encrypted:object
  • float
  • hashed
  • integer
  • object
  • real
  • string
  • timestamp

为了演示属性转换,让我们将 is_admin 属性转换为布尔值,该属性以整数(01)的形式存储在数据库中:

php
    <?php

    namespace App\Models;

    use Illuminate\Database\Eloquent\Model;

    class User extends Model
    {
        /**
         * 获取应强制转换的属性。
         *
         * @return array<string, string>
         */
        protected function casts(): array
        {
            return [
                'is_admin' => 'boolean',
            ];
        }
    }

定义强制转换后,is_admin 属性在访问时将始终强制转换为布尔值,即使底层值作为整数存储在数据库中:

php
    $user = App\Models\User::find(1);

    if ($user->is_admin) {
        // ...
    }

如果需要在运行时添加新的临时强制转换,可以使用 mergeCasts 方法。这些强制转换定义将添加到模型上已定义的任何强制转换中:

php
    $user->mergeCasts([
        'is_admin' => 'integer',
        'options' => 'object',
    ]);

WARNING

不会强制转换 null 的属性。此外,您永远不应定义与关系同名的强制转换(或属性),也不应将强制转换分配给模型的主键。

字符串化 Casting

你可以使用 Illuminate\Database\Eloquent\Casts\AsStringable cast 类将 model 属性转换为 Fluent Illuminate\Support\Stringable 对象:

php
    <?php

    namespace App\Models;

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

    class User extends Model
    {
        /**
         * 获取应强制转换的属性
         *
         * @return array<string, string>
         */
        protected function casts(): array
        {
            return [
                'directory' => AsStringable::class,
            ];
        }
    }

数组和 JSON 强制转换

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

php
    <?php

    namespace App\Models;

    use Illuminate\Database\Eloquent\Model;

    class User extends Model
    {
        /**
         * 获取应强制转换的属性
         *
         * @return array<string, string>
         */
        protected function casts(): array
        {
            return [
                'options' => 'array',
            ];
        }
    }

定义强制转换后,您可以访问 options 属性,它将自动从 JSON 反序列化为 PHP 数组。当你设置 options 属性的值时,给定的数组将自动序列化回 JSON 进行存储:

php
    use App\Models\User;

    $user = User::find(1);

    $options = $user->options;

    $options['key'] = 'value';

    $user->options = $options;

    $user->save();

要使用更简洁的语法更新 JSON 属性的单个字段,您可以使属性可批量分配,并在调用 update 方法时使用 -> 运算符:

php
    $user = User::find(1);

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

数组对象和集合强制转换

尽管标准array转换对于许多应用程序来说已经足够了,但它确实有一些缺点。由于数组强制转换返回原始类型,因此无法直接更改数组的偏移量。例如,以下代码将触发 PHP 错误:

php
    $user = User::find(1);

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

为了解决这个问题,Laravel 提供了一个 AsArrayObject 强制转换,将您的 JSON 属性强制转换为 ArrayObject 类。此功能是使用 Laravel 的自定义强制转换实现实现的,它允许 Laravel 智能地缓存和转换可修改的对象,以便可以在不触发 PHP 错误的情况下修改单个偏移量。要使用 AsArrayObject 强制转换,只需将其分配给一个属性:

php
    use Illuminate\Database\Eloquent\Casts\AsArrayObject;

    /**
     * 获取应强制转换的属性
     *
     * @return array<string, string>
     */
    protected function casts(): array
    {
        return [
            'options' => AsArrayObject::class,
        ];
    }

同样,Laravel 提供了一个 AsCollection 强制转换,将您的 JSON 属性转换为 Laravel Collection 实例:

php
    use Illuminate\Database\Eloquent\Casts\AsCollection;

    /**
     * 获取应强制转换的属性
     *
     * @return array<string, string>
     */
    protected function casts(): array
    {
        return [
            'options' => AsCollection::class,
        ];
    }

如果你希望 AsCollection 强制转换实例化自定义集合类而不是 Laravel 的基本集合类,你可以提供集合类名称作为强制转换参数:

php
    use App\Collections\OptionCollection;
    use Illuminate\Database\Eloquent\Casts\AsCollection;

    /**
     * 获取应强制转换的属性
     *
     * @return array<string, string>
     */
    protected function casts(): array
    {
        return [
            'options' => AsCollection::using(OptionCollection::class),
        ];
    }

日期转换

默认情况下,Eloquent 会将 created_atupdated_at 列转换为 Carbon 实例,它扩展了 PHP DateTime 类并提供了各种有用的方法。您可以通过在模型的 casts 方法中定义其他日期强制转换来强制转换其他日期属性。通常,应使用 datetimeimmutable_datetime 强制类型强制转换日期。

定义datedatetime强制转换时,您还可以指定日期的格式。当模型序列化为数组或 JSON 时,将使用此格式:

php
    /**
     * 获取应强制转换的属性
     *
     * @return array<string, string>
     */
    protected function casts(): array
    {
        return [
            'created_at' => 'datetime:Y-m-d',
        ];
    }

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

您可以通过在模型上定义 serializeDate 方法来自定义模型的所有日期的默认序列化格式。此方法不会影响存储在数据库中的日期格式:

php
    /**
     * 准备数组/JSON 序列化的日期。
     */
    protected function serializeDate(DateTimeInterface $date): string
    {
        return $date->format('Y-m-d');
    }

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

php
    /**
     * 模型日期列的存储格式
     *
     * @var string
     */
    protected $dateFormat = 'U';

日期转换、序列化和时区

默认情况下,datedatetime强制转换会将日期序列化为 UTC ISO-8601 日期字符串 (YYYY-MM-DDTHH:MM:SS.uuuuuuZ),而不管应用程序的 timezone 配置选项中指定的时区如何。强烈建议您始终使用此序列化格式,并通过不要更改应用程序的 timezone 配置选项的默认 UTC 值,将应用程序的日期存储在 UTC 时区中。在整个应用程序中始终使用 UTC 时区将提供与用 PHP 和 JavaScript 编写的其他日期操作库的最大级别的互操作性。

如果自定义格式应用于datedatetime强制转换,例如 datetime:Y-m-d H:i:s,则在日期序列化期间将使用 Carbon 实例的内部时区。通常,这将是应用程序的 timezone 配置选项中指定的 timezone

枚举强制转换

Eloquent 还允许您将属性值转换为 PHP 枚举。为此,你可以在模型的 casts 方法中指定要强制转换的属性和枚举:

php
    use App\Enums\ServerStatus;

    /**
     * 获取应强制转换的属性。
     *
     * @return array<string, string>
     */
    protected function casts(): array
    {
        return [
            'status' => ServerStatus::class,
        ];
    }

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

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

        $server->save();
    }

转换枚举数组

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

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

    /**
     * 获取应强制转换的属性。
     *
     * @return array<string, string>
     */
    protected function casts(): array
    {
        return [
            'statuses' => AsEnumCollection::of(ServerStatus::class),
        ];
    }

加密转换

encrypted的转换将使用 Laravel 的内置encryption功能加密模型的属性值。此外,encrypted:arrayencrypted:collectionencrypted:objectAsEncryptedArrayObjectAsEncryptedCollection 强制转换的工作方式与未加密的转换类似;但是,正如您所料,底层值在存储在数据库中时是加密的。

由于加密文本的最终长度不可预测,并且比其纯文本长度长,因此请确保关联的数据库列为 TEXT 类型或更大。此外,由于这些值在数据库中是加密的,因此您将无法查询或搜索加密的属性值。

密钥轮换

您可能知道,Laravel 使用应用程序的应用程序配置文件 app 中指定的 key 配置值来加密字符串。通常,此值对应于 APP_KEY 环境变量的值。如果您需要轮换应用程序的加密密钥,则需要使用新密钥手动重新加密加密的属性。

查询时间强制转换

有时,您可能需要在执行查询时应用强制转换,例如从表中选择原始值时。例如,请考虑以下查询:

php
    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 方法来实现这一点:

php
    $users = User::select([
        'users.*',
        'last_posted_at' => Post::selectRaw('MAX(created_at)')
                ->whereColumn('user_id', 'users.id')
    ])->withCasts([
        'last_posted_at' => 'datetime'
    ])->get();

自定义 Casts

Laravel 具有各种内置的、有用的转换类型;但是,您可能偶尔需要定义自己的强制转换类型。要创建强制转换,请执行 make:cast Artisan 命令。新的 cast 类将放置在您的 app/Casts 目录中:

shell
php artisan make:cast Json

所有自定义强制转换类都实现 CastsAttributes 接口。实现此接口的类必须定义 getset 方法。get 方法负责将数据库中的原始值转换为强制转换值,而 set 方法应将强制转换值转换为可存储在数据库中的原始值。例如,我们将把内置的 json 强制类型重新实现为自定义强制类型:

php
    <?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);
        }
    }

定义自定义强制转换类型后,可以使用其类名将其附加到 model 属性:

php
    <?php

    namespace App\Models;

    use App\Casts\Json;
    use Illuminate\Database\Eloquent\Model;

    class User extends Model
    {
        /**
         * Get the attributes that should be cast.
         *
         * @return array<string, string>
         */
        protected function casts(): array
        {
            return [
                'options' => Json::class,
            ];
        }
    }

值对象转换

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

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

php
   <?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('The given value is not an Address instance.');
            }

            return [
                'address_line_one' => $value->lineOne,
                'address_line_two' => $value->lineTwo,
            ];
        }
    }

当强制转换为 value 对象时,对 value 对象所做的任何更改都会在保存模型之前自动同步回模型:

php
    use App\Models\User;

    $user = User::find(1);

    $user->address->lineOne = 'Updated Address Value';

    $user->save();

NOTE

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

值对象缓存

当强制转换为值对象的属性被解析时,它们由 Eloquent 缓存。因此,如果再次访问该属性,将返回相同的对象实例。

如果你想禁用自定义强制转换类的对象缓存行为,你可以在你的自定义强制转换类上声明一个 public withoutObjectCaching 属性:

php
class Address implements CastsAttributes
{
    public bool $withoutObjectCaching = true;

    // ...
}

数组 / JSON 序列化

当使用 toArraytoJson 方法将 Eloquent 模型转换为数组或 JSON 时,您的自定义强制转换值对象通常会被序列化,只要它们实现 Illuminate\Contracts\Support\Arrayable 和 JsonSerializable 接口。但是,当使用第三方库提供的值对象时,您可能无法将这些接口添加到对象中。

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

php
    /**
     * 获取值的序列化表示形式
     *
     * @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 选项来生成一个仅入站的强制转换类:

shell
php artisan make:cast Hash --inbound

仅入站强制转换的一个典型示例是“哈希”强制转换。例如,我们可以定义一个通过给定算法对入站值进行哈希处理的强制转换:

php
    <?php

    namespace App\Casts;

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

    class Hash implements CastsInboundAttributes
    {
        /**
         * 创建新的 cast 类实例
         */
        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);
        }
    }

强制转换参数

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

php
    /**
     * 获取应强制转换的属性
     *
     * @return array<string, string>
     */
    protected function casts(): array
    {
        return [
            'secret' => Hash::class.':sha256',
        ];
    }

可转换类 Castables

您可能希望允许应用程序的 value 对象定义它们自己的自定义强制转换类。除了将自定义强制转换类附加到模型之外,您还可以附加实现接口的 Illuminate\Contracts\Database\Eloquent\Castable value 对象类:

php
    use App\ValueObjects\Address;

    protected function casts(): array
    {
        return [
            'address' => Address::class,
        ];
    }

实现 Castable 接口的对象必须定义一个 castUsing 方法,该方法返回负责向 Castable 类转换和从 Castable 类转换的自定义 caster 类的类名:

php
    <?php

    namespace App\ValueObjects;

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

    class Address implements Castable
    {
        /**
         * 获取从/向此强制转换目标强制转换时要使用的 caster 类的名称
         *
         * @param  array<string, mixed>  $arguments
         */
        public static function castUsing(array $arguments): string
        {
            return AddressCast::class;
        }
    }

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

php
    use App\ValueObjects\Address;

    protected function casts(): array
    {
        return [
            'address' => Address::class.':argument',
        ];
    }

可转换类和匿名转换类

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

php
    <?php

    namespace App\ValueObjects;

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

    class Address implements Castable
    {
        // ...

        /**
         * 获取从/向此投射目标投射时要使用的 caster 类
         *
         * @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,
                    ];
                }
            };
        }
    }