Lzh on GitHub

引言

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

访问器与修改器

定义访问器

访问器(Accessor)用于在访问 Eloquent 模型属性时对其值进行转换。要定义访问器,需要在模型中创建一个受保护的方法,该方法对应可访问的属性。方法名应使用“驼峰命名法”,对应实际的模型属性或数据库字段名。

在下面的示例中,我们为 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
{
    /**
     * 获取用户的名字。
     */
    protected function firstName(): Attribute
    {
        return Attribute::make(
            get: fn (string $value) => ucfirst($value),
        );
    }
}

所有访问器方法都返回一个 Attribute 实例,用于定义属性的访问方式(以及可选的修改方式)。在这个示例中,我们只定义了属性的访问方式,通过 get 参数传入闭包即可。闭包接收原始列值作为参数,你可以在其中对值进行处理并返回。访问访问器的值非常简单:

use App\Models\User;

$user = User::find(1);

$firstName = $user->first_name;

如果希望这些计算属性在模型的数组或 JSON 表示中显示,需要将它们追加到模型的 $appends 属性中。

从多个属性构建值对象

有时访问器需要将多个模型属性组合成一个“值对象”。此时,get 闭包可以接受第二个参数 $attributes,它包含模型的所有当前属性:

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

访问器缓存

当访问器返回值对象时,对值对象的修改会在模型保存前自动同步回模型,因为 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 方法:

/**
 * 与用户地址交互。
 */
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
{
    /**
     * 与用户的 first_name 属性交互。
     */
    protected function firstName(): Attribute
    {
        return Attribute::make(
            get: fn (string $value) => ucfirst($value),
            set: fn (string $value) => strtolower($value),
        );
    }
}

set 闭包会接收到正在赋给属性的值,你可以在其中对该值进行处理,然后返回处理后的结果。要使用这个修改器,只需给 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;

/**
 * 与用户的 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,
        ],
    );
}

在这个示例中,set 闭包会返回一个数组,将 Address 对象的属性分别映射到模型对应的数据库字段上,实现同时修改多个底层属性。

属性类型转换

属性类型转换提供了类似访问器(Accessor)和修改器(Mutator)的功能,但无需在模型上定义额外的方法。相反,你可以通过模型的 casts 方法,方便地将属性转换为常用的数据类型。

casts 方法应返回一个数组,其中 键(key) 是要转换的属性名称,值(value) 是希望将该属性转换成的类型。支持的类型包括:

  • array
  • AsFluent::class
  • AsStringable::class
  • AsUri::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
{
    /**
     * 获取需要进行类型转换的属性。
     *
     * @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 方法。这些类型转换会被合并到模型已定义的 casts 中:

$user->mergeCasts([
    'is_admin' => 'integer',
    'options' => 'object',
]);
  • 值为 null 的属性不会进行类型转换。
  • 永远不要为与关系(relationship)同名的属性定义类型转换,也不要为模型主键分配类型转换。

Stringable 类型转换

你可以使用 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,
        ];
    }
}

这样,访问 directory 属性时,你可以直接使用 Stringable 提供的链式字符串操作方法。

数组与 JSON 类型转换

数组类型转换在处理存储为序列化 JSON 的列时尤其有用。例如,如果数据库中有一个 JSON 或 TEXT 类型字段包含序列化的 JSON,为该属性添加数组类型转换后,在 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

如果希望将数组属性以 JSON 格式存储,并且保留 Unicode 字符不转义,可以使用 json:unicode 类型转换:

protected function casts(): array
{
    return [
        'options' => 'json:unicode',
    ];
}

ArrayObject 与 Collection 类型转换

虽然标准的数组类型转换适用于大多数场景,但它也有一些限制:由于数组类型转换返回的是原始类型,无法直接修改数组的某个偏移量,例如下面的代码会触发 PHP 错误:

$user = User::find(1);

$user->options['key'] = $value; // 会报错

为了解决这个问题,Laravel 提供了 AsArrayObject 类型转换,它会将 JSON 属性转换为 ArrayObject 对象。使用 Laravel 自定义类型转换实现后,可以智能缓存和转换对象,从而允许直接修改单个偏移量而不会报错:

use Illuminate\Database\Eloquent\Casts\AsArrayObject;

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

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

use Illuminate\Database\Eloquent\Casts\AsCollection;

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

如果希望 AsCollection 转换时使用自定义集合类,而不是 Laravel 的基础 Collection 类,可以在类型转换中指定集合类名:

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

protected function casts(): array
{
    return [
        'options' => AsCollection::using(OptionCollection::class),
    ];
}

如果希望将集合中的每个元素映射为特定对象类,可以使用 of 方法:

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

protected function casts(): array
{
    return [
        'options' => AsCollection::of(Option::class),
    ];
}

当将集合映射为对象时,对象应实现 Illuminate\Contracts\Support\ArrayableJsonSerializable 接口,以定义如何将实例序列化为数据库中的 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, value: mixed, is_locked: bool}
     */
    public function toArray(): array
    {
        return [
            'name' => $this->name,
            'value' => $this->value,
            'is_locked' => $this->isLocked,
        ];
    }

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

