属性修改器 / 类型转换
这是一篇协同翻译的文章,你可以点击『我来翻译』按钮来参与翻译。
Eloquent: Mutators & Casting
Introduction
Accessors, mutators, and attribute casting allow you to transform Eloquent attribute values when you retrieve or set them on model instances. For example, you may want to use the Laravel encrypter to encrypt a value while it is stored in the database, and then automatically decrypt the attribute when you access it on an Eloquent model. Or, you may want to convert a JSON string that is stored in your database to an array when it is accessed via your Eloquent model.
Accessors and Mutators
Defining an Accessor
An accessor transforms an Eloquent attribute value when it is accessed. To define an accessor, create a protected method on your model to represent the accessible attribute. This method name should correspond to the "camel case" representation of the true underlying model attribute / database column when applicable.
In this example, we'll define an accessor for the first_name
attribute. The accessor will automatically be called by Eloquent when attempting to retrieve the value of the first_name
attribute. All attribute accessor / mutator methods must declare a return type-hint of 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),
);
}
}
All accessor methods return an Attribute
instance which defines how the attribute will be accessed and, optionally, mutated. In this example, we are only defining how the attribute will be accessed. To do so, we supply the get
argument to the Attribute
class constructor.
As you can see, the original value of the column is passed to the accessor, allowing you to manipulate and return the value. To access the value of the accessor, you may simply access the first_name
attribute on a model instance:
use App\Models\User;
$user = User::find(1);
$firstName = $user->first_name;
[!NOTE]
If you would like these computed values to be added to the array / JSON representations of your model, you will need to append them.
Building Value Objects From Multiple Attributes
Sometimes your accessor may need to transform multiple model attributes into a single "value object". To do so, your get
closure may accept a second argument of $attributes
, which will be automatically supplied to the closure and will contain an array of all of the model's current 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'],
),
);
}
Accessor Caching
When returning value objects from accessors, any changes made to the value object will automatically be synced back to the model before the model is saved. This is possible because Eloquent retains instances returned by accessors so it can return the same instance each time the accessor is invoked:
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();
However, you may sometimes wish to enable caching for primitive values like strings and booleans, particularly if they are computationally intensive. To accomplish this, you may invoke the shouldCache
method when defining your accessor:
protected function hash(): Attribute
{
return Attribute::make(
get: fn (string $value) => bcrypt(gzuncompress($value)),
)->shouldCache();
}
If you would like to disable the object caching behavior of attributes, you may invoke the withoutObjectCaching
method when defining the 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'],
),
)->withoutObjectCaching();
}
Defining a Mutator
A mutator transforms an Eloquent attribute value when it is set. To define a mutator, you may provide the set
argument when defining your attribute. Let's define a mutator for the first_name
attribute. This mutator will be automatically called when we attempt to set the value of the first_name
attribute on the model:
<?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),
);
}
}
The mutator closure will receive the value that is being set on the attribute, allowing you to manipulate the value and return the manipulated value. To use our mutator, we only need to set the first_name
attribute on an Eloquent model:
use App\Models\User;
$user = User::find(1);
$user->first_name = 'Sally';
In this example, the set
callback will be called with the value Sally
. The mutator will then apply the strtolower
function to the name and set its resulting value in the model's internal $attributes
array.
Mutating Multiple Attributes
Sometimes your mutator may need to set multiple attributes on the underlying model. To do so, you may return an array from the set
closure. Each key in the array should correspond with an underlying attribute / database column associated with the model:
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,
],
);
}
Attribute Casting
Attribute casting provides functionality similar to accessors and mutators without requiring you to define any additional methods on your model. Instead, your model's casts
method provides a convenient way of converting attributes to common data types.
The casts
method should return an array where the key is the name of the attribute being cast and the value is the type you wish to cast the column to. The supported cast types are:
array
AsUri::class
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
To demonstrate attribute casting, let's cast the is_admin
attribute, which is stored in our database as an integer (0
or 1
) to a boolean value:
<?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',
];
}
}
After defining the cast, the is_admin
attribute will always be cast to a boolean when you access it, even if the underlying value is stored in the database as an integer:
$user = App\Models\User::find(1);
if ($user->is_admin) {
// ...
}
If you need to add a new, temporary cast at runtime, you may use the mergeCasts
method. These cast definitions will be added to any of the casts already defined on the model:
$user->mergeCasts([
'is_admin' => 'integer',
'options' => 'object',
]);
[!WARNING]
Attributes that arenull
will not be cast. In addition, you should never define a cast (or an attribute) that has the same name as a relationship or assign a cast to the model's primary key.
Stringable Casting
You may use the Illuminate\Database\Eloquent\Casts\AsStringable
cast class to cast a model attribute to a fluent Illuminate\Support\Stringable object:
<?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,
];
}
}
Array and JSON Casting
The array
cast is particularly useful when working with columns that are stored as serialized JSON. For example, if your database has a JSON
or TEXT
field type that contains serialized JSON, adding the array
cast to that attribute will automatically deserialize the attribute to a PHP array when you access it on your Eloquent model:
<?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',
];
}
}
Once the cast is defined, you may access the options
attribute and it will automatically be deserialized from JSON into a PHP array. When you set the value of the options
attribute, the given array will automatically be serialized back into JSON for storage:
use App\Models\User;
$user = User::find(1);
$options = $user->options;
$options['key'] = 'value';
$user->options = $options;
$user->save();
To update a single field of a JSON attribute with a more terse syntax, you may make the attribute mass assignable and use the ->
operator when calling the update
method:
$user = User::find(1);
$user->update(['options->key' => 'value']);
JSON and Unicode
If you would like to store an array attribute as JSON with unescaped Unicode characters, you may use the json:unicode
cast:
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'options' => 'json:unicode',
];
}
Array Object and Collection Casting
Although the standard array
cast is sufficient for many applications, it does have some disadvantages. Since the array
cast returns a primitive type, it is not possible to mutate an offset of the array directly. For example, the following code will trigger a PHP error:
$user = User::find(1);
$user->options['key'] = $value;
To solve this, Laravel offers an AsArrayObject
cast that casts your JSON attribute to an ArrayObject class. This feature is implemented using Laravel's custom cast implementation, which allows Laravel to intelligently cache and transform the mutated object such that individual offsets may be modified without triggering a PHP error. To use the AsArrayObject
cast, simply assign it to an attribute:
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,
];
}
Similarly, Laravel offers an AsCollection
cast that casts your JSON attribute to a Laravel Collection instance:
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,
];
}
If you would like the AsCollection
cast to instantiate a custom collection class instead of Laravel's base collection class, you may provide the collection class name as a cast argument:
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),
];
}
The of
method may be used to indicate collection items should be mapped into a given class via the collection's mapInto method:
use App\ValueObjects\Option;
use Illuminate\Database\Eloquent\Casts\AsCollection;
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'options' => AsCollection::of(Option::class)
];
}
When mapping collections to objects, the object should implement the Illuminate\Contracts\Support\Arrayable
and JsonSerializable
interfaces to define how their instances should be serialized into the database as 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;
/**
* Create a new Option instance.
*/
public function __construct(array $data)
{
$this->name = $data['name'];
$this->value = $data['value'];
$this->isLocked = $data['is_locked'];
}
/**
* Get the instance as an array.
*
* @return array{name: string, data: string, is_locked: bool}
*/
public function toArray(): array
{
return [
'name' => $this->name,
'value' => $this->value,
'is_locked' => $this->isLocked,
];
}
/**
* Specify the data which should be serialized to JSON.
*
* @return array{name: string, data: string, is_locked: bool}
*/
public function jsonSerialize(): array
{
return $this->toArray();
}
}
Date Casting
By default, Eloquent will cast the created_at
and updated_at
columns to instances of Carbon, which extends the PHP DateTime
class and provides an assortment of helpful methods. You may cast additional date attributes by defining additional date casts within your model's casts
method. Typically, dates should be cast using the datetime
or immutable_datetime
cast types.
When defining a date
or datetime
cast, you may also specify the date's format. This format will be used when the model is serialized to an array or JSON:
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'created_at' => 'datetime:Y-m-d',
];
}
When a column is cast as a date, you may set the corresponding model attribute value to a UNIX timestamp, date string (Y-m-d
), date-time string, or a DateTime
/ Carbon
instance. The date's value will be correctly converted and stored in your database.
You may customize the default serialization format for all of your model's dates by defining a serializeDate
method on your model. This method does not affect how your dates are formatted for storage in the database:
/**
* Prepare a date for array / JSON serialization.
*/
protected function serializeDate(DateTimeInterface $date): string
{
return $date->format('Y-m-d');
}
To specify the format that should be used when actually storing a model's dates within your database, you should define a $dateFormat
property on your model:
/**
* The storage format of the model's date columns.
*
* @var string
*/
protected $dateFormat = 'U';
Date Casting, Serialization, and Timezones
By default, the date
and datetime
casts will serialize dates to a UTC ISO-8601 date string (YYYY-MM-DDTHH:MM:SS.uuuuuuZ
), regardless of the timezone specified in your application's timezone
configuration option. You are strongly encouraged to always use this serialization format, as well as to store your application's dates in the UTC timezone by not changing your application's timezone
configuration option from its default UTC
value. Consistently using the UTC timezone throughout your application will provide the maximum level of interoperability with other date manipulation libraries written in PHP and JavaScript.
If a custom format is applied to the date
or datetime
cast, such as datetime:Y-m-d H:i:s
, the inner timezone of the Carbon instance will be used during date serialization. Typically, this will be the timezone specified in your application's timezone
configuration option. However, it's important to note that timestamp
columns such as created_at
and updated_at
are exempt from this behavior and are always formatted in UTC, regardless of the application's timezone setting.
Enum Casting
Eloquent also allows you to cast your attribute values to PHP Enums. To accomplish this, you may specify the attribute and enum you wish to cast in your model's casts
method:
use App\Enums\ServerStatus;
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'status' => ServerStatus::class,
];
}
Once you have defined the cast on your model, the specified attribute will be automatically cast to and from an enum when you interact with the attribute:
if ($server->status == ServerStatus::Provisioned) {
$server->status = ServerStatus::Ready;
$server->save();
}
Casting Arrays of Enums
Sometimes you may need your model to store an array of enum values within a single column. To accomplish this, you may utilize the AsEnumArrayObject
or AsEnumCollection
casts provided by Laravel:
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 Casting
The encrypted
cast will encrypt a model's attribute value using Laravel's built-in encryption features. In addition, the encrypted:array
, encrypted:collection
, encrypted:object
, AsEncryptedArrayObject
, and AsEncryptedCollection
casts work like their unencrypted counterparts; however, as you might expect, the underlying value is encrypted when stored in your database.
As the final length of the encrypted text is not predictable and is longer than its plain text counterpart, make sure the associated database column is of TEXT
type or larger. In addition, since the values are encrypted in the database, you will not be able to query or search encrypted attribute values.
Key Rotation
As you may know, Laravel encrypts strings using the key
configuration value specified in your application's app
configuration file. Typically, this value corresponds to the value of the APP_KEY
environment variable. If you need to rotate your application's encryption key, you will need to manually re-encrypt your encrypted attributes using the new key.
Query Time Casting
Sometimes you may need to apply casts while executing a query, such as when selecting a raw value from a table. For example, consider the following query:
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();
The last_posted_at
attribute on the results of this query will be a simple string. It would be wonderful if we could apply a datetime
cast to this attribute when executing the query. Thankfully, we may accomplish this using the withCasts
method:
$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,
];
}
}
值对象类型转换
不限于将值转换为原始类型,你还可以将值转换为对象。定义将值转换为对象的自定义转换与转换为原始类型非常相似;但是,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
方法。调用带 --inbound
选项的 make:cast
Artisan 命令来生成仅入站的转换类:
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\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,
];
}
};
}
}
本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
推荐文章: