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
インスタンスを返します。この例では、属性のアクセス方法のみを定義しています。そのためには、Attribute
クラスコンストラクターにget
引数を渡します。
ご覧のとおり、列の元の値はアクセサーに渡されるため、値を操作して返すことができます。アクセサーの値にアクセスするには、モデルインスタンスのfirst_name
属性にアクセスするだけです。
use App\Models\User; $user = User::find(1); $firstName = $user->first_name;
これらの計算された値をモデルの配列/JSON表現に追加したい場合は、追加する必要があります。
複数の属性から値オブジェクトを構築する
アクセサーでは、複数のモデル属性を単一の「値オブジェクト」に変換する必要がある場合があります。そのためには、get
クロージャに$attributes
という2番目の引数を使用できます。これはクロージャに自動的に渡され、モデルの現在の属性の配列が含まれます。
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
メソッドは、キーがキャストされる属性の名前で、値が列をキャストする型である配列を返す必要があります。サポートされているキャスト型は次のとおりです。
-
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キャスト
fluent Illuminate\Support\Stringable
オブジェクトにモデル属性をキャストするには、Illuminate\Database\Eloquent\Casts\AsStringable
キャストクラスを使用できます。
<?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が含まれているJSON
またはTEXT
フィールド型がある場合、その属性に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のコレクションインスタンスにキャストします。
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, ];}
Laravelの基本コレクションクラスではなく、カスタムコレクションクラスをインスタンス化するためにAsCollection
キャストを使用したい場合は、コレクションクラス名をキャスト引数として指定できます。
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のインスタンスにキャストします。CarbonはPHPのDateTime
クラスを拡張し、多くの便利なメソッドを提供します。モデルのcasts
メソッド内で追加の日付キャストを定義することにより、追加の日付属性をキャストできます。通常、日付はdatetime
またはimmutable_datetime
キャストタイプを使用してキャストする必要があります。
date
またはdatetime
キャストを定義する際には、日付のフォーマットも指定できます。モデルが配列または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';
日付のキャスト、シリアライズ、タイムゾーン
デフォルトでは、date
とdatetime
キャストは、アプリケーションのtimezone
設定オプションで指定されたタイムゾーンに関係なく、日付をUTC ISO-8601日付文字列(YYYY-MM-DDTHH:MM:SS.uuuuuuZ
)にシリアライズします。このシリアライズ形式を常に使用し、アプリケーションのtimezone
設定オプションをデフォルトのUTC
値から変更しないことで、アプリケーションの日付をUTCタイムゾーンに保存することを強く推奨します。アプリケーション全体でUTCタイムゾーンを常に使用することで、PHPとJavaScriptで記述された他の日付操作ライブラリとの相互運用性を最大限に高めることができます。
datetime:Y-m-d H:i:s
など、date
またはdatetime
キャストにカスタムフォーマットが適用されている場合、日付シリアライズ中にCarbonインスタンスの内側のタイムゾーンが使用されます。通常、これはアプリケーションのtimezone
設定オプションで指定されたタイムゾーンです。ただし、created_at
やupdated_at
などのtimestamp
カラムはこの動作から除外され、アプリケーションのタイムゾーン設定に関係なく、常に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によって提供されるAsEnumArrayObject
またはAsEnumCollection
キャストを使用できます。
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:array
、encrypted:collection
、encrypted:object
、AsEncryptedArrayObject
、AsEncryptedCollection
キャストは、暗号化されていない対応物と同様に機能しますが、予想されるように、基になる値はデータベースに保存されるときに暗号化されます。
暗号化されたテキストの最終的な長さは予測不可能であり、プレーンテキストよりも長いため、関連付けられたデータベースカラムが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
インターフェースを実装します。このインターフェースを実装するクラスは、get
メソッドとset
メソッドを定義する必要があります。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
値には、lineOne
とlineTwo
の2つの公開プロパティがあると仮定します。
<?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によってキャッシュされます。したがって、属性に再度アクセスした場合、同じオブジェクトインスタンスが返されます。
カスタムキャストクラスのオブジェクトキャッシング動作を無効にする場合は、カスタムキャストクラスに公開withoutObjectCaching
プロパティを宣言できます。
class Address implements CastsAttributes{ public bool $withoutObjectCaching = true; // ...}
配列/JSONシリアライゼーション
toArray
メソッドとtoJson
メソッドを使用してEloquentモデルが配列またはJSONに変換されると、Illuminate\Contracts\Support\Arrayable
インターフェースとJsonSerializable
インターフェースを実装している限り、カスタムキャスト値オブジェクトは通常シリアライズされます。ただし、サードパーティライブラリによって提供される値オブジェクトを使用する場合、オブジェクトにこれらのインターフェースを追加できない場合があります。
したがって、カスタムキャストクラスが値オブジェクトのシリアライズを担当することを指定できます。そのためには、カスタムキャストクラスが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
インターフェースを実装するオブジェクトは、Castable
クラスとの間をキャストする責任を持つカスタムキャスタークラスのクラス名を返すcastUsing
メソッドを定義する必要があります。
<?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', ];}
Castableと匿名キャストクラス
「castable」とPHPの匿名クラスを組み合わせることで、値オブジェクトとそのキャスティングロジックを単一のcastableオブジェクトとして定義できます。これを実現するには、値オブジェクトの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, ]; } }; }}