Skip to content

Eloquent: 变换器与类型转换

介绍

访问器、变换器和属性类型转换允许您在检索或设置模型实例上的 Eloquent 属性值时进行转换。例如,您可能希望使用 Laravel 加密器 在将值存储到数据库时加密它,并在访问 Eloquent 模型上的属性时自动解密。或者,您可能希望将存储在数据库中的 JSON 字符串在通过 Eloquent 模型访问时转换为数组。

访问器与变换器

定义访问器

访问器在访问 Eloquent 属性值时进行转换。要定义访问器,请在模型上创建一个 get{Attribute}Attribute 方法,其中 {Attribute} 是您希望访问的列的 "studly" 大小写名称。

在此示例中,我们将为 first_name 属性定义一个访问器。访问器将在尝试检索 first_name 属性的值时由 Eloquent 自动调用:

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    /**
     * 获取用户的名字。
     *
     * @param  string  $value
     * @return string
     */
    public function getFirstNameAttribute($value)
    {
        return ucfirst($value);
    }
}

如您所见,列的原始值会传递给访问器,允许您操作并返回该值。要访问访问器的值,您只需访问模型实例上的 first_name 属性:

php
use App\Models\User;

$user = User::find(1);

$firstName = $user->first_name;

您不仅限于在访问器中与单个属性交互。您还可以使用访问器从现有属性返回新的计算值:

php
/**
 * 获取用户的全名。
 *
 * @return string
 */
public function getFullNameAttribute()
{
    return "{$this->first_name} {$this->last_name}";
}
lightbulb

如果您希望将这些计算值添加到模型的数组 / JSON 表示中,您需要将它们附加

定义变换器

变换器在设置 Eloquent 属性值时进行转换。要定义变换器,请在模型上定义一个 set{Attribute}Attribute 方法,其中 {Attribute} 是您希望访问的列的 "studly" 大小写名称。

让我们为 first_name 属性定义一个变换器。当我们尝试在模型上设置 first_name 属性的值时,该变换器将自动调用:

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    /**
     * 设置用户的名字。
     *
     * @param  string  $value
     * @return void
     */
    public function setFirstNameAttribute($value)
    {
        $this->attributes['first_name'] = strtolower($value);
    }
}

变换器将接收正在设置在属性上的值,允许您操作该值并将操作后的值设置在 Eloquent 模型的内部 $attributes 属性上。要使用我们的变换器,我们只需在 Eloquent 模型上设置 first_name 属性:

php
use App\Models\User;

$user = User::find(1);

$user->first_name = 'Sally';

在此示例中,setFirstNameAttribute 函数将以值 Sally 调用。变换器将对名称应用 strtolower 函数,并在内部 $attributes 数组中设置其结果值。

属性类型转换

属性类型转换提供了类似于访问器和变换器的功能,而无需在模型上定义任何额外的方法。相反,模型的 $casts 属性提供了一种方便的方法来将属性转换为常见数据类型。

$casts 属性应为一个数组,其中键是要转换的属性的名称,值是您希望将列转换为的类型。支持的转换类型有:

  • array
  • AsStringable::class
  • boolean
  • collection
  • date
  • datetime
  • immutable_date
  • immutable_datetime
  • decimal:<digits>
  • double
  • encrypted
  • encrypted:array
  • encrypted:collection
  • encrypted:object
  • float
  • integer
  • object
  • real
  • string
  • timestamp

为了演示属性类型转换,让我们将 is_admin 属性(在我们的数据库中存储为整数 01)转换为布尔值:

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    /**
     * 应该转换的属性。
     *
     * @var array
     */
    protected $casts = [
        'is_admin' => 'boolean',
    ];
}

定义转换后,即使底层值在数据库中存储为整数,is_admin 属性在访问时也将始终转换为布尔值:

php
$user = App\Models\User::find(1);

if ($user->is_admin) {
    //
}

如果您需要在运行时添加新的临时转换,可以使用 mergeCasts 方法。这些转换定义将添加到模型上已定义的任何转换中:

php
$user->mergeCasts([
    'is_admin' => 'integer',
    'options' => 'object',
]);
exclamation

null 的属性将不会被转换。此外,您不应定义与关系同名的转换(或属性)。

Stringable 转换

您可以使用 Illuminate\Database\Eloquent\Casts\AsStringable 转换类将模型属性转换为 流畅的 Illuminate\Support\Stringable 对象

php
<?php

namespace App\Models;

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

class User extends Model
{
    /**
     * 应该转换的属性。
     *
     * @var array
     */
    protected $casts = [
        'directory' => AsStringable::class,
    ];
}

数组与 JSON 转换

