mcpField
L
laravel-controllers

laravel-controllers

github.com/leeovery開発1552 回インストール2 週間前に更新
リポジトリ

laravel-controllers について

Laravel開発者向けのコントローラー設計ガイドです。HTTP処理に役割を絞り、ドメインロジックをActionに委譲する方針を示します。REST規約、Webと公開APIの構成、クエリオブジェクト、ルートモデルバインディングも扱います。

主な機能

  1. 01コントローラーを薄く保つ
  2. 02標準RESTメソッドを使う
  3. 03非REST処理を単一アクションに分離
  4. 04Webとバージョン付きAPIを分ける
  5. 05APIクエリにクエリオブジェクトを使う
  6. 06認可とモデルバインディングのパターン

活用シーン

  1. 01リソースコントローラーの命名とメソッドを決める
  2. 02ビジネスルールをコントローラーから移す
  3. 03Webと公開APIのコントローラーを構成する
  4. 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 の構成

基本方針

コントローラーが行うのは、次の処理のみです。

  1. 依存関係を型指定する
  2. バリデーションを行う(Form Request 経由)
  3. アクションを呼び出す
  4. レスポンスを返す(リソース、リダイレクト、ビュー)

コントローラーでは、次の処理を行ってはいけません。

  • ドメインロジックを含める
  • データベースに直接クエリーする
  • 計算を行う
  • 複雑なビジネスルールを処理する

コントローラーの命名規則

コントローラーには、主要リソースの複数形の名前を付けます。

標準的なリソースコントローラー
// ✅ 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());
}

コメント