> ## 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 访问器、修改器与类型转换

> 介绍在 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
{
    /**
     * 获取用户的名字（first name）。
     */
    protected function firstName(): Attribute
    {
        return Attribute::make(
            get: fn (string $value) => ucfirst($value),
        );
    }
}
```

`get` 闭包会收到数据库中的原始值。通过模型实例可以以 `first_name` 属性的形式访问。

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

$firstName = $user->first_name; // 已经应用了 ucfirst()
```

<Info>
  如果希望访问器计算得到的值也出现在 JSON/数组中，请把它以蛇形命名的形式加入模型的 `$appends` 属性。
</Info>

### 用多个属性生成值对象

`get` 闭包的第二个参数是 `$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,
        ],
    );
}
```

## 属性类型转换

**类型转换**允许你在不写访问器和修改器的情况下声明属性的类型转换，是一种简洁的方式。在模型的 `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 转换为 Collection |
| `date`                      | 日期（Carbon）          |
| `datetime`                  | 日期时间（Carbon）        |
| `immutable_date`            | 不可变日期               |
| `immutable_datetime`        | 不可变日期时间             |
| `timestamp`                 | UNIX 时间戳            |
| `hashed`                    | 保存时哈希               |
| `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 中的某个键。

```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 形式使用
    ];
}
```

如果需要使用自定义 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()`（不影响数据库中的存储格式）。

```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,
    ];
}
```

数据库中存储的是 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 模式、单向转换、Castables 等），请参考下面的高级页面。

<Card title="自定义类型转换详解" icon="book" href="/zh/advanced/eloquent-casts">
  介绍 CastsAttributes 接口的实现方式，以及 Value Object 模式、Castables 等高级自定义类型转换。
</Card>

## 相关页面

<Card title="Eloquent API 资源" icon="link" href="/zh/eloquent-resources">
  查看如何使用资源类将模型转换为一致的 JSON API 响应。
</Card>


## Related topics

- [Eloquent 自定义类型转换](/zh/advanced/eloquent-casts.md)
- [Eloquent 序列化](/zh/eloquent-serialization.md)
- [Artisan 控制台](/zh/artisan.md)
- [Eloquent 作用域](/zh/advanced/eloquent-scopes.md)
- [SessionEvent](/zh/packages/laravel-copilot-sdk/session-event.md)
