#はじめに
APIを構築する際、Eloquentモデルと実際にアプリケーションのユーザーに返されるJSONレスポンスの間に変換レイヤーが必要になることがあります。例えば、特定のユーザーのサブセットにだけ属性を表示したり、モデルのJSON表現に常に特定のリレーションシップを含めたい場合などです。Eloquentのリソースクラスを使うと、モデルやモデルコレクションを表現豊かに簡単にJSONに変換できます。
もちろん、EloquentモデルやコレクションをtoJsonメソッドで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
{
/**
* リソースを配列に変換します。
*
* @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
{
/**
* リソースコレクションを配列に変換します。
*
* @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
{
/**
* リソースのコレクションキーを保持するかどうかを示します。
*
* @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
{
/**
* このリソースが収集するリソース。
*
* @var string
*/
public $collects = Member::class;
}
#リソースの作成
概念の概要をまだ読んでいない場合は、このドキュメントを進める前にぜひお読みください。
リソースは与えられたモデルを配列に変換するだけでよいので、各リソースはモデルの属性をAPI向けの配列に変換するtoArrayメソッドを持ちます。この配列はアプリケーションのルートやコントローラーから返せます。
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class UserResource extends JsonResource
{
/**
* リソースを配列に変換します。
*
* @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;
/**
* リソースを配列に変換します。
*
* @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
{
/**
* リソースコレクションを配列に変換します。
*
* @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.",
"email": "[email protected]"
},
{
"id": 2,
"name": "Liliana Mayert",
"email": "[email protected]"
}
]
}
最も外側のリソースのラッピングを無効にしたい場合は、ベースの 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
{
/**
* アプリケーションサービスの登録。
*/
public function register(): void
{
// ...
}
/**
* アプリケーションサービスの起動処理。
*/
public function boot(): void
{
JsonResource::withoutWrapping();
}
}
withoutWrapping メソッドは最も外側のレスポンスにのみ影響し、自分でリソースコレクションに追加した data キーは削除されません。
#ネストされたリソースのラッピング
リソースのリレーションシップのラッピング方法は自由に決められます。すべてのリソースコレクションをネストに関係なく data キーでラップしたい場合は、各リソースに対してリソースコレクションクラスを定義し、data キー内にコレクションを返すようにしてください。
最も外側のリソースが二重に data キーでラップされるのではと心配になるかもしれませんが、Laravel はリソースが誤って二重ラップされることを防ぐため、リソースコレクションのネストレベルを気にする必要はありません。
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\ResourceCollection;
class CommentsCollection extends ResourceCollection
{
/**
* リソースコレクションを配列に変換する。
*
* @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.",
"email": "[email protected]"
},
{
"id": 2,
"name": "Liliana Mayert",
"email": "[email protected]"
}
],
"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
}
}
#ページネーション
Laravel のページネーターインスタンスをリソースの collection メソッドやカスタムリソースコレクションに渡せます。
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.",
"email": "[email protected]"
},
{
"id": 2,
"name": "Liliana Mayert",
"email": "[email protected]"
}
],
"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 データと、links と meta キーを含む $default 配列を受け取ります。
/**
* リソースのページネーション情報をカスタマイズする。
*
* @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 の場合にのみ属性群をレスポンスに含められます。
/**
* リソースを配列に変換する。
*
* @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;
/**
* リソースを配列に変換する。
*
* @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 メソッドはリレーションシップのカウントを条件付きでリソースレスポンスに含めるために使えます。リレーションシップのカウントが存在しない場合は属性を含めません。
/**
* リソースを配列に変換する。
*
* @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番目の引数にピボット情報がモデルにある場合に返す値を返すクロージャを受け取ります。
/**
* リソースを配列に変換する。
*
* @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 メソッドを使えます。
/**
* リソースを配列に変換する。
*
* @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 情報を含めることができます。
/**
* リソースを配列に変換する。
*
* @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
{
/**
* リソースコレクションを配列に変換します。
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return parent::toArray($request);
}
/**
* リソース配列と共に返す追加データを取得します。
*
* @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
{
/**
* リソースを配列に変換します。
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
];
}
/**
* リソースの送信レスポンスをカスタマイズします。
*/
public function withResponse(Request $request, JsonResponse $response): void
{
$response->header('X-Value', 'True');
}
}