array 转换在处理存储为序列化 JSON 的列时特别有用。例如,如果您的数据库有一个 JSONTEXT 字段类型,其中包含序列化的 JSON,向该属性添加 array 转换将在您访问 Eloquent 模型上的属性时自动将其反序列化为 PHP 数组:

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    /**
     * 应该转换的属性。
     *
     * @var array
     */
    protected $casts = [
        'options' => 'array',
    ];
}

一旦定义了转换,您可以访问 options 属性,它将自动从 JSON 反序列化为 PHP 数组。当您设置 options 属性的值时,给定的数组将自动序列化回 JSON 以进行存储:

php
use App\Models\User;

$user = User::find(1);

$options = $user->options;

$options['key'] = 'value';

$user->options = $options;

$user->save();

要使用更简洁的语法更新 JSON 属性的单个字段,您可以在调用 update 方法时使用 -> 操作符:

php
$user = User::find(1);

$user->update(['options->key' => 'value']);

数组对象与集合转换

虽然标准的 array 转换对于许多应用程序来说已经足够,但它确实有一些缺点。由于 array 转换返回的是原始类型,因此无法直接修改数组的偏移量。例如,以下代码将触发 PHP 错误:

php
$user = User::find(1);

$user->options['key'] = $value;

为了解决这个问题,Laravel 提供了一个 AsArrayObject 转换,将您的 JSON 属性转换为 ArrayObject 类。此功能是使用 Laravel 的 自定义转换 实现的,允许 Laravel 智能地缓存和转换变换后的对象,以便可以修改单个偏移量而不会触发 PHP 错误。要使用 AsArrayObject 转换,只需将其分配给一个属性:

php
use Illuminate\Database\Eloquent\Casts\AsArrayObject;

/**
 * 应该转换的属性。
 *
 * @var array
 */
protected $casts = [
    'options' => AsArrayObject::class,
];

类似地,Laravel 提供了一个 AsCollection 转换,将您的 JSON 属性转换为 Laravel 集合 实例:

php
use Illuminate\Database\Eloquent\Casts\AsCollection;

/**
 * 应该转换的属性。
 *
 * @var array
 */
protected $casts = [
    'options' => AsCollection::class,
];

日期转换

默认情况下,Eloquent 会将 created_atupdated_at 列转换为 Carbon 实例,Carbon 扩展了 PHP 的 DateTime 类,并提供了一系列有用的方法。您可以通过在模型的 $casts 属性数组中定义额外的日期转换来转换其他日期属性。通常,日期应使用 datetimeimmutable_datetime 转换类型进行转换。

在定义 datedatetime 转换时,您还可以指定日期的格式。此格式将在 模型序列化为数组或 JSON 时 使用:

php
/**
 * 应该转换的属性。
 *
 * @var array
 */
protected $casts = [
    'created_at' => 'datetime:Y-m-d',
];

当列被转换为日期时,您可以将相应的模型属性值设置为 UNIX 时间戳、日期字符串 (Y-m-d)、日期时间字符串或 DateTime / Carbon 实例。日期的值将被正确转换并存储在您的数据库中。

您可以通过在模型上定义 serializeDate 方法来自定义所有模型日期的默认序列化格式。此方法不会影响您的日期在数据库中存储的格式:

php
/**
 * 准备日期以进行数组 / JSON 序列化。
 *
 * @param  \DateTimeInterface  $date
 * @return string
 */
protected function serializeDate(DateTimeInterface $date)
{
    return $date->format('Y-m-d');
}

要指定在数据库中实际存储模型日期时应使用的格式,您应在模型上定义 $dateFormat 属性:

php
/**
 * 模型日期列的存储格式。
 *
 * @var string
 */
protected $dateFormat = 'U';

日期转换、序列化与时区

默认情况下,datedatetime 转换将在序列化日期时使用 UTC ISO-8601 日期字符串 (1986-05-28T21:05:54.000000Z),无论您应用程序的 timezone 配置选项中指定的时区如何。强烈建议您始终使用此序列化格式,并通过不更改应用程序的 timezone 配置选项的默认 UTC 值来将应用程序的日期存储在 UTC 时区中。在整个应用程序中一致使用 UTC 时区将提供与其他用 PHP 和 JavaScript 编写的日期操作库的最大互操作性。

如果对 datedatetime 转换应用了自定义格式,例如 datetime:Y-m-d H:i:s,则在日期序列化期间将使用 Carbon 实例的内部时区。通常,这将是您应用程序的 timezone 配置选项中指定的时区。

枚举转换

exclamation

枚举转换仅适用于 PHP 8.1+。

Eloquent 还允许您将属性值转换为 PHP 枚举。为此,您可以在模型的 $casts 属性数组中指定要转换的属性和枚举:

php
use App\Enums\ServerStatus;

/**
 * 应该转换的属性。
 *
 * @var array
 */