日期类型转换

日期类型转换(Date Casting)

默认情况下,Eloquent 会将 created_atupdated_at 列转换为 Carbon 实例,Carbon 是 PHP 的 DateTime 类的扩展,提供了丰富的日期处理方法。你可以在模型的 casts 方法中定义额外的日期类型转换,以将更多属性转换为日期类型。通常,日期属性应使用 datetimeimmutable_datetime 类型转换。

定义日期或日期时间类型转换时,可以指定日期格式。该格式会在模型序列化为数组或 JSON 时生效:

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

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

你可以通过在模型上定义 serializeDate 方法,自定义模型日期属性序列化为数组或 JSON 时的格式。注意,这不会影响日期在数据库中的存储格式:

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

如果希望指定模型日期在数据库中的存储格式,可以在模型上定义 $dateFormat 属性:

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

日期类型转换、序列化与时区

默认情况下,datedatetime 类型转换会将日期序列化为 UTC ISO-8601 日期字符串YYYY-MM-DDTHH:MM:SS.uuuuuuZ),不受应用程序时区配置影响。强烈建议始终使用此序列化格式,并将应用程序的日期存储在 UTC 时区。在整个应用中统一使用 UTC 时区,可以最大化与 PHP 和 JavaScript 的其他日期处理库的兼容性。

如果对日期或日期时间类型转换应用了自定义格式,例如 datetime:Y-m-d H:i:s,则在序列化时会使用 Carbon 实例的内部时区,通常为应用程序配置的时区。但需要注意,像 created_atupdated_at 这样的时间戳列不受此影响,它们始终以 UTC 格式序列化,无论应用程序时区设置为何。

枚举类型转换

Eloquent 允许将模型属性值转换为 PHP 枚举(Enum)。你只需在模型的 casts 方法中指定属性和对应的枚举类:

use App\Enums\ServerStatus;

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

定义转换后,当你访问或设置该属性时,Eloquent 会自动在枚举对象和底层值之间进行转换:

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

    $server->save();
}

数组枚举类型转换

有时,你可能希望在单个数据库列中存储 枚举值数组。Laravel 提供了 AsEnumArrayObjectAsEnumCollection 类型转换来实现这一功能。

例如,使用 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),
    ];
}

这样,访问 statuses 属性时,你会得到一个包含枚举对象的集合,赋值时也可以使用枚举对象数组,Eloquent 会自动进行序列化和反序列化。

加密类型转换

encrypted 类型转换可以使用 Laravel 内置的加密功能对模型属性值进行加密。此外,encrypted:arrayencrypted:collectionencrypted:objectAsEncryptedArrayObjectAsEncryptedCollection 类型转换的使用方式与其未加密版本类似,但如你所料,存储到数据库时其底层值会被加密。

由于加密后的文本长度不可预测,并且通常比明文更长,请确保数据库中对应的列为 TEXT 类型或更大。另外,由于数据库中存储的是加密值,你将无法对加密属性进行查询或搜索。

密钥轮换(Key Rotation)

如你所知,Laravel 使用应用配置文件中指定的 key 值对字符串进行加密,通常对应于 APP_KEY 环境变量的值。如果需要更换应用的加密密钥,你必须手动使用新密钥重新加密所有已加密的属性值。

查询时类型转换

