Eloquent:更動子與型別轉換

簡介

通過存取子 (Accessor)、更動子 (Mutator)、與型別轉換,便可在從 Model 實體上存取 Eloquent 屬性時轉換其值。舉例來說,我們可能想用 Laravel 的加密功能 來在資料庫內加密某個值,並在從 Eloquent Model 上存取該屬性時自動解密。或者,我們可能會想將某個值轉換為 JSON 字串來儲存進資料庫,然後在 Eloquent Model 上以陣列來存取。

存取子 (Accessor) 與更動子 (Mutator)

定義存取子

存取子會在存取 Eloquent 屬性時變換起值。若要定義存取子,請在 Model 上建立一個 protected 方法,用來代表可存取的屬性。當有對應的 Model 屬性或資料庫欄位時,該方法的名稱應為對應屬性或欄位的「駝峰命名法 (camelCase)」形式。

在此範例中,我們會為 first_name 屬性定義一個存取子。當嘗試取得 first_name 屬性時,Eloquent 會自動呼叫這個存取子。所有的存取子與更動子方法都必須為回傳值標示型別提示為 Illuminate\Database\Eloquent\Casts\Attribute

1<?php
2 
3namespace App\Models;
4 
5use Illuminate\Database\Eloquent\Casts\Attribute;
6use Illuminate\Database\Eloquent\Model;
7 
8class User extends Model
9{
10 /**
11 * Get the user's first name.
12 */
13 protected function firstName(): Attribute
14 {
15 return Attribute::make(
16 get: fn (string $value) => ucfirst($value),
17 );
18 }
19}
1<?php
2 
3namespace App\Models;
4 
5use Illuminate\Database\Eloquent\Casts\Attribute;
6use Illuminate\Database\Eloquent\Model;
7 
8class User extends Model
9{
10 /**
11 * Get the user's first name.
12 */
13 protected function firstName(): Attribute
14 {
15 return Attribute::make(
16 get: fn (string $value) => ucfirst($value),
17 );
18 }
19}

回傳 Attribute 實體的存取子方法可用來定義要如何存取該值,以及可選地定義要如何更動值。在此番黎中,我們只有定義該屬性要被如何存取。為此,我們給 Attribute 類別的建構函式提供一個 get 引數。

如上所見,該欄位的原始值會傳給該存取子,讓你可以進行操作並回傳值。若要存取存取子的值,只需要在 Model 實體上存取 first_name 屬性即可:

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

若想讓過這些計算過的值包含在 Model 的陣列或 JSON 呈現上,則需要將這些欄位附加上去

從多個屬性建立數值物件

有時候,我們的存取子可能需要將多個物件屬性轉換為單一「數值物件 (Value Object)」。為此,我們的 get 閉包應接受第二個引數 $attributes,Laravel 會自動將該變數提供給閉包,而該變數為一組包含 Model 目前屬性的陣列:

1use App\Support\Address;
2use Illuminate\Database\Eloquent\Casts\Attribute;
3 
4/**
5 * Interact with the user's address.
6 */
7protected function address(): Attribute
8{
9 return Attribute::make(
10 get: fn (mixed $value, array $attributes) => new Address(
11 $attributes['address_line_one'],
12 $attributes['address_line_two'],
13 ),
14 );
15}
1use App\Support\Address;
2use Illuminate\Database\Eloquent\Casts\Attribute;
3 
4/**
5 * Interact with the user's address.
6 */
7protected function address(): Attribute
8{
9 return Attribute::make(
10 get: fn (mixed $value, array $attributes) => new Address(
11 $attributes['address_line_one'],
12 $attributes['address_line_two'],
13 ),
14 );
15}

Accessor 的快取

從 Accessor 內回傳數值物件時,任何對數值物件作出的更改也會在保存 Model 前自動同步回來。這是因為 Eloquent 會保留 Accessor 回傳的實體,好讓 Eloquent 能在每次叫用 Accessor 時都回傳相同的實體:

1use App\Models\User;
2 
3$user = User::find(1);
4 
5$user->address->lineOne = 'Updated Address Line 1 Value';
6$user->address->lineTwo = 'Updated Address Line 2 Value';
7 
8$user->save();
1use App\Models\User;
2 
3$user = User::find(1);
4 
5$user->address->lineOne = 'Updated Address Line 1 Value';
6$user->address->lineTwo = 'Updated Address Line 2 Value';
7 
8$user->save();

不過,有時候我們也會想快取一些如字串或布林等的原生型別值,尤其是當需要大量運算時。若要快取原生型別值時,可在定義 Accessor 時叫用 shouldCache 方法:

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

若想進用這個屬性的物件快取行為,可在定義屬性時叫用 withoutObjectCaching 方法:

