laravel-controllers について
Laravel開発者向けのコントローラー設計ガイドです。HTTP処理に役割を絞り、ドメインロジックをActionに委譲する方針を示します。REST規約、Webと公開APIの構成、クエリオブジェクト、ルートモデルバインディングも扱います。
主な機能
- 01コントローラーを薄く保つ
- 02標準RESTメソッドを使う
- 03非REST処理を単一アクションに分離
- 04Webとバージョン付きAPIを分ける
- 05APIクエリにクエリオブジェクトを使う
- 06認可とモデルバインディングのパターン
活用シーン
- 01リソースコントローラーの命名とメソッドを決める
- 02ビジネスルールをコントローラーから移す
- 03Webと公開APIのコントローラーを構成する
- 04APIのフィルター、並べ替え、関連データを設計する
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ leeovery/ agentic-skills --skill laravel-controllersインストール後、Claude Code で/laravel-controllersを入力して読み込まれたか確認します。
同梱ファイルreferences/query-objects.md
詳細
Laravel コントローラー
コントローラーは極めて薄くします。HTTP に関する処理のみを担当し、ドメインロジックは一切含めません。
関連ガイド:
- query-objects.md - API のフィルタリング/ソート用クエリーオブジェクト
- Actions - アクションにドメインロジックを記述します
- form-requests.md - バリデーション層
- DTOs - データ転送用 DTO
- structure.md - Web と API の構成
基本方針
コントローラーが行うのは、次の処理のみです。
- 依存関係を型指定する
- バリデーションを行う(Form Request 経由)
- アクションを呼び出す
- レスポンスを返す(リソース、リダイレクト、ビュー)
コントローラーでは、次の処理を行ってはいけません。
- ドメインロジックを含める
- データベースに直接クエリーする
- 計算を行う
- 複雑なビジネスルールを処理する
コントローラーの命名規則
コントローラーには、主要リソースの複数形の名前を付けます。
標準的なリソースコントローラー
// ✅ CORRECT - Plural resource names
CalendarsController // manages calendar resources
EventsController // manages event resources
OrdersController // manages order resources
UsersController // manages user resources
// ❌ INCORRECT - Singular form
CalendarController
EventController
ネストされたリソースコントローラー
ネストされたリソースでは、親と子の両方のリソース名を組み合わせます。
// Route: /calendars/{calendar}/events
CalendarEventsController // manages events within a calendar
// Route: /orders/{order}/items
OrderItemsController // manages items within an order
パターン: {ParentSingular}{ChildPlural}Controller
ルート:
// Standard resource routes
Route::resource('calendars', CalendarsController::class);
// Nested resource routes
Route::resource('calendars.events', CalendarEventsController::class);
RESTful メソッドのみを使用する
コントローラーでは、Laravel の標準 RESTful メソッド名のみを使用してください。
標準 RESTful メソッド
Web アプリケーションの場合(フォームあり):
index- リソースの一覧を表示するcreate- 新しいリソースを作成するフォームを表示するstore- 新しく作成したリソースを保存するshow- 指定したリソースを表示するedit- リソースを編集するフォームを表示するupdate- 指定したリソースを更新するdestroy- 指定したリソースを削除する
API の場合(フォームビューなし):
index、show、store、update、destroy- API に
createメソッドやeditメソッドを含めてはいけません(これらは HTML フォーム用です)
使用禁止のメソッド名
リソースコントローラーでは、カスタムメソッド名を使用しないでください。
// ❌ INCORRECT
class OrdersController extends Controller
{
public function all() { } // Use index
public function get() { } // Use show
public function add() { } // Use store
public function remove() { } // Use destroy
public function cancel() { } // Extract to CancelOrderController
}
RESTful ではないアクション:呼び出し可能なコントローラーに切り出す
標準 RESTful メソッドに当てはまらないエンドポイントが必要な場合は、専用の呼び出し可能なコントローラーに切り出します。
// app/Http/Api/V1/Controllers/CancelOrderController.php
class CancelOrderController extends Controller
{
public function __invoke(
Order $order,
CancelOrderAction $action
): OrderResource {
$order = $action($order);
return OrderResource::make($order);
}
}
ルート:
Route::apiResource('orders', OrdersController::class);
Route::post('/orders/{order:uuid}/cancel', CancelOrderController::class);
RESTful ではないアクションに呼び出し可能なコントローラーを使う理由:
- 単一責任の原則に沿う
- コントローラー名から意図が明確になる
- 個別にテストできる
- リソースコントローラーの肥大化を防ぐ
Web 層とパブリック API
Web 層コントローラー
目的: アプリケーションの Web 層を提供します(別のフロントエンド向け API、Blade ビュー、Inertia)
配置場所: app/Http/Web/Controllers/
ルート: routes/web.php
特徴:
- バージョン管理しない
- 自由に変更できる
- プライベート(自分のアプリケーションのみが利用)
パブリック API コントローラー
目的: 外部/サードパーティーからの利用向け
配置場所: app/Http/Api/V1/Controllers/
ルート: routes/api/v1.php
特徴:
- バージョン管理する(
/api/v1、/api/v2) - 安定した契約
- 破壊的変更には新しいバージョンが必要
主な違い: 名前空間(Http\Web と Http\Api\V1)。コントローラーの構造は同じです。
コントローラーの全体例
<?php
declare(strict_types=1);
namespace App\Http\Web\Controllers;
use App\Actions\Order\CreateOrderAction;
use App\Actions\Order\DeleteOrderAction;
use App\Actions\Order\UpdateOrderAction;
use App\Data\Transformers\Web\OrderDataTransformer;
use App\Http\Controllers\Controller;
use App\Http\Web\Queries\OrderIndexQuery;
use App\Http\Web\Requests\CreateOrderRequest;
use App\Http\Web\Requests\UpdateOrderRequest;
use App\Http\Web\Resources\OrderResource;
use App\Models\Order;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use Illuminate\Http\Response;
class OrdersController extends Controller
{
public function index(OrderIndexQuery $query): AnonymousResourceCollection
{
return OrderResource::collection($query->jsonPaginate());
}
public function show(Order $order): OrderResource
{
return OrderResource::make($order->load('items', 'customer'));
}
public function store(
CreateOrderRequest $request,
CreateOrderAction $action
): OrderResource {
$order = $action(
user(),
OrderDataTransformer::fromRequest($request)
);
return OrderResource::make($order);
}
public function update(
UpdateOrderRequest $request,
Order $order,
UpdateOrderAction $action
): OrderResource {
$order = $action(
$order,
OrderDataTransformer::fromRequest($request)
);
return OrderResource::make($order);
}
public function destroy(
Order $order,
DeleteOrderAction $action
): Response {
$action($order);
return response()->noContent();
}
}
API コントローラーの場合: 同じ構造ですが、名前空間が異なります(App\Http\Api\V1\Controllers)。
クエリーオブジェクト
API のフィルタリング、ソート、関連データの読み込みには、Spatie Query Builder とクエリーオブジェクトを使用します。
public function index(OrderIndexQuery $query): AnonymousResourceCollection
{
return OrderResource::collection($query->jsonPaginate());
}
→ クエリーオブジェクトの完全ガイド:query-objects.md
認可
public function store(
CreateOrderRequest $request,
CreateOrderAction $action
): OrderResource {
$this->authorize('create', Order::class);
$order = $action(user(), OrderDataTransformer::fromRequest($request));
return OrderResource::make($order);
}
または、ルートミドルウェアを使用します。
Route::post('/orders', [OrdersController::class, 'store'])
->can('create', Order::class);
ルートモデルバインディング
コントローラーを簡潔にするため、ルートモデルバインディングを使用します。
Route::get('/orders/{order}', [OrdersController::class, 'show']);
Route::get('/orders/{order:uuid}', [OrdersController::class, 'show']); // Custom key
コントローラーには、モデルが自動的に渡されます。
public function show(Order $order): OrderResource
{
return OrderResource::make($order->load('items', 'customer'));
}
コントローラーのテスト
コントローラーにはフィーチャーテストを作成します。
use function Pest\Laravel\actingAs;
use function Pest\Laravel\postJson;
it('creates an order', function () {
$user = User::factory()->create();
$data = CreateOrderData::testFactory()->make();
actingAs($user)
->postJson('/orders', $data->toArray())
->assertCreated()
->assertJsonStructure(['data' => ['id', 'status']]);
});
it('requires authentication', function () {
postJson('/orders', [])->assertUnauthorized();
});
it('validates required fields', function () {
actingAs(User::factory()->create())
->postJson('/orders', [])
->assertUnprocessable()
->assertJsonValidationErrors(['customer_email', 'items']);
});
よくある間違い
❌ コントローラーにドメインロジックを記述する
// BAD
public function store(Request $request)
{
$order = Order::create($request->validated());
$order->items()->createMany($request->items);
$total = $order->items->sum('total');
$order->update(['total' => $total]);
}
✅ アクションに委譲する
// GOOD
public function store(
CreateOrderRequest $request,
CreateOrderAction $action
): OrderResource {
$order = $action(
user(),
OrderDataTransformer::fromRequest($request)
);
return OrderResource::make($order);
}
❌ コントローラーでデータベースにクエリーする
// BAD
public function index()
{
$orders = Order::with('items')
->where('status', 'pending')
->latest()
->paginate();
}
✅ クエリーオブジェクトを使用する
// GOOD
public function index(OrderIndexQuery $query): AnonymousResourceCollection
{
return OrderResource::collection($query->jsonPaginate());
}