Eloquent: APIリソース
はじめに
APIを構築する際には、Eloquentモデルとアプリケーションのユーザーに実際に返されるJSONレスポンスの間に位置する変換レイヤーが必要になる場合があります。たとえば、特定のユーザーのサブセットに対しては特定の属性を表示し、他のユーザーに対しては表示しないようにしたり、モデルのJSON表現に常に特定のリレーションシップを含めるようにしたりする場合があります。Eloquentのリソースクラスを使用すると、モデルとモデルコレクションをJSONに変換する処理を簡潔かつ容易に行うことができます。
もちろん、`toJson`メソッドを使用してEloquentモデルまたはコレクションをJSONに変換することもできますが、Eloquentリソースは、モデルとそのリレーションシップのJSONシリアライゼーションに対して、より詳細で堅牢な制御を提供します。
リソースの生成
リソースクラスを生成するには、`make:resource` Artisanコマンドを使用します。デフォルトでは、リソースはアプリケーションの`app/Http/Resources`ディレクトリに配置されます。リソースは`Illuminate\Http\Resources\Json\JsonResource`クラスを拡張します。
php artisan make:resource UserResource
リソースコレクション
個々のモデルを変換するリソースを生成することに加えて、モデルのコレクションを変換する役割を担うリソースを生成することもできます。これにより、JSONレスポンスに、特定のリソースの全コレクションに関連するリンクやその他のメタ情報を含めることができます。
リソースコレクションを作成するには、リソースを作成する際に`--collection`フラグを使用します。または、リソース名に`Collection`という単語を含めると、Laravelはコレクションリソースを作成する必要があることを認識します。コレクションリソースは`Illuminate\Http\Resources\Json\ResourceCollection`クラスを拡張します。
php artisan make:resource User --collection php artisan make:resource UserCollection
概念の概要
これは、リソースとリソースコレクションの概要です。リソースによって提供されるカスタマイズと機能についてより深く理解するには、このドキュメントの他のセクションを読むことを強くお勧めします。
リソースの作成時に利用できるすべてのオプションについて詳しく説明する前に、まずLaravel内でリソースがどのように使用されるかについて、概要を見てみましょう。リソースクラスは、JSON構造に変換する必要がある単一のモデルを表します。たとえば、単純な`UserResource`リソースクラスを以下に示します。
<?php namespace App\Http\Resources; use Illuminate\Http\Request;use Illuminate\Http\Resources\Json\JsonResource; class UserResource extends JsonResource{ /** * Transform the resource into an array. * * @return array<string, mixed> */ public function toArray(Request $request): array { return [ 'id' => $this->id, 'name' => $this->name, 'email' => $this->email, 'created_at' => $this->created_at, 'updated_at' => $this->updated_at, ]; }}
各リソースクラスは`toArray`メソッドを定義しており、リソースがルートまたはコントローラーメソッドからのレスポンスとして返されたときにJSONに変換されるべき属性の配列を返します。
`$this`変数から直接モデルのプロパティにアクセスできることに注意してください。これは、リソースクラスがプロパティとメソッドへのアクセスを基になるモデルに自動的にプロキシするため、便利なアクセスが可能になるためです。リソースが定義されたら、ルートまたはコントローラーから返すことができます。リソースは、そのコンストラクターを介して基になるモデルインスタンスを受け入れます。
use App\Http\Resources\UserResource;use App\Models\User; Route::get('/user/{id}', function (string $id) { return new UserResource(User::findOrFail($id));});
リソースコレクション
リソースのコレクションまたはページネーションされたレスポンスを返している場合は、ルートまたはコントローラーでリソースインスタンスを作成する際に、リソースクラスによって提供される`collection`メソッドを使用する必要があります。
use App\Http\Resources\UserResource;use App\Models\User; Route::get('/users', function () { return UserResource::collection(User::all());});
これでは、コレクションと共に返す必要があるカスタムメタデータを追加することはできません。リソースコレクションレスポンスをカスタマイズする場合は、コレクションを表す専用の資源を作成する必要があります。
php artisan make:resource UserCollection
リソースコレクションクラスが生成されたら、レスポンスに含めるべきメタデータを簡単に定義できます。
<?php namespace App\Http\Resources; use Illuminate\Http\Request;use Illuminate\Http\Resources\Json\ResourceCollection; class UserCollection extends ResourceCollection{ /** * Transform the resource collection into an array. * * @return array<int|string, mixed> */ public function toArray(Request $request): array { return [ 'data' => $this->collection, 'links' => [ 'self' => 'link-value', ], ]; }}
リソースコレクションを定義したら、ルートまたはコントローラーから返すことができます。
use App\Http\Resources\UserCollection;use App\Models\User; Route::get('/users', function () { return new UserCollection(User::all());});
コレクションキーの保持
ルートからリソースコレクションを返す場合、Laravelはコレクションのキーをリセットして数値順に並べ替えます。ただし、コレクションの元のキーを保持するかどうかを示す`preserveKeys`プロパティをリソースクラスに追加することができます。
<?php namespace App\Http\Resources; use Illuminate\Http\Resources\Json\JsonResource; class UserResource extends JsonResource{ /** * Indicates if the resource's collection keys should be preserved. * * @var bool */ public $preserveKeys = true;}
`preserveKeys`プロパティを`true`に設定すると、コレクションがルートまたはコントローラーから返されたときに、コレクションキーは保持されます。
use App\Http\Resources\UserResource;use App\Models\User; Route::get('/users', function () { return UserResource::collection(User::all()->keyBy->id);});
基になるリソースクラスのカスタマイズ
通常、リソースコレクションの`$this->collection`プロパティは、コレクションの各アイテムをその単一のリソースクラスにマッピングした結果によって自動的に設定されます。単一のリソースクラスは、クラス名の末尾の`Collection`部分を削除したコレクションのクラス名とみなされます。さらに、個人の好みに応じて、単一のリソースクラスに`Resource`という接尾辞が付いている場合と付いていない場合があります。
たとえば、`UserCollection`は、与えられたユーザーインスタンスを`UserResource`リソースにマッピングしようとします。この動作をカスタマイズするには、リソースコレクションの`$collects`プロパティをオーバーライドします。
<?php namespace App\Http\Resources; use Illuminate\Http\Resources\Json\ResourceCollection; class UserCollection extends ResourceCollection{ /** * The resource that this resource collects. * * @var string */ public $collects = Member::class;}
リソースの作成
概念の概要 をまだ読んでいない場合は、このドキュメントを読み進める前に読むことを強くお勧めします。
リソースは、与えられたモデルを配列に変換するだけで済みます。そのため、各リソースには`toArray`メソッドが含まれており、モデルの属性をアプリケーションのルートまたはコントローラーから返すことができるAPIフレンドリーな配列に変換します。
<?php namespace App\Http\Resources; use Illuminate\Http\Request;use Illuminate\Http\Resources\Json\JsonResource; class UserResource extends JsonResource{ /** * Transform the resource into an array. * * @return array<string, mixed> */ public function toArray(Request $request): array { return [ 'id' => $this->id, 'name' => $this->name, 'email' => $this->email, 'created_at' => $this->created_at, 'updated_at' => $this->updated_at, ]; }}
リソースが定義されたら、ルートまたはコントローラーから直接返すことができます。
use App\Http\Resources\UserResource;use App\Models\User; Route::get('/user/{id}', function (string $id) { return new UserResource(User::findOrFail($id));});
リレーションシップ
レスポンスに関連するリソースを含める場合は、リソースの`toArray`メソッドによって返される配列に追加します。この例では、`PostResource`リソースの`collection`メソッドを使用して、ユーザーのブログ投稿をリソースレスポンスに追加します。
use App\Http\Resources\PostResource;use Illuminate\Http\Request; /** * Transform the resource into an array. * * @return array<string, mixed> */public function toArray(Request $request): array{ return [ 'id' => $this->id, 'name' => $this->name, 'email' => $this->email, 'posts' => PostResource::collection($this->posts), 'created_at' => $this->created_at, 'updated_at' => $this->updated_at, ];}
リレーションシップが既にロードされている場合にのみリレーションシップを含めたい場合は、条件付きリレーションシップに関するドキュメントを参照してください。
リソースコレクション
リソースは単一のモデルを配列に変換しますが、リソースコレクションはモデルのコレクションを配列に変換します。しかし、すべてのリソースが`collection`メソッドを提供して、その場で「アドホック」リソースコレクションを生成するため、すべてのモデルに対してリソースコレクションクラスを定義する必要はありません。
use App\Http\Resources\UserResource;use App\Models\User; Route::get('/users', function () { return UserResource::collection(User::all());});
ただし、コレクションと共に返されるメタデータをカスタマイズする必要がある場合は、独自のリソースコレクションを定義する必要があります。
<?php namespace App\Http\Resources; use Illuminate\Http\Request;use Illuminate\Http\Resources\Json\ResourceCollection; class UserCollection extends ResourceCollection{ /** * Transform the resource collection into an array. * * @return array<string, mixed> */ public function toArray(Request $request): array { return [ 'data' => $this->collection, 'links' => [ 'self' => 'link-value', ], ]; }}
単一のリソースと同様に、リソースコレクションはルートまたはコントローラーから直接返すことができます。
use App\Http\Resources\UserCollection;use App\Models\User; Route::get('/users', function () { return new UserCollection(User::all());});
データのラップ
デフォルトでは、最上位のリソースは、リソースレスポンスがJSONに変換されるときに`data`キーでラップされます。そのため、一般的なリソースコレクションレスポンスは次のようになります。
{ "data": [ { "id": 1, "name": "Eladio Schroeder Sr.", }, { "id": 2, "name": "Liliana Mayert", } ]}
最上位のリソースのラップを無効にするには、ベースのIlluminate\Http\Resources\Json\JsonResource
クラスでwithoutWrapping
メソッドを呼び出します。通常、このメソッドはAppServiceProvider
またはアプリケーションのすべてのリクエストで読み込まれる他のサービスプロバイダーから呼び出す必要があります。
<?php namespace App\Providers; use Illuminate\Http\Resources\Json\JsonResource;use Illuminate\Support\ServiceProvider; class AppServiceProvider extends ServiceProvider{ /** * Register any application services. */ public function register(): void { // ... } /** * Bootstrap any application services. */ public function boot(): void { JsonResource::withoutWrapping(); }}
withoutWrapping
メソッドは最上位のレスポンスのみに影響し、リソースコレクションに手動で追加したdata
キーは削除しません。
ネストされたリソースのラップ
リソースの関係をどのようにラップするかは、完全に自由に決定できます。ネストに関わらず、すべてのリソースコレクションをdata
キーでラップする場合は、各リソースのリソースコレクションクラスを定義し、data
キー内にコレクションを返す必要があります。
これにより、最上位のリソースが2つのdata
キーでラップされるのではないかと疑問に思うかもしれません。ご心配なく、Laravelではリソースが誤って二重にラップされることはありませんので、変換するリソースコレクションのネストレベルを気にする必要はありません。
<?php namespace App\Http\Resources; use Illuminate\Http\Resources\Json\ResourceCollection; class CommentsCollection extends ResourceCollection{ /** * Transform the resource collection into an array. * * @return array<string, mixed> */ public function toArray(Request $request): array { return ['data' => $this->collection]; }}
データのラップとページネーション
リソースレスポンスを介してページネーションされたコレクションを返す場合、withoutWrapping
メソッドが呼び出されていても、Laravelはリソースデータをdata
キーでラップします。これは、ページネーションされたレスポンスには常に、ページネータの状態に関する情報を含むmeta
キーとlinks
キーが含まれているためです。
{ "data": [ { "id": 1, "name": "Eladio Schroeder Sr.", }, { "id": 2, "name": "Liliana Mayert", } ], "links":{ "first": "http://example.com/users?page=1", "last": "http://example.com/users?page=1", "prev": null, "next": null }, "meta":{ "current_page": 1, "from": 1, "last_page": 1, "path": "http://example.com/users", "per_page": 15, "to": 10, "total": 10 }}
ページネーション
リソースのcollection
メソッドまたはカスタムリソースコレクションにLaravelのページネータインスタンスを渡すことができます。
use App\Http\Resources\UserCollection;use App\Models\User; Route::get('/users', function () { return new UserCollection(User::paginate());});
ページネーションされたレスポンスには、常にページネータの状態に関する情報を含むmeta
キーとlinks
キーが含まれています。
{ "data": [ { "id": 1, "name": "Eladio Schroeder Sr.", }, { "id": 2, "name": "Liliana Mayert", } ], "links":{ "first": "http://example.com/users?page=1", "last": "http://example.com/users?page=1", "prev": null, "next": null }, "meta":{ "current_page": 1, "from": 1, "last_page": 1, "path": "http://example.com/users", "per_page": 15, "to": 10, "total": 10 }}
ページネーション情報のカスタマイズ
ページネーションレスポンスのlinks
キーまたはmeta
キーに含まれる情報をカスタマイズする場合は、リソースにpaginationInformation
メソッドを定義できます。このメソッドは、$paginated
データと$default
情報(links
キーとmeta
キーを含む配列)を受け取ります。
/** * Customize the pagination information for the resource. * * @param \Illuminate\Http\Request $request * @param array $paginated * @param array $default * @return array */public function paginationInformation($request, $paginated, $default){ $default['links']['custom'] = 'https://example.com'; return $default;}
条件付き属性
特定の条件が満たされた場合にのみ、リソースレスポンスに属性を含めたい場合があります。たとえば、現在のユーザーが「管理者」の場合にのみ値を含めたい場合があります。Laravelは、このような状況を支援するためのさまざまなヘルパーメソッドを提供しています。when
メソッドを使用して、リソースレスポンスに属性を条件付きで追加できます。
/** * Transform the resource into an array. * * @return array<string, mixed> */public function toArray(Request $request): array{ return [ 'id' => $this->id, 'name' => $this->name, 'email' => $this->email, 'secret' => $this->when($request->user()->isAdmin(), 'secret-value'), 'created_at' => $this->created_at, 'updated_at' => $this->updated_at, ];}
この例では、認証されたユーザーのisAdmin
メソッドがtrue
を返す場合にのみ、secret
キーが最終的なリソースレスポンスに返されます。メソッドがfalse
を返す場合、secret
キーはクライアントに送信される前にリソースレスポンスから削除されます。when
メソッドを使用すると、配列を構築するときの条件文に頼ることなく、リソースを明確に定義できます。
when
メソッドは、第2引数としてクロージャも受け入れるため、指定された条件がtrue
の場合にのみ結果の値を計算できます。
'secret' => $this->when($request->user()->isAdmin(), function () { return 'secret-value';}),
whenHas
メソッドは、基になるモデルに実際に存在する場合に属性を含めるために使用できます。
'name' => $this->whenHas('name'),
さらに、whenNotNull
メソッドを使用して、属性がnullでない場合にリソースレスポンスに属性を含めることができます。
'name' => $this->whenNotNull($this->name),
条件付き属性のマージ
同じ条件に基づいてリソースレスポンスに含める必要がある属性が複数ある場合があります。この場合、mergeWhen
メソッドを使用して、指定された条件がtrue
の場合にのみレスポンスに属性を含めることができます。
/** * Transform the resource into an array. * * @return array<string, mixed> */public function toArray(Request $request): array{ return [ 'id' => $this->id, 'name' => $this->name, 'email' => $this->email, $this->mergeWhen($request->user()->isAdmin(), [ 'first-secret' => 'value', 'second-secret' => 'value', ]), 'created_at' => $this->created_at, 'updated_at' => $this->updated_at, ];}
繰り返しますが、指定された条件がfalse
の場合、これらの属性はクライアントに送信される前にリソースレスポンスから削除されます。
mergeWhen
メソッドは、文字列キーと数値キーを混在させた配列内では使用しないでください。さらに、順番に並んでいない数値キーを持つ配列内では使用しないでください。
条件付きリレーションシップ
属性の条件付き読み込みに加えて、モデルで関係が既に読み込まれているかどうかを基に、リソースレスポンスに関係を条件付きで含めることができます。これにより、コントローラーでモデルに読み込む関係を決定でき、リソースは実際に読み込まれた場合にのみ簡単に含めることができます。最終的に、これにより、リソース内の「N+1」クエリの問題を回避しやすくなります。
whenLoaded
メソッドは、関係を条件付きで読み込むために使用できます。関係を不必要に読み込むことを避けるために、このメソッドは関係自体ではなく、関係の名前を受け入れます。
use App\Http\Resources\PostResource; /** * Transform the resource into an array. * * @return array<string, mixed> */public function toArray(Request $request): array{ return [ 'id' => $this->id, 'name' => $this->name, 'email' => $this->email, 'posts' => PostResource::collection($this->whenLoaded('posts')), 'created_at' => $this->created_at, 'updated_at' => $this->updated_at, ];}
この例では、関係が読み込まれていない場合、posts
キーはクライアントに送信される前にリソースレスポンスから削除されます。
条件付きの関係カウント
関係を条件付きで含めることに加えて、モデルで関係のカウントが読み込まれているかどうかを基に、リソースレスポンスに関係の「カウント」を条件付きで含めることができます。
new UserResource($user->loadCount('posts'));
whenCounted
メソッドは、リソースレスポンスに関係のカウントを条件付きで含めるために使用できます。このメソッドは、関係のカウントが存在しない場合に属性を不必要に含めることを回避します。
/** * Transform the resource into an array. * * @return array<string, mixed> */public function toArray(Request $request): array{ return [ 'id' => $this->id, 'name' => $this->name, 'email' => $this->email, 'posts_count' => $this->whenCounted('posts'), 'created_at' => $this->created_at, 'updated_at' => $this->updated_at, ];}
この例では、posts
関係のカウントが読み込まれていない場合、posts_count
キーはクライアントに送信される前にリソースレスポンスから削除されます。
avg
、sum
、min
、max
などの他の種類の集計も、whenAggregated
メソッドを使用して条件付きで読み込むことができます。
'words_avg' => $this->whenAggregated('posts', 'words', 'avg'),'words_sum' => $this->whenAggregated('posts', 'words', 'sum'),'words_min' => $this->whenAggregated('posts', 'words', 'min'),'words_max' => $this->whenAggregated('posts', 'words', 'max'),
条件付きピボット情報
リソースレスポンスに関係情報を条件付きで含めることに加えて、whenPivotLoaded
メソッドを使用して、多対多関係の中間テーブルからのデータを条件付きで含めることができます。whenPivotLoaded
メソッドは、中間テーブルの名前を最初の引数として受け入れます。2番目の引数は、ピボット情報がモデルで使用可能な場合に返される値を返すクロージャである必要があります。
/** * Transform the resource into an array. * * @return array<string, mixed> */public function toArray(Request $request): array{ return [ 'id' => $this->id, 'name' => $this->name, 'expires_at' => $this->whenPivotLoaded('role_user', function () { return $this->pivot->expires_at; }), ];}
カスタム中間テーブルモデルを使用している関係の場合、whenPivotLoaded
メソッドの最初の引数として中間テーブルモデルのインスタンスを渡すことができます。
'expires_at' => $this->whenPivotLoaded(new Membership, function () { return $this->pivot->expires_at;}),
中間テーブルがpivot
以外のアクセサを使用している場合、whenPivotLoadedAs
メソッドを使用できます。
/** * Transform the resource into an array. * * @return array<string, mixed> */public function toArray(Request $request): array{ return [ 'id' => $this->id, 'name' => $this->name, 'expires_at' => $this->whenPivotLoadedAs('subscription', 'role_user', function () { return $this->subscription->expires_at; }), ];}
メタデータの追加
一部のJSON API標準では、リソースとリソースコレクションのレスポンスにメタデータを追加する必要があります。これには、リソースまたは関連リソースへのlinks
や、リソース自体のメタデータなどが含まれます。リソースに関する追加のメタデータを返す必要がある場合は、toArray
メソッドに含めます。たとえば、リソースコレクションを変換する際にlinks
情報を含めることができます。
/** * Transform the resource into an array. * * @return array<string, mixed> */public function toArray(Request $request): array{ return [ 'data' => $this->collection, 'links' => [ 'self' => 'link-value', ], ];}
リソースから追加のメタデータを返す場合、ページネーションされたレスポンスを返すときにLaravelによって自動的に追加されるlinks
キーまたはmeta
キーを誤って上書きする心配はありません。定義した追加のlinks
は、ページネータによって提供されるリンクとマージされます。
最上位レベルのメタデータ
リソースが返される最上位のリソースである場合にのみ、特定のメタデータをリソースレスポンスに含めたい場合があります。通常、これにはレスポンス全体のメタ情報が含まれます。このメタデータを定義するには、リソースクラスにwith
メソッドを追加します。このメソッドは、リソースが変換される最上位のリソースである場合にのみ、リソースレスポンスに含めるメタデータの配列を返す必要があります。
<?php namespace App\Http\Resources; use Illuminate\Http\Resources\Json\ResourceCollection; class UserCollection extends ResourceCollection{ /** * Transform the resource collection into an array. * * @return array<string, mixed> */ public function toArray(Request $request): array { return parent::toArray($request); } /** * Get additional data that should be returned with the resource array. * * @return array<string, mixed> */ public function with(Request $request): array { return [ 'meta' => [ 'key' => 'value', ], ]; }}
リソースの構築時のメタデータの追加
ルートまたはコントローラーでリソースインスタンスを構築する際に、最上位レベルのデータを追加することもできます。すべてのリソースで使用可能なadditional
メソッドは、リソースレスポンスに追加するデータの配列を受け入れます。
return (new UserCollection(User::all()->load('roles'))) ->additional(['meta' => [ 'key' => 'value', ]]);
リソースレスポンス
既に説明したように、リソースはルートとコントローラーから直接返すことができます。
use App\Http\Resources\UserResource;use App\Models\User; Route::get('/user/{id}', function (string $id) { return new UserResource(User::findOrFail($id));});
ただし、クライアントに送信される前に送信されるHTTPレスポンスをカスタマイズする必要がある場合があります。これを行うには2つの方法があります。まず、リソースにresponse
メソッドをチェーンできます。このメソッドはIlluminate\Http\JsonResponse
インスタンスを返し、レスポンスのヘッダーを完全に制御できます。
use App\Http\Resources\UserResource;use App\Models\User; Route::get('/user', function () { return (new UserResource(User::find(1))) ->response() ->header('X-Value', 'True');});
または、リソース自体にwithResponse
メソッドを定義できます。このメソッドは、リソースがレスポンスの最上位のリソースとして返されたときに呼び出されます。
<?php namespace App\Http\Resources; use Illuminate\Http\JsonResponse;use Illuminate\Http\Request;use Illuminate\Http\Resources\Json\JsonResource; class UserResource extends JsonResource{ /** * Transform the resource into an array. * * @return array<string, mixed> */ public function toArray(Request $request): array { return [ 'id' => $this->id, ]; } /** * Customize the outgoing response for the resource. */ public function withResponse(Request $request, JsonResponse $response): void { $response->header('X-Value', 'True'); }}