Skip to content

Latest commit

 

History

History
886 lines (687 loc) · 28 KB

eloquent-mutators.md

File metadata and controls

886 lines (687 loc) · 28 KB

Eloquent: 賦值器與型別轉換

簡介

取值器、賦值器和屬性型別轉換允許您在檢索或設置模型實例上的 Eloquent 屬性值時對其進行轉換。例如,您可能希望在將值存儲在資料庫中時使用 Laravel 加密器 對其進行加密,然後在訪問 Eloquent 模型時自動解密屬性。或者,當通過您的 Eloquent 模型訪問時,您可能希望將存儲在資料庫中的 JSON 字串轉換為陣列。

取值器與賦值器

定義一個取值器

取值器在訪問時轉換 Eloquent 屬性值。要定義取值器,請在您的模型上創建一個受保護的方法,以表示可訪問的屬性。當適用時,此方法名應對應於真實底層模型屬性 / 資料庫欄位的 "駝峰命名法" 表示。

在此示例中,我們將為 first_name 屬性定義一個取值器。當嘗試檢索 first_name 屬性的值時,Eloquent 將自動調用取值器。所有屬性取值器 / 賦值器方法都必須聲明 Illuminate\Database\Eloquent\Casts\Attribute 的返回型別提示:

所有取值器方法都會返回一個 Attribute 實例,該實例定義了屬性的訪問方式,並可選地進行變異。在此示例中,我們僅定義了屬性的訪問方式。為此,我們向 Attribute 類構造函數提供 get 引數。

如您所見,列的原始值被傳遞給取值器,使您能夠操作並返回該值。要訪問取值器的值,您可以在模型實例上簡單地訪問 first_name 屬性:

use App\Models\User;

$user = User::find(1);

$firstName = $user->first_name;

Note

如果您希望將這些計算值添加到模型的數組 / JSON 表示中,您需要將它們附加

從多個屬性構建值對象

有時,您的取值器可能需要將多個模型屬性轉換為單個“值對象”。為此,您的 get 閉包可以接受第二個 $attributes 參數,該參數將自動提供給閉包,並包含模型當前所有屬性的陣列:

use App\Support\Address;
use Illuminate\Database\Eloquent\Casts\Attribute;

/**
 * Interact with the user's address.
 */
protected function address(): Attribute
{
    return Attribute::make(
        get: fn (mixed $value, array $attributes) => new Address(
            $attributes['address_line_one'],
            $attributes['address_line_two'],
        ),
    );
}

取值器快取

當從取值器返回值對象時,對值對象所做的任何更改都將在模型保存之前自動同步回模型。這是可能的,因為 Eloquent 保留了取值器返回的實例,以便每次調用取值器時都能返回相同的實例:

use App\Models\User;

$user = User::find(1);

$user->address->lineOne = 'Updated Address Line 1 Value';
$user->address->lineTwo = 'Updated Address Line 2 Value';

$user->save();

但是,有時您可能希望為像字符串和布爾值這樣的原始值啟用快取,特別是如果它們在計算上很耗費。為此,您可以在定義取值器時調用 shouldCache 方法:

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

如果您希望禁用屬性的對象快取行為,則可以在定義屬性時調用 withoutObjectCaching 方法:

/**
 * Interact with the user's address.
 */
protected function address(): Attribute
{
    return Attribute::make(
        get: fn (mixed $value, array $attributes) => new Address(
            $attributes['address_line_one'],
            $attributes['address_line_two'],
        ),
    )->withoutObjectCaching();
}

定義賦值器

當設置屬性時,賦值器會轉換 Eloquent 屬性值。要定義賦值器,您可以在定義屬性時提供 set 引數。讓我們為 first_name 屬性定義一個賦值器。當我們嘗試設置模型上 first_name 屬性的值時,這個賦值器將自動調用:

<?php

namespace App\Models;

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

class User extends Model
{
    /**
     * Interact with the user's first name.
     */
    protected function firstName(): Attribute
    {
        return Attribute::make(
            get: fn (string $value) => ucfirst($value),
            set: fn (string $value) => strtolower($value),
        );
    }
}

賦值器閉包將接收正在設置的屬性值,讓您可以操作該值並返回操作後的值。要使用我們的賦值器,我們只需要在 Eloquent 模型上設置 first_name 屬性:

use App\Models\User;

$user = User::find(1);

$user->first_name = 'Sally';

