引言

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

访问器与修改器

定义访问器

访问器会在属性被访问时转换 Eloquent 属性值。要定义访问器,需在模型上创建一个 protected 方法来表示可访问的属性。该方法的名称应与实际底层模型属性/数据库列的“驼峰式命名”表示相对应(如适用)。

在本示例中,我们将为 first_name 属性定义一个访问器。当尝试检索 first_name 属性的值时,Eloquent 会自动调用该访问器。所有属性访问器/修改器方法都必须声明返回类型 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 实例,该实例定义了属性的访问方式,也可选择性地定义其修改方式。在本示例中,我们仅定义了属性的访问方式。为此,我们向 Attribute 类的构造函数传入了 get 参数。

正如你所见,列的原始值会被传递给访问器,以便你对其进行处理并返回结果。若要获取访问器的返回值,只需在模型实例上访问 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;

/**
 * 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();
}

定义修改器

修改器会在属性被设置时转换 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),
        );
    }
}

修改器闭包会接收即将设置到属性上的值,以便你对该值进行处理并返回处理后的结果。若要使用该修改器,我们只需在 Eloquent 模型上设置 first_name 属性即可:

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 方法提供了一种便捷方式,可将属性转换为常见的数据类型。

casts 方法应返回一个数组,其中键是待转换属性的名称,值是希望将列转换为的类型。支持的转换类型如下:

  • array
  • AsFluent::class
  • AsStringable::class
  • AsUri::class
  • AsVector::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 属性(该属性在数据库中存储为整数(0 或 1))转换为布尔值:

<?php

namespace App\Models;

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 [
            '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
{
    /**
     * 获取需要类型转换的属性。
     *
     * @return array<string, string>
     */
    protected function casts(): array
    {
        return [
            'directory' => AsStringable::class,
        ];
    }
}

数组与 JSON 类型转换

array 类型转换在处理以序列化 JSON 存储的列时特别有用。例如,如果您的数据库中有包含序列化 JSON 的 JSON 或 TEXT 字段类型,对该属性添加 array 类型转换后,当您在 Eloquent 模型中访问该属性时,会自动将其反序列化为 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 以进行存储:

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

JSON 与 Unicode

如果您希望将数组属性以不转义 Unicode 字符的 JSON 形式存储,可以使用 json:unicode 类型转换:

/**
 * 获取需要类型转换的属性。
 *
 * @return array<string, string>
 */
protected function casts(): array
{
    return [
        'options' => 'json:unicode',
    ];
}

数组对象与集合类型转换

尽管标准的 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;

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

同样,Laravel 还提供了 AsCollection 类型转换,可将您的 JSON 属性转换为 Laravel 的Collection实例:

use Illuminate\Database\Eloquent\Casts\AsCollection;

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

默认情况下,将 null 分配给使用 AsArrayObject 或 AsCollection 类型转换的属性时,会在数据库中存储 JSON 的 null 值。如果您希望 null 值以原生 SQL 的 NULL 值存储,可在定义类型转换时调用 nullable 方法:

use Illuminate\Database\Eloquent\Casts\AsCollection;

/**
 * 获取需要类型转换的属性。
 *
 * @return array<string, string>
 */
protected function casts(): array
{
    return [
        'options' => AsCollection::nullable(),
    ];
}

nullable 方法也可与自定义集合类结合使用:

'options' => AsCollection::nullable(OptionCollection::class),

AsArrayObject 类型转换也提供了具有相同行为的 nullable 方法。

如果您希望 AsCollection 类型转换实例化自定义集合类,而非 Laravel 的基础集合类,可将集合类名作为类型转换参数传入:

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

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

可以使用 of 方法指定集合元素应通过集合的mapInto 方法映射到给定类:

use App\ValueObjects\Option;
use Illuminate\Database\Eloquent\Casts\AsCollection;

/**
 * 获取需要类型转换的属性。
 *
 * @return array<string, string>
 */
protected function casts(): array
{
    return [
        'options' => AsCollection::of(Option::class)
    ];
}

当将集合映射为对象时,该对象应实现 Illuminate\Contracts\Support\Arrayable 和 JsonSerializable 接口,以定义其实例如何序列化为 JSON 并存储到数据库:

<?php

namespace App\ValueObjects;

use Illuminate\Contracts\Support\Arrayable;
use JsonSerializable;

class Option implements Arrayable, JsonSerializable
{
    public string $name;
    public mixed $value;
    public bool $isLocked;

