Laravelでアプリケーションを開発する際、MVCモデルの中心となるのが「コントローラー(Controller)」です。初心者のうちはルーティングから直接コントローラーに繋ぎ、データベースの操作からバリデーションまで全てを記述してしまいがちです。
しかし、システムが成長するにつれてコントローラーが肥大化する「Fat Controller(ファットコントローラー)」問題が発生します。これは可読性を下げ、テストを困難にする大きな要因です。
この記事では、Laravelのコントローラーの基本的な役割から一歩踏み込み、保守性の高い実践的なコントローラーの設計手法を深掘りして解説します。
1. コントローラーの本来の役割とは?
コントローラーの本来の役割は「HTTPリクエストを受け取り、適切な処理を呼び出し、HTTPレスポンスを返すこと」のみに留めるべきです。
以下のような処理は、コントローラーから切り離すことが推奨されます。
- 複雑なバリデーションロジック
- データベースの複雑なクエリ(ビジネスロジック)
- 外部APIとの通信
- レスポンスデータの細かいフォーマット変換
これらを適切に分離するための具体的なアプローチを見ていきましょう。
2. FormRequestによるバリデーションの分離
コントローラー内で $request->validate() を使用するのは手軽ですが、コードが長くなりがちです。LaravelのFormRequestを使用することで、バリデーションロジックを完全に分離できます。
【改善前】Fatになりがちなコントローラー
public function store(Request $request)
{
$validated = $request->validate([
'title' => 'required|string|max:255',
'body' => 'required|string',
'email' => 'required|email|unique:users',
]);
// 保存処理...
}【改善後】FormRequestの活用
まず、Artisanコマンドで専用のリクエストクラスを作成します。
php artisan make:request StorePostRequest生成された app/Http/Requests/StorePostRequest.php の中に、コントローラーに書いていたルールを移動させます。
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StorePostRequest extends FormRequest
{
/**
* ユーザーがこのリクエストを行う権限があるかどうかを判断する
*/
public function authorize(): bool
{
return true; // 今回は常に許可
}
/**
* リクエストに適用するバリデーションルールを取得する
*/
public function rules(): array
{
return [
'title' => 'required|string|max:255',
'body' => 'required|string',
'email' => 'required|email|unique:users',
];
}
}そして、コントローラー側のメソッドでは、標準の Request の代わりに作成した StorePostRequest をタイプヒントとして指定します。
public function store(StorePostRequest $request)
{
// ここに到達した時点でバリデーションは通過済み
$validated = $request->validated();
// 保存処理...
}これにより、コントローラーは「バリデーション成功後の処理」のみに集中でき、単一責任の原則(SRP)を満たしやすくなります。
3. シングルアクションコントローラー(__invoke)の活用
Laravelでは、1つのコントローラーに1つのメソッドのみを持たせるシングルアクションコントローラー(Invokable Controller)がサポートされています。
複雑なドメインや、機能が多いリソースの場合、標準の7つのメソッド(index, create, store, show, edit, update, destroy)だけでは名前付けに無理が生じることがあります。
php artisan make:controller PublishPostController --invokableclass PublishPostController extends Controller
{
public function __invoke(Post $post)
{
$post->publish();
return redirect()->route('posts.show', $post);
}
}ルーティングの記述もシンプルに:
Route::post('/posts/{post}/publish', PublishPostController::class);特定のビジネスアクション(今回であれば「公開する」という振る舞い)ごとにコントローラーを独立させることで、影響範囲を限定し、コードの検索性を劇的に向上させることができます。
4. サービス層(Service Class)とDI(依存性の注入)
データベースへの保存や、ポイントの付与、メール送信などが連続するような複雑な処理(ビジネスロジック)は、サービスクラスとして切り出します。
コントローラーのコンストラクタやメソッドでDI(Dependency Injection)を利用して注入することで、テスト時にはモック(Mock)に差し替えることが容易になり、テストコードの品質が向上します。
サービスクラスの作成
namespace App\Services;
use App\Models\Post;
class PostService
{
public function createPost(array $data): Post
{
// 複雑なトランザクションや関連モデルの保存など
return Post::create($data);
}
}コントローラーでのDIの利用
use App\Services\PostService;
class PostController extends Controller
{
public function __construct(
private PostService $postService
) {}
public function store(StorePostRequest $request)
{
$post = $this->postService->createPost($request->validated());
return response()->json($post, 201);
}
}Laravelの強力なサービスコンテナが自動的に PostService をインスタンス化して注入してくれるため、コントローラーは非常にスッキリとした状態を保てます。
まとめ:コントローラーをスリムに保つためのチェックリスト
Laravelのコントローラーを深掘りし、保守性の高い設計にするためのポイントをまとめました。
- バリデーションは FormRequest に任せる
- 1つのエンドポイントで完結する複雑な処理は Invokable Controller を検討する
- ビジネスロジックは Service クラス等に抽出し、DIを活用する
- コントローラーは「交通整理」に徹する
これらを意識するだけで、フレームワークの恩恵を最大限に受けながら、テストしやすく拡張性の高いアプリケーションを構築することが可能です。ぜひ、現在のプロジェクトのコントローラーを見直してみてください。



コメント