こんにちは!
CYBIRD Advent Calendar 2022の13日目担当の@dave_cです。
前日は@whitemage_yuさんの「Discord用のbotを作ってみた」でした。それも是非ご覧ください!
はじめに
最近、個人的にもアクションクラスを幅広く使いましたので、この記事の形でLukeさんの話を要約するのに良いタイミングでした。
Actionsとは?
簡単に言うと、1つの責任しか持たない分割されたサービスクラスです。 つまり、通常、特定のタスクを実行するためのメソッドが1つだけ含まれており、再利用できます。 当然ですが、これはSeparation of Concerns
(関心事の分離)の原則とうまく守る方法です。
うんうん、コードを見たいですね。それでは、例を見ていきましょう!
例: ユーザ登録
例えば、簡単なサインアップフォームがあるとしましょう。 通常、コントローラーでリクエストを受信し、データを検証してから、コーディングスタイルに応じて、モデルを直接呼び出すか、以下のようにユーザーをデータベースに保存するためのリポジトリクラスを呼び出します。
class RegisterController extends Controller
{
public function store(RegisterRequest $request): RedirectResponse
{
$data = request->validated();
$user = User::create([
...$data,
'secret' => Hash::make($data['secret'])
]);
$auth->login($user);
return redirect(route('home'))->with('success', true);
}
}
一般的なアプローチであり、問題なく動いています。新しい要件が入ってくるまで。例えば、コマンドラインからユーザーを登録する必要になるとしましょう。 普段、カスタムのArtisan
コマンドを作成します。 そちらのクラスは以下のような例となります。
public function execute(): int
{
$data = $this->validateData($this->getInput());
$user = User::create([
...$data,
'secret' => Hash::make($data['secret']);
]);
$this->line("User {$user->email} has been registered.");
return Command::SUCCESS;
}
protected function validateData(array $data): array
{
return Validator::make($data, [
'email' => ['required', 'email', 'unique:users'],
'name' => ['required', 'string', 'max:255'],
'secret' => ['required', 'min:6']
])->validate();
}
protected function getInput(): array
{
return [
'email' => $this->argument('email') ?? $this->ask("What is the user's name?"),
'name' => $this->argument('email') ?? $this->ask("What is the user's email?"),
'secret' => $this->argument('secret') ?? $this->secret("What is the user's secret?")
];
}
これだけを見ると、特に問題がありません。 ただし、重複したコードを導入しました。 そうすると、データ検証とユーザー作成の部分は、いくつかの場所で保守する必要がありますので、保守性の観点から見るとよろしくないです。
Actionクラスを登場する
エレガントな解決方法の1つは、アクションクラスを使用することです。 上の例にどのように適用されるかを見てみましょう。 そのために、app/Actions
の下にRegisterUser.php
というファイルを作成します。Actions
フォルダーが存在しない場合では、作成するが必要です。
class RegisterUser
{
public function __invoke(array $data): User
{
$data = this->validateData($data);
return User::create([
...$data,
'secret' => Hash::make($data['secret']);
]);
}
protected function validateData(array $data): array
{
return Validator::make($data, [
'email' => ['required', 'email', 'unique:users'],
'name' => ['required', 'string', 'max:255'],
'secret' => ['required', 'min:6']
])->validate();
}
}
クラスの名前に注意してください。 動詞+名詞の形式です。 この形にすると、クラスの機能が理解しやすくなります。
ロジックを抽象化ができましたので、コントローラーも、Artisan
コマンドも修正しましょう。
すると、コントローラは次のようになります。
class RegisterController extends Controller
{
public function store(RegisterRequest $request, RegisterUser $registerUser): RedirectResponse
{
$user = $registerUser($request->all());
$auth->login($user);
return redirect(route('home'))->with('success', true);
}
}
そしてArtisan
コマンドクラス:
public function execute(RegisterUser $registerUser): int
{
$user = $registerUser($this->getInput());
$this->line("User {$user->email} has been registered.");
return Command::SUCCESS;
}
protected function getInput(): array
{
return [
'name' => $this->argument('name') ?? $this->ask("What is the user's name?"),
'email' => $this->argument('email') ?? $this->ask("What is the user's email?"),
'secret' => $this->argument('secret') ?? $this->secret("What is the user's secret?")
];
}
これで修正が完了となります。 クリーンな抽象化であり、保守性の向上ができています。
最後に
この記事がお役に立てば幸いです。
CYBIRD Advent Calendar 2022の14日目は@cy-naullさんの「面倒な手作業をシェルスクリプトに任せる」です。
明日の記事でもお楽しみに!