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;
如果您希望將這些計算值新增至模型的陣列 / 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
若要示範屬性型別轉換,讓我們將儲存在資料庫中為整數 (0
或 1
) 的 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',]);
為 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 的 JSON
或 TEXT
欄位類型,將 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_at
和 updated_at
欄位轉換為 Carbon 的實例,它擴展了 PHP 的 DateTime
類別並提供各種有用的方法。您可以透過在模型的 casts
方法中定義其他日期 cast 來轉換其他日期屬性。通常,日期應使用 datetime
或 immutable_datetime
cast 類型進行轉換。
當定義 date
或 datetime
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
設定選項中指定的時區為何,date
和 datetime
cast 都會將日期序列化為 UTC ISO-8601 日期字串 (YYYY-MM-DDTHH:MM:SS.uuuuuuZ
)。強烈建議您始終使用此序列化格式,並且透過不將應用程式的 timezone
設定選項從其預設的 UTC
值變更,來將您應用程式的日期儲存在 UTC 時區中。在您的整個應用程式中持續使用 UTC 時區,將為使用 PHP 和 JavaScript 編寫的其他日期操作程式庫提供最大程度的互通性。
如果自訂格式應用於 date
或 datetime
cast,例如 datetime:Y-m-d H:i:s
,則在日期序列化期間將會使用 Carbon 實例的內部時區。通常,這會是您的應用程式的 timezone
設定選項中指定的時區。但是,請務必注意,諸如 created_at
和 updated_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 提供的 AsEnumArrayObject
或 AsEnumCollection
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:array
、encrypted:collection
、encrypted:object
、AsEncryptedArrayObject
和 AsEncryptedCollection
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
介面。實作此介面的類別必須定義 get
和 set
方法。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
值具有兩個公用屬性:lineOne
和 lineTwo
。
<?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();
如果您計劃將包含值物件的 Eloquent 模型序列化為 JSON 或陣列,您應該在值物件上實作 Illuminate\Contracts\Support\Arrayable
和 JsonSerializable
介面。
值物件快取
當解析轉換為值物件的屬性時,它們會被 Eloquent 快取。因此,如果再次存取該屬性,則將傳回相同的物件實例。
如果您想要停用自訂 cast 類別的物件快取行為,您可以在自訂 cast 類別上宣告公用的 withoutObjectCaching
屬性。
class Address implements CastsAttributes{ public bool $withoutObjectCaching = true; // ...}
陣列 / JSON 序列化
當使用 toArray
和 toJson
方法將 Eloquent 模型轉換為陣列或 JSON 時,您的自訂 cast 值物件通常也會被序列化,只要它們實作了 Illuminate\Contracts\Support\Arrayable
和 JsonSerializable
介面。但是,當使用第三方程式庫提供的值物件時,您可能無法將這些介面新增到物件。
因此,您可以指定您的自訂 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, ]; } }; }}