404 Not Found

404 Not Found


nginx

Laravelコントローラとリクエスト処理

コントローラはLaravelの「ビジネス指揮官」です—リクエストを受信し, モデルとビューを調整し, レスポンスを返します。すべてのビジネスロジックはここから統制されます。

1. 学ぶ内容


2. バックエンド開発者の実話

(1) 悩み:2000行の「神コントローラ」

初期の頃, BobはShopMetricsの全ロジックを単一のShopControllerに詰め込みました—商品管理, 注文処理, ユーザー認証, レポート生成がすべて一つにまとめられていました。コードが2000行に膨れ上がると, ある機能を変更すると別の機能が壊れる可能性がありました。Charlieが引き継いだ時, コード構造を解きほぐすだけで3日かかり, Aliceは小さな機能追加のリクエストを2週間待たなければなりませんでした。

(2) リソースコントローラによる解決策

LaravelリソースコントローラはCRUD操作を7つの個別メソッドに分解し, 各メソッドが単一のタスクを実行します。ルートバインディングと組み合わせることで, URLとメソッドが自動的に相互マッピングされます。

BASH
# 1つのコマンドで完全なCRUDコントローラを生成
php artisan make:controller ShopController --resource
# 生成されるメソッド: index(), create(), store(), show(), edit(), update(), destroy()

(3) 成果

Bobがリソースコントローラを使ってコードをリファクタリングした後, 各メソッドは30行以下に収まりました。Aliceの小さな機能は2週間から2日で完了し, Charlieは新しい機能を引き受ける際に既存機能を壊す心配がなくなりました。


3. 基本コントローラ

(1) 作成と構造

BASH
php artisan make:controller HomeController
# 作成されるファイル: app/Http/Controllers/HomeController.php
PHP
// app/Http/Controllers/HomeController.php
class HomeController extends Controller
{
    public function index(): View
    {
        return view('home.index');
    }

    public function about(): View
    {
        return view('home.about');
    }
}

(2) 単一アクションコントローラ __invoke

コントローラが1つのメソッドだけで済む場合, 名前付きメソッドの代わりに__invokeを使用します。

BASH
php artisan make:controller GenerateReportController --invokable
PHP
// app/Http/Controllers/GenerateReportController.php
class GenerateReportController extends Controller
{
    public function __invoke(Request $request): RedirectResponse
    {
        $report = ReportGenerator::create($request->all());
        return redirect()->route('reports.show', $report->id);
    }
}

// ルート登録
Route::post('/reports/generate', GenerateReportController::class);
観点 標準コントローラ 単一アクションコントローラ
メソッド数 複数 1 __invoke
ルート登録 [Ctrl::class, 'method'] Ctrl::class
用途 関連操作 単一責任操作
ShopController GenerateReportController

(1) ▶ サンプル:ShopMetrics単一アクションコントローラ

PHP
// app/Http/Controllers/ExportOrdersController.php
class ExportOrdersController extends Controller
{
    public function __invoke(Request $request): StreamedResponse
    {
        $shop = Shop::findOrFail($request->shop_id);
        $csv = OrderExporter::toCsv($shop->orders);

        return response()->streamDownload(
            callback: fn () => print($csv),
            name: "orders-{$shop->slug}.csv",
            headers: ['Content-Type' => 'text/csv'],
        );
    }
}

// routes/web.php
Route::post('/shops/{shop}/export', ExportOrdersController::class)
    ->name('shops.export');

出力:

TEXT
// 実行成功

4. リソースコントローラ

(1) リソースコントローラの作成

BASH
php artisan make:controller ShopController --resource

7つのCRUDメソッドが自動生成されます:

HTTP動詞 URI メソッド 目的
GET /shops index 一覧
GET /shops/create create 作成フォーム
POST /shops store 新規レコード保存
GET /shops/{shop} show 詳細
GET /shops/{shop}/edit edit 編集フォーム
PUT/PATCH /shops/{shop} update 更新
DELETE /shops/{shop} destroy 削除

(2) ルート登録

PHP
// 1行で7つのルートをすべて登録
Route::resource('shops', ShopController::class);

