Laravelコントローラとリクエスト処理
コントローラはLaravelの「ビジネス指揮官」です—リクエストを受信し, モデルとビューを調整し, レスポンスを返します。すべてのビジネスロジックはここから統制されます。
1. 学ぶ内容
- 基本コントローラと単一アクションコントローラ
__invoke - リソースコントローラ:
--resourceフラグのCRUDメソッドへのマッピング - APIリソースコントローラ:
--apiフラグのルートバインディング - 依存注入:コンストラクタレベルとメソッドレベルのDI
- コントローラミドルウェアの割り当て
2. バックエンド開発者の実話
(1) 悩み:2000行の「神コントローラ」
初期の頃, BobはShopMetricsの全ロジックを単一のShopControllerに詰め込みました—商品管理, 注文処理, ユーザー認証, レポート生成がすべて一つにまとめられていました。コードが2000行に膨れ上がると, ある機能を変更すると別の機能が壊れる可能性がありました。Charlieが引き継いだ時, コード構造を解きほぐすだけで3日かかり, Aliceは小さな機能追加のリクエストを2週間待たなければなりませんでした。
(2) リソースコントローラによる解決策
LaravelリソースコントローラはCRUD操作を7つの個別メソッドに分解し, 各メソッドが単一のタスクを実行します。ルートバインディングと組み合わせることで, URLとメソッドが自動的に相互マッピングされます。
# 1つのコマンドで完全なCRUDコントローラを生成
php artisan make:controller ShopController --resource
# 生成されるメソッド: index(), create(), store(), show(), edit(), update(), destroy()
(3) 成果
Bobがリソースコントローラを使ってコードをリファクタリングした後, 各メソッドは30行以下に収まりました。Aliceの小さな機能は2週間から2日で完了し, Charlieは新しい機能を引き受ける際に既存機能を壊す心配がなくなりました。
3. 基本コントローラ
(1) 作成と構造
php artisan make:controller HomeController
# 作成されるファイル: app/Http/Controllers/HomeController.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を使用します。
php artisan make:controller GenerateReportController --invokable
// 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単一アクションコントローラ
// 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');
出力:
// 実行成功
4. リソースコントローラ
(1) リソースコントローラの作成
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) ルート登録
// 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ショップリソースコントローラ
// 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.');
}
}
出力:
// 実行成功
5. APIリソースコントローラ
(1) APIコントローラの作成
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コントローラ
// 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();
}
}
出力:
// 実行成功
6. 依存注入
(1) コンストラクタ注入
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) メソッドレベル注入
class ReportController extends Controller
{
public function show(Request $request, Report $report): View
{
// $request はコンテナによって注入される
// $report はルートモデルバインディングによって解決される
return view('reports.show', compact('report'));
}
}
(3) ルートモデルバインディング
// 暗黙的バインディング — コントローラメソッドでタイプヒント
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依存注入付き注文コントローラ
// 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'));
}
}
出力:
// 実行成功
7. コントローラミドルウェア
(1) コンストラクタでの割り当て
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) ルートレベルでの割り当て
Route::middleware(['auth', 'admin'])->group(function () {
Route::resource('plans', PlanController::class);
});
| 割り当て場所 | 粒度 | 用途 |
|---|---|---|
| コンストラクタ | メソッドレベル | コントローラ内の異なるメソッドに異なるミドルウェアが必要な場合 |
| ルート定義 | ルートグループ | ミドルウェアを共有するルート群 |
| Kernelグローバル | グローバル | すべてのリクエストで必ず実行する必要がある場合 |
(1) ▶ サンプル:ShopMetrics管理パネルコントローラ
// 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.');
}
}
出力:
// 実行成功
8. リクエスト → コントローラ → モデル → ビューレスポンスチェーン
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コントローラ
// ============================================
// 総合例: 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.');
}
}
❓ よくある質問
Route::bind()を使ってカスタム解決ロジックを定義するか, モデルのresolveRouteBindingQuery()メソッドをオーバーライドすることで, この動作をカスタマイズできます。newの違いは何ですか?newは手動でオブジェクトを作成し, 依存チェーンを自分で管理する必要があります。DIではLaravelコンテナが自動的に依存関係を解決して注入し, インターフェースバインディング, シングルトン管理, モックテストをサポートしています。Resourceを使うのですか?ResourceクラスはJSON出力形式を標準化し, 機密フィールドの非表示, フィールド名の変更, 関連データのネストを可能にします。モデルを直接返すとすべてのフィールドが露出し, 一貫性のない形式になります。DB::rawを使ってSQLを書いてもいいですか?DB::rawはEloquentのセキュリティ層をバイパスするため, SQLインジェクションのリスクがあります。📖 まとめ
- コントローラはリクエストの受信, モデルとビューの調整, レスポンスの返却を担当します
- 単一アクションコントローラは
__invokeを使ってCRUD以外の単一操作を処理します - リソースコントローラは7つのCRUDメソッドをHTTP動詞に自動マッピングします
- APIコントローラは
createとeditを省略し,apiResourceと組み合わせて使用します - 依存注入により, コントローラ内で
newを使ってオブジェクトを作成する必要がなくなり, コンテナが自動的に依存関係を解決します - ルートモデルバインディングはURLパラメータをモデルインスタンスに自動的に解析します
📝 練習問題
-
基本問題 (⭐):
make:controller --resourceを使ってShopMetricsのProductControllerを作成し, リソースルートを登録し,indexとshowメソッドを実装してシンプルなビューを返してください。 -
応用問題 (⭐⭐):
ExportOrdersController単一アクションコントローラを作成し, CSVエクスポート機能を実装してください。OrderServiceを注入してデータ変換を処理し,streamDownloadでファイルを返してください。 -
チャレンジ (⭐⭐⭐):
TenantProductControllerを設計し, ルートモデルバインディングで{tenant}と{product}を解析し, テナント分離された商品CRUD操作を実装してください。ユーザーが自分のテナント内の商品のみ管理できるようにしてください。



