[Laravel] シングルアクションコントローラで責務を分割する

# 導入

Laravelでルートに関する処理を記述する場合、以下のように操作する対象のオブジェクト毎にコントローラクラスを生成し、ルート毎にメソッドを作るのが一般的かと思います。

```php
namespace App\Http\Controllers;
 
use App\Models\User;
use Illuminate\View\View;
 
class UserController extends Controller
{
    /**
     * Show the profile for a given user.
     */
    public function show(string $id): View
    {
        return view('user.profile', [
            'user' => User::findOrFail($id)
        ]);
    }
}
```

ルート定義は以下のようになります。

```php
use App\Http\Controllers\UserController;
 
Route::get('/user/{id}', [UserController::class, 'show']);
```

ただ、この方法だとルートが増えるたびにコントローラのメソッドが増えていき、各ルート処理が複雑になるとコントローラのファイルがどんどん肥大化していきます。
また、クラス毎の責務も曖昧になり、コントローラのクラスがどんどん読みづらくなっていきます。

この問題に対する解決策の一つとして、今回は**シングルアクションコントローラ**の使い方についてご紹介します。

# 基本的な使い方

シングルアクションコントローラとは、その名の通り、１つのクラス内で１つのルートのアクションのみを処理するコントローラのことです。

まずは以下のように `__invoke` メソッドを持ったコントローラを定義します。

```php
namespace App\Http\Controllers;
 
class ProvisionServer extends Controller
{
    /**
     * Provision a new web server.
     */
    public function __invoke()
    {
        // ...
    }
}
```

次に、ルート定義ファイルでは以下のようにコントローラの完全修飾名を指定します。

```php
use App\Http\Controllers\ProvisionServer;
 
Route::post('/server', ProvisionServer::class);
```

ルート定義でクラス名が指定されていて、そのクラスが`__invoke`メソッドを実装していれば、通常のコントローラと同様に`__invoke`メソッドが呼ばれて処理が実行されます。

ルートバインディングモデルの受け取りやサービスコンテナからの注入などは、`__invoke`メソッドで行います。

# シングルアクションコントローラを整理したい

シングルアクションコントローラを使うと、ルート毎にクラスができるため、コントローラクラスがどんどん増えていきます。
すべて `Controller` の中に配置してたのではすごいことになるので、名前空間を用いて整理する必要が出てきます。

ここでは一例として、ルートと名前空間の切り方を同期させた考え方をご紹介します。

## ユーザー登録
例えば、`User`というモデルに対する一連の処理を実装するとします。
ユーザーを登録する一連の処理を実装した、`UserCreateController`を、シングルアクションコントローラとして実装します。

```php
namespace App\Http\Controllers\User;
 
class UserCreateController extends Controller
{
    public function __invoke()
    {
        // ...
    }
}
```

注目は`namespace`です。`App\Http\Controllers\User`と、`Controller`の1階層下に`User`という名前空間を切り、そこに配置しています。
Userに関する処理はこの名前空間にまとめましょう、ということです。

ルート定義は以下のようになります。

```php
Route::prefix('/users')->group(function () {
    Route::post('/', UserCreateController::class);
});
```

`Route::prefix('/users')->group(function () {})`で囲むことで、この中で定義されたルートはすべて`/users`で始まるパスである、というグループ化を行うことができます。

## ユーザー詳細取得
次に、ユーザーの詳細を取得するルートについて考えます。パスは`/users/:id`になると思います。

同じようにコントローラから定義します。

```php
namespace App\Http\Controllers\User;

class UserReadController extends Controller
{
    public function __invoke(User $user)
    {
        // ...
    }
}
```

実際には`UserResource`等を返すことになると思いますが、ここでは省略しています。
`namaspace`を見ていただければ分かる通り、ユーザー登録の処理と同じ名前空間に配置しています。

同様にルート定義も追加します。

```php
Route::prefix('/users')->group(function () {
    Route::post('/', UserCreateController::class);
    Route::prefix('{user}')->group(function () {
        Route::get('/',, UserReadController::class);
    });
});
```

