Eloquent:修改器 & 类型转换
简介
访问器、修改器和属性类型转换允许您在检索或设置模型实例时转换 Eloquent 属性值。例如,您可能希望使用 Laravel 加密器在将值存储在数据库中时对其进行加密,然后在您访问 Eloquent 模型上的属性时自动解密该属性。或者,您可能希望将存储在数据库中的 JSON 字符串在通过 Eloquent 模型访问时转换为数组。
访问器和修改器
定义访问器
访问器在访问时转换 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{ /** * 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;
如果您希望将这些计算值添加到模型的数组/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
-
AsStringable::class
-
boolean
-
collection
-
date
-
datetime
-
immutable_date
-
immutable_datetime
-
decimal:<精度>
-
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{ /** * Get the attributes that should be cast. * * @return array<string, string> */ protected function casts(): array { return [ 'directory' => AsStringable::class, ]; }}
数组和 JSON 类型转换
当处理存储为序列化 JSON 的列时,array
类型转换特别有用。例如,如果您的数据库有一个包含序列化 JSON 的 JSON
或 TEXT
字段类型,则将 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 集合实例。
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_at
和 updated_at
列转换为 Carbon 的实例,它扩展了 PHP 的 DateTime
类并提供了各种有用的方法。您可以通过在模型的 casts
方法中定义其他日期类型转换来转换其他日期属性。通常,日期应使用 datetime
或 immutable_datetime
类型转换类型进行转换。
在定义 date
或 datetime
类型转换时,您还可以指定日期的格式。当模型序列化为数组或 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';
日期类型转换、序列化和时区
默认情况下,无论应用程序的 timezone
配置选项中指定的时区如何,date
和 datetime
类型转换都会将日期序列化为 UTC ISO-8601 日期字符串 (YYYY-MM-DDTHH:MM:SS.uuuuuuZ
)。强烈建议您始终使用此序列化格式,并通过不更改应用程序的 timezone
配置选项的默认 UTC
值来将应用程序的日期存储在 UTC 时区中。在整个应用程序中一致使用 UTC 时区将为 PHP 和 JavaScript 中编写的其他日期操作库提供最大程度的互操作性。
如果将自定义格式应用于 date
或 datetime
类型转换,例如 datetime:Y-m-d H:i:s
,则在日期序列化期间将使用 Carbon 实例的内部时区。通常,这将是应用程序的 timezone
配置选项中指定的时区。但是,需要注意的是,timestamp
列(例如 created_at
和 updated_at
)不受此行为的约束,并且始终以 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 提供的 AsEnumArrayObject
或 AsEnumCollection
类型转换。
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: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 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
值具有两个公共属性:lineOne
和 lineTwo
。
<?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();
如果您计划将包含值对象的 Eloquent 模型序列化为 JSON 或数组,则应在值对象上实现 Illuminate\Contracts\Support\Arrayable
和 JsonSerializable
接口。
值对象缓存
当解析转换为值对象的属性时,它们会被 Eloquent 缓存。因此,如果再次访问该属性,将返回相同的对象实例。
如果您想禁用自定义类型转换类的对象缓存行为,可以在自定义类型转换类上声明一个公共的 withoutObjectCaching
属性。
class Address implements CastsAttributes{ public bool $withoutObjectCaching = true; // ...}
数组 / JSON 序列化
当使用 toArray
和 toJson
方法将 Eloquent 模型转换为数组或 JSON 时,您的自定义类型转换值对象通常也会被序列化,只要它们实现了 Illuminate\Contracts\Support\Arrayable
和 JsonSerializable
接口。但是,当使用第三方库提供的值对象时,您可能无法将这些接口添加到对象中。
因此,您可以指定您的自定义类型转换类将负责序列化值对象。为此,您的自定义类型转换类应实现 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
方法。可以使用 --inbound
选项调用 make:cast
Artisan 命令以生成仅限入站的类型转换类。
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', ];}
Castable & 匿名类型转换类
通过将“castable”与 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, ]; } }; }}