この記事では、Laravelで開発をする際に弊社で新しく採用したアーキテクチャの一部をご紹介します。
新人エンジニアでもハードルが高くならずプロジェクトに参画出来るよう、各クラスの役割が簡潔になるようにしています。また、記事の内容もどちらかというと新人向けの内容です。
この記事はMVCモデルを知っていることを前提に書いているため、
詳しく知らないよ!という方は先に調べてみてください。
(Laravel + MVC + 処理 + 流れ で検索するのがおすすめです)
コントローラーの役割
LaravelのMVCモデルでは、コントローラーの役割は以下になります。
- ビューからリクエストを受け取る
- 受け取ったリクエストをもとにモデルに処理をさせる
- レスポンスとともにビューを返す
ビュー ⇄ コントローラー ⇄ モデル
コントローラーはビューとモデルの間に入って、たくさんの役割を請け負います。
この役割を分割して見通しを良くし、メンテナンスをしやすくしよう!というのが今回の狙いです。
コントローラーの行数がすごく多くなってしまった状態は、よく「Fat Controller」と言われています。
(モデルが太った Fat Model という問題もよく起きるのですが、それはまた次回扱います)
実際のコードでの簡単な例
すべてコントローラーに書いた場合
今回は例が簡単なので短くまとまっていますが、実際にはメソッドはもっとたくさんあり、それぞれバリデーションがすごく複雑で、アカウント作成処理の前後に関連の処理が数十行あると考えてください…(Fatなコントローラーが見えてきましたか?)
use App\Models\Account;
use Illuminate\Validation\Rules\Password;
/**
* アカウント管理のコントローラー
*/
class AccountController extends Controller
{
public function create(Request $request)
{
// バリデーション
$validatedData = $request->validate([
'name' => ['required', 'string', 'max:100'],
'password' => ['required', 'confirmed', Password::min(8)],
]);
// パスワードをハッシュ化
$data['password'] = Hash::make($data['password']);
// アカウント作成処理
$newAccount = Account::create($validatedData);
// ビューを返す
return view('newAccount', $newAccount);
}
}
役割を分割した場合
今回は、以下のように役割を分担します。
ビュー ⇄ リクエストクラス ⇄ コントローラー ⇄ ユースケース ⇄ モデル
リクエストクラスとユースケースという新しい単語が出てきました。
リクエストクラス
リクエストクラスは先ほどのコードで言うと、
public function create(Request $request)
の引数にあるRequestの部分のことです。
Laravelではコントローラーに辿り着くまでに、FormやAjax通信などから受け取ったHTTPリクエストを、このRequestクラスにまとめてくれています。その際、上記のコードでも使っているバリデーションなど便利なメソッドを色々生やしてくれています。
リクエストクラスを拡張すると、その中でバリデーションの指定ができたり、バリデーション前後でデータをいじることができます。
例えば、createメソッドのリクエストクラスをCreateRequestとして分割してみましょう。
CreateRequest
use Illuminate\Foundation\Http\FormRequest;
class CreateRequest extends FormRequest
{
public function rules()
{
return [
'name' => ['required', 'string', 'max:100'],
'password' => ['required', 'confirmed', Password::min(8)],
];
}
}
AccountController
use App\Http\Requests\Account\CreateRequest; // 追加
use App\Models\Account;
use Illuminate\Validation\Rules\Password;
/**
* アカウント管理のコントローラー
*/
class AccountController extends Controller
{
public function create(CreateRequest $request)
{
// パスワードをハッシュ化
$data['password'] = Hash::make($data['password']);
// アカウント作成処理
$newAccount = Account::create($request->validated());
// ビューを返す
return view('newAccount', $newAccount);
}
}
これで、バリデーションという役割をコントローラーから取り上げることに成功しました!
また、処理の前後に受け取った値をバリデーションしたり、整形する必要がある場合には、CreateRequestの中に処理を書くといいでしょう。
リクエストの下処理はすべてCreateRequestに任せるイメージです。
他にもこのリクエストクラスで出来ることはたくさんあるので、調べてみてください。
(参考リンク)
Laravel – Form Request Validation
ReadDouble(日本語訳) – フォームリクエストバリデーション
ユースケース
ユースケースでは、主にモデルを使った処理を行います。
コントローラーの中ではモデルを参照しないようになることがまずは目標です。
実際に、Accountモデルを参照してアカウント作成処理を行なっている部分をユースケースに移してみましょう。
Create(ユースケース)
今回は App\UseCases\Account\Create.php として作成しています。
クラス名は、意図的に動詞の形にしています。1つの動詞で表せる処理を1つのユースケースにまとめるくらいがちょうど良い単位になるためです。
また、メソッドはどのユースケースでも invoke() の1つのみにしています。
ユースケースのinvokeを呼び出せばいいんだな、ということをプロジェクト全体で統一すると分かりやすくなると思います。
(余談:invoke = 呼び出す という意味で、PHPには__invokeというマジックメソッドもあります)
use App\Models\Account;
use Hash;
/**
* アカウント作成処理
*/
class Create
{
public function invoke(array $data): Account
{
// パスワードをハッシュ化
$data['password'] = Hash::make($data['password']);
// アカウント作成処理
$newAccount = Account::create($data);
return $newAccount;
}
}
AccountController
use App\Http\Requests\Account\CreateRequest;
use App\Models\Account;
use App\UseCases\Create as CreateUseCase; // 追加
use Illuminate\Validation\Rules\Password;
/**
* アカウント管理のコントローラー
*/
class AccountController extends Controller
{
public function create(CreateRequest $request, CreateUseCase $createUC)
{
// バリデーション済みのデータをリクエストクラスから取得
$data = $request->validated();
// アカウント作成のユースケースに渡す
$newAccount = $createUC->invoke($data);
// ビューを返す
return view('newAccount', $newAccount);
}
}
これで、コントローラーはモデルを参照することもなく、リクエストクラスとユースケースの橋渡しをするだけの役割となりました。
もしコントローラーの他のメソッドでアカウント作成処理を行う場合には、CreateUseCaseを呼べば共通化にもなりますね。
今回は以下の記事を参考にさせていただきました。
(もっと詳しく知りたい方はぜひこちらもお読みください)
Qiita – Controller, UseCase, Service (および Model) の役割分担についての考察
Qiita – Laravel で Request, UseCase, Resource を使いコントロールフローをシンプルにする
ここまでお読みいただきありがとうございました!