【Laravel】コントローラーを深掘り!Fat Controllerを防ぐ設計とテクニック

Laravel

Laravelでアプリケーションを開発する際、MVCモデルの中心となるのが「コントローラー(Controller)」です。初心者のうちはルーティングから直接コントローラーに繋ぎ、データベースの操作からバリデーションまで全てを記述してしまいがちです。

しかし、システムが成長するにつれてコントローラーが肥大化する「Fat Controller(ファットコントローラー)」問題が発生します。これは可読性を下げ、テストを困難にする大きな要因です。

この記事では、Laravelのコントローラーの基本的な役割から一歩踏み込み、保守性の高い実践的なコントローラーの設計手法を深掘りして解説します。

1. コントローラーの本来の役割とは?

コントローラーの本来の役割は「HTTPリクエストを受け取り、適切な処理を呼び出し、HTTPレスポンスを返すこと」のみに留めるべきです。

以下のような処理は、コントローラーから切り離すことが推奨されます。

  • 複雑なバリデーションロジック
  • データベースの複雑なクエリ(ビジネスロジック)
  • 外部APIとの通信
  • レスポンスデータの細かいフォーマット変換

これらを適切に分離するための具体的なアプローチを見ていきましょう。

2. FormRequestによるバリデーションの分離

コントローラー内で $request->validate() を使用するのは手軽ですが、コードが長くなりがちです。LaravelのFormRequestを使用することで、バリデーションロジックを完全に分離できます。

【改善前】Fatになりがちなコントローラー

PHP
public function store(Request $request)
{
    $validated = $request->validate([
        'title' => 'required|string|max:255',
        'body'  => 'required|string',
        'email' => 'required|email|unique:users',
    ]);

    // 保存処理...
}

【改善後】FormRequestの活用

まず、Artisanコマンドで専用のリクエストクラスを作成します。

Bash
php artisan make:request StorePostRequest

生成された app/Http/Requests/StorePostRequest.php の中に、コントローラーに書いていたルールを移動させます。

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 をタイプヒントとして指定します。

PHP
public function store(StorePostRequest $request)
{
    // ここに到達した時点でバリデーションは通過済み
    $validated = $request->validated();
    
    // 保存処理...
}

これにより、コントローラーは「バリデーション成功後の処理」のみに集中でき、単一責任の原則(SRP)を満たしやすくなります。

3. シングルアクションコントローラー(__invoke)の活用

Laravelでは、1つのコントローラーに1つのメソッドのみを持たせるシングルアクションコントローラー(Invokable Controller)がサポートされています。

複雑なドメインや、機能が多いリソースの場合、標準の7つのメソッド(index, create, store, show, edit, update, destroy)だけでは名前付けに無理が生じることがあります。

Bash
php artisan make:controller PublishPostController --invokable
PHP
class PublishPostController extends Controller
{
    public function __invoke(Post $post)
    {
        $post->publish();
        return redirect()->route('posts.show', $post);
    }
}

ルーティングの記述もシンプルに:

PHP
Route::post('/posts/{post}/publish', PublishPostController::class);

特定のビジネスアクション(今回であれば「公開する」という振る舞い)ごとにコントローラーを独立させることで、影響範囲を限定し、コードの検索性を劇的に向上させることができます。

4. サービス層(Service Class)とDI(依存性の注入)

データベースへの保存や、ポイントの付与、メール送信などが連続するような複雑な処理(ビジネスロジック)は、サービスクラスとして切り出します。

コントローラーのコンストラクタやメソッドでDI(Dependency Injection)を利用して注入することで、テスト時にはモック(Mock)に差し替えることが容易になり、テストコードの品質が向上します。

サービスクラスの作成

PHP
namespace App\Services;

use App\Models\Post;

class PostService
{
    public function createPost(array $data): Post
    {
        // 複雑なトランザクションや関連モデルの保存など
        return Post::create($data);
    }
}

コントローラーでのDIの利用

PHP
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のコントローラーを深掘りし、保守性の高い設計にするためのポイントをまとめました。

  1. バリデーションは FormRequest に任せる
  2. 1つのエンドポイントで完結する複雑な処理は Invokable Controller を検討する
  3. ビジネスロジックは Service クラス等に抽出し、DIを活用する
  4. コントローラーは「交通整理」に徹する

これらを意識するだけで、フレームワークの恩恵を最大限に受けながら、テストしやすく拡張性の高いアプリケーションを構築することが可能です。ぜひ、現在のプロジェクトのコントローラーを見直してみてください。

コメント