1/**
2 * Interact with the user's address.
3 */
4protected function address(): Attribute
5{
6 return Attribute::make(
7 get: fn (mixed $value, array $attributes) => new Address(
8 $attributes['address_line_one'],
9 $attributes['address_line_two'],
10 ),
11 )->withoutObjectCaching();
12}
1/**
2 * Interact with the user's address.
3 */
4protected function address(): Attribute
5{
6 return Attribute::make(
7 get: fn (mixed $value, array $attributes) => new Address(
8 $attributes['address_line_one'],
9 $attributes['address_line_two'],
10 ),
11 )->withoutObjectCaching();
12}

定義更動子

更動子會在保存 Eloquent 屬性值時更改其值。若要定義更動子,可在定義屬性時提供一個 set 引數。讓我們來為 first_name 屬性定義一個更動子。每次我們嘗試在該 Model 上設定 first_name 屬性值的時候都會自動呼叫這個更動子:

1<?php
2 
3namespace App\Models;
4 
5use Illuminate\Database\Eloquent\Casts\Attribute;
6use Illuminate\Database\Eloquent\Model;
7 
8class User extends Model
9{
10 /**
11 * Interact with the user's first name.
12 */
13 protected function firstName(): Attribute
14 {
15 return Attribute::make(
16 get: fn (string $value) => ucfirst($value),
17 set: fn (string $value) => strtolower($value),
18 );
19 }
20}
1<?php
2 
3namespace App\Models;
4 
5use Illuminate\Database\Eloquent\Casts\Attribute;
6use Illuminate\Database\Eloquent\Model;
7 
8class User extends Model
9{
10 /**
11 * Interact with the user's first name.
12 */
13 protected function firstName(): Attribute
14 {
15 return Attribute::make(
16 get: fn (string $value) => ucfirst($value),
17 set: fn (string $value) => strtolower($value),
18 );
19 }
20}

該更動子閉包會接收目前正在設定的屬性的值,讓你可以更改其值並回傳更改過的值。若要使用這個更動子,只需要在 Eloquent Model 上設定 first_name 屬性即可:

1use App\Models\User;
2 
3$user = User::find(1);
4 
5$user->first_name = 'Sally';
1use App\Models\User;
2 
3$user = User::find(1);
4 
5$user->first_name = 'Sally';

在此範例中,set 閉包會以 Sally 值呼叫。更動子接著會在名字上套用 strtolower 函式,並將其結果設定到 Model 內部的 $attribuets 陣列上。

更動多個屬性

有時候,我們的更動子可能需要在底層的 Model 上設定多個屬性。為此,我們可以在 set 閉包內回傳一個陣列。陣列中的索引鍵應對應與 Model 關聯之底層的屬性或資料庫欄位:

1use App\Support\Address;
2use Illuminate\Database\Eloquent\Casts\Attribute;
3 
4/**
5 * Interact with the user's address.
6 */
7protected function address(): Attribute
8{
9 return Attribute::make(
10 get: fn (mixed $value, array $attributes) => new Address(
11 $attributes['address_line_one'],
12 $attributes['address_line_two'],
13 ),
14 set: fn (Address $value) => [
15 'address_line_one' => $value->lineOne,
16 'address_line_two' => $value->lineTwo,
17 ],
18 );
19}
1use App\Support\Address;
2use Illuminate\Database\Eloquent\Casts\Attribute;
3 
4/**
5 * Interact with the user's address.
6 */
7protected function address(): Attribute
8{
9 return Attribute::make(
10 get: fn (mixed $value, array $attributes) => new Address(
11 $attributes['address_line_one'],
12 $attributes['address_line_two'],
13 ),
14 set: fn (Address $value) => [
15 'address_line_one' => $value->lineOne,
16 'address_line_two' => $value->lineTwo,
17 ],
18 );
19}

屬性型別轉換

屬性型別轉換提供了與存取子及更動子類似的方法。不過,你不需要手動在 Model 內定義任何額外的方法。通過 Model 上的 $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

為了演示屬性型別轉換,我們來對 is_admin 屬性進行型別轉換。該欄位在資料庫中是以整數 (01) 來表示布林值的:

1<?php
2 
3namespace App\Models;
4 
5use Illuminate\Database\Eloquent\Model;
6 
7class User extends Model
8{
9 /**
10 * The attributes that should be cast.
11 *
12 * @var array
13 */
14 protected $casts = [
15 'is_admin' => 'boolean',
16 ];
17}
1<?php
2 
3namespace App\Models;
4 
5use Illuminate\Database\Eloquent\Model;
6 
7class User extends Model
8{
9 /**
10 * The attributes that should be cast.
11 *
12 * @var array
13 */
14 protected $casts = [
15 'is_admin' => 'boolean',
16 ];
17}