在這個例子中,set 回調將使用值 Sally 被調用。然後賦值器將對名稱應用 strtolower 函數並將其結果值設置在模型的內部 $attributes 陣列中。

賦值多個屬性

有時您的賦值器可能需要在底層模型上設置多個屬性。為此,您可以從 set 閉包返回一個陣列。陣列中的每個鍵應對應於與模型關聯的底層屬性 / 資料庫列:

use App\Support\Address;
use Illuminate\Database\Eloquent\Casts\Attribute;

/**
 * Interact with the user's address.
 */
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 方法提供了一種將屬性轉換為常見資料類型的便捷方式。

casts 方法應該返回一個陣列,其中鍵是要轉換的屬性名稱,值是您希望將該列轉換為的類型。支持的轉換類型有:

  • 陣列
  • AsStringable::class
  • 布林值
  • 集合
  • 日期
  • 日期時間
  • 不可變日期
  • 不可變日期時間
  • 十進位:<精度>
  • 浮點數
  • 加密
  • 加密:陣列
  • 加密:集合
  • 加密:物件
  • 浮點數
  • 雜湊
  • 整數
  • 物件
  • 實數
  • 字串
  • 時間戳記

為了展示屬性轉換,讓我們將 is_admin 屬性進行轉換,該屬性在我們的資料庫中以整數 (01) 儲存,轉換為布林值:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    /**
     * Get the attributes that should be cast.
     *
     * @return array<string, string>
     */
    protected function casts(): array
    {
        return [
            'is_admin' => 'boolean',
        ];
    }
}

在定義轉換後,當您訪問時,is_admin 屬性將始終被轉換為布林值,即使底層值以整數形式儲存在資料庫中:

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

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

如果您需要在運行時添加新的臨時轉換,您可以使用 mergeCasts 方法。這些轉換定義將添加到模型已經定義的任何轉換中:

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

Warning

null 的屬性將不會被轉換。此外,您永遠不應定義一個與關係同名的轉換(或屬性),或將轉換分配給模型的主鍵。

可轉換為字串

您可以使用 Illuminate\Database\Eloquent\Casts\AsStringable 轉換類別將模型屬性轉換為 流暢的 Illuminate\Support\Stringable 物件

<?php

namespace App\Models;

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

class User extends Model
{
    /**
     * Get the attributes that should be cast.
     *
     * @return array<string, string>
     */
    protected function casts(): array
    {
        return [
            'directory' => AsStringable::class,
        ];
    }
}

陣列和 JSON 轉換

array 轉換在處理存儲為序列化 JSON 的列時特別有用。例如,如果您的資料庫具有包含序列化 JSON 的 JSONTEXT 欄位類型,將 array 轉換添加到該屬性將在您訪問 Eloquent 模型時自動將屬性反序列化為 PHP 陣列:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    /**
     * Get the attributes that should be cast.
     *
     * @return array<string, string>
     */
    protected function casts(): array
    {
        return [
            'options' => 'array',
        ];
    }
}

一旦定義了轉換,您可以訪問 options 屬性,它將自動從 JSON 反序列化為 PHP 陣列。當您設置 options 屬性的值時,給定的陣列將自動序列化回 JSON 進行儲存:

use App\Models\User;

$user = User::find(1);

$options = $user->options;

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

$user->options = $options;

$user->save();

要使用更簡潔的語法更新 JSON 屬性的單個字段,您可以使屬性可批量賦值,並在調用 update 方法時使用 -> 運算符:

$user = User::find(1);

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

陣列物件和集合轉換

雖然標準的 array 轉換對許多應用程式已足夠,但它確實有一些缺點。由於 array 轉換返回一種基本類型,因此無法直接變更陣列的偏移量。例如,以下程式碼將觸發 PHP 錯誤:

$user = User::find(1);

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

為了解決這個問題,Laravel 提供了 AsArrayObject 轉換,將您的 JSON 屬性轉換為 ArrayObject 類別。此功能是使用 Laravel 的 自訂轉換 實現的,這使得 Laravel 能夠智能地快取和轉換變更的物件,以便可以修改個別偏移量而不觸發 PHP 錯誤。要使用 AsArrayObject 轉換,只需將其指定給屬性:

use Illuminate\Database\Eloquent\Casts\AsArrayObject;

/**
 * Get the attributes that should be cast.
 *
 * @return array<string, string>
 */
protected function casts(): array
{
    return [
        'options' => AsArrayObject::class,
    ];
}

