> ## 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 存取器、修改器與型別轉換（Casts）

> 說明 Eloquent 模型在取得與儲存屬性值時進行轉換的機制，並實務性地介紹存取器 / 修改器的定義方式與內建轉換的使用方法。

## 簡介

**存取器（Accessor）**、\*\*修改器（Mutator）**與**屬性轉換（Cast）\*\*是 Eloquent 模型在模型實例中取得、設定屬性值時進行轉換的機制。

* **存取器**：加工從資料庫取得的原始值後傳給應用程式
* **修改器**：加工由應用程式設定的值後儲存至資料庫
* **轉換**：不必額外撰寫方法，即可宣告式地定義屬性型別的轉換

```mermaid theme={null}
flowchart LR
  DB["資料庫<br>（原始值）"] -->|"取得時<br>存取器 / 轉換"| APP["應用程式<br>（轉換後的值）"]
  APP -->|"儲存時<br>修改器 / 轉換"| DB
```

## 定義存取器

要定義存取器，請在模型中加入 `protected` 方法。方法名稱使用駝峰式命名，回傳型別為 `Illuminate\Database\Eloquent\Casts\Attribute`。

```php theme={null}
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    /**
     * 取得使用者的名字。
     */
    protected function firstName(): Attribute
    {
        return Attribute::make(
            get: fn (string $value) => ucfirst($value),
        );
    }
}
```

DB 的原始值會傳入 `get` 閉包中。你可以從模型實例以 `first_name` 屬性存取。

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

$firstName = $user->first_name; // 已套用 ucfirst()
```

<Info>
  若想讓存取器算出的值也出現在 JSON / 陣列中，請將對應的 snake\_case 名稱加入模型的 `$appends` 屬性。
</Info>

### 由多個屬性生成值物件

`get` 閉包的第 2 個參數是 `$attributes`（模型的所有屬性）。可以將多個欄位組合，回傳單一值物件。

```php theme={null}
use App\Support\Address;
use Illuminate\Database\Eloquent\Casts\Attribute;

protected function address(): Attribute
{
    return Attribute::make(
        get: fn (mixed $value, array $attributes) => new Address(
            $attributes['address_line_one'],
            $attributes['address_line_two'],
        ),
    );
}
```

### 存取器的快取

回傳值物件的存取器，Eloquent 會自動快取以回傳同一個實例。若字串或數值等基本型也想要快取，可呼叫 `shouldCache()`。

```php theme={null}
protected function hash(): Attribute
{
    return Attribute::make(
        get: fn (string $value) => bcrypt(gzuncompress($value)),
    )->shouldCache();
}
```

若要停用物件快取，可使用 `withoutObjectCaching()`。

```php theme={null}
protected function address(): Attribute
{
    return Attribute::make(
        get: fn (mixed $value, array $attributes) => new Address(
            $attributes['address_line_one'],
            $attributes['address_line_two'],
        ),
    )->withoutObjectCaching();
}
```

## 定義修改器

修改器透過 `Attribute::make()` 的 `set` 參數定義。可以與存取器整合在同一個方法中。

```php theme={null}
protected function firstName(): Attribute
{
    return Attribute::make(
        get: fn (string $value) => ucfirst($value),
        set: fn (string $value) => strtolower($value),
    );
}
```

當你為模型指派值時，`set` 閉包會被呼叫。

```php theme={null}
$user = User::find(1);
$user->first_name = 'SALLY'; // 會套用 strtolower() 後儲存為 'sally'
```

### 一次寫入多個屬性

從 `set` 閉包回傳陣列，可一次更新多個欄位。

```php theme={null}
use App\Support\Address;
use Illuminate\Database\Eloquent\Casts\Attribute;

protected function address(): Attribute
{
    return Attribute::make(
        get: fn (mixed $value, array $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,
        ],
    );
}
```

## 屬性轉換

\*\*轉換（Cast）\*\*是一種不必撰寫存取器 / 修改器，即可宣告屬性型別轉換的簡便方式。透過模型的 `casts()` 方法回傳陣列。

```php theme={null}
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    protected function casts(): array
    {
        return [
            'is_admin'   => 'boolean',
            'score'      => 'float',
            'settings'   => 'array',
            'created_at' => 'datetime',
        ];
    }
}
```

### 內建轉換一覽

| 轉換                          | 說明             |
| --------------------------- | -------------- |
| `integer` / `int`           | 整數             |
| `float` / `double` / `real` | 浮點數            |
| `decimal:<位數>`              | 指定位數的小數        |
| `string`                    | 字串             |
| `boolean` / `bool`          | 布林（包含 `0`/`1`） |
| `array`                     | JSON 與陣列互轉     |
| `object`                    | JSON 與物件互轉     |
| `collection`                | JSON 轉為集合      |
| `date`                      | 日期（Carbon）     |
| `datetime`                  | 日期時間（Carbon）   |
| `immutable_date`            | 不可變日期          |
| `immutable_datetime`        | 不可變日期時間        |
| `timestamp`                 | UNIX 時間戳       |
| `hashed`                    | 儲存時 hash       |
| `encrypted`                 | 儲存時加密          |

<Warning>
  `null` 屬性不會被轉換。此外，請勿定義與關聯名稱相同的轉換，或對主鍵進行轉換。
</Warning>

### Stringable 轉換

使用 `AsStringable` 可將屬性視為 `Illuminate\Support\Stringable` 物件。

```php theme={null}
use Illuminate\Database\Eloquent\Casts\AsStringable;

