> ## 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 的自訂 Cast

> 解說如何實作 CastsAttributes 介面來建立自訂 cast。同時介紹 Value Object 模式、Inbound 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 timestamp       |
| `encrypted`            | 儲存時加密、取得時解密               |
| `hashed`               | 儲存時 hash 化（與唯讀 cast 結合使用） |
| `AsStringable::class`  | 字串 ↔ Stringable 物件        |
| `AsArrayObject::class` | JSON ↔ ArrayObject 實例     |
| `AsCollection::class`  | JSON ↔ Collection 實例      |

<Info>
  `AsArrayObject` 與 `AsCollection` 為了讓陣列的特定 offset 可以直接被修改，於 Laravel 內部作為自訂 cast 實作。
</Info>

## 建立自訂 Cast 類別

若內建 cast 無法對應所需轉換，可建立實作 `CastsAttributes` 介面的自訂 cast。

### 介面的定義

框架本體的 contract 定義如下。

```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 會將 key 視為欄位名，將值分別存入各欄位。單一欄位的 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;

    // ...
}
```

## Inbound Cast（僅寫入時）

僅於寫入 DB 時進行轉換，讀取時不轉換的 cast。實作 `CastsInboundAttributes` 介面。

典型用途為 hash 化。僅在儲存密碼或秘密值時進行轉換，讀取時直接回傳 hash 值。

```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,
    ) {}

    /**
     * 儲存時 hash 化
     */
    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;
    }
}
```

模型端不再指定 cast 類別，改指定 Value Object 類別。

```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-TW/advanced/eloquent-observers">
  學習掛勾模型的儲存、刪除等生命週期事件以新增處理的方法。
</Card>


## Related topics

- [Eloquent 存取器、修改器與型別轉換（Casts）](/zh-TW/eloquent-mutators.md)
- [Eloquent 的 Scope](/zh-TW/advanced/eloquent-scopes.md)
- [速率限制的自訂](/zh-TW/advanced/rate-limiting.md)
- [自訂 Agent](/zh-TW/packages/laravel-copilot-sdk/custom-agents.md)
- [Eloquent 集合](/zh-TW/eloquent-collections.md)