同樣地,Laravel 還提供了 AsCollection 轉換,將您的 JSON 屬性轉換為 Laravel Collection 實例:

use Illuminate\Database\Eloquent\Casts\AsCollection;

/**
 * Get the attributes that should be cast.
 *
 * @return array<string, string>
 */
protected function casts(): array
{
    return [
        'options' => AsCollection::class,
    ];
}

如果您希望 AsCollection 轉換實例化自定義集合類別而不是 Laravel 的基本集合類別,可以將集合類別名稱作為轉換參數提供:

use App\Collections\OptionCollection;
use Illuminate\Database\Eloquent\Casts\AsCollection;

/**
 * Get the attributes that should be cast.
 *
 * @return array<string, string>
 */
protected function casts(): array
{
    return [
        'options' => AsCollection::using(OptionCollection::class),
    ];
}

日期轉換

預設情況下,Eloquent 會將 created_atupdated_at 欄位轉換為 Carbon 的實例,Carbon 擴展了 PHP 的 DateTime 類別並提供了各種有用的方法。您可以透過在模型的 casts 方法中定義額外的日期轉換來轉換其他日期屬性。通常,日期應該使用 datetimeimmutable_datetime 轉換類型進行轉換。

在定義 datedatetime 轉換時,您還可以指定日期的格式。當 模型序列化為陣列或 JSON 時,將使用此格式:

/**
 * Get the attributes that should be cast.
 *
 * @return array<string, string>
 */
protected function casts(): array
{
    return [
        'created_at' => 'datetime:Y-m-d',
    ];
}

當一個欄位被轉換為日期時,您可以將相應的模型屬性值設置為 UNIX 時間戳、日期字串(Y-m-d)、日期時間字串,或是 DateTime / Carbon 實例。日期的值將被正確轉換並存儲在您的資料庫中。

您可以通過在模型上定義一個 serializeDate 方法來自定義所有模型日期的默認序列化格式。這個方法不會影響日期在資料庫中存儲的格式:

/**
 * Prepare a date for array / JSON serialization.
 */
protected function serializeDate(DateTimeInterface $date): string
{
    return $date->format('Y-m-d');
}

要指定實際存儲模型日期時應使用的格式,您應該在模型上定義一個 $dateFormat 屬性:

/**
 * The storage format of the model's date columns.
 *
 * @var string
 */
protected $dateFormat = 'U';

日期轉換、序列化和時區

默認情況下,datedatetime 轉換將日期序列化為 UTC ISO-8601 日期字串(YYYY-MM-DDTHH:MM:SS.uuuuuuZ),不管應用程式的 timezone 配置選項中指定的時區是什麼。強烈建議您始終使用這個序列化格式,並通過不更改應用程式的 timezone 配置選項的預設值 UTC,將應用程式的日期存儲在 UTC 時區。在整個應用程式中一致使用 UTC 時區將提供最大程度的與 PHP 和 JavaScript 中其他日期操作庫的互操作性。

如果對 datedatetime 轉換應用了自定義格式,例如 datetime:Y-m-d H:i:s,則在日期序列化期間將使用 Carbon 實例的內部時區。通常,這將是您應用程式的 timezone 配置選項中指定的時區。但是,重要的是要注意,timestamp 欄位(如 created_atupdated_at)豁免於此行為,並且始終以 UTC 格式化,不管應用程式的時區設置如何。

列舉轉換

Eloquent 也允許您將屬性值轉換為 PHP 列舉。為了實現這一點,您可以在模型的 casts 方法中指定您希望轉換的屬性和列舉:

use App\Enums\ServerStatus;

/**
 * Get the attributes that should be cast.
 *
 * @return array<string, string>
 */
protected function casts(): array
{
    return [
        'status' => ServerStatus::class,
    ];
}

一旦您在模型上定義了轉換,當您與該屬性互動時,指定的屬性將自動轉換為列舉並從列舉轉換:

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

    $server->save();
}

轉換列舉的陣列

有時您可能需要您的模型將一個列舉值陣列存儲在單個列中。為了實現這一點,您可以使用 Laravel 提供的 AsEnumArrayObjectAsEnumCollection 轉換:

use App\Enums\ServerStatus;
use Illuminate\Database\Eloquent\Casts\AsEnumCollection;

/**
 * Get the attributes that should be cast.
 *
 * @return array<string, string>
 */
protected function casts(): array
{
    return [
        'statuses' => AsEnumCollection::of(ServerStatus::class),
    ];
}

