什么是 Cast
Eloquent 的 Cast 是将从数据库取出的原始值转换为 PHP 数据类型的机制,保存时会反向转换回去。在 casts 方法中定义。
protected function casts(): array
{
return [
'is_admin' => 'boolean',
'settings' => 'array',
'created_at' => 'datetime',
];
}
内置 Cast 一览
Laravel 标准提供的 Cast 列表如下。
| Cast | 说明 |
|---|
integer / int | 转换为整数 |
float / double | 转换为浮点数 |
string | 转换为字符串 |
boolean / bool | 转换为布尔值(包括 0/1) |
array | JSON 字符串 ↔ PHP 数组 |
collection | JSON 字符串 ↔ Collection 实例 |
object | JSON 字符串 ↔ stdObject 实例 |
datetime | 字符串 ↔ Carbon 实例 |
immutable_datetime | 字符串 ↔ CarbonImmutable 实例 |
date | 字符串 ↔ Carbon(无时间) |
timestamp | 字符串 ↔ UNIX 时间戳 |
encrypted | 保存时加密,取出时解密 |
hashed | 保存时哈希(与只读 Cast 组合使用) |
AsStringable::class | 字符串 ↔ Stringable 对象 |
AsArrayObject::class | JSON ↔ ArrayObject 实例 |
AsCollection::class | JSON ↔ Collection 实例 |
AsArrayObject 与 AsCollection 为了让你能直接修改数组的特定偏移,在 Laravel 内部实际是以自定义 Cast 实现的。
创建自定义 Cast 类
当内置 Cast 无法满足转换需求时,可以通过实现 CastsAttributes 接口创建自定义 Cast。
接口定义
框架本体的契约定义如下。
// src/Illuminate/Contracts/Database/Eloquent/CastsAttributes.php
interface CastsAttributes
{
/**
* 将 DB 的原始值转换为 PHP 值(读取时)
*
* @param array<string, mixed> $attributes 模型的全部属性值
*/
public function get(Model $model, string $key, mixed $value, array $attributes);
/**
* 将 PHP 值转换为可存入 DB 的形式(写入时)
*
* @param array<string, mixed> $attributes 模型的全部属性值
*/
public function set(Model $model, string $key, mixed $value, array $attributes);
}
$attributes 参数中包含模型的全部属性,因此也可以做跨多列的转换(参见后文的 Value Object 模式)。
基础自定义 Cast 实现
使用 make:cast 命令生成模板。
php artisan make:cast AsMoney
将生成 app/Casts/AsMoney.php。举例来说,我们实现一个将金额(以整数存储)转换为 Money Value Object 的 Cast。
<?php
namespace App\Casts;
use App\ValueObjects\Money;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Database\Eloquent\Model;
class AsMoney implements CastsAttributes
{
/**
* 将 DB 的整数(以最小货币单位)转换为 Money 对象
*/
public function get(
Model $model,
string $key,
mixed $value,
array $attributes,
): Money {
return new Money((int) $value);
}
/**
* 将 Money 对象转换为可存入 DB 的整数
*/
public function set(
Model $model,
string $key,
mixed $value,
array $attributes,
): int {
if ($value instanceof Money) {
return $value->amount;
}
return (int) $value;
}
}
在模型上应用该 Cast。
<?php
namespace App\Models;
use App\Casts\AsMoney;
use Illuminate\Database\Eloquent\Model;
class Order extends Model
{
protected function casts(): array
{
return [
'price' => AsMoney::class,
];
}
}
这样 $order->price 就会返回 Money 实例。
Value Object Cast
将多个 DB 列打包为一个 Value Object 的模式。
实现示例:地址 Cast
将 address_line_one 与 address_line_two 两列合并为 Address Value Object。
<?php
namespace App\ValueObjects;
use Illuminate\Contracts\Support\Arrayable;
class Address implements Arrayable, \JsonSerializable
{
public function __construct(
public readonly string $lineOne,
public readonly string $lineTwo,
) {}
public function toArray(): array
{
return [
'line_one' => $this->lineOne,
'line_two' => $this->lineTwo,
];
}
public function jsonSerialize(): array
{
return $this->toArray();
}
}
<?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
{
/**
* 从多列组装出 Address 对象
*/
public function get(
Model $model,
string $key,
mixed $value,
array $attributes,
): Address {
return new Address(
$attributes['address_line_one'],
$attributes['address_line_two'],
);
}
/**
* 将 Address 对象拆分为按列的数组返回
*
* @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,
];
}
}
当 set 方法返回数组时,Eloquent 会以键作为列名,将值保存到相应的列。单列 Cast 时则返回字符串或整数。
在模型中应用并使用如下:
<?php
namespace App\Models;
use App\Casts\AsAddress;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
protected function casts(): array
{
return [
'address' => AsAddress::class,
];
}
}
$user = User::find(1);
// 以 Address 对象形式取出
echo $user->address->lineOne;
// 修改 Value Object 后,保存时会自动写入 DB
$user->address = new Address('123 Main St', 'Apt 4B');
$user->save();
Value Object 的缓存
被转换为 Value Object 的属性值会被 Eloquent 缓存。对同一属性访问两次会返回同一对象实例。
若要禁用缓存,在 Cast 类中添加 $withoutObjectCaching 属性。
class AsAddress implements CastsAttributes
{
public bool $withoutObjectCaching = true;
// ...
}
入站 Cast(仅写入)
只在写入 DB 时进行转换,读取时不做转换的 Cast。实现 CastsInboundAttributes 接口即可。
典型用途是哈希。仅在保存密码或密钥时转换,读取时直接返回哈希值。
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,
) {}
/**
* 保存时进行哈希
*/
public function set(
Model $model,
string $key,
mixed $value,
array $attributes,
): string {
return is_null($this->algorithm)
? bcrypt($value)
: hash($this->algorithm, $value);
}
}
Cast 参数
若要向自定义 Cast 传入参数,在类名后用冒号分隔指定,多参数用逗号分隔。
protected function casts(): array
{
return [
'secret' => AsHash::class.':sha256',
'data' => AsHash::class.':sha512',
];
}
参数会传递给 Cast 类的构造函数。
class AsHash implements CastsInboundAttributes
{
public function __construct(
protected string|null $algorithm = null, // 会传入 'sha256'
) {}
}
Castables:让 Value Object 持有 Cast 逻辑
实现 Castable 接口的 Value Object 拥有返回其 Cast 类的 castUsing 方法。这样模型侧无需知道具体的 Cast 类,领域逻辑更加整洁。
<?php
namespace App\ValueObjects;
use App\Casts\AsAddress;
use Illuminate\Contracts\Database\Eloquent\Castable;
class Address implements Castable
{
/**
* 返回用于该对象 Cast 的类
*
* @param array<string, mixed> $arguments
*/
public static function castUsing(array $arguments): string
{
return AsAddress::class;
}
}
模型侧改为指定 Value Object 类,而不是 Cast 类。
protected function casts(): array
{
return [
'address' => Address::class,
];
}
将 Castable 与匿名类结合,可以把 Value Object 与 Cast 逻辑写在同一个文件里。class Address implements Castable
{
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,
];
}
};
}
}
与 $appends、$hidden 的交互
Cast 与 $appends、$hidden 是相互独立的机制,但组合使用时需要注意。
class User extends Model
{
protected function casts(): array
{
return [
'address' => AsAddress::class,
];
}
// 在 toArray() / toJson() 时排除 address
protected $hidden = ['address_line_one', 'address_line_two'];
// 将 Cast 后的 address 加入序列化结果
protected $appends = ['address'];
}
$hidden 中要指定的是 DB 列名。请填写原列名(address_line_one, address_line_two),而不是通过 Cast 生成的属性名(address)。
运行时追加 Cast
当只想为特定查询或请求追加 Cast 时,使用 mergeCasts 方法。
$user = User::find(1);
$user->mergeCasts([
'extra_data' => 'array',
]);
下一步
Eloquent Observer 与模型事件
学习如何在模型的保存、删除等生命周期事件上挂钩以追加处理。