    /**
     * 创建新的 Option 实例。
     */
    public function __construct(array $data)
    {
        $this->name = $data['name'];
        $this->value = $data['value'];
        $this->isLocked = $data['is_locked'];
    }

    /**
     * 以数组形式获取实例。
     *
     * @return array{name: string, data: string, is_locked: bool}
     */
    public function toArray(): array
    {
        return [
            'name' => $this->name,
            'value' => $this->value,
            'is_locked' => $this->isLocked,
        ];
    }

    /**
     * 指定需要序列化为 JSON 的数据。
     *
     * @return array{name: string, data: string, is_locked: bool}
     */
    public function jsonSerialize(): array
    {
        return $this->toArray();
    }
}

向量类型转换

您可以使用 Illuminate\Database\Eloquent\Casts\AsVector 类型转换类,将数据库向量列与 PHP 数组进行相互转换:

use Illuminate\Database\Eloquent\Casts\AsVector;

/**
 * 获取需要类型转换的属性。
 *
 * @return array<string, string>
 */
protected function casts(): array
{
    return [
        'embedding' => AsVector::class,
    ];
}

设置属性时,该类型转换接受 PHP 数组或 Arrayable 实例(如 Laravel 集合)。获取属性时,该类型转换会返回浮点数数组。

二进制类型转换

如果您的 Eloquent 模型除了自动递增的 ID 列外,还包含二进制类型的 uuid 或 ulid 列,您可以使用 AsBinary 类型转换,在属性值与其二进制表示之间自动进行转换:

use Illuminate\Database\Eloquent\Casts\AsBinary;

/**
 * 获取需要类型转换的属性。
 *
 * @return array<string, string>
 */
protected function casts(): array
{
    return [
        'uuid' => AsBinary::uuid(),
        'ulid' => AsBinary::ulid(),
    ];
}

在模型上定义该类型转换后,您可以将 UUID / ULID 属性值设置为对象实例或字符串。Eloquent 会自动将该值转换为其二进制表示。获取属性值时,您将始终得到纯文本字符串值:

use Illuminate\Support\Str;

$user->uuid = Str::uuid();

return $user->uuid;

// "6e8cdeed-2f32-40bd-b109-1e4405be2140"

日期转换

默认情况下,Eloquent 会将 created_at 和 updated_at 列转换为 Carbon 的实例,Carbon 继承自 PHP 的 DateTime 类,并提供了多种实用方法。你可以在模型的 casts 方法中定义额外的日期转换,以转换更多日期属性。通常,日期应使用 datetime 或 immutable_datetime 转换类型进行转换。

定义 date 或 datetime 转换时,你也可以指定日期格式。该格式将在模型被序列化为数组或 JSON时使用:

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

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

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

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

若要指定在数据库中实际存储模型日期时使用的格式,应在模型的 Table 特性上使用 dateFormat 参数:

use Illuminate\Database\Eloquent\Attributes\Table;

#[Table(dateFormat: 'U')]
class Flight extends Model
{
    // ...
}

日期转换、序列化与时区

默认情况下,date 和 datetime 转换会将日期序列化为 UTC ISO-8601 日期字符串(YYYY-MM-DDTHH:MM:SS.uuuuuuZ),无论应用程序的 timezone 配置选项中指定的时区是什么。强烈建议你始终使用此序列化格式,并且通过不将应用程序的 timezone 配置选项从默认的 UTC 值更改,将应用程序的日期以 UTC 时区存储。在整个应用程序中一致使用 UTC 时区,可最大程度提升与用 PHP 和 JavaScript 编写的其他日期操作库的互操作性。

如果对 date 或 datetime 转换应用了自定义格式,例如 datetime:Y-m-d H:i:s,则在日期序列化时会使用 Carbon 实例的内部时区。通常,该时区会是应用程序的 timezone 配置选项中指定的时区。不过需要注意的是,像 created_at 和 updated_at 这类的 timestamp 列不受此行为限制,无论应用程序的时区设置如何,它们始终以 UTC 格式呈现。

枚举转换

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

use App\Enums\ServerStatus;

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

在模型上定义好转换后,当你操作该属性时,指定的属性会自动与枚举进行相互转换:

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

    $server->save();
}

转换枚举数组

有时你可能需要让模型在单个列中存储一组枚举值。要实现这一点,可以使用 Laravel 提供的 AsEnumArrayObject 或 AsEnumCollection 转换:

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 内置的加密功能对模型的属性值进行加密。此外,encrypted:array、encrypted:collection、encrypted:object、AsEncryptedArrayObject 和 AsEncryptedCollection 转换的作用与它们未加密的对应转换类似;不过,正如你所预期的,存储到数据库中时,底层值会被加密。