加密轉換

encrypted 轉換將使用 Laravel 內建的 加密 功能加密模型的屬性值。此外,encrypted:arrayencrypted:collectionencrypted:objectAsEncryptedArrayObjectAsEncryptedCollection 轉換的工作方式與其未加密的對應方式相同;但是,作為您可能預期的,存儲在數據庫中時,底層值是加密的。

由於加密文本的最終長度是不可預測的,並且比其明文對應物更長,請確保相關的數據庫列是 TEXT 類型或更大。此外,由於數據庫中的值是加密的,您將無法查詢或搜索加密的屬性值。

金鑰輪替

如您所知,Laravel 使用在應用程式的 app 配置文件中指定的 key 配置值來加密字符串。通常,此值對應於 APP_KEY 環境變數的值。如果您需要輪替應用程式的加密金鑰,您將需要使用新金鑰手動重新加密加密的屬性。

查詢時轉換

有時您可能需要在執行查詢時應用轉換,例如從表中選擇原始值時。例如,考慮以下查詢:

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 方法來實現這一點:

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

自訂轉換器

Laravel 內建了各種有用的轉換器類型;然而,您偶爾可能需要定義自己的轉換器類型。要建立一個轉換器,請執行 make:cast Artisan 指令。新的轉換器類別將被放置在您的 app/Casts 目錄中:

php artisan make:cast Json

所有自訂轉換器類別都實作了 CastsAttributes 介面。實作此介面的類別必須定義 getset 方法。get 方法負責將來自資料庫的原始值轉換為轉換值,而 set 方法應該將轉換值轉換為可以存儲在資料庫中的原始值。作為範例,我們將重新實作內建的 json 轉換器類型為自訂轉換器類型:

<?php

namespace App\Casts;

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

class Json implements CastsAttributes
{
    /**
     * Cast the given value.
     *
     * @param  array<string, mixed>  $attributes
     * @return array<string, mixed>
     */
    public function get(Model $model, string $key, mixed $value, array $attributes): array
    {
        return json_decode($value, true);
    }

    /**
     * Prepare the given value for storage.
     *
     * @param  array<string, mixed>  $attributes
     */
    public function set(Model $model, string $key, mixed $value, array $attributes): string
    {
        return json_encode($value);
    }
}

定義了自訂轉換器類型後,您可以使用其類別名稱將其附加到模型屬性:

<?php

namespace App\Models;

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

class User extends Model
{
    /**
     * Get the attributes that should be cast.
     *
     * @return array<string, string>
     */
    protected function casts(): array
    {
        return [
            'options' => Json::class,
        ];
    }
}

值物件轉換

您不僅限於將值轉換為基本類型。您也可以將值轉換為物件。定義將值轉換為物件的自訂轉換器與將值轉換為基本類型非常相似;但是,set 方法應該返回一個鍵/值對的陣列,這些對將用於在模型上設置原始、可存儲的值。

作為範例,我們將定義一個自訂轉換器類別,將多個模型值轉換為單一的 Address 值物件。我們假設 Address 值物件具有兩個公共屬性:lineOnelineTwo

<?php

namespace App\Casts;

use App\ValueObjects\Address as AddressValueObject;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Database\Eloquent\Model;
use InvalidArgumentException;

class Address implements CastsAttributes
{
    /**
     * Cast the given value.
     *
     * @param  array<string, mixed>  $attributes
     */
    public function get(Model $model, string $key, mixed $value, array $attributes): AddressValueObject
    {
        return new AddressValueObject(
            $attributes['address_line_one'],
            $attributes['address_line_two']
        );
    }