protected $casts = [
    'status' => ServerStatus::class,
];

一旦您在模型上定义了转换,指定的属性将在您与属性交互时自动转换为枚举并从枚举转换:

php
if ($server->status == ServerStatus::provisioned) {
    $server->status = ServerStatus::ready;

    $server->save();
}

加密转换

encrypted 转换将使用 Laravel 的内置 加密 功能加密模型的属性值。此外,encrypted:arrayencrypted:collectionencrypted:objectAsEncryptedArrayObjectAsEncryptedCollection 转换的工作方式与其未加密的对应物相同;然而,正如您可能预期的那样,底层值在存储在数据库中时是加密的。

由于加密文本的最终长度不可预测,并且比其明文对应物长,请确保关联的数据库列为 TEXT 类型或更大。此外,由于值在数据库中是加密的,您将无法查询或搜索加密的属性值。

查询时转换

有时您可能需要在执行查询时应用转换,例如在从表中选择原始值时。例如,考虑以下查询:

php
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')
])->get();

此查询结果中的 last_posted_at 属性将是一个简单的字符串。如果我们可以在执行查询时对该属性应用 datetime 转换,那将是很好的。幸运的是,我们可以使用 withCasts 方法实现这一点:

php
$users = User::select([
    'users.*',
    'last_posted_at' => Post::selectRaw('MAX(created_at)')
            ->whereColumn('user_id', 'users.id')
])->withCasts([
    'last_posted_at' => 'datetime'
])->get();

自定义转换

Laravel 有多种内置的、实用的转换类型;然而,您可能偶尔需要定义自己的转换类型。您可以通过定义一个实现 CastsAttributes 接口的类来实现这一点。

实现此接口的类必须定义一个 getset 方法。get 方法负责将数据库中的原始值转换为转换后的值,而 set 方法应将转换后的值转换为可以存储在数据库中的原始值。作为示例,我们将重新实现内置的 json 转换类型作为自定义转换类型:

php
<?php

namespace App\Casts;

use Illuminate\Contracts\Database\Eloquent\CastsAttributes;

class Json implements CastsAttributes
{
    /**
     * 转换给定的值。
     *
     * @param  \Illuminate\Database\Eloquent\Model  $model
     * @param  string  $key
     * @param  mixed  $value
     * @param  array  $attributes
     * @return array
     */
    public function get($model, $key, $value, $attributes)
    {
        return json_decode($value, true);
    }

    /**
     * 准备给定的值以进行存储。
     *
     * @param  \Illuminate\Database\Eloquent\Model  $model
     * @param  string  $key
     * @param  array  $value
     * @param  array  $attributes
     * @return string
     */
    public function set($model, $key, $value, $attributes)
    {
        return json_encode($value);
    }
}

一旦您定义了自定义转换类型,您可以使用其类名将其附加到模型属性:

php
<?php

namespace App\Models;

use App\Casts\Json;
use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    /**
     * 应该转换的属性。
     *
     * @var array
     */
    protected $casts = [
        'options' => Json::class,
    ];
}

值对象转换

您不仅限于将值转换为原始类型。您还可以将值转换为对象。定义将值转换为对象的自定义转换与转换为原始类型非常相似;然而,set 方法应返回一个键 / 值对数组,这些数组将用于在模型上设置原始的、可存储的值。

作为示例,我们将定义一个自定义转换类,该类将多个模型值转换为单个 Address 值对象。我们将假设 Address 值有两个公共属性:lineOnelineTwo

php
<?php

namespace App\Casts;

use App\Models\Address as AddressModel;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use InvalidArgumentException;

class Address implements CastsAttributes
{
    /**
     * 转换给定的值。
     *
     * @param  \Illuminate\Database\Eloquent\Model  $model
     * @param  string  $key
     * @param  mixed  $value
     * @param  array  $attributes
     * @return \App\Models\Address
     */
    public function get($model, $key, $value, $attributes)
    {
        return new AddressModel(
            $attributes['address_line_one'],
            $attributes['address_line_two']
        );
    }

    /**
     * 准备给定的值以进行存储。
     *
     * @param  \Illuminate\Database\Eloquent\Model  $model
     * @param  string  $key
     * @param  \App\Models\Address  $value
     * @param  array  $attributes
     * @return array
     */
    public function set($model, $key, $value, $attributes)
    {
        if (! $value instanceof AddressModel) {
            throw new InvalidArgumentException('给定的值不是 Address 实例。');
        }

        return [
            'address_line_one' => $value->lineOne,
            'address_line_two' => $value->lineTwo,
        ];
    }
}

在将值转换为对象时,对值对象所做的任何更改将在模型保存之前自动同步回模型:

php
use App\Models\User;

$user = User::find(1);

$user->address->lineOne = 'Updated Address Value';

