属性修改器
Eloquent: 修改器 & 类型转换
简介
当你在 Eloquent 模型实例中获取或设置某些属性值的时候,访问器和修改器允许你对 Eloquent 属性值进行格式化。例如,你可能需要使用 Laravel 加密器 来加密保存在数据库中的值,而在使用 Eloquent 模型访问该属性的时候自动进行解密其值。
或者,当通过 Eloquent 模型访问存储在数据库的 JSON 字符串时,你可能希望将其转换为数组。
访问器 & 修改器
定义一个访问器
若要定义一个访问器, 则需在模型上创建一个 getFooAttribute
方法,要访问的 Foo
字段需使用「驼峰式」命名。
在这个示例中,我们将为 first_name
属性定义一个访问器。当 Eloquent 尝试获取 first_name
属性时,将自动调用此访问器:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
/**
* 获取用户的姓名
*
* @param string $value
* @return string
*/
public function getFirstNameAttribute($value)
{
return ucfirst($value);
}
}
如你所见,字段的原始值被传递到访问器中,允许你对它进行处理并返回结果。如果想获取被修改后的值,你可以在模型实例上访问 first_name
属性:
use App\Models\User;
$user = User::find(1);
$firstName = $user->first_name;
当然,你也可以通过已有的属性值,使用访问器返回新的计算值:
/**
* 获取用户的姓名
*
* @return string
*/
public function getFullNameAttribute()
{
return "{$this->first_name} {$this->last_name}";
}
技巧:如果你需要将这些计算值添加到模型的数组 / JSON 中,你得追加它们。
定义一个修改器
若要定义一个修改器,则需在模型上面定义 setFooAttribute
方法。要访问的 Foo
字段使用「驼峰式」命名。让我们再来定义一个 first_name
属性的修改器。当我们尝试在模式上在设置 first_name
属性值时,该修改器将被自动调用:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
/**
* 设置用户的姓名
*
* @param string $value
* @return void
*/
public function setFirstNameAttribute($value)
{
$this->attributes['first_name'] = strtolower($value);
}
}
修改器会获取属性已经被设置的值,并允许你修改并且将其值设置到 Eloquent 模型内部的 $attributes
属性上。
使用修改器,我们只需要设置 Eloquent 模型的 first_name
属性即可:
use App\Models\User;
$user = User::find(1);
$user->first_name = 'Sally';
在这个例子中,setFirstNameAttribute
方法在调用的时候接受 Sally
这个值作为参数。 接着修改器会调用 strtolower
函数并将处理的结果设置到内部的 $attributes
数组。
注意:通过
Eloquent
进行批量更新时,saved
和updated
模型事件将不会在更新模型时触发。
即当你使用查询构造器的insert
、update
等方法时,修改器无效。
属性转换
属性转换提供了类似于访问器和修改器的功能,且无需在模型上定义任何其他方法。模型中的 $casts
属性提供了一个便利的方法来将属性转换为常见的数据类型。
$casts
属性应是一个数组,且数组的键是那些需要被转换的属性名称,值则是你希望转换的数据类型。支持转换的数据类型有:
array
boolean
collection
date
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) {
//
}
注意:
null
值属性将不会被转换。此外,禁止定义与关联同名的类型转换 ( 或属性 ) 。
数组 & JSON 转换
当你在数据库存储序列化的 JSON 的数据时,array
类型的转换非常有用。比如:如果你的数据库具有被序列化为 JSON 的 JSON
或 TEXT
字段类型,并且在 Eloquent 模型中加入了 array
类型转换,那么当你访问的时候就会自动被转换为 PHP 数组:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
/**
* 类型转换
*
* @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 Collection 实例:
use Illuminate\Database\Eloquent\Casts\AsCollection;
/**
* 类型转换
*
* @var array
*/
protected $casts = [
'options' => AsCollection::class,
];
Date 转换
默认情况下,Eloquent 会将 created_at
和 updated_at
字段转换为 Carbon 实例,它继承了 PHP 原生的 DateTime
类并提供了各种有用的方法。你可以通过在模型的 $cast
属性数组中定义额外的日期类型转换,用来转换其他的日期属性。通常来说,日期应该使用 datetime
类型转换来转换。
当使用 date
或 datetime
类型转换时,你也可以指定日期的格式。这种格式会被用在 模型序列化为数组或者 JSON:
/**
* The attributes that should be cast.
*
* @var array
*/
protected $casts = [
'created_at' => 'datetime:Y-m-d',
];
将列类型转换为日期时,可以将其值设置为 UNIX 时间戳、日期字符串(Y-m-d
)、日期时间字符串或 DateTime
/ Carbon
实例。日期值将会被准确的转换并存储在数据库中。
通过在模型中定义 serializeDate
方法,你可以自定义所有模型日期的默认序列化格式。此方法不会影响日期在数据库中存储的格式:
/**
* 为 array / JSON 序列化准备日期格式
*
* @param \DateTimeInterface $date
* @return string
*/
protected function serializeDate(DateTimeInterface $date)
{
return $date->format('Y-m-d');
}
在模型上定义 $dateFormat
属性后,模型的日期将会以你指定的格式实际存储于数据库中:
/**
* 模型日期列的存储格式
*
* @var string
*/
protected $dateFormat = 'U';
查询时转换
有时候需要在查询执行过程中对特定属性进行类型转换,例如需要从数据库表中获取数据的时候。举个例子,请参考以下查询:
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 内置了多种常用的类型转换。但是,用户偶尔会需要将数据转换成自定义类型。现在,该需求可以通过定义一个实现 CastsAttributes
接口的类来完成。
实现了该接口的类必须事先定义一个 get
和 set
方法。get
方法负责将从数据库中获取的原始数据转换成对应的类型,而 set
方法则是将数据转换成对应的数据库类型以便存入数据库中。举个例子,下面我们将内置的 json
类型转换以自定义类型转换的形式重新实现一遍:
<?php
namespace App\Casts;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
class Json implements CastsAttributes
{
/**
* 将取出的数据进行转换
*
* @param \Illuminate\Database\Eloquent\Model $model
* @param string $key
* @param mixed $value
* @param array $attributes
* @return array
*/
public function get($model, $key, $value, $attributes)
{
return json_decode($value, true);
}
/**
* 转换成将要进行存储的值
*
* @param \Illuminate\Database\Eloquent\Model $model
* @param string $key
* @param array $value
* @param array $attributes
* @return string
*/
public function set($model, $key, $value, $attributes)
{
return json_encode($value);
}
}
定义好自定义类型转换后,可以使用其类名称将其附加到模型属性里:
<?php
namespace App\Models;
use App\Casts\Json;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
/**
* 应被强制转换的属性
*
* @var array
*/
protected $casts = [
'options' => Json::class,
];
}
值对象转换
你不仅可以将数据转换成原生的数据类型,还可以将数据转换成对象。两种自定义类型转换的定义方式非常类似。但是将数据转换成对象的自定义转换类中的 set
方法需要返回键值对数组,用于设置原始、可存储的值到对应的模型中。
举个例子,定义一个自定义类型转换类用于将多个模型属性值转换成单个 Address
值对象,假设 Address
对象有两个公有属性 lineOne
和 lineTwo
:
<?php
namespace App\Casts;
use App\Models\Address as AddressModel;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use InvalidArgumentException;
class Address implements CastsAttributes
{
/**
* 将取出的数据进行转换
*
* @param \Illuminate\Database\Eloquent\Model $model
* @param string $key
* @param mixed $value
* @param array $attributes
* @return \App\Models\Address
*/
public function get($model, $key, $value, $attributes)
{
return new AddressModel(
$attributes['address_line_one'],
$attributes['address_line_two']
);
}
/**
* 转换成将要进行存储的值
*
* @param \Illuminate\Database\Eloquent\Model $model
* @param string $key
* @param \App\Models\Address $value
* @param array $attributes
* @return array
*/
public function set($model, $key, $value, $attributes)
{
if (! $value instanceof AddressModel) {
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
接口。
数组 / JSON 序列化
当使用 toArray
和 toJson
方法将 Eloquent 的模型转换为数组或 JSON 时,你自定义的类型转换值对象通常会被序列化,只要它们实现了 illighte\Contracts\Support\Arrayable
和 JsonSerializable
接口。
然而,在使用第三方库提供的值对象时,你可能无法将这些接口添加到对象中。
因此,你可以指定你自定义的类型转换类,它将负责序列化成值对象。为此,你自定义的类型转换类需要实现 illighte\Contracts\Database\Eloquent\SerializesCastableAttributes
接口。此接口声明类应包含 serialize
方法,该方法应返回值对象的序列化形式:
/**
* 获取值的序列化表示形式
*
* @param \Illuminate\Database\Eloquent\Model $model
* @param string $key
* @param mixed $value
* @param array $attributes
* @return mixed
*/
public function serialize($model, string $key, $value, array $attributes)
{
return (string) $value;
}
入站转换
有时候,你可能只需要对写入模型的属性值进行类型转换而不需要对从模型中获取的属性值进行任何处理。一个典型入站类型转换的例子就是 「hashing」。入站类型转换类需要实现 CastsInboundAttributes
接口,只需要实现 set
方法。
<?php
namespace App\Casts;
use Illuminate\Contracts\Database\Eloquent\CastsInboundAttributes;
class Hash implements CastsInboundAttributes
{
/**
* 哈希算法
*
* @var string
*/
protected $algorithm;
/**
* 创建一个新的类型转换类实例
*
* @param string|null $algorithm
* @return void
*/
public function __construct($algorithm = null)
{
$this->algorithm = $algorithm;
}
/**
* 转换成将要进行存储的值
*
* @param \Illuminate\Database\Eloquent\Model $model
* @param string $key
* @param array $value
* @param array $attributes
* @return string
*/
public function set($model, $key, $value, $attributes)
{
return is_null($this->algorithm)
? bcrypt($value)
: hash($this->algorithm, $value);
}
}
类型转换参数
当将自定义类型转换附加到模型时,可以指定传入的类型转换参数。传入类型转换参数需使用 :
将参数与类名分隔,多个参数之间使用逗号分隔。这些参数将会传递到类型转换类的构造函数中:
/**
* The attributes that should be cast.
*
* @var array
*/
protected $casts = [
'secret' => Hash::class.':sha256',
];
Castables
你可能希望允许应用程序的值对象能够定义自己的自定义类型转换类。你可以附加一个实现 Illuminate\Contracts\Database\Eloquent\Castable
接口的值对象类,而无需将自定义的类型转换类附加到模型中:
use App\Models\Address;
protected $casts = [
'address' => Address::class,
];
实现 Castable
接口的对象必须定义一个 castUsing
方法,此方法返回的是负责将 Castable
类进行自定义转换的转换器类名:
<?php
namespace App\Models;
use Illuminate\Contracts\Database\Eloquent\Castable;
use App\Casts\Address as AddressCast;
class Address implements Castable
{
/**
* 获取转换器的类名用以转换当前类型转换对象
*
* @param array $arguments
* @return string
*/
public static function castUsing(array $arguments)
{
return AddressCast::class;
}
}
使用 Castable
类时,仍然可以在 $casts
定义中提供参数。参数将传递给 castUsing
方法:
use App\Models\Address;
protected $casts = [
'address' => Address::class.':argument',
];
Castables & 匿名类型转换类
通过将 castables
与 PHP 的 匿名类 相结合,可以将值对象及其转换逻辑定义为单个可转换对象。为此,请从值对象的 castUsing
方法返回一个匿名类。匿名类应该实现 CastsAttributes
接口:
<?php
namespace App\Models;
use Illuminate\Contracts\Database\Eloquent\Castable;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
class Address implements Castable
{
// ...
/**
* 获取转换器类用以转换当前类型转换对象
*
* @param array $arguments
* @return object|string
*/
public static function castUsing(array $arguments)
{
return new class implements CastsAttributes
{
public function get($model, $key, $value, $attributes)
{
return new Address(
$attributes['address_line_one'],
$attributes['address_line_two']
);
}
public function set($model, $key, $value, $attributes)
{
return [
'address_line_one' => $value->lineOne,
'address_line_two' => $value->lineTwo,
];
}
};
}
}