    /**
     * Prepare the given value for storage.
     *
     * @param  array<string, mixed>  $attributes
     * @return array<string, string>
     */
    public function set(Model $model, string $key, mixed $value, array $attributes): array
    {
        if (! $value instanceof AddressValueObject) {
            throw new InvalidArgumentException('The given value is not an Address instance.');
        }

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

當轉換為值物件時,對值物件所做的任何更改都將在模型保存之前自動同步回模型:

use App\Models\User;

$user = User::find(1);

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

$user->save();

Note

如果您計劃將包含值物件的 Eloquent 模型序列化為 JSON 或陣列,您應該在值物件上實作 Illuminate\Contracts\Support\ArrayableJsonSerializable 介面。

值物件快取

當被轉換為值物件的屬性被解析時,它們會被 Eloquent 快取。因此,如果再次存取該屬性,將返回相同的物件實例。

如果您想要停用自訂轉換類別的物件快取行為,您可以在自訂轉換類別上宣告一個公共 withoutObjectCaching 屬性:

class Address implements CastsAttributes
{
    public bool $withoutObjectCaching = true;

    // ...
}

陣列 / JSON 序列化

當使用 toArraytoJson 方法將 Eloquent 模型轉換為陣列或 JSON 時,您的自訂轉換值物件通常也會被序列化,只要它們實作了 Illuminate\Contracts\Support\ArrayableJsonSerializable 介面。但是,當使用第三方庫提供的值物件時,您可能無法將這些介面添加到物件中。

因此,您可以指定您的自訂轉換類別將負責序列化值物件。為此,您的自訂轉換類別應實作 Illuminate\Contracts\Database\Eloquent\SerializesCastableAttributes 介面。該介面規定您的類別應包含一個 serialize 方法,該方法應返回您值物件的序列化形式:

/**
 * Get the serialized representation of the value.
 *
 * @param  array<string, mixed>  $attributes
 */
public function serialize(Model $model, string $key, mixed $value, array $attributes): string
{
    return (string) $value;
}

入站轉換

偶爾,您可能需要編寫一個僅在設置模型的值時轉換值的自訂轉換類別,並且在從模型擷取屬性時不執行任何操作。

僅入站自訂轉換應實作 CastsInboundAttributes 介面,該介面僅需要定義一個 set 方法。make:cast Artisan 命令可以使用 --inbound 選項來生成僅入站轉換類別:

php artisan make:cast Hash --inbound

一個經典的僅入站轉換的範例是 "雜湊" 轉換。例如,我們可以定義一個通過給定演算法對入站值進行雜湊的轉換:

<?php

namespace App\Casts;

use Illuminate\Contracts\Database\Eloquent\CastsInboundAttributes;
use Illuminate\Database\Eloquent\Model;

class Hash implements CastsInboundAttributes
{
    /**
     * Create a new cast class instance.
     */
    public function __construct(
        protected string|null $algorithm = null,
    ) {}

    /**
     * Prepare the given value for storage.
     *
     * @param  array<string, mixed>  $attributes
     */
    public function set(Model $model, string $key, mixed $value, array $attributes): string
    {
        return is_null($this->algorithm)
            ? bcrypt($value)
            : hash($this->algorithm, $value);
    }
}

轉換參數

當將自訂轉換附加到模型時,可以通過使用 : 字元將參數與類別名稱分開,並使用逗號分隔多個參數來指定轉換參數。這些參數將傳遞給轉換類別的建構子:

/**
 * Get the attributes that should be cast.
 *
 * @return array<string, string>
 */
protected function casts(): array
{
    return [
        'secret' => Hash::class.':sha256',
    ];
}

轉換物件

您可能希望允許應用程式的值物件定義其自訂轉換類別。您可以將實作 Illuminate\Contracts\Database\Eloquent\Castable 介面的值物件類別附加到模型,而不是將自訂轉換類別附加到模型:

use App\ValueObjects\Address;

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

實作 Castable 介面的物件必須定義一個 castUsing 方法,該方法返回負責將資料轉換為 Castable 類別及從中轉換的自訂轉換器類別的類別名稱:

<?php

namespace App\ValueObjects;

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

class Address implements Castable
{
    /**
     * Get the name of the caster class to use when casting from / to this cast target.
     *
     * @param  array<string, mixed>  $arguments
     */
    public static function castUsing(array $arguments): string
    {
        return AddressCast::class;
    }
}

使用 Castable 類別時,仍然可以在 casts 方法定義中提供引數。這些引數將傳遞給 castUsing 方法:

use App\ValueObjects\Address;

protected function casts(): array
{
    return [
        'address' => Address::class.':argument',
    ];
}

轉換物件與匿名轉換類別

通過將 "轉換物件" 與 PHP 的 匿名類別 結合,您可以將值物件及其轉換邏輯定義為單一的可轉換物件。為此,從您的值物件的 castUsing 方法返回一個匿名類別。匿名類別應該實作 CastsAttributes 介面:

<?php

namespace App\ValueObjects;

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

class Address implements Castable
{
    // ...

    /**
     * Get the caster class to use when casting from / to this cast target.
     *
     * @param  array<string, mixed>  $arguments
     */
    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,
                ];
            }
        };
    }
}