// 特定のメソッドのみに制限
Route::resource('shops', ShopController::class)->only([
    'index', 'show', 'store', 'update', 'destroy',
]);

// 特定のメソッドを除外
Route::resource('shops', ShopController::class)->except([
    'create', 'edit',
]);

(1) ▶ サンプル:ShopMetricsショップリソースコントローラ

PHP
// app/Http/Controllers/ShopController.php
class ShopController extends Controller
{
    public function __construct()
    {
        $this->middleware('auth');
        $this->middleware('tenant.resolve')->except('index', 'show');
    }

    public function index(): View
    {
        $shops = Shop::with('tenant')->paginate(15);
        return view('shops.index', compact('shops'));
    }

    public function create(): View
    {
        return view('shops.create');
    }

    public function store(StoreShopRequest $request): RedirectResponse
    {
        $shop = Shop::create($request->validated());
        return redirect()->route('shops.show', $shop)
            ->with('success', 'Shop created successfully.');
    }

    public function show(Shop $shop): View
    {
        $shop->load('products', 'orders');
        return view('shops.show', compact('shop'));
    }

    public function edit(Shop $shop): View
    {
        return view('shops.edit', compact('shop'));
    }

    public function update(UpdateShopRequest $request, Shop $shop): RedirectResponse
    {
        $shop->update($request->validated());
        return redirect()->route('shops.show', $shop)
            ->with('success', 'Shop updated successfully.');
    }

    public function destroy(Shop $shop): RedirectResponse
    {
        $shop->delete();
        return redirect()->route('shops.index')
            ->with('success', 'Shop deleted successfully.');
    }
}

出力:

TEXT
// 実行成功

5. APIリソースコントローラ

(1) APIコントローラの作成

BASH
php artisan make:controller Api/ShopController --api

--api--resource --except=create,editと同等です。APIはフォームページを必要としないためです。

メソッド Webリソース APIリソース
index
create
store
show
edit
update
destroy

(1) ▶ サンプル:ShopMetrics APIコントローラ

PHP
// app/Http/Controllers/Api/ShopController.php
class ShopController extends Controller
{
    public function __construct()
    {
        $this->middleware('auth:sanctum');
    }

    public function index(Request $request): JsonResponse
    {
        $shops = Shop::query()
            ->when($request->search, fn($q, $search) => $q->where('name', 'like', "%{$search}%"))
            ->paginate($request->per_page ?? 15);

        return ShopResource::collection($shops);
    }

    public function store(StoreShopRequest $request): JsonResponse
    {
        $shop = Shop::create($request->validated());
        return new ShopResource($shop);
    }

    public function show(Shop $shop): JsonResponse
    {
        return new ShopResource($shop->load('products', 'orders'));
    }

    public function update(UpdateShopRequest $request, Shop $shop): JsonResponse
    {
        $shop->update($request->validated());
        return new ShopResource($shop);
    }

    public function destroy(Shop $shop): Response
    {
        $shop->delete();
        return response()->noContent();
    }
}

出力:

TEXT
// 実行成功

6. 依存注入

(1) コンストラクタ注入

PHP
class OrderController extends Controller
{
    public function __construct(
        private OrderService $orderService,
        private PaymentGateway $payment,
    ) {}

    public function store(StoreOrderRequest $request): RedirectResponse
    {
        $order = $this->orderService->create($request->validated());
        $this->payment->charge($order);
        return redirect()->route('orders.show', $order);
    }
}

(2) メソッドレベル注入

PHP
class ReportController extends Controller
{
    public function show(Request $request, Report $report): View
    {
        // $request はコンテナによって注入される
        // $report はルートモデルバインディングによって解決される
        return view('reports.show', compact('report'));
    }
}

(3) ルートモデルバインディング

PHP
// 暗黙的バインディング — コントローラメソッドでタイプヒント
Route::get('/shops/{shop}', [ShopController::class, 'show']);

public function show(Shop $shop): View
{
    // $shopは{shop}によってDBから自動取得される
    // Shop::findOrFail($shop)と同等
    return view('shops.show', compact('shop'));
}

