> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# Eloquent 自定义类型转换

> 讲解如何通过实现 CastsAttributes 接口创建自定义 Cast。同时介绍 Value Object 模式、入站 Cast、Castables 等进阶模式。

## 什么是 Cast

Eloquent 的 Cast 是将从数据库取出的原始值转换为 PHP 数据类型的机制，保存时会反向转换回去。在 `casts` 方法中定义。

```php theme={null}
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 实例     |

<Info>
  `AsArrayObject` 与 `AsCollection` 为了让你能直接修改数组的特定偏移，在 Laravel 内部实际是以自定义 Cast 实现的。
</Info>

## 创建自定义 Cast 类

当内置 Cast 无法满足转换需求时，可以通过实现 `CastsAttributes` 接口创建自定义 Cast。

### 接口定义

框架本体的契约定义如下。

```php theme={null}
// 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` 命令生成模板。

```bash theme={null}
php artisan make:cast AsMoney
```

将生成 `app/Casts/AsMoney.php`。举例来说，我们实现一个将金额（以整数存储）转换为 `Money` Value Object 的 Cast。

```php theme={null}
<?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 theme={null}
<?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 theme={null}
<?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 theme={null}
<?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,
        ];
    }
}
```

<Info>
  当 `set` 方法返回数组时，Eloquent 会以键作为列名，将值保存到相应的列。单列 Cast 时则返回字符串或整数。
</Info>

在模型中应用并使用如下：

```php theme={null}
<?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,
        ];
    }
}
```

```php theme={null}
$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` 属性。

```php theme={null}
class AsAddress implements CastsAttributes
{
    public bool $withoutObjectCaching = true;

    // ...
}
```

## 入站 Cast（仅写入）

只在写入 DB 时进行转换，读取时不做转换的 Cast。实现 `CastsInboundAttributes` 接口即可。

典型用途是哈希。仅在保存密码或密钥时转换，读取时直接返回哈希值。

```bash theme={null}
php artisan make:cast AsHash --inbound
```

```php theme={null}
<?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 传入参数，在类名后用冒号分隔指定，多参数用逗号分隔。

```php theme={null}
protected function casts(): array
{
    return [
        'secret' => AsHash::class.':sha256',
        'data'   => AsHash::class.':sha512',
    ];
}
```

参数会传递给 Cast 类的构造函数。

```php theme={null}
class AsHash implements CastsInboundAttributes
{
    public function __construct(
        protected string|null $algorithm = null, // 会传入 'sha256'
    ) {}
}
```

## Castables：让 Value Object 持有 Cast 逻辑

实现 `Castable` 接口的 Value Object 拥有返回其 Cast 类的 `castUsing` 方法。这样模型侧无需知道具体的 Cast 类，领域逻辑更加整洁。

```php theme={null}
<?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 类。

```php theme={null}
protected function casts(): array
{
    return [
        'address' => Address::class,
    ];
}
```

<Tip>
  将 `Castable` 与匿名类结合，可以把 Value Object 与 Cast 逻辑写在同一个文件里。

  ```php theme={null}
  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,
                  ];
              }
          };
      }
  }
  ```
</Tip>

## 与 `$appends`、`$hidden` 的交互

Cast 与 `$appends`、`$hidden` 是相互独立的机制，但组合使用时需要注意。

```php theme={null}
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'];
}
```

<Warning>
  `$hidden` 中要指定的是 DB 列名。请填写原列名（`address_line_one`, `address_line_two`），而不是通过 Cast 生成的属性名（`address`）。
</Warning>

## 运行时追加 Cast

当只想为特定查询或请求追加 Cast 时，使用 `mergeCasts` 方法。

```php theme={null}
$user = User::find(1);

$user->mergeCasts([
    'extra_data' => 'array',
]);
```

## 下一步

<Card title="Eloquent Observer 与模型事件" icon="bell" href="/zh/advanced/eloquent-observers">
  学习如何在模型的保存、删除等生命周期事件上挂钩以追加处理。
</Card>


## Related topics

- [Eloquent 访问器、修改器与类型转换](/zh/eloquent-mutators.md)
- [Eloquent 作用域](/zh/advanced/eloquent-scopes.md)
- [Eloquent 序列化](/zh/eloquent-serialization.md)
- [集合的高阶消息](/zh/advanced/higher-order-messages.md)
- [InteractsWithData trait](/zh/advanced/interacts-with-data.md)