protected function casts(): array
{
    return [
        'bio' => AsStringable::class,
    ];
}
```

## 陣列 / JSON 轉換

JSON / TEXT 欄位可以透明地視為 PHP 陣列處理。

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

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

// 自動作為 PHP 陣列取得
$options = $user->options;

// 修改後儲存時會自動 JSON 序列化
$user->options = array_merge($options, ['theme' => 'dark']);
$user->save();
```

也可以用 `->` 運算子只更新 JSON 中特定的 key。

```php theme={null}
$user->update(['options->theme' => 'dark']);
```

### AsArrayObject / AsCollection 轉換

標準的 `array` 轉換若嘗試直接修改陣列特定位置會出錯。可使用 `AsArrayObject` 或 `AsCollection` 避開此問題。

```php theme={null}
use Illuminate\Database\Eloquent\Casts\AsArrayObject;
use Illuminate\Database\Eloquent\Casts\AsCollection;

protected function casts(): array
{
    return [
        'options' => AsArrayObject::class,   // 視為 ArrayObject
        'tags'    => AsCollection::class,    // 視為 Collection
    ];
}
```

若要使用自訂集合類別，可透過 `using()` 指定。

```php theme={null}
use App\Collections\TagCollection;
use Illuminate\Database\Eloquent\Casts\AsCollection;

protected function casts(): array
{
    return [
        'tags' => AsCollection::using(TagCollection::class),
    ];
}
```

## 日期時間轉換

`created_at` / `updated_at` 預設會轉為 Carbon。其他日期時間欄位也可用相同方式定義。

```php theme={null}
protected function casts(): array
{
    return [
        'published_at' => 'datetime',
        'expires_at'   => 'immutable_datetime',
    ];
}
```

指定格式後，JSON 序列化時將採用該格式。

```php theme={null}
protected function casts(): array
{
    return [
        'published_at' => 'datetime:Y-m-d',
    ];
}
```

若想更改所有日期預設的序列化格式，可覆寫 `serializeDate()`（不會影響 DB 儲存格式）。

```php theme={null}
use DateTimeInterface;

protected function serializeDate(DateTimeInterface $date): string
{
    return $date->format('Y-m-d');
}
```

<Tip>
  `immutable_datetime` 會回傳 CarbonImmutable 而非 Carbon。因為不修改原有實例，能寫出無副作用的程式。
</Tip>

## Enum 轉換

PHP 8.1 之後的 Backed Enum 可作為轉換目標指定。

```php theme={null}
<?php

namespace App\Enums;

enum ServerStatus: string
{
    case Provisioned = 'provisioned';
    case Ready       = 'ready';
    case Archived    = 'archived';
}
```

```php theme={null}
use App\Enums\ServerStatus;

protected function casts(): array
{
    return [
        'status' => ServerStatus::class,
    ];
}
```

DB 儲存的是 Enum 的 backing 值（`string` 或 `int`），取得時會轉為 Enum 實例。

```php theme={null}
$server = Server::find(1);

if ($server->status === ServerStatus::Provisioned) {
    $server->status = ServerStatus::Ready;
    $server->save();
}
```

### Enum 的陣列轉換

若要把多個 Enum 值以陣列形式存到單一欄位，可使用 `AsEnumCollection`。

```php theme={null}
use App\Enums\ServerStatus;
use Illuminate\Database\Eloquent\Casts\AsEnumCollection;

protected function casts(): array
{
    return [
        'statuses' => AsEnumCollection::of(ServerStatus::class),
    ];
}
```

## 查詢時的轉換

若要在查詢時動態套用轉換，可使用 `withCasts()`。

```php theme={null}
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'),
])->withCasts([
    'last_posted_at' => 'datetime',
])->get();
```

## 自訂轉換

你也可以建立自訂的轉換類別。實作 `CastsAttributes` 介面並定義 `get` 與 `set` 方法即可。

```shell theme={null}
php artisan make:cast AsJson
```

詳細的實作方法（Value Object 模式、Inbound Cast、Castables 等）請參閱下方進階頁面。

<Card title="自訂轉換詳解" icon="book" href="/zh-TW/advanced/eloquent-casts">
  說明 CastsAttributes 介面的實作方式，以及 Value Object 模式、Castables 等進階自訂轉換。
</Card>

## 相關頁面

<Card title="Eloquent API 資源" icon="link" href="/zh-TW/eloquent-resources">
  瞭解將模型轉為一致 JSON API 回應的資源類別的用法。
</Card>


## Related topics

- [Eloquent 的自訂 Cast](/zh-TW/advanced/eloquent-casts.md)
- [Laravel Pint](/zh-TW/pint.md)
- [SessionEvent](/zh-TW/packages/laravel-copilot-sdk/session-event.md)
- [Artisan Console](/zh-TW/artisan.md)
- [Eloquent 序列化](/zh-TW/eloquent-serialization.md)