// カスタムキー — idの代わりにslugでバインド
Route::get('/shops/{shop:slug}', [ShopController::class, 'show']);
// 結果: /shops/alice-store → slug = 'alice-store' のShop
注入方法 用途 ライフサイクル
コンストラクタ すべてのコントローラメソッドで必要なもの リクエスト全体
メソッドレベル 特定のメソッドでのみ必要なもの 単一メソッド
ルートモデルバインディング URLからモデルを自動取得 単一メソッド

(1) ▶ サンプル:ShopMetrics依存注入付き注文コントローラ

PHP
// app/Http/Controllers/OrderController.php
class OrderController extends Controller
{
    public function __construct(
        private OrderService $orderService,
    ) {
        $this->middleware('auth');
    }

    public function index(Request $request): View
    {
        $orders = $request->user()->orders()
            ->with('shop', 'products')
            ->latest()
            ->paginate(15);

        return view('orders.index', compact('orders'));
    }

    public function show(Order $order): View
    {
        $this->authorize('view', $order);
        $order->load('items.product', 'shop', 'payment');
        return view('orders.show', compact('order'));
    }
}

出力:

TEXT
// 実行成功

7. コントローラミドルウェア

(1) コンストラクタでの割り当て

PHP
class ShopController extends Controller
{
    public function __construct()
    {
        $this->middleware('auth');
        $this->middleware('tenant.resolve')->except('index');
        $this->middleware('can:update,shop')->only('update', 'edit');
    }
}

(2) ルートレベルでの割り当て

PHP
Route::middleware(['auth', 'admin'])->group(function () {
    Route::resource('plans', PlanController::class);
});
割り当て場所 粒度 用途
コンストラクタ メソッドレベル コントローラ内の異なるメソッドに異なるミドルウェアが必要な場合
ルート定義 ルートグループ ミドルウェアを共有するルート群
Kernelグローバル グローバル すべてのリクエストで必ず実行する必要がある場合

(1) ▶ サンプル:ShopMetrics管理パネルコントローラ

PHP
// app/Http/Controllers/Admin/PlanController.php
class PlanController extends Controller
{
    public function __construct()
    {
        $this->middleware(['auth', 'role:admin']);
    }

    public function index(): View
    {
        $plans = Plan::withCount('subscriptions')->get();
        return view('admin.plans.index', compact('plans'));
    }

    public function store(StorePlanRequest $request): RedirectResponse
    {
        Plan::create($request->validated());
        return redirect()->route('admin.plans.index')
            ->with('success', 'Plan created.');
    }
}

出力:

TEXT
// 実行成功

8. リクエスト → コントローラ → モデル → ビューレスポンスチェーン

100%
sequenceDiagram
    participant C as クライアント
    participant R as ルーター
    participant M as ミドルウェア
    participant Ctrl as コントローラ
    participant Model as モデル
    participant V as ビュー

    C->>R: HTTPリクエスト
    R->>M: ミドルウェアパイプラインを実行
    M->>Ctrl: コントローラメソッドを呼び出し
    Ctrl->>Model: データをクエリ
    Model-->>Ctrl: 結果を返す
    Ctrl->>V: ビューにデータを渡す
    V-->>Ctrl: レンダリングされたHTML
    Ctrl-->>C: HTTPレスポンス

9. 総合例:ShopMetrics商品CRUDコントローラ

PHP
// ============================================
// 総合例: ShopMetrics ProductController
// 対象: リソースコントローラ, DI, ミドルウェア, モデルバインディング
// ============================================

// app/Http/Controllers/ProductController.php
class ProductController extends Controller
{
    public function __construct(
        private ProductService $productService,
    ) {
        $this->middleware('auth');
        $this->middleware('tenant.resolve');
    }

    public function index(Request $request): View
    {
        $products = Product::query()
            ->where('tenant_id', tenant()->id)
            ->when($request->search, fn($q, $s) => $q->where('name', 'like', "%{$s}%"))
            ->when($request->category, fn($q, $c) => $q->where('category_id', $c))
            ->with('category')
            ->orderBy($request->sort ?? 'created_at', $request->direction ?? 'desc')
            ->paginate(20);

        return view('products.index', compact('products'));
    }