有时,你可能希望在执行查询时对属性应用类型转换,例如从表中选择原始值(raw value)。

例如,考虑以下查询:

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();

这样,last_posted_at 属性在查询结果中就会被自动转换为 Carbon 日期对象,而不是简单的字符串。

自定义类型转换

Laravel 内置了多种有用的类型转换,但有时你可能需要定义自己的类型转换。要创建自定义类型转换,可以使用 Artisan 命令:

php artisan make:cast AsJson

新建的类型转换类会被放置在 app/Casts 目录下。

所有自定义类型转换类都需要实现 CastsAttributes 接口。实现该接口的类必须定义两个方法:

  • 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,
        ];
    }
}

这样,访问 options 属性时,Eloquent 会自动调用 AsJson 自定义类型转换的 getset 方法,实现 JSON 的自动编码与解码。

值对象类型转换

在 Eloquent 中,你不仅可以将属性转换为原始类型,还可以将其转换为对象。自定义将属性转换为对象的类型转换与原始类型类似;不同之处在于:

  • 如果你的值对象涉及多个数据库列,则 set 方法必须返回一个键值数组,用于设置模型的原始可存储值。
  • 如果值对象只影响单个列,则直接返回可存储值即可。

假设我们有一个 Address 值对象,包含两个公共属性:lineOnelineTwo。我们可以定义如下自定义类型转换:

<?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
{
    /**
     * 转换给定值为 Address 对象
     *
     * @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('给定值不是 Address 实例。');
        }

        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 序列化

当使用 Eloquent 模型的 toArraytoJson 方法将模型转换为数组或 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;
}

通过这种方式,即使值对象无法直接实现 Arrayable 或 JsonSerializable,也可以在模型序列化为数组或 JSON 时正确输出。

入站类型转换

有时,你可能需要编写一个自定义类型转换类,仅在给模型设置属性值时进行转换,而在获取属性值时不进行任何操作。

此类 仅入站的自定义类型转换 应实现 CastsInboundAttributes 接口,该接口只要求定义一个 set 方法。

可以使用 Artisan 命令 make:cast 并添加 --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);
    }
}

在此示例中:

  • 如果未指定算法,则使用 Laravel 内置的 bcrypt 函数进行哈希。
  • 如果指定了算法,则使用 PHP 的 hash 函数对值进行哈希。

这种类型转换只在 写入数据库时生效,读取模型属性时不会改变原始值。

类型转换参数

下面是对你提供内容的优雅简体中文翻译:


为自定义类型转换传递参数

在将自定义类型转换绑定到模型时,可以通过在类名后使用 冒号(:) 来指定参数,如果有多个参数,可以用 逗号分隔。这些参数会被传递给自定义类型转换类的构造函数。

例如:

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

在这个例子中,sha256 参数会传递给 AsHash 类型转换类的构造函数,用于指定哈希算法。

这样,你就可以在绑定类型转换时灵活地为自定义转换类传递所需参数。

比较转换值

如果你希望自定义类型转换的两个值如何比较,以判断它们是否发生变化,可以让自定义类型转换类实现 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;
}

通过实现 compare 方法,你可以自定义比较逻辑,例如忽略大小写、比较对象属性,或使用自定义算法判断值是否等效,从而更精细地控制模型的更新行为。

可转换属性

在某些情况下,你可能希望应用中的值对象自行定义其自定义类型转换类,而不是在模型中直接绑定自定义类型转换类。

为此,可以让值对象类实现 Illuminate\Contracts\Database\Eloquent\Castable 接口:

use App\ValueObjects\Address;

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

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

<?php

namespace App\ValueObjects;

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

class Address implements Castable
{
    /**
     * 获取用于此值对象类型转换的 caster 类名
     *
     * @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',
    ];
}

Castables 与匿名类型转换类

结合 Castable 与 PHP 的匿名类,可以将值对象和其类型转换逻辑定义为一个单独的可转换对象。

为此,可在值对象的 castUsing 方法中返回一个匿名类,该匿名类实现 CastsAttributes 接口:

<?php

namespace App\ValueObjects;

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

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

通过这种方式,值对象本身就定义了其类型转换逻辑,无需在模型中单独绑定自定义转换类。