Laravel Actionsのご紹介

こんにちは！

CYBIRD Advent Calendar 2022の13日目担当の@dave_cです。
前日は@whitemage_yuさんの[「Discord用のbotを作ってみた」](https://qiita.com/whitemage_yu/items/ad5c4563614a6bdd146b)でした。それも是非ご覧ください！

## はじめに

最近、個人的にもアクションクラスを幅広く使いましたので、この記事の形でLukeさんの話を要約するのに良いタイミングでした。

## Actionsとは？
簡単に言うと、1つの責任しか持たない分割されたサービスクラスです。 つまり、通常、特定のタスクを実行するためのメソッドが1つだけ含まれており、再利用できます。 当然ですが、これは`Separation of Concerns` (関心事の分離)の原則とうまく守る方法です。

うんうん、コードを見たいですね。それでは、例を見ていきましょう！

## 例: ユーザ登録
例えば、簡単なサインアップフォームがあるとしましょう。 通常、コントローラーでリクエストを受信し、データを検証してから、コーディングスタイルに応じて、モデルを直接呼び出すか、以下のようにユーザーをデータベースに保存するためのリポジトリクラスを呼び出します。

```php
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`コマンドを作成します。 そちらのクラスは以下のような例となります。

```php
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`フォルダーが存在しない場合では、作成するが必要です。

```php
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`コマンドも修正しましょう。
すると、コントローラは次のようになります。

```php
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`コマンドクラス:

```php
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さんの「面倒な手作業をシェルスクリプトに任せる」です。
明日の記事でもお楽しみに！