Eloquent: 修改器 & 类型转换
简介
当你在 Eloquent 模型实例中获取或设置某些属性值的时候,访问器和修改器允许你对 Eloquent 属性值进行格式化。例如,你可能需要使用 Laravel 加密器 来加密保存在数据库中的值, 而在使用 Eloquent 模型访问该属性的时候自动进行解密其值。
或者,当通过 Eloquent 模型访问存储在数据库的 JSON 字符串时,你可能希望将其转换为数组。
访问器 & 修改器
定义一个访问器
访问器会在访问一个模型的属性时转换 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
{
/**
* 获取用户的名字。
*
* @return \Illuminate\Database\Eloquent\Casts\Attribute
*/
protected function firstName(): Attribute
{
return Attribute::make(
get: fn ($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;
/**
* 与用户地址交互。
*
* @return \Illuminate\Database\Eloquent\Casts\Attribute
*/
public function address(): Attribute
{
return new Attribute(
get: fn ($value, $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 ($value) => bcrypt(gzuncompress($value)),
)->shouldCache();
}
如果要禁用属性的缓存,可以在定义属性时调用 withoutObjectCaching
方法:
/**
* 与 user 的 address 交互.
*
* @return \Illuminate\Database\Eloquent\Casts\Attribute
*/
public function address(): Attribute
{
return (new Attribute(
get: fn ($value, $attributes) => new Address(
$attributes['address_line_one'],
$attributes['address_line_two'],
),
))->withoutObjectCaching();
}
定义修改器
修改器会在设置属性时生效。要定义修改器,可以在定义属性时提供 set
参数。让我们为 first_name
属性定义一个修改器。这个修改器将会在我们修改 first_name
属性的值时自动调用:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
/**
* 与 user 的first name 交互
*
* @param string $value
* @return \Illuminate\Database\Eloquent\Casts\Attribute
*/
protected function firstName(): Attribute
{
return new Attribute(
get: fn ($value) => ucfirst($value),
set: fn ($value) => strtolower($value),
);
}
}
修改器的闭包会接收将要设置的值,并允许我们使用和返回该值。要使该修改器生效,只需在模型上设置 first_name
即可:
use App\Models\User;
$user = User::find(1);
$user->first_name = 'Sally';
在本例中,值 Sally
将会触发 set
回调。然后,修改器会使用 strtolower
函数处理姓名,并将结果值设置在模型的 $attributes
数组中。
修改多个属性
有时你的修改器可能需要修改底层模型的多个属性。 为此,你的 set
闭包可以返回一个数组,数组中的每个键都应该与模型的属性/数据库列相对应:
use App\Support\Address;
use Illuminate\Database\Eloquent\Casts\Attribute;
/**
* 与user模型的address交互.
*
* @return \Illuminate\Database\Eloquent\Casts\Attribute
*/
public function address(): Attribute
{
return new Attribute(
get: fn ($value, $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:
<digits>
double
encrypted
encrypted:array
encrypted:collection
encrypted:object
float
integer
object
real
string
timestamp
示例, 让我们把以整数 (0
或 1
) 形式存储在数据库中的 is_admin
属性转成布尔值:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
/**
* 类型转换
*
* @var array
*/
protected $casts = [
'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
{
/**
* The attributes that should be cast.
*
* @var array
*/
protected $casts = [
'directory' => AsStringable::class,
];
}
数组 & JSON 转换
当你在数据库存储序列化的 JSON 的数据时, array
类型的转换非常有用。比如:如果你的数据库具有被序列化为 JSON 的 JSON
或 TEXT
字段类型,并且在 Eloquent 模型中加入了 array
类型转换,那么当你访问的时候就会自动被转换为 PHP 数组:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
/**
* The attributes that should be cast.
*
* @var array
*/
protected $casts = [
'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();
当使用 update
方法更新 JSON 属性的单个字段时,可以使用 ->
操作符让语法更加简洁:
$user = User::find(1);
$user->update(['options->key' => 'value']);
数组对象 & 集合 类型转换
虽然标准的 array
类型转换对于许多应用程序来说已经足够了,但它确实有一些缺点。由于 array
类型转换返回一个基础类型,因此不可能直接改变数组键的值。例如,以下代码将触发一个 PHP 错误:
$user = User::find(1);
$user->options['key'] = $value;
为了解决这个问题,Laravel 提供了一个 AsArrayObject
类型转换,它将 JSON 属性转换为一个 数组对象 类。这个特性是使用 Laravel 的 自定义类型转换 实现的,它允许 Laravel 智能地缓存和转换修改的对象,这样可以在不触发 PHP 错误的情况下修改各个键的值。要使用 AsArrayObject
类型转换,只需将其指定给一个属性即可:
use Illuminate\Database\Eloquent\Casts\AsArrayObject;
/**
* 类型转换.
*
* @var array
*/
protected $casts = [
'options' => AsArrayObject::class,
];
类似的,Laravel 提供了一个 AsCollection
类型转换,它将 JSON 属性转换为 Laravel 集合 实例:
use Illuminate\Database\Eloquent\Casts\AsCollection;
/**
* The attributes that should be cast.
*
* @var array
*/
protected $casts = [
'options' => AsCollection::class,
];