$user->save();
lightbulb

如果您计划将包含值对象的 Eloquent 模型序列化为 JSON 或数组,您应在值对象上实现 Illuminate\Contracts\Support\ArrayableJsonSerializable 接口。

数组 / JSON 序列化

当 Eloquent 模型使用 toArraytoJson 方法转换为数组或 JSON 时,只要它们实现了 Illuminate\Contracts\Support\ArrayableJsonSerializable 接口,您的自定义转换值对象通常也会被序列化。然而,当使用第三方库提供的值对象时,您可能无法将这些接口添加到对象中。

因此,您可以指定您的自定义转换类将负责序列化值对象。为此,您的自定义转换类应实现 Illuminate\Contracts\Database\Eloquent\SerializesCastableAttributes 接口。此接口声明您的类应包含一个 serialize 方法,该方法应返回值对象的序列化形式:

php
/**
 * 获取值的序列化表示。
 *
 * @param  \Illuminate\Database\Eloquent\Model  $model
 * @param  string  $key
 * @param  mixed  $value
 * @param  array  $attributes
 * @return mixed
 */
public function serialize($model, string $key, $value, array $attributes)
{
    return (string) $value;
}

入站转换

有时,您可能需要编写一个仅转换设置在模型上的值的自定义转换,而不对从模型中检索的属性执行任何操作。入站仅自定义转换应实现 CastsInboundAttributes 接口,该接口只需要定义一个 set 方法。

php
<?php

namespace App\Casts;

use Illuminate\Contracts\Database\Eloquent\CastsInboundAttributes;

class Hash implements CastsInboundAttributes
{
    /**
     * 哈希算法。
     *
     * @var string
     */
    protected $algorithm;

    /**
     * 创建一个新的转换类实例。
     *
     * @param  string|null  $algorithm
     * @return void
     */
    public function __construct($algorithm = null)
    {
        $this->algorithm = $algorithm;
    }

    /**
     * 准备给定的值以进行存储。
     *
     * @param  \Illuminate\Database\Eloquent\Model  $model
     * @param  string  $key
     * @param  array  $value
     * @param  array  $attributes
     * @return string
     */
    public function set($model, $key, $value, $attributes)
    {
        return is_null($this->algorithm)
                    ? bcrypt($value)
                    : hash($this->algorithm, $value);
    }
}

转换参数

在将自定义转换附加到模型时,可以通过使用 : 字符分隔类名并用逗号分隔多个参数来指定转换参数。参数将传递给转换类的构造函数:

php
/**
 * 应该转换的属性。
 *
 * @var array
 */
protected $casts = [
    'secret' => Hash::class.':sha256',
];

可转换对象

您可能希望允许应用程序的值对象定义自己的自定义转换类。与其将自定义转换类附加到模型,您可以选择附加一个实现 Illuminate\Contracts\Database\Eloquent\Castable 接口的值对象类:

php
use App\Models\Address;

protected $casts = [
    'address' => Address::class,
];

实现 Castable 接口的对象必须定义一个 castUsing 方法,该方法返回负责从 Castable 类进行转换的自定义转换器类的类名:

php
<?php

namespace App\Models;

use Illuminate\Contracts\Database\Eloquent\Castable;
use App\Casts\Address as AddressCast;

class Address implements Castable
{
    /**
     * 获取从 / 到此转换目标进行转换时要使用的转换器类的名称。
     *
     * @param  array  $arguments
     * @return string
     */
    public static function castUsing(array $arguments)
    {
        return AddressCast::class;
    }
}

在使用 Castable 类时,您仍然可以在 $casts 定义中提供参数。参数将传递给 castUsing 方法:

php
use App\Models\Address;

protected $casts = [
    'address' => Address::class.':argument',
];

可转换对象与匿名转换类

通过将 "可转换对象" 与 PHP 的 匿名类 结合使用,您可以将值对象及其转换逻辑定义为单个可转换对象。为此,从值对象的 castUsing 方法返回一个匿名类。匿名类应实现 CastsAttributes 接口:

php
<?php

namespace App\Models;

use Illuminate\Contracts\Database\Eloquent\Castable;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;

class Address implements Castable
{
    // ...

    /**
     * 获取从 / 到此转换目标进行转换时要使用的转换器类。
     *
     * @param  array  $arguments
     * @return object|string
     */
    public static function castUsing(array $arguments)
    {
        return new class implements CastsAttributes
        {
            public function get($model, $key, $value, $attributes)
            {
                return new Address(
                    $attributes['address_line_one'],
                    $attributes['address_line_two']
                );
            }

            public function set($model, $key, $value, $attributes)
            {
                return [
                    'address_line_one' => $value->lineOne,
                    'address_line_two' => $value->lineTwo,
                ];
            }
        };
    }
}