由于加密文本的最终长度无法预测,且比对应的明文更长,所以请确保相关数据库列的类型为 TEXT 或更大。另外,由于值在数据库中是加密的,你将无法对加密的属性值进行查询或搜索。

密钥轮换

你或许知道,Laravel 会使用应用程序 app 配置文件中指定的 key 配置值对字符串进行加密。通常,该值对应 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 Artisan 命令。新的转换类会放置在 app/Casts 目录下:

php artisan make:cast AsJson

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

<?php

namespace App\Casts;

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

class AsJson 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\AsJson;
use Illuminate\Database\Eloquent\Model;

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

值对象转换

您不必局限于将值转换为原始类型。您还可以将值转换为对象。定义将值转换为对象的自定义转换(cast)与转换为原始类型非常相似;不过,如果您的值对象包含多个数据库列,set 方法必须返回一个键/值对数组,该数组将被用于在模型上设置原始、可存储的值。如果您的值对象只影响单个列,您只需返回可存储的值即可。

举个例子,我们将定义一个自定义转换类,该类会把多个模型值转换为单个 Address 值对象。我们假设 Address 值对象具有两个公共属性:lineOne 和 lineTwo:

<?php

namespace App\Casts;

use App\ValueObjects\Address;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Database\Eloquent\Model;
use InvalidArgumentException;

class AsAddress implements CastsAttributes
{
    /**
     * 转换给定的值。
     *
     * @param  array<string, mixed>  $attributes
     */
    public function get(
        Model $model,
        string $key,
        mixed $value,
        array $attributes,
    ): Address {
        return new Address(
            $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 Address) {
            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 缓存。因此,如果再次访问该属性,将返回同一个对象实例。

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

class AsAddress implements CastsAttributes
{
    public bool $withoutObjectCaching = true;

    // ...
}

数组 / JSON 序列化

当使用 toArray 和 toJson 方法将 Eloquent 模型转换为数组或 JSON 时,只要您的自定义转换值对象实现了 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 方法。可以使用 make:cast Artisan 命令并搭配 --inbound 选项来生成一个仅入站的转换类:

php artisan make:cast AsHash --inbound

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

<?php

namespace App\Casts;

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

class AsHash 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);
    }
}

转换参数

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

/**
 * 获取应该进行转换的属性。
 *
 * @return array<string, string>
 */
protected function casts(): array
{
    return [
        'secret' => AsHash::class.':sha256',
    ];
}

比较转换后的值

如果您想定义两个给定的转换值应如何比较以确定它们是否发生了更改,您的自定义转换类可以实现 Illuminate\Contracts\Database\Eloquent\ComparesCastableAttributes 接口。这使您能够对哪些值会被 Eloquent 视为已更改,从而在更新模型时保存到数据库,进行细粒度控制。

该接口规定您的类应包含一个 compare 方法,该方法应在给定值被视为相等时返回 true:

/**
 * 判断给定的值是否相等。
 *
 * @param  \Illuminate\Database\Eloquent\Model  $model
 * @param  string  $key
 * @param  mixed  $firstValue
 * @param  mixed  $secondValue
 * @return bool
 */
public function compare(
    Model $model,
    string $key,
    mixed $firstValue,
    mixed $secondValue
): bool {
    return $firstValue === $secondValue;
}

可转换对象(Castables)

您可能希望允许应用中的值对象定义它们自己的自定义转换类。您可以选择不将自定义转换类附加到模型上,而是附加一个实现了 Illuminate\Contracts\Database\Eloquent\Castable 接口的值对象类:

use App\ValueObjects\Address;

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

实现了 Castable 接口的对象必须定义一个 castUsing 方法,该方法返回负责在该 Castable 类与自定义转换类之间进行转换的转换器类的类名:

<?php

namespace App\ValueObjects;

use Illuminate\Contracts\Database\Eloquent\Castable;
use App\Casts\AsAddress;

class Address implements Castable
{
    /**
     * 获取在从该转换目标进行转换/转换为该转换目标时要使用的转换器类的名称。
     *
     * @param  array<string, mixed>  $arguments
     */
    public static function castUsing(array $arguments): string
    {
        return AsAddress::class;
    }
}

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

use App\ValueObjects\Address;

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

可转换对象与匿名转换类

通过将“可转换对象”与 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,
                ];
            }
        };
    }
}