#はじめに
アクセサ、ミューテーター、属性のキャストを使うと、モデルインスタンスの属性値を取得・設定する際に変換できます。例えば、Laravelの暗号化機能を使ってデータベースに保存する値を暗号化し、Eloquentモデルでアクセスするときに自動的に復号化したい場合があります。また、データベースに保存されたJSON文字列をEloquentモデル経由でアクセスするときに配列に変換したい場合もあります。
#アクセサとミューテーター
#アクセサの定義
アクセサは、Eloquent属性値にアクセスするときに値を変換します。アクセサを定義するには、モデルに保護されたメソッドを作成し、アクセス可能な属性を表現します。このメソッド名は、実際のモデル属性やデータベースカラムの「キャメルケース」表記に対応させます。
この例では、first_name属性のアクセサを定義します。Eloquentがfirst_name属性の値を取得しようとするときに、このアクセサが自動的に呼び出されます。すべてのアクセサ/ミューテーターメソッドは、Illuminate\Database\Eloquent\Casts\Attributeの戻り値型ヒントを宣言する必要があります:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
/**
* ユーザーのファーストネームを取得します。
*/
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表現に追加したい場合は、appendメソッドで追加する必要があります。
#複数属性から値オブジェクトを作成する
アクセサで複数のモデル属性を1つの「値オブジェクト」に変換する必要がある場合があります。その場合、getクロージャは第2引数に$attributesを受け取れます。これはモデルの現在のすべての属性を含む配列で、自動的に渡されます:
use App\Support\Address;
use Illuminate\Database\Eloquent\Casts\Attribute;
/**
* ユーザーの住所を操作します。
*/
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メソッドを呼び出せます:
/**
* ユーザーの住所を操作します。
*/
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
{
/**
* ユーザーのファーストネームを操作します。
*/
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;
/**
* ユーザーの住所を操作します。
*/
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プロパティは配列で、キーにキャスト対象の属性名、値にキャストしたい型を指定します。サポートされているキャストタイプは以下の通りです:
arrayAsStringable::classbooleancollectiondatedatetimeimmutable_dateimmutable_datetimedecimal:<precision>doubleencryptedencrypted:arrayencrypted:collectionencrypted:objectfloathashedintegerobjectrealstringtimestamp
属性のキャストを示すために、データベースに整数(0または1)として保存されているis_admin属性をブール値にキャストしてみましょう:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
/**
* キャストすべき属性。
*
* @var array
*/
protected $casts = [
'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キャストクラスを使うと、モデル属性をフルーエントなIlluminate\Support\Stringableオブジェクトにキャストできます:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Casts\AsStringable;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
/**
* キャストすべき属性。
*
* @var array
*/
protected $casts = [
'directory' => AsStringable::class,
];
}
#配列とJSONのキャスト
arrayキャストは、シリアライズされたJSONを格納するカラムを扱う際に特に便利です。例えば、データベースにJSONまたはTEXT型のフィールドがあり、そこにシリアライズされたJSONが保存されている場合、その属性にarrayキャストを追加すると、Eloquentモデルでアクセスしたときに自動的にPHPの配列にデシリアライズされます:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
/**
* キャストすべき属性。
*
* @var array
*/
protected $casts = [
'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 は JSON 属性を ArrayObject クラスにキャストする AsArrayObject キャストを提供しています。この機能は Laravel の カスタムキャスト 実装を使っており、個々のオフセットを変更しても PHP エラーが発生しないように、ミューテートされたオブジェクトを賢くキャッシュおよび変換します。AsArrayObject キャストを使うには、単に属性に割り当てるだけです:
use Illuminate\Database\Eloquent\Casts\AsArrayObject;
/**
* キャストすべき属性
*
* @var array
*/
protected $casts = [
'options' => AsArrayObject::class,
];
同様に、Laravel は JSON 属性を Laravel の Collection インスタンスにキャストする AsCollection キャストも提供しています:
use Illuminate\Database\Eloquent\Casts\AsCollection;
/**
* キャストすべき属性
*
* @var array
*/
protected $casts = [
'options' => AsCollection::class,
];
AsCollection キャストで Laravel の基本コレクションクラスではなくカスタムコレクションクラスをインスタンス化したい場合は、キャストの引数としてコレクションクラス名を指定できます:
use App\Collections\OptionCollection;
use Illuminate\Database\Eloquent\Casts\AsCollection;
/**
* キャストすべき属性
*
* @var array
*/
protected $casts = [
'options' => AsCollection::class.':'.OptionCollection::class,
];
#日付キャスト
デフォルトで、Eloquent は created_at と updated_at カラムを PHP の DateTime クラスを拡張し便利なメソッドを提供する Carbon のインスタンスにキャストします。追加の日時属性をキャストしたい場合は、モデルの $casts 配列に追加の日時キャストを定義できます。通常、日時は datetime または immutable_datetime キャストタイプでキャストします。
date または datetime キャストを定義する際に、日時のフォーマットを指定することもできます。このフォーマットは モデルが配列や JSON にシリアライズされる際に使われます:
/**
* キャストすべき属性
*
* @var array
*/
protected $casts = [
'created_at' => 'datetime:Y-m-d',
];
カラムが日付としてキャストされている場合、対応するモデル属性には UNIX タイムスタンプ、日付文字列(Y-m-d)、日時文字列、または DateTime / Carbon インスタンスを設定できます。日付の値は正しく変換されデータベースに保存されます。
モデルのすべての日付のデフォルトシリアライズフォーマットをカスタマイズしたい場合は、モデルに serializeDate メソッドを定義してください。このメソッドはデータベースに保存される際の日付フォーマットには影響しません:
/**
* 配列 / JSON シリアライズ用に日付を準備する
*/
protected function serializeDate(DateTimeInterface $date): string
{
return $date->format('Y-m-d');
}
モデルの日付を実際にデータベースに保存する際のフォーマットを指定したい場合は、モデルに $dateFormat プロパティを定義してください:
/**
* モデルの日付カラムの保存フォーマット
*
* @var string
*/
protected $dateFormat = 'U';
#日付キャスト、シリアライズ、タイムゾーン
デフォルトでは、date と datetime キャストは、アプリケーションの timezone 設定に関係なく、UTC の ISO-8601 日付文字列(YYYY-MM-DDTHH:MM:SS.uuuuuuZ)にシリアライズされます。このシリアライズフォーマットを常に使い、アプリケーションの timezone 設定をデフォルトの UTC のままにして日付を UTC タイムゾーンで保存することを強く推奨します。アプリケーション全体で UTC タイムゾーンを一貫して使うことで、PHP や JavaScript の他の日時操作ライブラリとの最大限の互換性が得られます。
date または datetime キャストにカスタムフォーマット(例:datetime:Y-m-d H:i:s)を適用した場合、Carbon インスタンスの内部タイムゾーンが日付のシリアライズ時に使われます。通常はアプリケーションの timezone 設定で指定されたタイムゾーンです。
#Enum キャスト
Eloquent は属性値を PHP の Enum にキャストすることもできます。これを行うには、モデルの $casts 配列にキャストしたい属性と Enum を指定します:
use App\Enums\ServerStatus;
/**
* キャストすべき属性
*
* @var array
*/
protected $casts = [
'status' => ServerStatus::class,
];
モデルにキャストを定義すると、指定した属性にアクセスするときに自動的に Enum にキャストされ、また Enum から元の値にキャストされます:
if ($server->status == ServerStatus::Provisioned) {
$server->status = ServerStatus::Ready;
$server->save();
}
#Enum 配列のキャスト
モデルの単一カラムに Enum 値の配列を保存したい場合があります。その場合は、Laravel が提供する AsEnumArrayObject または AsEnumCollection キャストを利用できます:
use App\Enums\ServerStatus;
use Illuminate\Database\Eloquent\Casts\AsEnumCollection;
/**
* キャストすべき属性
*
* @var array
*/
protected $casts = [
'statuses' => AsEnumCollection::class.':'.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
{
/**
* 指定された値をキャストする
*
* @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);
}
/**
* 保存用に指定された値を準備する
*
* @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
{
/**
* キャストすべき属性
*
* @var array
*/
protected $casts = [
'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
{
/**
* 指定された値をキャストする
*
* @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']
);
}
/**
* 保存用に指定された値を準備する
*
* @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('指定された値は Address インスタンスではありません。');
}
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 シリアライズ
Eloquentモデルを toArray や toJson メソッドで配列やJSONに変換するとき、カスタムキャストの値オブジェクトは通常、Illuminate\Contracts\Support\Arrayable と JsonSerializable インターフェイスを実装していればシリアライズされます。しかし、サードパーティ製の値オブジェクトを使う場合は、これらのインターフェイスを追加できないことがあります。
そのため、カスタムキャストクラスが値オブジェクトのシリアライズを担当するよう指定できます。その場合、カスタムキャストクラスは Illuminate\Contracts\Database\Eloquent\SerializesCastableAttributes インターフェイスを実装する必要があります。このインターフェイスは、値オブジェクトのシリアライズ形式を返す serialize メソッドを含むことを要求します。
/**
* 値のシリアライズされた表現を取得します。
*
* @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
{
/**
* 新しいキャストクラスのインスタンスを作成します。
*/
public function __construct(
protected string|null $algorithm = null,
) {}
/**
* 保存用に値を準備します。
*
* @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);
}
}
#キャストパラメータ
モデルにカスタムキャストを付与するとき、クラス名の後に : で区切り、複数のパラメータはカンマ区切りで指定できます。これらのパラメータはキャストクラスのコンストラクタに渡されます。
/**
* キャストすべき属性
*
* @var array
*/
protected $casts = [
'secret' => Hash::class.':sha256',
];
#Castables
アプリケーションの値オブジェクトに独自のカスタムキャストクラスを定義させたい場合、モデルにカスタムキャストクラスを付与する代わりに、Illuminate\Contracts\Database\Eloquent\Castable インターフェイスを実装した値オブジェクトクラスを付与できます。
use App\ValueObjects\Address;
protected $casts = [
'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
{
/**
* このキャスト対象のキャスト時に使用するキャスタークラス名を取得します。
*
* @param array<string, mixed> $arguments
*/
public static function castUsing(array $arguments): string
{
return AddressCast::class;
}
}
Castable クラスを使う場合でも、$casts 定義で引数を渡せます。引数は castUsing メソッドに渡されます。
use App\ValueObjects\Address;
protected $casts = [
'address' => Address::class.':argument',
];
#Castables と匿名キャストクラス
"castables" と PHPの 匿名クラス を組み合わせることで、値オブジェクトとそのキャストロジックを単一のcastableオブジェクトとして定義できます。これを実現するには、値オブジェクトの castUsing メソッドから匿名クラスを返します。匿名クラスは CastsAttributes インターフェイスを実装する必要があります。
<?php
namespace App\ValueObjects;
use Illuminate\Contracts\Database\Eloquent\Castable;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
class Address implements Castable
{
// ...
/**
* このキャスト対象のキャスト時に使用するキャスタークラスを取得します。
*
* @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,
];
}
};
}
}