跳至内容

Eloquent:Mutator 和 Cast

简介

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

访问器和 Mutator

定义访问器

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

在此示例中,我们将为 first_name 属性定义一个访问器。在尝试检索 first_name 属性的值时,Eloquent 会自动调用该访问器。所有属性访问器/mutator 方法必须声明 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;
lightbulb

如果您希望将这些计算值添加到模型的数组/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

Mutator 在设置 Eloquent 属性值时对其进行转换。要定义 Mutator,您可以在定义属性时提供 set 参数。让我们为 first_name 属性定义一个 Mutator。在尝试设置模型上 first_name 属性的值时,将自动调用此 Mutator。

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

Mutator 闭包将接收正在设置为属性的值,允许您操作该值并返回操作后的值。要使用我们的 Mutator,我们只需要设置 Eloquent 模型上的 first_name 属性即可。

use App\Models\User;
 
$user = User::find(1);
 
$user->first_name = 'Sally';

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

转换多个属性

有时您的 Mutator 可能需要在底层模型上设置多个属性。为此,您可以从 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,
],
);
}

属性转换

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

casts 方法应返回一个数组,其中键是要转换的属性的名称,值是要转换到的类型。支持的转换类型为:

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

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

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
{
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'directory' => AsStringable::class,
];
}
}

数组和 JSON 转换

array 转换在处理存储为序列化 JSON 的列时特别有用。例如,如果您的数据库具有包含序列化 JSON 的 JSONTEXT 字段类型,则向该属性添加 array 转换将在您访问 Eloquent 模型上的属性时自动将该属性反序列化为 PHP 数组。

<?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 [
'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 类型转换对于许多应用来说已经足够了,但它也有一些缺点。由于 array 类型转换返回的是一个基本类型,因此无法直接修改数组的偏移量。例如,以下代码将触发 PHP 错误

$user = User::find(1);
 
$user->options['key'] = $value;

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

use Illuminate\Database\Eloquent\Casts\AsArrayObject;
 
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'options' => AsArrayObject::class,
];
}

类似地,Laravel 提供了一个 AsCollection 类型转换,它将你的 JSON 属性转换为 Laravel 的 Collection 实例

use Illuminate\Database\Eloquent\Casts\AsCollection;
 
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'options' => AsCollection::class,
];
}

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

use App\Collections\OptionCollection;
use Illuminate\Database\Eloquent\Casts\AsCollection;
 
/**
* Get the attributes that should be cast.
*
* @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 时使用

/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'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 日期字符串 (YYYY-MM-DDTHH:MM:SS.uuuuuuZ),而不管应用程序的 timezone 配置选项中指定的时区是什么。强烈建议始终使用此序列化格式,以及通过不将应用程序的 timezone 配置选项从其默认 UTC 值更改来在 UTC 时区中存储应用程序的日期。在整个应用程序中始终如一地使用 UTC 时区将提供与用 PHP 和 JavaScript 编写的其他日期处理库的最大程度的互操作性。

如果将自定义格式应用于 datedatetime 类型转换,例如 datetime:Y-m-d H:i:s,则在日期序列化期间将使用 Carbon 实例的内部时区。通常,这将是应用程序的 timezone 配置选项中指定的时区。但是,需要注意的是,created_atupdated_attimestamp 列不受此行为的影响,并且始终以 UTC 格式化,而不管应用程序的时区设置是什么。

枚举转换

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

use App\Enums\ServerStatus;
 
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'status' => ServerStatus::class,
];
}

在模型上定义了类型转换后,当你与属性交互时,指定的属性将自动转换为枚举并从枚举转换回来

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

转换枚举数组

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

use App\Enums\ServerStatus;
use Illuminate\Database\Eloquent\Casts\AsEnumCollection;
 
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'statuses' => AsEnumCollection::of(ServerStatus::class),
];
}

加密转换

encrypted 类型转换将使用 Laravel 内置的 加密 功能加密模型的属性值。此外,encrypted:arrayencrypted:collectionencrypted:objectAsEncryptedArrayObjectAsEncryptedCollection 类型转换的工作方式与其未加密的对应类型相同;但是,正如你可能预期的那样,底层值在存储到数据库中时会被加密。

由于加密文本的最终长度是不可预测的,并且比其纯文本对应文本更长,因此请确保关联的数据库列为 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 Json

所有自定义类型转换类都实现了 CastsAttributes 接口。实现此接口的类必须定义一个 get 方法和一个 set 方法。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
{
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'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();
lightbulb

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

值对象缓存

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

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

class Address implements CastsAttributes
{
public bool $withoutObjectCaching = true;
 
// ...
}

数组/JSON 序列化

当使用 toArraytoJson 方法将 Eloquent 模型转换为数组或 JSON 时,你的自定义类型转换值对象通常也会被序列化,只要它们实现了 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|null $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);
}
}

转换参数

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

/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'secret' => Hash::class.':sha256',
];
}

可转换

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

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

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

<?php
 
namespace App\ValueObjects;
 
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\ValueObjects\Address;
 
protected function casts(): array
{
return [
'address' => Address::class.':argument',
];
}

Castables 和匿名类型转换类

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

<?php
 
namespace App\ValueObjects;
 
use Illuminate\Contracts\Database\Eloquent\Castable;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
 
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,
];
}
};
}
}