`user`モデルをルートバインディングする部分もグループ化してみました。
（これはケースバイケースかも）

## ユーザーに関するその他のルート

他にもユーザー情報の更新や、削除、またはそれ以外の処理など、ユーザーに対する処理を行うコントローラは、すべて`App\Http\Controllers\User`以下に配置していきます。

ディレクトリ構成とルート定義はこんな感じになります。

```
app
└ Http
 └ Controllers
  └ User
   └ UserCreateController.php
   └ UserDeleteController.php
   └ UserReadController.php
   └ UserUpdateController.php
```

```php
Route::prefix('/users')->group(function () {
    Route::post('/', UserCreateController::class);
    Route::prefix('{user}')->group(function () {
        Route::get('/',, UserReadController::class);
        Route::patch('/',, UserUpdateController::class);
        Route::delete('/',, UserDeleteController::class);
    });
});
```

## ユーザー以外のモデルに対する処理

ユーザー以外のモデルに関する処理も、それぞれ専用の名前空間に配置していきます。
基本的には`Controller`直下にコントローラを配置しません。

例えば`Post`だったら`App\Http\Controllers\Post`、`Account`だったら`App\Http\Controllers\Account`です。

イメージとしてはこうなります。

```
app
└ Http
 └ Controllers
  └ User
   └ UserCreateController.php
   └ UserDeleteController.php
   └ UserReadController.php
   └ UserUpdateController.php
  └ Post
   └ PostCreateController.php
   └ PostDeleteController.php
   └ PostReadController.php
   └ PostUpdateController.php
```

```php
Route::prefix('/users')->group(function () {
    Route::post('/', UserCreateController::class);
    Route::prefix('{user}')->group(function () {
        Route::get('/',, UserReadController::class);
        Route::patch('/',, UserUpdateController::class);
        Route::delete('/',, UserDeleteController::class);
    });
});
Route::prefix('/posts')->group(function () {
    Route::post('/', PostCreateController::class);
    Route::prefix('{post}')->group(function () {
        Route::get('/',, PostReadController::class);
        Route::patch('/',, PostUpdateController::class);
        Route::delete('/',, PostDeleteController::class);
    });
});
```

## ちょっと複雑なパターン
例えば、あるユーザーが投稿した全てのコメントを取得するために、以下のようなパスを定義したとします。

`/users/:userId/comments/:commentId`

その場合、`App\Http\Controllers\User\Comment`という形で、`User`の下に更に名前空間を切ります。クラス名も階層に合わせて `UserCommentCreateController`などとします。

```
app
└ Http
 └ Controllers
  └ User
   └ UserCreateController.php
   └ UserDeleteController.php
   └ UserReadController.php
   └ UserUpdateController.php
   └ Comment
    └ UserCommentCreateController.php
    └ UserCommentDeleteController.php
    └ UserCommentReadController.php
    └ UserCommentUpdateController.php
```

まあ、あまりこだわりすぎるとファイル名が長くなったりするので、この辺はケースバイケースなのかなーとは思います。

# まとめ

シングルアクションコントローラには、`__invoke`以外に`public`なメソッドは定義しません。どこがエントリポイントなのか非常にわかりやすくなります。
また、１クラス１責務が明確になったことで、処理が複雑になってきた場合には、`private`なメソッドに処理を分割するなどしてコードを整理することが容易になります。

他にも、1つのアクションでしか使わないサービスクラスのインジェクションを、コンストラクタに分けることができるというメリットもあります。
通常の方法だとメソッドの引数としてバインディングされたモデルやFormRequestなどと一緒にDIしなければならないため、DIするクラスが多いと見た目がガチャガチャしがちですが、シングルアクションコントローラなら、DIするクラスはコンストラクタに、バインディングしたモデル等は__invokeに分割することができ、見た目の上でもスッキリします。

なお、名前空間の切り方はあくまで私が採用している方法というだけなので、参考程度に御覧ください。
チームの規模や規約に沿って適切な方法を選択するのが良いかと思います。