導入
Laravelでルートに関する処理を記述する場合、以下のように操作する対象のオブジェクト毎にコントローラクラスを生成し、ルート毎にメソッドを作るのが一般的かと思います。
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)
]);
}
}
ルート定義は以下のようになります。
use App\Http\Controllers\UserController;
Route::get('/user/{id}', [UserController::class, 'show']);
ただ、この方法だとルートが増えるたびにコントローラのメソッドが増えていき、各ルート処理が複雑になるとコントローラのファイルがどんどん肥大化していきます。
また、クラス毎の責務も曖昧になり、コントローラのクラスがどんどん読みづらくなっていきます。
この問題に対する解決策の一つとして、今回はシングルアクションコントローラの使い方についてご紹介します。
基本的な使い方
シングルアクションコントローラとは、その名の通り、1つのクラス内で1つのルートのアクションのみを処理するコントローラのことです。
まずは以下のように __invoke
メソッドを持ったコントローラを定義します。
namespace App\Http\Controllers;
class ProvisionServer extends Controller
{
/**
* Provision a new web server.
*/
public function __invoke()
{
// ...
}
}
次に、ルート定義ファイルでは以下のようにコントローラの完全修飾名を指定します。
use App\Http\Controllers\ProvisionServer;
Route::post('/server', ProvisionServer::class);
ルート定義でクラス名が指定されていて、そのクラスが__invoke
メソッドを実装していれば、通常のコントローラと同様に__invoke
メソッドが呼ばれて処理が実行されます。
ルートバインディングモデルの受け取りやサービスコンテナからの注入などは、__invoke
メソッドで行います。
シングルアクションコントローラを整理したい
シングルアクションコントローラを使うと、ルート毎にクラスができるため、コントローラクラスがどんどん増えていきます。
すべて Controller
の中に配置してたのではすごいことになるので、名前空間を用いて整理する必要が出てきます。
ここでは一例として、ルートと名前空間の切り方を同期させた考え方をご紹介します。
ユーザー登録
例えば、User
というモデルに対する一連の処理を実装するとします。
ユーザーを登録する一連の処理を実装した、UserCreateController
を、シングルアクションコントローラとして実装します。
namespace App\Http\Controllers\User;
class UserCreateController extends Controller
{
public function __invoke()
{
// ...
}
}
注目はnamespace
です。App\Http\Controllers\User
と、Controller
の1階層下にUser
という名前空間を切り、そこに配置しています。
Userに関する処理はこの名前空間にまとめましょう、ということです。
ルート定義は以下のようになります。
Route::prefix('/users')->group(function () {
Route::post('/', UserCreateController::class);
});
Route::prefix('/users')->group(function () {})
で囲むことで、この中で定義されたルートはすべて/users
で始まるパスである、というグループ化を行うことができます。
ユーザー詳細取得
次に、ユーザーの詳細を取得するルートについて考えます。パスは/users/:id
になると思います。
同じようにコントローラから定義します。
namespace App\Http\Controllers\User;
class UserReadController extends Controller
{
public function __invoke(User $user)
{
// ...
}
}
実際にはUserResource
等を返すことになると思いますが、ここでは省略しています。
namaspace
を見ていただければ分かる通り、ユーザー登録の処理と同じ名前空間に配置しています。
同様にルート定義も追加します。
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
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
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
なメソッドは定義しません。どこがエントリポイントなのか非常にわかりやすくなります。
また、1クラス1責務が明確になったことで、処理が複雑になってきた場合には、private
なメソッドに処理を分割するなどしてコードを整理することが容易になります。
他にも、1つのアクションでしか使わないサービスクラスのインジェクションを、コンストラクタに分けることができるというメリットもあります。
通常の方法だとメソッドの引数としてバインディングされたモデルやFormRequestなどと一緒にDIしなければならないため、DIするクラスが多いと見た目がガチャガチャしがちですが、シングルアクションコントローラなら、DIするクラスはコンストラクタに、バインディングしたモデル等は__invokeに分割することができ、見た目の上でもスッキリします。
なお、名前空間の切り方はあくまで私が採用している方法というだけなので、参考程度に御覧ください。
チームの規模や規約に沿って適切な方法を選択するのが良いかと思います。