定義好型別轉換後,只要存取 is_admin 屬性,即使該屬性在資料庫中以整數來儲存,該屬性值總是會被轉換為布林值:

1$user = App\Models\User::find(1);
2 
3if ($user->is_admin) {
4 // ...
5}
1$user = App\Models\User::find(1);
2 
3if ($user->is_admin) {
4 // ...
5}

若有需要在執行階段加上新的、臨時的型別轉換,則可使用 mergeCasts 方法。這些型別轉換定義會被加到所有在 Model 中已定義的型別轉換上:

1$user->mergeCasts([
2 'is_admin' => 'integer',
3 'options' => 'object',
4]);
1$user->mergeCasts([
2 'is_admin' => 'integer',
3 'options' => 'object',
4]);
exclamation

null 的屬性將不會進行型別轉換。此外,定義型別轉換 (或屬性) 時,其名稱不應與關聯的名稱相同,且也不可為 Model 的主索引鍵指派型別轉換。

Stringable 的型別轉換

可以使用 Illuminate\Database\Eloquent\Casts\AsStringable 型別轉換類別來講 Model 屬性轉換為 [Fluent Illuminate\Support\Stringable 物件] (/docs/10.x/strings#fluent-strings-method-list):

1<?php
2 
3namespace App\Models;
4 
5use Illuminate\Database\Eloquent\Casts\AsStringable;
6use Illuminate\Database\Eloquent\Model;
7 
8class User extends Model
9{
10 /**
11 * The attributes that should be cast.
12 *
13 * @var array
14 */
15 protected $casts = [
16 'directory' => AsStringable::class,
17 ];
18}
1<?php
2 
3namespace App\Models;
4 
5use Illuminate\Database\Eloquent\Casts\AsStringable;
6use Illuminate\Database\Eloquent\Model;
7 
8class User extends Model
9{
10 /**
11 * The attributes that should be cast.
12 *
13 * @var array
14 */
15 protected $casts = [
16 'directory' => AsStringable::class,
17 ];
18}

陣列與 JSON 的型別轉換

array 型別轉換特別適合搭配宜 JSON 序列化保存的欄位。舉例來說,說資料庫內有個包含了序列化 JSON 的 JSONTEXT 欄位型別,則加上 array 型別轉換,就可以在從 Eloquent Model 上存取該欄位時自動將屬性反串聯化為 PHP 陣列:

1<?php
2 
3namespace App\Models;
4 
5use Illuminate\Database\Eloquent\Model;
6 
7class User extends Model
8{
9 /**
10 * The attributes that should be cast.
11 *
12 * @var array
13 */
14 protected $casts = [
15 'options' => 'array',
16 ];
17}
1<?php
2 
3namespace App\Models;
4 
5use Illuminate\Database\Eloquent\Model;
6 
7class User extends Model
8{
9 /**
10 * The attributes that should be cast.
11 *
12 * @var array
13 */
14 protected $casts = [
15 'options' => 'array',
16 ];
17}

定義好型別轉換後,存取 options 屬性時就會自動從 JSON 反序列化為 PHP 陣列。為 options 賦值時,提供的陣列也會被序列化回 JSON 以進行儲存:

1use App\Models\User;
2 
3$user = User::find(1);
4 
5$options = $user->options;
6 
7$options['key'] = 'value';
8 
9$user->options = $options;
10 
11$user->save();
1use App\Models\User;
2 
3$user = User::find(1);
4 
5$options = $user->options;
6 
7$options['key'] = 'value';
8 
9$user->options = $options;
10 
11$user->save();

若要使用更精簡的方法來更新 JSON 屬性中的單一欄位,可以在呼叫 update 方法時使用 -> 運算子:

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

陣列物件與 Collection 的型別轉換

雖然使用標準的 array 型別轉換對於大多數專案就夠用了,但 array 也有一些缺點。由於 array 型別轉換回傳的是原生型別,因此我們無法直接更改陣列的元素。舉例來說,下列程式碼會觸發 PHP 錯誤:

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

為了解決這個問題,Laravel 提供了一個 AsArrayObject 型別轉換,可用來將 JSON 屬性轉換為 ArrayObject 類別。改功能使用 Laravel 的自訂型別轉換實作,可讓 Laravel 進行智慧快取並變換更改過的物件,也能讓個別元素在修改時不觸發 PHP 錯誤。若要使用 AsArrayObject 型別轉換,只需要將其指派給屬性即可:

1use Illuminate\Database\Eloquent\Casts\AsArrayObject;
2 
3/**
4 * The attributes that should be cast.
5 *
6 * @var array
7 */
8protected $casts = [
9 'options' => AsArrayObject::class,
10];
1use Illuminate\Database\Eloquent\Casts\AsArrayObject;
2 
3/**
4 * The attributes that should be cast.
5 *
6 * @var array
7 */
8protected $casts = [
9 'options' => AsArrayObject::class,
10];

Laravel 還提供了一個類似的 AsCollection 型別轉換,可將 JSON 屬性轉換為 Laravel 的 Collection 實體:

1use Illuminate\Database\Eloquent\Casts\AsCollection;
2 
3/**
4 * The attributes that should be cast.
5 *
6 * @var array
7 */
8protected $casts = [
9 'options' => AsCollection::class,
10];
1use Illuminate\Database\Eloquent\Casts\AsCollection;
2 
3/**
4 * The attributes that should be cast.
5 *
6 * @var array
7 */
8protected $casts = [
9 'options' => AsCollection::class,
10];

若想讓 AsCollection 型別轉換使用自定 Collection 類別而不用 Laravel 的基礎 Collection 類別,可以使用型別轉換引數的方式提供 Collection 類別的名稱:

1use App\Collections\OptionCollection;
2use Illuminate\Database\Eloquent\Casts\AsCollection;
3 
4/**
5 * The attributes that should be cast.
6 *
7 * @var array
8 */
9protected $casts = [
10 'options' => AsCollection::class.':'.OptionCollection::class,
11];
1use App\Collections\OptionCollection;
2use Illuminate\Database\Eloquent\Casts\AsCollection;
3 
4/**
5 * The attributes that should be cast.
6 *
7 * @var array
8 */
9protected $casts = [
10 'options' => AsCollection::class.':'.OptionCollection::class,
11];

日期的型別轉換

預設情況下,Eloquent 會將 created_atupdated_at 欄位轉換為 Carbon 實體。Carbon 繼承自 PHP 的 DateTime 類別,並提供了各種實用方法。可以通過往 Model 的 $casts 屬性陣列內定義額外的日期型別轉換來給其他日期屬性進行轉換。通常來說,日期應使用 datetimeimmutable_datetime 型別轉換類型。

在定義 datedatetime 型別轉換時,也可以指定日期的格式。該格式會在 Model 被序列化成陣列或 JSON 時使用:

1/**
2 * The attributes that should be cast.
3 *
4 * @var array
5 */
6protected $casts = [
7 'created_at' => 'datetime:Y-m-d',
8];
1/**
2 * The attributes that should be cast.
3 *
4 * @var array
5 */
6protected $casts = [
7 'created_at' => 'datetime:Y-m-d',
8];

在將欄位轉換為日期時,可以將相應的 Model 屬性值設為 UNIX 時戳、日期字串 (Y-m-d)、日期與時間字串、或是 DateTime / Carbon 實體。日期的值會被正確地轉換並保存在資料庫中。

在 Model 中定義 serializeDate 方法,即可為 Model 中所有的日期定義預設的序列化方法。改方法並不會影響日期儲存到資料庫時的格式化方法:

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

若要指定用來將 Model 日期保存在資料庫時使用的格式,可在 Model 中定義 $dateFormat 屬性:

1/**
2 * The storage format of the model's date columns.
3 *
4 * @var string
5 */
6protected $dateFormat = 'U';
1/**
2 * The storage format of the model's date columns.
3 *
4 * @var string
5 */
6protected $dateFormat = 'U';

日期型別轉換、序列化、與時區

預設情況下,不論專案的 timezone 設定選項設為哪個時區,datedatetime 都會將日期序列化為 UTC 的 ISO-8601 日期字串 (YYYY-MM-DDTHH:MM:SS.uuuuuuZ)。我們強烈建議你保持使用這個序列化格式,也建議你只將專案的 timezone 設定選項設為預設的 UTC,並讓專案中以 UTC 來儲存所有的日期時間。在專案中保持一致地使用 UTC 時區,可為其他 PHP 與 JavaScript 的日期操作函示庫提供最大的互用性。

若有在 datedatetime 型別轉換內提供自訂格式,如 datetime:Y-m-d H:i:s,則在進行日期序列化時,會使用 Carbon 實體內部的時區。一般來說,這個時區就是專案的 timezone 設定選項。

Enum 的型別轉換

Eloquent 也能讓我們將屬性值轉換為 PHP 的 Enum。若要轉換成 Enum,可在 Model 中的 $casts 屬性陣列中指定要型別轉換的屬性與 Enum:

1use App\Enums\ServerStatus;
2 
3/**
4 * The attributes that should be cast.
5 *
6 * @var array
7 */
8protected $casts = [
9 'status' => ServerStatus::class,
10];
1use App\Enums\ServerStatus;
2 
3/**
4 * The attributes that should be cast.
5 *
6 * @var array
7 */
8protected $casts = [
9 'status' => ServerStatus::class,
10];

定義好 Model 的型別轉換後,每次存取該屬性時就會自動轉換對 Enum 進行轉換:

1if ($server->status == ServerStatus::Provisioned) {
2 $server->status = ServerStatus::Ready;
3 
4 $server->save();
5}
1if ($server->status == ServerStatus::Provisioned) {
2 $server->status = ServerStatus::Ready;
3 
4 $server->save();
5}

型別轉換一組 Enum 的陣列

有時候,我們需要在 Model 中將一組 Enum 值的陣列保存在單一一個欄位裡。這時,可以使用 Laravel 所提供的 AsEnumArrayObjectAsEnumCollection Cast:

1use App\Enums\ServerStatus;
2use Illuminate\Database\Eloquent\Casts\AsEnumCollection;
3 
4/**
5 * The attributes that should be cast.
6 *
7 * @var array
8 */
9protected $casts = [
10 'statuses' => AsEnumCollection::class.':'.ServerStatus::class,
11];
1use App\Enums\ServerStatus;
2use Illuminate\Database\Eloquent\Casts\AsEnumCollection;
3 
4/**
5 * The attributes that should be cast.
6 *
7 * @var array
8 */
9protected $casts = [
10 'statuses' => AsEnumCollection::class.':'.ServerStatus::class,
11];

加密的型別轉換

encrypted 型別轉換會通過 Laravel 的內建加密功能來加密 Model 的屬性值。此外,還有 encrypted:array, encrypted:collection, encrypted:object, AsEncryptedArrayObject, 與 AsEncryptedCollection 等型別轉換,這些型別轉換都與其未加密的版本一樣以相同方式運作。不過,可想而知,底層的值會先加密才保存進資料庫。

由於加密後的文字長度時無法預測的,且通常比明文的版本還要長,因此請確保其資料庫欄位為 TEXT 型別或更大的型別。此外,由於在資料庫中值都是經過加密的,因此你也沒辦法查詢或搜尋加密過的屬性質。

更改密鑰

讀者可能已經知道,Laravel 會使用專案 app 設定檔中的 key 設定值來加密字串。一般來說,這個設定值對應的是 APP_KEY 環境變數。若有需要更改專案的加密密鑰,則我們需要使用新的密鑰來重新加密這些經過加密的屬性。

查詢時的型別轉換

有時候我們可能會需要在執行查詢時套用型別轉換,例如從資料表中選擇原始資料時。舉例來說,假設有下列查詢:

1use App\Models\Post;
2use App\Models\User;
3 
4$users = User::select([
5 'users.*',
6 'last_posted_at' => Post::selectRaw('MAX(created_at)')
7 ->whereColumn('user_id', 'users.id')
8])->get();
1use App\Models\Post;
2use App\Models\User;
3 
4$users = User::select([
5 'users.*',
6 'last_posted_at' => Post::selectRaw('MAX(created_at)')
7 ->whereColumn('user_id', 'users.id')
8])->get();

查詢結果中的 last_posted_at 屬性會是字串。如果我們可以將 datetime 型別轉換在執行查詢時套用到這個屬性上就好了。好佳在,我們可以通過使用 withCasts 方法來達成:

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

自訂型別轉換

Laravel 中有各種內建的實用型別轉換類型。不過,有時候,我們還是需要定義自定 Cast。若要建立型別轉換程式,請執行 make:cast Artisan 指令。新建立的 Cast 類別會被放在 app/Casts 目錄下:

1php artisan make:cast Json
1php artisan make:cast Json

所有的自定 Cast 類別都實作了 CastsAttributes 界面。實作了這個介面的類別必須定義一組 getset 方法。get 方法用於將儲存在資料庫內的原始值轉換為型別值;set 方法則負責將型別值轉換為可儲存在資料庫內的原始值。在這裡,我們將重新實作一個內建的 json 型別轉換類型為例:

1<?php
2 
3namespace App\Casts;
4 
5use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
6use Illuminate\Database\Eloquent\Model;
7 
8class Json implements CastsAttributes
9{
10 /**
11 * Cast the given value.
12 *
13 * @param array<string, mixed> $attributes
14 * @return array<string, mixed>
15 */
16 public function get(Model $model, string $key, mixed $value, array $attributes): array
17 {
18 return json_decode($value, true);
19 }
20 
21 /**
22 * Prepare the given value for storage.
23 *
24 * @param array<string, mixed> $attributes
25 */
26 public function set(Model $model, string $key, mixed $value, array $attributes): string
27 {
28 return json_encode($value);
29 }
30}
1<?php
2 
3namespace App\Casts;
4 
5use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
6use Illuminate\Database\Eloquent\Model;
7 
8class Json implements CastsAttributes
9{
10 /**
11 * Cast the given value.
12 *
13 * @param array<string, mixed> $attributes
14 * @return array<string, mixed>
15 */
16 public function get(Model $model, string $key, mixed $value, array $attributes): array
17 {
18 return json_decode($value, true);
19 }
20 
21 /**
22 * Prepare the given value for storage.
23 *
24 * @param array<string, mixed> $attributes
25 */
26 public function set(Model $model, string $key, mixed $value, array $attributes): string
27 {
28 return json_encode($value);
29 }
30}

定義好自訂的型別轉換類型後,就可以使用類別名稱將其附加到 Model 屬性內:

1<?php
2 
3namespace App\Models;
4 
5use App\Casts\Json;
6use Illuminate\Database\Eloquent\Model;
7 
8class User extends Model
9{
10 /**
11 * The attributes that should be cast.
12 *
13 * @var array
14 */
15 protected $casts = [
16 'options' => Json::class,
17 ];
18}
1<?php
2 
3namespace App\Models;
4 
5use App\Casts\Json;
6use Illuminate\Database\Eloquent\Model;
7 
8class User extends Model
9{
10 /**
11 * The attributes that should be cast.
12 *
13 * @var array
14 */
15 protected $casts = [
16 'options' => Json::class,
17 ];
18}

數值物件的型別轉換

進行型別轉換時,我們不只可以將值轉換為 PHP 的原生型別,我們還可以將值轉換為物件。定義這種將值轉換為物件的自訂型別轉換就跟轉換成原生型別類似。不過,在這種型別轉換類別中的 set 方法應回傳一組在 Model 上用於設定原始、可儲存值的索引鍵/值配對。

在這裡,我們以將多個 Model 值轉換到單一 Address 數值物件的自訂型別轉換類別為例。我們假設 Address 值有兩個公用屬性:lineOnelineTwo

1<?php
2 
3namespace App\Casts;
4 
5use App\ValueObjects\Address as AddressValueObject;
6use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
7use Illuminate\Database\Eloquent\Model;
8use InvalidArgumentException;
9 
10class Address implements CastsAttributes
11{
12 /**
13 * Cast the given value.
14 *
15 * @param array<string, mixed> $attributes
16 */
17 public function get(Model $model, string $key, mixed $value, array $attributes): AddressValueObject
18 {
19 return new AddressValueObject(
20 $attributes['address_line_one'],
21 $attributes['address_line_two']
22 );
23 }
24 
25 /**
26 * Prepare the given value for storage.
27 *
28 * @param array<string, mixed> $attributes
29 * @return array<string, string>
30 */
31 public function set(Model $model, string $key, mixed $value, array $attributes): array
32 {
33 if (! $value instanceof AddressValueObject) {
34 throw new InvalidArgumentException('The given value is not an Address instance.');
35 }
36 
37 return [
38 'address_line_one' => $value->lineOne,
39 'address_line_two' => $value->lineTwo,
40 ];
41 }
42}
1<?php
2 
3namespace App\Casts;
4 
5use App\ValueObjects\Address as AddressValueObject;
6use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
7use Illuminate\Database\Eloquent\Model;
8use InvalidArgumentException;
9 
10class Address implements CastsAttributes
11{
12 /**
13 * Cast the given value.
14 *
15 * @param array<string, mixed> $attributes
16 */
17 public function get(Model $model, string $key, mixed $value, array $attributes): AddressValueObject
18 {
19 return new AddressValueObject(
20 $attributes['address_line_one'],
21 $attributes['address_line_two']
22 );
23 }
24 
25 /**
26 * Prepare the given value for storage.
27 *
28 * @param array<string, mixed> $attributes
29 * @return array<string, string>
30 */
31 public function set(Model $model, string $key, mixed $value, array $attributes): array
32 {
33 if (! $value instanceof AddressValueObject) {
34 throw new InvalidArgumentException('The given value is not an Address instance.');
35 }
36 
37 return [
38 'address_line_one' => $value->lineOne,
39 'address_line_two' => $value->lineTwo,
40 ];
41 }
42}

對數值物件進行型別轉換時,對數值物件進行的所有更改都會在 Model 儲存前同步回 Model 上:

1use App\Models\User;
2 
3$user = User::find(1);
4 
5$user->address->lineOne = 'Updated Address Value';
6 
7$user->save();
1use App\Models\User;
2 
3$user = User::find(1);
4 
5$user->address->lineOne = 'Updated Address Value';
6 
7$user->save();
lightbulb

若有打算要將包含數值物件的 Eloquent Model 序列化為 JSON 或陣列,則該數值物件應實作 Illuminate\Contracts\Support\ArrayableJsonSerializable 介面。

數值物件的快取

當在解析會被轉換為數值物件的屬性時,Eloquent 會快取這些物件。因此,再次存取該屬性時,會回傳同一個物件實體。

若要禁用自定 Cast 類別的物件快取行為,可在自定 Cast 類別上宣告一個 public 的 withoutObjectCaching 屬性:

1class Address implements CastsAttributes
2{
3 public bool $withoutObjectCaching = true;
4 
5 // ...
6}
1class Address implements CastsAttributes
2{
3 public bool $withoutObjectCaching = true;
4 
5 // ...
6}

Array / JSON 的序列化

當 Eloquent Model 通過 toArraytoJson 轉換為陣列或 JSON 時,只要自訂的型別轉換數值物件有實作 Illuminate\Contracts\Support\ArrayableJsonSerializable 介面,該數值物件也會一併被序列化。不過,若我們使用的數值物件是來自第三方套件的,那我們可能就沒辦法提供這些負責序列化介面。

因此,我們可以指定讓自訂型別轉換類別來負責處理數值物件的序列化。為此,自訂型別轉換類別應實作 Illuminate\Contracts\Database\Eloquent\SerializesCastableAttributes 介面。實作這個介面,就代表該類別中應包含一個 serialize 方法,該方法應回傳數值物件的序列化形式:

1/**
2 * Get the serialized representation of the value.
3 *
4 * @param array<string, mixed> $attributes
5 */
6public function serialize(Model $model, string $key, mixed $value, array $attributes): string
7{
8 return (string) $value;
9}
1/**
2 * Get the serialized representation of the value.
3 *
4 * @param array<string, mixed> $attributes
5 */
6public function serialize(Model $model, string $key, mixed $value, array $attributes): string
7{
8 return (string) $value;
9}

輸入型別轉換

有時候,我們可能會需要撰寫只在值被寫入 Model 時要進行轉換的自定 Cast 類別,而在從 Model 中取值時不進行任何操作。

Inbound Only(傳入限定) 自定 Cast 應實作 CastsInboundAttributes 介面,該介面只要求定義 set 方法。在呼叫 make:cast Artisan 指令時使用 --inbound 選項,就可產生 Inbound Only 的 Cast 類別:

1php artisan make:cast Hash --inbound
1php artisan make:cast Hash --inbound

Inbound Only Cast 的典型例子就是「雜湊」Cast。舉例來說,我們可以定義一個 Cast,以使用給定演算法來雜湊傳入的值:

1<?php
2 
3namespace App\Casts;
4 
5use Illuminate\Contracts\Database\Eloquent\CastsInboundAttributes;
6use Illuminate\Database\Eloquent\Model;
7 
8class Hash implements CastsInboundAttributes
9{
10 /**
11 * Create a new cast class instance.
12 */
13 public function __construct(
14 protected string|null $algorithm = null,
15 ) {}
16 
17 /**
18 * Prepare the given value for storage.
19 *
20 * @param array<string, mixed> $attributes
21 */
22 public function set(Model $model, string $key, mixed $value, array $attributes): string
23 {
24 return is_null($this->algorithm)
25 ? bcrypt($value)
26 : hash($this->algorithm, $value);
27 }
28}
1<?php
2 
3namespace App\Casts;
4 
5use Illuminate\Contracts\Database\Eloquent\CastsInboundAttributes;
6use Illuminate\Database\Eloquent\Model;
7 
8class Hash implements CastsInboundAttributes
9{
10 /**
11 * Create a new cast class instance.
12 */
13 public function __construct(
14 protected string|null $algorithm = null,
15 ) {}
16 
17 /**
18 * Prepare the given value for storage.
19 *
20 * @param array<string, mixed> $attributes
21 */
22 public function set(Model $model, string $key, mixed $value, array $attributes): string
23 {
24 return is_null($this->algorithm)
25 ? bcrypt($value)
26 : hash($this->algorithm, $value);
27 }
28}

型別轉換的參數

在 Model 上設定自訂型別轉換時,可以指定型別轉換的參數,請使用 : 字元來區分型別轉換類別名稱與參數,並使用逗號來區分多個參數。這些參數會傳給型別轉換類別的建構函式:

1/**
2 * The attributes that should be cast.
3 *
4 * @var array
5 */
6protected $casts = [
7 'secret' => Hash::class.':sha256',
8];
1/**
2 * The attributes that should be cast.
3 *
4 * @var array
5 */
6protected $casts = [
7 'secret' => Hash::class.':sha256',
8];

Castable

我們可以讓專案中的數值物件自己定義自己的自訂型別轉換類別。與在 Model 中設定自訂的型別轉換類別相比,我們可以設定實作了 Illuminate\Contracts\Database\Eloquent\Castable 介面的數值物件類別:

1use App\Models\Address;
2 
3protected $casts = [
4 'address' => Address::class,
5];
1use App\Models\Address;
2 
3protected $casts = [
4 'address' => Address::class,
5];

實作了 Castable 介面的物件必須定義 castUsing 方法。該方法則應回傳用於對 Castable 類別進行型別轉換的自訂型別轉換類別名稱:

1<?php
2 
3namespace App\Models;
4 
5use Illuminate\Contracts\Database\Eloquent\Castable;
6use App\Casts\Address as AddressCast;
7 
8class Address implements Castable
9{
10 /**
11 * Get the name of the caster class to use when casting from / to this cast target.
12 *
13 * @param array<string, mixed> $arguments
14 */
15 public static function castUsing(array $arguments): string
16 {
17 return AddressCast::class;
18 }
19}
1<?php
2 
3namespace App\Models;
4 
5use Illuminate\Contracts\Database\Eloquent\Castable;
6use App\Casts\Address as AddressCast;
7 
8class Address implements Castable
9{
10 /**
11 * Get the name of the caster class to use when casting from / to this cast target.
12 *
13 * @param array<string, mixed> $arguments
14 */
15 public static function castUsing(array $arguments): string
16 {
17 return AddressCast::class;
18 }
19}

即使是使用 Castable 類別,也可以在 $casts 定義中提供引數。這些引數會被傳給 castUsing 方法:

1use App\Models\Address;
2 
3protected $casts = [
4 'address' => Address::class.':argument',
5];
1use App\Models\Address;
2 
3protected $casts = [
4 'address' => Address::class.':argument',
5];

Castable 與匿名型別轉換類別

通過將「Castable」與 PHP 的匿名函式搭配使用,我們就能在單一 Castable 物件內定義數值物件與其型別轉換邏輯。為此,請在數值物件的 castUsing 方法內回傳一個匿名類別。這個匿名類別應實作 CastsAttributes 介面:

1<?php
2 
3namespace App\Models;
4 
5use Illuminate\Contracts\Database\Eloquent\Castable;
6use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
7use Illuminate\Database\Eloquent\Model;
8 
9class Address implements Castable
10{
11 // ...
12 
13 /**
14 * Get the caster class to use when casting from / to this cast target.
15 *
16 * @param array<string, mixed> $arguments
17 */
18 public static function castUsing(array $arguments): CastsAttributes
19 {
20 return new class implements CastsAttributes
21 {
22 public function get(Model $model, string $key, mixed $value, array $attributes): Address
23 {
24 return new Address(
25 $attributes['address_line_one'],
26 $attributes['address_line_two']
27 );
28 }
29 
30 public function set(Model $model, string $key, mixed $value, array $attributes): array
31 {
32 return [
33 'address_line_one' => $value->lineOne,
34 'address_line_two' => $value->lineTwo,
35 ];
36 }
37 };
38 }
39}
1<?php
2 
3namespace App\Models;
4 
5use Illuminate\Contracts\Database\Eloquent\Castable;
6use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
7use Illuminate\Database\Eloquent\Model;
8 
9class Address implements Castable
10{
11 // ...
12 
13 /**
14 * Get the caster class to use when casting from / to this cast target.
15 *
16 * @param array<string, mixed> $arguments
17 */
18 public static function castUsing(array $arguments): CastsAttributes
19 {
20 return new class implements CastsAttributes
21 {
22 public function get(Model $model, string $key, mixed $value, array $attributes): Address
23 {
24 return new Address(
25 $attributes['address_line_one'],
26 $attributes['address_line_two']
27 );
28 }
29 
30 public function set(Model $model, string $key, mixed $value, array $attributes): array
31 {
32 return [
33 'address_line_one' => $value->lineOne,
34 'address_line_two' => $value->lineTwo,
35 ];
36 }
37 };
38 }
39}
翻譯進度
100% 已翻譯
更新時間:
2024年6月30日 上午8:18:00 [世界標準時間]
翻譯人員:
  • cornch
幫我們翻譯此頁

留言

尚無留言

“Laravel” is a Trademark of Taylor Otwell.
The source documentation is released under MIT license. See laravel/docs on GitHub for details.
The translated documentations are released under MIT license. See cornch/laravel-docs-l10n on GitHub for details.