システム設計において、アーキテクチャパターンの選択は重要です。
それは開発効率と保守性に大きく影響するからです。
特にWebアプリケーション開発では、MVCとMiddlewareという2つの概念が注目されています。
これらは対立するものではありません。
それぞれ異なる役割を持ち、組み合わせるとより強力なシステムを構築できます。
本記事では、MVCとMiddlewareの違いと使い分けについて解説します。
また、効果的な組み合わせ方も紹介します。
MVCとMiddlewareの基本的な違い
MVCとMiddlewareは全く異なる目的を持つ概念です。
これらを「対決」させるのは適切ではありません。
ハンマーとドライバーのどちらが優れているかを比較するようなものだからです。
MVCの役割
MVCは「Model-View-Controller」の略です。
アプリケーションの構造を次の3つの役割に分ける設計パターンになります。
- Model:データとビジネスロジックを管理
- View:ユーザーインターフェースを表示
- Controller:ユーザー入力を受け取り、ModelとViewを連携
コントローラーはルートに対する処理を担当します。
特定のリクエストに対してどのような動作をするかを決定するのです。
Middlewareの役割
一方、Middlewareはリクエストとレスポンスの間に位置します。
複数のルートで共通する処理を担当するのが特徴です。
リクエスト → [Middleware] → コントローラー → [Middleware] → レスポンス
主な用途としては、認証やロギングがあります。
また、レートリミットやリクエスト/レスポンスの変換なども含まれます。
適切な使い分け
それぞれのパターンをどのように使い分けるべきでしょうか。
状況に応じた選択が重要です。
Middlewareに適した処理
Middlewareは複数のルートで共有される横断的関心事に最適です。
次のような処理に向いています。
- 認証・認可
- リクエストのバリデーション
- レートリミット
- CORS設定
- ロギング
- キャッシュ
例えば、APIのレートリミットを実装する場合のコードは以下のようになります。
// レートリミットのMiddleware例 public function handle(Request $request, Closure $next) { // リクエスト数をチェック if ($this->tooManyAttempts($request)) { return response('リクエスト制限を超えました', 429); } return $next($request); }
コントローラーに適した処理
コントローラーは特定のルートに関連するロジックの調整役です。
主に次のような処理を担当します。
- ルートに固有の処理の実行
- 適切なモデルやサービスの呼び出し
- レスポンスの構築と返却
コントローラーの例を見てみましょう。
// コントローラー例 public function show($id) { $product = $this->productService->find($id); if (!$product) { return response()->json(['error' => '商品が見つかりません'], 404); } return view('products.show', ['product' => $product]); }
シングルアクションハンドラーとコントローラー
コントローラークラスにたくさんのアクションメソッドを入れるべきでしょうか。
それとも、シングルアクションハンドラー(1つのルートに対して1つのクラス)を使うべきでしょうか。
この点についても議論があります。
コントローラークラスの利点
- 関連する処理をまとめられる
- ルートの整理がしやすい
- クラス数が少なく管理しやすい
シングルアクションハンドラーの利点
- 単一責任の原則に準拠している
- 個々の機能がより明確になる
- テストがしやすい
- リファクタリングが容易
PSR-15で定義されているリクエストハンドラーは、シングルアクションハンドラーの一種です。
各リクエストに対して1つのハンドラーを使用するパターンを推奨しています。
実際の実装例
上記の概念を理解するために、簡単な実装例を見てみましょう。
コードで理解すると、より明確になります。
Middlewareの例
// 認証Middleware class AuthMiddleware implements MiddlewareInterface { public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { $token = $request->getHeaderLine('Authorization'); if (!$this->validateToken($token)) { return new JsonResponse(['error' => '認証に失敗しました'], 401); } return $handler->handle($request); } private function validateToken(string $token): bool { // トークン検証ロジック return true; } }
コントローラーの例
// 商品コントローラー class ProductController { private $productService; public function __construct(ProductService $productService) { $this->productService = $productService; } public function list(ServerRequestInterface $request): ResponseInterface { $products = $this->productService->getAllProducts(); return new JsonResponse(['products' => $products]); } public function detail(ServerRequestInterface $request, array $args): ResponseInterface { $product = $this->productService->getProduct($args['id']); if ($product === null) { return new JsonResponse(['error' => '商品が見つかりません'], 404); } return new JsonResponse(['product' => $product]); } }
シングルアクションハンドラーの例
// 商品詳細ハンドラー class GetProductDetailHandler implements RequestHandlerInterface { private $productService; public function __construct(ProductService $productService) { $this->productService = $productService; } public function handle(ServerRequestInterface $request): ResponseInterface { $id = $request->getAttribute('id'); $product = $this->productService->getProduct($id); if ($product === null) { return new JsonResponse(['error' => '商品が見つかりません'], 404); } return new JsonResponse(['product' => $product]); } }
リファクタリングのタイミング
コードの設計は固定的なものではありません。
プロジェクトの成長に合わせて進化させる必要があります。
リファクタリングのタイミングについて、いくつかのポイントを紹介します。
- コントローラーが大きくなりすぎたら、シングルアクションハンドラーへの分割を検討する
- 複数のルートで同じ処理が繰り返されるなら、Middlewareへの抽出を検討する
- ビジネスロジックがコントローラーに入り込んでいたら、サービスやモデルへの移動を検討する
「過度に考えすぎない」というアプローチが実用的です。
最初から完璧なアーキテクチャを目指すより、必要に応じてリファクタリングする方が効果的です。
責任の分離
効果的なコード設計の鍵は「関心の分離」(Separation of Concerns)です。
各コンポーネントには明確な役割を与えましょう。
- Middleware: 横断的関心事(認証、ロギングなど)
- コントローラー/ハンドラー: ルートロジック(リクエストの処理と適切なサービスの呼び出し)
- サービス/モデル: ビジネスロジック
このような分離により、コードの可読性が向上します。
また、保守性やテスト容易性も高まります。
まとめ
MVCとMiddlewareは「対立」するものではありません。
それぞれ異なる役割を持ち、組み合わせて使うと効果的です。
- Middlewareは複数のルートで共有される処理に適しています(認証、ロギングなど)
- コントローラーやハンドラーは特定のルートに関連するロジックを担当します
- 必要に応じてコントローラーからシングルアクションハンドラーへリファクタリングしましょう
- 「関心の分離」を意識し、各コンポーネントに明確な役割を与えることが大切です
アーキテクチャ設計に絶対的な正解はありません。
プロジェクトの要件を考慮しましょう。
また、チームの経験や将来の保守性も大切な要素です。
コードの成長に合わせて柔軟に調整することが、長期的な成功への鍵となります。