LaravelでSPA(Vue.js / React / Next.js / Nuxt)やモバイルアプリ(Flutter / React Native)向けのWeb APIを構築する際、データベースから取得したEloquentモデルをどのようにJSONレスポンスへ変換してクライアントに返却すべきか、設計に迷うことはありませんか?
コントローラーで return response()->json($user); や return $user; のようにEloquentモデルを直接返却してしまうと、不要な内部カラムの露出やパスワード・個人情報といった機密データの漏洩、データベース構造とAPI仕様の密結合、そしてループ処理での深刻なN+1問題など、数多くのセキュリティ・保守性・パフォーマンス上の重大なリスクを引き起こします。
これらの課題を根本から解決し、クリーンで保守性の高いAPIレスポンス層を提供するLaravel標準の仕組みが APIリソース(Eloquent API Resources / JsonResource) です。APIリソースを活用することで、Eloquentモデルとクライアント向けJSONスキーマの間に変換レイヤー(トランスフォーマー)を配置し、単一責任の原則に基づいた堅牢なAPI設計が実現できます。
この記事では、JsonResourceの基本概念と導入手順から、単一モデルとコレクション(ResourceCollection)の使い分け、whenLoaded() によるネストリレーションのN+1問題完全防止、when() / mergeWhen() による権限・条件付き属性の制御、paginate() 連携時のメタ情報カスタマイズ、実務RESTful APIでのコントローラー完全実装サンプル まで、実践的なコードとともに徹底解説します。
- Eloquentモデルを直接JSON化してはいけない3大リスクとAPIリソース導入の意義
php artisan make:resourceによるリソースクラス生成とtoArray()の基本構文- 単一モデル(
new UserResource)とコレクション(UserResource::collection)の使い分け - ネストリレーション構築と
whenLoaded()によるN+1クエリの完全防止テクニック when()とmergeWhen()を用いた管理者権限・条件付きフィールド出力paginate()との連携とページネーションメタ情報(data,links,meta)の構造カスタマイズ- FormRequestバリデーション・サービス層・APIリソースを連携させた実務CRUD完全実装
【早見表】Laravel APIレスポンス手法の比較(API Resource vs toArray vs Fractal)
LaravelでAPIレスポンスを整形する手法には、主に「Laravel標準のAPIリソース(JsonResource)」「Eloquentモデル標準の toArray() / toJson()」「サードパーティライブラリである The PHP LeagueのFractal」の3つが存在します。
それぞれのメリット・デメリット、保守性、推奨ユースケースを比較した早見表が以下です。
| 手法 / ライブラリ | メリット | デメリット・懸念点 | 推奨ユースケース |
|---|---|---|---|
| APIリソース(JsonResource) ★ Laravel標準・推奨 |
・Laravel標準搭載で追加依存なし ・モデルとJSONスキーマを疎結合に分離 ・ whenLoaded() でN+1問題を簡単に防止・ページネーション( LengthAwarePaginator)と自動連携・条件付き属性( when, mergeWhen)が強力
|
・リソースクラスのファイル作成が必要 ・小規模なプロトタイプでは記述量が多く感じる場合がある |
現代のLaravel API開発における第一選択。 SPA・モバイルアプリ連携、マイクロサービス、中長期で保守するすべてのWeb APIプロジェクト。 |
| モデルの toArray() / $hidden |
・追加クラス作成が不要で手軽 ・ $hidden や $visible で簡易的な隠蔽が可能
|
・APIエンドポイントごとの出力差異に対応しにくい ・DBカラム変更が直接APIレスポンスを破壊する ・リレーションの読み込み状況に応じた柔軟な制御が困難 ・モデルクラスの肥大化を招く |
社内向け管理画面の簡易スクリプトや、1回限りの使い捨てAPI・迅速な検証プロトタイプのみ。 |
| Fractal(League\Fractal) |
・リレーションの動的インクルード(include機能)が標準組み込み ・フレームワーク非依存で他PHPプロジェクトと共通化可能 |
・外部パッケージ(spatie/laravel-fractal 等)の導入が必要・Laravel標準の進化(JsonResource)により優位性が縮小 ・学習コストやボイラープレートがやや多い |
クエリパラメータによる高度な動的リレーション制御をFractalエコシステムで統一している大規模案件。 |
上記の通り、現代のLaravel環境においては 標準搭載されている APIリソース(JsonResource)を採用するのが最も合理的かつ安全なデファクトスタンダード です。余計なパッケージを導入することなく、Laravelのエコシステム(Eloquent、Paginator、FormRequest、Sanctumなど)と最高水準の親和性を誇ります。
1. APIリソース(JsonResource)の基本概念と導入手順
なぜEloquentモデルを直接JSON返却してはいけないのか(機密カラム漏洩リスク・スキーマ結合)
Laravelのチュートリアルや初期のプロトタイプ開発では、コントローラーで以下のようにEloquentモデルを直接返却するコードがよく見られます。
// ⚠️ アンチパターン:EloquentモデルをそのままJSON返却
public function show(User $user)
{
return response()->json($user);
// または return $user;
}
一見シンプルで問題なく動作しているように見えますが、実務の本番運用においてこの実装を放置することは極めて危険です。その理由は主に3つあります。
① 機密情報・内部カラムの意図しない漏洩リスク
Eloquentモデルの toJson() や toArray() は、デフォルトでテーブルの全カラムをそのままシリアライズします。将来的にマイグレーションで stripe_customer_id、two_factor_secret、internal_notes、deleted_at などの機密データや管理用カラムを追加した際、モデルの $hidden プロパティへの指定を忘れると、それらがクライアント側に漏洩して重大なインシデントに発展します。
② データベーススキーマとクライアント側API仕様の強固な密結合
データベースのカラム名(例: first_name, tel_number)がそのままJSONキーとなります。将来的なDBリファクタリング(カラム名変更やテーブル正規化)を行った際、APIクライアント側の実装まで破壊されてしまいます。また、フロントエンドが求めるキャメルケース(firstName)やデータ構造のネスト化に柔軟に対応できません。
③ 型の不整合と日付フォーマットのばらつき
データベース上の文字列型フラグ("1" や "0")を真偽値(true / false)に変換したい場合や、Carbonインスタンスの日時を統一フォーマット(Y-m-d H:i:s や ISO 8601形式)で返却したい場合、モデル層でアクセサを量産するとモデルクラスが肥大化し、Web画面とAPI間での要件の衝突が発生します。
これらの課題を解決するために、「データベース構造」と「APIクライアント向けレスポンス」の中間に位置する独立した変換層 として APIリソース(JsonResource)が必要不可欠となるのです。
php artisan make:resource UserResource によるリソース作成
LaravelでAPIリソースを作成するには、Artisanコマンド make:resource を使用します。
php artisan make:resource UserResource
このコマンドを実行すると、app/Http/Resources/UserResource.php が新規生成されます。自動生成されるクラスの基本コードは以下のようになります。
<?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 parent::toArray($request);
}
}
デフォルトでは parent::toArray($request) が記述されており、これは基底モデルの toArray() をそのまま呼び出します。ここをプロジェクトのAPI要件に合わせてカスタマイズしていきます。
toArray($request) メソッドの基本的なカスタマイズ構文
toArray($request) メソッド内に、クライアントへ返却したいキーと値の連想配列を明示的に定義します。
<?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,
'avatar_url' => $this->avatar_url ?? 'https://example.com/default-avatar.png',
'is_active' => (bool) $this->is_active,
'created_at' => $this->created_at?->toIso8601String(),
'updated_at' => $this->updated_at?->toIso8601String(),
];
}
}
リソースクラス内では
$this->resource->id のように基底モデルを取り出すことも可能ですが、JsonResource はPHPの __get() マジックメソッドを実装しているため、$this->id や $this->name と記述するだけでラッピングされているEloquentモデルのプロパティやメソッド、アクセサへ自動的に委譲されます。これにより、シンプルで直感的な構文でレスポンスを組み立てることができます。
2. 単一モデルとリソースコレクションの使い分け
APIリソースを作成したら、コントローラーからクライアントへ返却します。返却対象が「単一モデル」であるか「複数モデルの一覧(コレクション)」であるかによって呼び出し方が異なります。
単一モデルの返却:new UserResource($user)
詳細取得(show アクション)や新規作成(store アクション)のように、単一のモデルインスタンスを返却する場合は、リソースクラスのコンストラクタにモデルを渡してインスタンス化します。
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Resources\UserResource;
use App\Models\User;
class UserController extends Controller
{
/**
* 指定ユーザーの詳細を取得
*/
public function show(User $user): UserResource
{
return new UserResource($user);
}
}
コントローラーのアクションから UserResource インスタンスを直接リターンすると、Laravelが自動的にHTTPステータスコード 200 OK と Content-Type: application/json ヘッダーを付与し、以下のJSON形式でレスポンスを出力します。
{
"data": {
"id": 1,
"name": "山田 太郎",
"email": "yamada@example.com",
"avatar_url": "https://example.com/default-avatar.png",
"is_active": true,
"created_at": "2024-01-15T12:00:00+09:00",
"updated_at": "2024-01-15T12:00:00+09:00"
}
}
LaravelのAPIリソースは、最上位のレスポンスを自動的に
"data" キーでラップ(外包)します。これはJSON API標準に準拠した仕様ですが、フロントエンドの仕様などで最上位の "data" キーが不要な場合は、AppServiceProvider の boot() メソッド内で JsonResource::withoutWrapping(); を呼び出すことで、ラッピングをグローバルに無効化できます。
use Illuminate\Http\Resources\Json\JsonResource;
public function boot(): void
{
JsonResource::withoutWrapping();
}
複数モデル一覧の返却:UserResource::collection($users)
一覧取得(index アクション)のように、Eloquentコレクション(Illuminate\Database\Eloquent\Collection)を整形して返却する場合は、静的メソッド UserResource::collection($users) を使用します。
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Resources\UserResource;
use App\Models\User;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
class UserController extends Controller
{
/**
* ユーザー一覧を取得
*/
public function index(): AnonymousResourceCollection
{
$users = User::all();
return UserResource::collection($users);
}
}
返却されるJSONは、配列内の各要素が UserResource の定義に従って美しく整形されます。
{
"data": [
{
"id": 1,
"name": "山田 太郎",
"email": "yamada@example.com",
"avatar_url": "https://example.com/default-avatar.png",
"is_active": true,
"created_at": "2024-01-15T12:00:00+09:00",
"updated_at": "2024-01-15T12:00:00+09:00"
},
{
"id": 2,
"name": "佐藤 花子",
"email": "sato@example.com",
"avatar_url": "https://example.com/avatars/sato.png",
"is_active": true,
"created_at": "2024-01-15T12:05:00+09:00",
"updated_at": "2024-01-15T12:05:00+09:00"
}
]
}
独自コレクションクラスの作成:php artisan make:resource UserCollection
通常の一覧返却であれば UserResource::collection($users) で十分ですが、以下のようなケースでは 専用のリソースコレクションクラス を作成します。
- 一覧レスポンスの最上位に独自のメタデータ(全体の集計値、バージョン情報、実行ステータスなど)を追加したい
- コレクション全体のラッピングキーや構造をカスタマイズしたい
リソースコレクションを作成するには、--collection オプションを付与するか、クラス名末尾を Collection にしてArtisanコマンドを実行します。
php artisan make:resource UserCollection
# または php artisan make:resource UserCollection --collection
生成された app/Http/Resources/UserCollection.php は、JsonResource ではなく ResourceCollection を継承しています。
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;
class UserCollection extends ResourceCollection
{
/**
* コレクション内の各モデルを整形するリソースクラスを指定
* (クラス名が `UserCollection` の場合、Laravelは自動的に `UserResource` と推測)
*
* @var string
*/
public $collects = UserResource::class;
/**
* リソースコレクションを配列へ変換
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'data' => $this->collection, // $this->collection は UserResource のインスタンス群
'meta' => [
'api_version' => '1.0.0',
'active_count' => $this->collection->filter(fn ($u) => $u->is_active)->count(),
'server_time' => now()->toIso8601String(),
],
];
}
}
コントローラー側では new UserCollection($users) として呼び出します。これにより、個々のユーザー情報は UserResource のルールで整形されつつ、最上位に独自メタデータを付与したリッチなレスポンスが生成されます。
3. リレーションのネストと whenLoaded() によるN+1問題の完全防止
実務のWeb API開発において、最も頻発し、かつサーバーダウンや著しいレスポンス遅延を引き起こす最大のボトルネックが 「APIリソース内でのリレーション参照に伴うN+1問題」 です。
リソース内での不用意なリレーション参照が引き起こすN+1問題の再現
例えば、「ユーザー情報に加えて、そのユーザーが執筆した投稿一覧(posts)も返却したい」と考えたとします。以下のようにリソースを記述してしまうのは、典型的なアンチパターンです。
// ❌ 重大なアンチパターン:リレーションを無防備に直接参照
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
'posts' => PostResource::collection($this->posts), // ⚠️ ここでN+1が発生!
];
}
もしコントローラーで以下のように単純な User::all() を実行していた場合、何が起きるでしょうか?
public function index()
{
// コントローラー側で with('posts') を行わずに取得
$users = User::all();
return UserResource::collection($users);
}
リソースが各ユーザーを整形するたびに $this->posts にアクセスします。Eloquentの遅延ローディング(Lazy Loading)が発動し、ユーザーが50件あれば、ユーザー取得の1回に加えて投稿取得のためのSQLが50回、合計51回もデータベースへ発行 されてしまいます。
-- ① ユーザー一覧を取得(1回)
SELECT * FROM `users`;
-- ② 各ユーザーの投稿を取得(50回ループで発行!)
SELECT * FROM `posts` WHERE `posts`.`user_id` = 1;
SELECT * FROM `posts` WHERE `posts`.`user_id` = 2;
...
SELECT * FROM `posts` WHERE `posts`.`user_id` = 50;
N+1問題のメカニズムやEager Loadingの内部構造、開発環境での自動検知テクニックについては、Laravel N+1問題の完全解決ガイド で徹底解説していますので、合わせてご覧ください。
whenLoaded(‘relation’) によるEager Loading済みデータのみの安全な出力
この致命的なN+1問題をスマートかつ完全に防止するためにLaravelが提供しているのが、$this->whenLoaded('リレーション名') メソッドです。
whenLoaded() は、「コントローラー側でそのリレーションが明示的にEager Loading(with() や load())されている場合のみ」 属性をレスポンスに含めます。ロードされていない場合は、キーそのものがJSONレスポンスから自動的に除外 されます。
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class UserResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
// posts がロードされている時だけネストして出力(未ロード時はキーごと除外)
'posts' => PostResource::collection($this->whenLoaded('posts')),
// 1対1リレーションの場合
'profile' => new ProfileResource($this->whenLoaded('profile')),
];
}
}
whenLoaded() 導入後の動作の違い
① コントローラーで with() を指定した場合:
// コントローラー
$users = User::with(['posts', 'profile'])->get();
return UserResource::collection($users);
発行されるSQLはわずか3回(users, posts, profiles 各1回)に抑えられ、JSONには posts と profile が美しくネストされて出力されます。
② コントローラーで with() を指定しなかった場合:
// コントローラー
$users = User::all();
return UserResource::collection($users);
追加クエリは 0回(1回のSELECTのみ) で済み、出力されるJSONには posts や profile のキーが含まれません。これにより、同じ UserResource クラスを「軽量な一覧表示」と「リレーションを含む詳細表示」の両方で安全に再利用できます。
ネストリソース(UserResource内でPostResourceを呼び出す)の実装パターン
実務では、親子関係や多対多関係など、複雑なリレーションを階層化して返却する機会が多くあります。関係性ごとの推奨実装パターンを押さえておきましょう。
1対1リレーション(hasOne / belongsTo)のネスト
// 単一モデルリソースのインスタンスを生成して渡す
'profile' => new ProfileResource($this->whenLoaded('profile')),
'company' => new CompanyResource($this->whenLoaded('company')),
1対多リレーション(hasMany)のネスト
// コレクションメソッドを経由して渡す
'posts' => PostResource::collection($this->whenLoaded('posts')),
'comments' => CommentResource::collection($this->whenLoaded('comments')),
多対多リレーション(belongsToMany)の中間テーブル情報(whenPivotLoaded)
ロール(Role)とユーザーの中間テーブルに保存されている expires_at や assigned_by といったピボット情報を出力したい場合は、whenPivotLoaded() を活用します。
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class RoleResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
// 中間テーブル `role_user` がロードされている場合のみ付与
'membership' => $this->whenPivotLoaded('role_user', function () {
return [
'assigned_at' => $this->pivot->created_at?->toIso8601String(),
'expires_at' => $this->pivot->expires_at,
];
}),
];
}
}
投稿モデル全体をロードするのではなく、
User::withCount('posts')->get() で件数だけを集計した場合、リソース内では 'posts_count' => $this->whenCounted('posts') を使うことで、集計クエリが実行された時だけ posts_count カラムを安全に出力できます。
4. 条件付き属性・リレーションの追加(when / mergeWhen)
実務のWeb APIでは、「ログイン中のユーザーが管理者権限を持っている場合のみ、管理用フィールドを含めたい」「クライアントから特定のリクエストパラメータが送られた時だけ、詳細な計算プロパティを返却したい」といった、アクセス権限やコンテキストに応じた動的なフィールド制御 が日常的に求められます。
LaravelのAPIリソースでは、when() や mergeWhen() を使うことで、if 文による配列操作を行わずに宣言的でクリーンな条件分岐を記述できます。
権限やリクエスト条件に応じたフィールド追加:$this->when($condition, $value)
$this->when($condition, $value, $default = null) は、第1引数の条件($condition)が真(true)の時だけ、そのキーと値をレスポンスに含めます。偽(false)の場合は、キーそのものがJSONから完全に消去されます。
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class UserResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
// ログイン中のユーザーが管理者である場合のみ出力
'secret_note' => $this->when($request->user()?->isAdmin(), $this->secret_note),
// 自分自身のプロフィールを参照している時だけ電話番号を出力
'phone' => $this->when($request->user()?->id === $this->id, $this->phone),
// 値の生成にコストがかかる処理はクロージャ(無名関数)で遅延評価
'analytics_score' => $this->when($request->user()?->isAdmin(), function () {
return $this->calculateComplexScore();
}),
];
}
}
リソースクラスの
toArray(Request $request) メソッドには、現在のHTTPリクエストインスタンスが引数として渡されます。そのため、$request->user() を通じて認証済みユーザー情報やトークンのアビリティ(権限スコープ)に直接アクセスできます。SPAやモバイルアプリでセキュアなトークン認証基盤を構築する手順は、Laravel SanctumによるSPA・APIトークン認証完全ガイド を参考にしてください。
$this->when($condition, $this->heavyCalculation()) と直接メソッド呼び出しを記述すると、$condition が false であっても関数の引数評価の段階で重い処理が実行されてしまいます。無駄なCPU消費やDBクエリを防ぐため、計算コストのかかる値は必ず function () { return ...; } や fn() => ... のクロージャで渡しましょう。
複数属性の一括マージ:$this->mergeWhen($condition, […])
条件が成立した際に、単一のキーだけでなく複数の属性をまとめてレスポンスのトップレベルに展開したい場合は、$this->mergeWhen($condition, [...]) を使用します。
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class UserResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
// 管理者または本人の場合のみ、機密情報群をトップレベルに一括マージ
$this->mergeWhen($request->user()?->isAdmin() || $request->user()?->id === $this->id, [
'phone_number' => $this->phone_number,
'postal_code' => $this->postal_code,
'address' => $this->address,
'two_factor_enabled' => (bool) $this->two_factor_secret,
'last_login_at' => $this->last_login_at?->toIso8601String(),
]),
];
}
}
一般ユーザーがアクセスした際は phone_number や address などのキーは一切出力されず、管理者や本人がアクセスした時だけそれらのキーが配列の同階層に美しく展開されます。
よく使われる条件付きヘルパーまとめ
$this->when($boolean, $value): 条件が真の時のみ単一キーを出力$this->mergeWhen($boolean, $array): 条件が真の時のみ連想配列をフラットにマージ$this->whenNotNull($value): 値がnullでない場合のみ出力(nullのプロパティをキーごと消去したい時に重宝)$this->whenAppended('attribute'): Eloquentモデル側で動的にappend('custom_attribute')された時のみ出力
5. ページネーション(paginate)との連携とメタデータのカスタマイズ
実務のWeb API一覧エンドポイントでは、大量のデータを一度に返却せず、1ページあたり15〜50件程度に分割して返却する 「ページネーション(Pagination)」 が必須となります。
LaravelのAPIリソースは、Eloquentの paginate() メソッドが返す LengthAwarePaginator インスタンスと自動で高度に連動する仕組みが組み込まれています。
paginate() を Resource::collection() に渡した際の自動ラッピング挙動(data, links, meta)
コントローラーで paginate() を実行したクエリ結果を、そのまま UserResource::collection() に渡すだけで、ページネーション対応が完了します。
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Resources\UserResource;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
class UserController extends Controller
{
/**
* ページネーション付きユーザー一覧を取得
*/
public function index(Request $request): AnonymousResourceCollection
{
// 1ページあたり15件で取得(検索クエリ保持)
$users = User::paginate(15);
return UserResource::collection($users);
}
}
これだけで、Laravelは出力JSONを自動的に解析し、レコード配列の data に加えて、ページ遷移URLを保持する links と、総件数やページ番号を保持する meta を自動生成して付与します。
{
"data": [
{
"id": 1,
"name": "山田 太郎",
"email": "yamada@example.com",
"avatar_url": "https://example.com/default-avatar.png",
"is_active": true,
"created_at": "2024-01-15T12:00:00+09:00",
"updated_at": "2024-01-15T12:00:00+09:00"
}
],
"links": {
"first": "https://api.example.com/api/v1/users?page=1",
"last": "https://api.example.com/api/v1/users?page=10",
"prev": null,
"next": "https://api.example.com/api/v1/users?page=2"
},
"meta": {
"current_page": 1,
"from": 1,
"last_page": 10,
"links": [
{
"url": null,
"label": "« 前へ",
"active": false
},
{
"url": "https://api.example.com/api/v1/users?page=1",
"label": "1",
"active": true
},
{
"url": "https://api.example.com/api/v1/users?page=2",
"label": "2",
"active": false
},
{
"url": "https://api.example.com/api/v1/users?page=2",
"label": "次へ »",
"active": false
}
],
"path": "https://api.example.com/api/v1/users",
"per_page": 15,
"to": 15,
"total": 150
}
}
Laravelのページネーション機能の基本仕様や、BladeでのUI装飾、クエリ文字列の保持(withQueryString())については、Laravel ページネーション完全ガイド|UIカスタマイズ・日本語化・検索条件引き継ぎ(withQueryString)まで徹底解説 で詳しく解説しています。
withResponse や paginationInformation によるレスポンス構造のカスタマイズ
「フロントエンドのTypeScript型定義に合わせて、links と meta を統合したシンプルな構造にしたい」「HTTPレスポンスヘッダーにカスタム情報を追加したい」といった要件には、以下のカスタマイズ手法を活用します。
① ページネーション構造の再定義(paginationInformation)
独自のリソースコレクションクラス(UserCollection)を作成している場合、paginationInformation() メソッドをオーバーライドすることで、links や meta の出力形式を自由に再編できます。
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\ResourceCollection;
class UserCollection extends ResourceCollection
{
public $collects = UserResource::class;
/**
* ページネーションのメタ情報をカスタマイズ
*/
public function paginationInformation($request, $paginated, $default): array
{
// $paginated は LengthAwarePaginator の生データ
return [
'pagination' => [
'current_page' => $paginated['current_page'],
'total_pages' => $paginated['last_page'],
'per_page' => $paginated['per_page'],
'total_count' => $paginated['total'],
'has_more' => $paginated['current_page'] < $paginated['last_page'],
],
];
}
}
これにより、長大な meta.links 配列などを排除し、フロントエンド側で扱いやすい pagination: { ... } というすっきりとしたJSON構造にカスタマイズできます。
② コントローラーからの追加情報付与(additional)
コントローラー側で一時的にAPIの実行ステータスやメッセージをレスポンスに含めたい場合は、additional() メソッドをチェーンします。
return UserResource::collection($users)->additional([
'success' => true,
'message' => 'ユーザー一覧を正常に取得しました。',
]);
③ HTTPステータスやヘッダーの変更(withResponse)
リソースクラス内で withResponse() メソッドを定義すると、クライアントにレスポンスが送信される直前の Illuminate\Http\JsonResponse インスタンスに介入できます。キャッシュ制御(Cache-Control)やカスタムヘッダーの付加に最適です。
<?php
namespace App\Http\Resources;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class UserResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
];
}
/**
* 送出されるレスポンスのカスタマイズ
*/
public function withResponse(Request $request, JsonResponse $response): void
{
// カスタムHTTPヘッダーの追加
$response->header('X-Api-Version', '2.0');
$response->header('Cache-Control', 'private, max-age=60');
}
}
6. 実務RESTful APIでのコントローラー完全実装サンプル
ここまで学んだ知識を統合し、実務でそのまま利用できる 「FormRequestバリデーション ➔ Eloquent / サービス層処理 ➔ APIリソースでの返却」 という美しいアーキテクチャの完全な実装サンプルを紹介します。
ルーティング定義(routes/api.php)
まずはルーティングを定義します。RESTfulなエンドポイントを構築する際は、ルートパラメータに不正な文字列が渡されないよう正規表現制約を付与しておくのが安全です。
<?php
use App\Http\Controllers\Api\UserController;
use Illuminate\Support\Facades\Route;
Route::prefix('v1')->group(function () {
// ユーザーリソースのRESTful APIルート
Route::apiResource('users', UserController::class)
->whereNumber('user'); // IDパラメータが半角数字のみであることを保証
});
ルートパラメータの正規表現制約やグローバルパターン設定の詳細は、Laravel ルートパラメータと正規表現制約(where)の使い方完全ガイド で徹底解説しています。
FormRequestによる入力値の検証と認可(app/Http/Requests/UserStoreRequest.php)
新規作成(POST)リクエストを受け取る際は、コントローラーをFat化させないために専用のFormRequestを作成します。
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class UserStoreRequest extends FormRequest
{
public function authorize(): bool
{
return true; // 公開登録またはPolicy認可
}
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:50'],
'email' => ['required', 'string', 'email:rfc,dns', 'max:255', 'unique:users,email'],
'password' => ['required', 'string', 'min:8'],
'bio' => ['nullable', 'string', 'max:500'],
];
}
}
FormRequestを活用したバリデーションの分離や認可ロジックの実装テクニックについては、Laravel FormRequest完全ガイド も合わせてご参照ください。
コントローラー完全コード(app/Http/Controllers/Api/UserController.php)
CRUDすべての主要アクション(一覧・登録・詳細・更新・削除)を備えたコントローラーの完全な実装コードです。
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\UserStoreRequest;
use App\Http\Requests\UserUpdateRequest;
use App\Http\Resources\UserResource;
use App\Models\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
class UserController extends Controller
{
/**
* ユーザー一覧を取得(検索・Eager Loading・ページネーション)
*/
public function index(Request $request): AnonymousResourceCollection
{
$users = User::query()
// N+1問題を防止するためリレーションを事前ロード
->with(['profile', 'roles'])
// 条件絞り込み
->when($request->filled('keyword'), function ($query) use ($request) {
$keyword = '%' . $request->input('keyword') . '%';
$query->where(function ($q) use ($keyword) {
$q->where('name', 'like', $keyword)
->orWhere('email', 'like', $keyword);
});
})
->latest('id')
->paginate(15);
return UserResource::collection($users);
}
/**
* 新規ユーザーを登録(201 Created 返却)
*/
public function store(UserStoreRequest $request): JsonResponse
{
$validated = $request->validated();
$user = DB::transaction(function () use ($validated) {
$user = User::create([
'name' => $validated['name'],
'email' => $validated['email'],
'password' => Hash::make($validated['password']),
]);
if (!empty($validated['bio'])) {
$user->profile()->create(['bio' => $validated['bio']]);
}
return $user;
});
// 201 Created ステータスコードでリソースを返却
return (new UserResource($user->load('profile')))
->response()
->setStatusCode(Response::HTTP_CREATED);
}
/**
* ユーザー詳細を取得(リレーションをロードして返却)
*/
public function show(User $user): UserResource
{
// 詳細画面で必要なリレーションをロード
$user->loadMissing(['profile', 'posts' => fn ($q) => $q->latest()->limit(5)]);
return new UserResource($user);
}
/**
* ユーザー情報を更新
*/
public function update(UserUpdateRequest $request, User $user): UserResource
{
$validated = $request->validated();
$user->update($validated);
return new UserResource($user);
}
/**
* ユーザーを削除(204 No Content 返却)
*/
public function destroy(User $user): Response
{
$user->delete();
return response()->noContent(); // 204 No Content
}
}
HTTPステータスコード設計のポイント
- 一覧(index)/ 詳細(show)/ 更新(update):正常終了時は
200 OKを返却(APIリソースを直接リターンすれば自動付与)。 - 新規作成(store):リソース作成の成功を示すため
201 Createdを明示的に設定(->response()->setStatusCode(201))。 - 削除(destroy):本文なしの成功を示す
204 No Contentを返却(response()->noContent())。 - バリデーション失敗時:FormRequestが自動的に
422 Unprocessable Contentとエラー詳細JSONを返却。
7. まとめ:APIリソース設計のベストプラクティス
LaravelのAPIリソース(JsonResource)は、単なるJSON整形ツールにとどまらず、「データベース層の関心事」と「クライアント向けAPIインターフェースの関心事」を分離し、保守性と安全性を飛躍的に高めるための必須アーキテクチャ です。
最後に、APIリソースを設計・運用する際の実務ベストプラクティスをチェックリストとしてまとめます。
- [ ] コントローラーでEloquentモデルを直接
returnせず、必ず専用のリソースクラスを介しているか? - [ ] パスワードハッシュや内部管理フラグなど、機密カラムが露出しないよう明示的な連想配列で返却しているか?
- [ ] リソース内のリレーション参照はすべて
whenLoaded()で保護し、予期せぬN+1問題を完全に遮断しているか? - [ ] 単一モデルは
new UserResource($user)、コレクションはUserResource::collection($users)で使い分けているか? - [ ] 管理者限定フィールドや条件付き出力には
when()やmergeWhen()を活用しているか? - [ ] 計算コストの高い動的属性は直接呼び出さず、クロージャ(無名関数)で遅延評価しているか?
- [ ] ページネーション時は
User::paginate()の結果をそのままUserResource::collection()に渡し、data,links,metaの自動構造を活用しているか? - [ ] 入力検証はFormRequest、出力整形はAPIリソースという「単一責任の原則」を徹底しているか?
APIリソースを正しく導入することで、フロントエンドの開発者にとっても予測可能で信頼性の高い、堅牢なWeb APIを提供することができます。本記事のパターンをプロジェクトに適用し、洗練されたLaravel API開発を実践していきましょう!

コメント