    public function create(): View
    {
        $categories = Category::forTenant(tenant()->id)->get();
        return view('products.create', compact('categories'));
    }

    public function store(StoreProductRequest $request): RedirectResponse
    {
        $product = $this->productService->create(
            tenant()->id,
            $request->validated(),
        );
        return redirect()->route('products.show', $product)
            ->with('success', 'Product created successfully.');
    }

    public function show(Product $product): View
    {
        $this->authorize('view', $product);
        $product->load('category', 'orderItems.order');
        return view('products.show', compact('product'));
    }

    public function edit(Product $product): View
    {
        $this->authorize('update', $product);
        $categories = Category::forTenant(tenant()->id)->get();
        return view('products.edit', compact('product', 'categories'));
    }

    public function update(UpdateProductRequest $request, Product $product): RedirectResponse
    {
        $this->authorize('update', $product);
        $product->update($request->validated());
        return redirect()->route('products.show', $product)
            ->with('success', 'Product updated successfully.');
    }

    public function destroy(Product $product): RedirectResponse
    {
        $this->authorize('delete', $product);
        $product->delete();
        return redirect()->route('products.index')
            ->with('success', 'Product deleted successfully.');
    }
}

❓ よくある質問

Q コントローラはどのくらい「スリム」にすべきですか?
A コントローラは「調整役」に徹すべきです—リクエストを受信し, Service/Modelを呼び出し, レスポンスを返すだけです。ビジネスロジックはServiceクラスに, データアクセスはModel/Repositoryに配置し, コントローラのメソッドは1メソッド10〜30行に保つべきです。
Q 単一アクションコントローラはいつ使うべきですか?
A 操作がどのCRUDリソースにも属さない場合, 例えば「レポートのエクスポート」「メールの送信」「請求書の生成」などです。コントローラに公開メソッドが1つしかない場合は, __invokeを使用してください。
Q ルートモデルバインディングでレコードが見つからない場合はどうなりますか?
A デフォルトでは404エラーが返されます。RouteServiceProviderでRoute::bind()を使ってカスタム解決ロジックを定義するか, モデルのresolveRouteBindingQuery()メソッドをオーバーライドすることで, この動作をカスタマイズできます。
Q 依存注入とnewの違いは何ですか?
A newは手動でオブジェクトを作成し, 依存チェーンを自分で管理する必要があります。DIではLaravelコンテナが自動的に依存関係を解決して注入し, インターフェースバインディング, シングルトン管理, モックテストをサポートしています。
Q なぜAPIコントローラはJSONを直接返さずにResourceを使うのですか?
A ResourceクラスはJSON出力形式を標準化し, 機密フィールドの非表示, フィールド名の変更, 関連データのネストを可能にします。モデルを直接返すとすべてのフィールドが露出し, 一貫性のない形式になります。
Q コントローラ内でDB::rawを使ってSQLを書いてもいいですか?
A 可能ですが推奨されません。複雑なクエリにはEloquent ScopesやQuery Builderメソッドを使用してください。DB::rawはEloquentのセキュリティ層をバイパスするため, SQLインジェクションのリスクがあります。

📖 まとめ


📝 練習問題

  1. 基本問題 (⭐):make:controller --resourceを使ってShopMetricsのProductControllerを作成し, リソースルートを登録し, indexshowメソッドを実装してシンプルなビューを返してください。

  2. 応用問題 (⭐⭐):ExportOrdersController単一アクションコントローラを作成し, CSVエクスポート機能を実装してください。OrderServiceを注入してデータ変換を処理し, streamDownloadでファイルを返してください。

  3. チャレンジ (⭐⭐⭐):TenantProductControllerを設計し, ルートモデルバインディングで{tenant}{product}を解析し, テナント分離された商品CRUD操作を実装してください。ユーザーが自分のテナント内の商品のみ管理できるようにしてください。

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%