跳到內容

Eloquent:修改器 & 型別轉換

簡介

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

存取器和修改器

定義存取器

存取器會在存取時轉換 Eloquent 屬性值。若要定義存取器,請在您的模型上建立一個受保護的方法來表示可存取的屬性。當適用時,此方法名稱應對應於真實底層模型屬性 / 資料庫欄位的「駝峰式大小寫」表示法。

在此範例中,我們將定義 first_name 屬性的存取器。當嘗試檢索 first_name 屬性的值時,Eloquent 會自動呼叫存取器。所有屬性存取器/修改器方法都必須宣告 Illuminate\Database\Eloquent\Casts\Attribute 的回傳型別提示。

<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
 
class User extends Model
{
/**
* Get the user's first name.
*/
protected function firstName(): Attribute
{
return Attribute::make(
get: fn (string $value) => ucfirst($value),
);
}
}

所有存取器方法都會回傳一個 Attribute 實例,該實例定義了如何存取屬性,並可選擇性地修改屬性。在此範例中,我們僅定義如何存取屬性。為此,我們將 get 引數提供給 Attribute 類別建構函式。

如您所見,欄位的原始值會傳遞給存取器,讓您可以操作並回傳該值。若要存取存取器的值,您只需存取模型實例上的 first_name 屬性即可。

use App\Models\User;
 
$user = User::find(1);
 
$firstName = $user->first_name;
lightbulb

如果您希望將這些計算值新增至模型的陣列 / 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';

在此範例中,將使用值 Sally 呼叫 set 回呼。然後,修改器將 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 方法應回傳一個陣列,其中鍵是要型別轉換的屬性名稱,而值是您希望將欄位轉換為的型別。支援的型別轉換型別為:

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

若要示範屬性型別轉換,讓我們將儲存在資料庫中為整數 (01) 的 is_admin 屬性轉換為布林值。

<?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',
]);
exclamation

null 的屬性將不會被轉換。此外,您永遠不應定義與關係名稱相同的型別轉換(或屬性),也不應將型別轉換指派給模型的主索引鍵。

Stringable 型別轉換

您可以使用 Illuminate\Database\Eloquent\Casts\AsStringable cast 類別將模型屬性轉換為 流暢的 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 型別轉換

當處理儲存為序列化 JSON 的欄位時,array cast 特別有用。例如,如果您的資料庫有一個包含序列化 JSON 的 JSONTEXT 欄位類型,將 array cast 加入到該屬性,當您在 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',
];
}
}

一旦定義了 cast,您可以存取 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']);

陣列物件和集合 Casting

雖然標準的 array cast 對於許多應用程式來說已足夠,但它確實有一些缺點。由於 array cast 會傳回原始類型,因此無法直接修改陣列的偏移量。例如,以下程式碼將會觸發 PHP 錯誤

$user = User::find(1);
 
$user->options['key'] = $value;

為了解決這個問題,Laravel 提供了 AsArrayObject cast,它會將您的 JSON 屬性轉換為 ArrayObject 類別。此功能是使用 Laravel 的自訂 cast 實作,它允許 Laravel 智慧型地快取並轉換已變動的物件,這樣就可以修改個別偏移量,而不會觸發 PHP 錯誤。要使用 AsArrayObject cast,只需將它指派給屬性即可。

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 cast,它會將您的 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 cast 實例化自訂的集合類別而不是 Laravel 的基本集合類別,您可以提供集合類別名稱作為 cast 引數。

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 的實例,它擴展了 PHP 的 DateTime 類別並提供各種有用的方法。您可以透過在模型的 casts 方法中定義其他日期 cast 來轉換其他日期屬性。通常,日期應使用 datetimeimmutable_datetime cast 類型進行轉換。

當定義 datedatetime cast 時,您也可以指定日期的格式。當模型序列化為陣列或 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';

日期 Casting、序列化和時區

預設情況下,無論您的應用程式的 timezone 設定選項中指定的時區為何,datedatetime cast 都會將日期序列化為 UTC ISO-8601 日期字串 (YYYY-MM-DDTHH:MM:SS.uuuuuuZ)。強烈建議您始終使用此序列化格式,並且透過不將應用程式的 timezone 設定選項從其預設的 UTC 值變更,來將您應用程式的日期儲存在 UTC 時區中。在您的整個應用程式中持續使用 UTC 時區,將為使用 PHP 和 JavaScript 編寫的其他日期操作程式庫提供最大程度的互通性。

如果自訂格式應用於 datedatetime cast,例如 datetime:Y-m-d H:i:s,則在日期序列化期間將會使用 Carbon 實例的內部時區。通常,這會是您的應用程式的 timezone 設定選項中指定的時區。但是,請務必注意,諸如 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,
];
}

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

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

轉換列舉陣列

有時您可能需要模型將列舉值的陣列儲存在單一欄位中。要完成此操作,您可以使用 Laravel 提供的 AsEnumArrayObjectAsEnumCollection cast。

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 cast 會使用 Laravel 內建的加密功能來加密模型的屬性值。此外,encrypted:arrayencrypted:collectionencrypted:objectAsEncryptedArrayObjectAsEncryptedCollection cast 的工作方式與它們的未加密版本類似;但是,正如您可能預期的,基礎值在儲存在您的資料庫中時會被加密。

由於加密文字的最終長度是無法預測的,並且比其純文字長,請確保相關的資料庫欄位是 TEXT 類型或更大。此外,由於值在資料庫中被加密,您將無法查詢或搜尋加密的屬性值。

金鑰輪換

如您所知,Laravel 會使用您應用程式的 app 設定檔案中指定的 key 設定值來加密字串。通常,此值對應於 APP_KEY 環境變數的值。如果您需要輪換應用程式的加密金鑰,您將需要使用新的金鑰手動重新加密您的加密屬性。

查詢時型別轉換

有時您可能需要在執行查詢時套用 cast,例如從資料表中選取原始值時。例如,請考慮以下查詢

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 cast 套用到此屬性,那就太好了。值得慶幸的是,我們可以透過使用 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 有各種內建的、有用的 cast 類型;但是,您有時可能需要定義自己的 cast 類型。要建立 cast,請執行 make:cast Artisan 命令。新的 cast 類別將會放置在您的 app/Casts 目錄中。

php artisan make:cast Json

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

<?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);
}
}

一旦您定義了自訂 cast 類型,您可以使用其類別名稱將其附加到模型屬性。

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

值物件型別轉換

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

作為範例,我們將定義一個自訂 cast 類別,它會將多個模型值轉換為單一的 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();
lightbulb

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

值物件快取

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

如果您想要停用自訂 cast 類別的物件快取行為,您可以在自訂 cast 類別上宣告公用的 withoutObjectCaching 屬性。

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

陣列 / JSON 序列化

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

因此,您可以指定您的自訂 cast 類別將負責序列化值物件。為此,您的自訂 cast 類別應實作 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;
}

輸入型別轉換

有時,您可能需要編寫一個自訂 cast 類別,該類別僅轉換正在模型上設定的值,並且在從模型中擷取屬性時不執行任何操作。

僅限輸入的自訂 cast 應實作 CastsInboundAttributes 介面,該介面僅需要定義 set 方法。可以使用 --inbound 選項來叫用 make:cast Artisan 命令以產生僅限輸入的 cast 類別。

php artisan make:cast Hash --inbound

僅限輸入的 cast 的經典範例是「雜湊」cast。例如,我們可以定義一個 cast,它透過給定的演算法雜湊輸入值。

<?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);
}
}

型別轉換參數

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

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