メインコンテンツまでスキップ

エロクエント: API リソース

はじめに

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
{
/**
* 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,
];
}
}

すべてのリソースクラスは、リソースがルートまたはコントローラメソッドから応答として返されるときにJSONに変換される属性の配列を返す toArray メソッドを定義します。

モデルのプロパティには $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;
}

リソースの記述

注記

もし 概念の概要 をまだ読んでいない場合は、このドキュメントを進める前に読むことを強くお勧めします。

リソースは与えられたモデルを配列に変換するだけです。そのため、各リソースには、モデルの属性を API フレンドリーな配列に変換する toArray メソッドが含まれています。この配列は、アプリケーションのルートやコントローラーから返すことができます:

    <?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.",
"email": "therese28@example.com"
},
{
"id": 2,
"name": "Liliana Mayert",
"email": "evandervort@example.com"
}
]
}

最も外側のリソースのラッピングを無効にしたい場合は、基本となる 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はリソースが誤って2重にラップされることは決してありませんので、変換するリソースコレクションのネストレベルについて心配する必要はありません。

    <?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];
}
}

データのラッピングとページネーション

リソースレスポンス経由でページネートされたコレクションを返す場合、LaravelはwithoutWrappingメソッドが呼び出されていても、リソースデータをdataキーでラップします。これは、ページネーションされたレスポンスには常に、ページネータの状態に関する情報を含むmetaおよびlinksキーが含まれるためです。

{
"data": [
{
"id": 1,
"name": "Eladio Schroeder Sr.",
"email": "therese28@example.com"
},
{
"id": 2,
"name": "Liliana Mayert",
"email": "evandervort@example.com"
}
],
"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.",
"email": "therese28@example.com"
},
{
"id": 2,
"name": "Liliana Mayert",
"email": "evandervort@example.com"
}
],
"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情報の配列を受け取ります。

    /**
* 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;
}

<a name="conditional-attributes"></a>

条件付き属性

時には、特定の条件が満たされた場合にのみリソースレスポンスに属性を含めたいことがあります。たとえば、現在のユーザーが"管理者"である場合にのみ値を含めたいと思うかもしれません。このような状況でお手伝いするために、Laravelはさまざまなヘルパーメソッドを提供しています。whenメソッドは、条件付きで属性をリソースレスポンスに追加するために使用できます。

この例では、secret キーは、認証されたユーザーの isAdmin メソッドが true を返す場合にのみ、最終的なリソースレスポンスで返されます。メソッドが false を返す場合、secret キーはクライアントに送信される前にリソースレスポンスから削除されます。when メソッドを使用すると、配列を構築する際に条件文を使わずにリソースを表現的に定義することができます。

when メソッドは、第二引数としてクロージャを受け入れることもでき、指定された条件が 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キーがクライアントに送信される前にリソースのレスポンスから削除されます。

avgsumminmaxなどの他の種類の集計も、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 メソッドにそれを含めてください。たとえば、リソースコレクションを変換する際に link 情報を含めることがあります:

    /**
* 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 がページネーションされたレスポンスを返す際に自動的に追加される linksmeta キーを誤って上書きする心配はありません。定義した追加の 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
{
/**
* リソースを配列に変換します。
*
* @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');
}
}