Produced by Fourier

LaravelのActionと使い方

Hirayama Hirayama カレンダーアイコン 2022.07.27

Laravel 8 くらいから、Laravelの公式パッケージである Fortify をインストールした際、 Actions ディレクトリが生成されるようになりました。( Laravel Actions ではないです)

Laravel 公式ドキュメントにも載っていないこの謎の Action の役割や使われ方を Laravel のMVCやその問題点を絡めて説明し、自分でActionを作るときの考え方について解説します。

Laravelのアーキテクチャについて

まずは、 Laravel のアーキテクチャについて軽く説明します。

Laravel はMVCパターンと呼ばれるアーキテクチャを採用しており、Model、View、Controllerに処理を分離し、ModelにDBの操作やデータの格納、Viewに画面表示の制御、Controllerにサーバー内部の処理を記述します。

MVCの問題点とService層の追加

単純なアプリケーションであればMVCでも問題ないのですが、複雑なアプリケーションになると、ModelやControllerに大量の処理が記述されたり、共通化されてない部分が出てきてくるため、保守性や品質の担保が難しくなってきます。

そこでよく取られる解決策として、Service層の追加があります。

複数のControllerに記述されるような処理をService層のクラスとして定義し、共通化することで上記の問題点を解決することができます。

Actionについて

前段としてMVCの問題点とよく取られる解決策を解説しましたが、 Fortify では Action の追加という若干異なるアプローチでMVCの問題点を解決しており、その実装方法にも工夫があります。

工夫1: Contractの使用

Fortify ではリンク先のファイル一覧のように、全てのActionにContractファイルを用意しています。

https://github.com/laravel/fortify/tree/1.x/src/Contracts

また、 FortifyServiceProvider にて、Contractと実装を結合しています。

use App\Actions\Fortify\CreateNewUser;
use App\Actions\Fortify\ResetUserPassword;
use App\Actions\Fortify\UpdateUserPassword;
use App\Actions\Fortify\UpdateUserProfileInformation;
use App\Http\Responses\LoginResponse;
use App\Http\Responses\RegisterResponse;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\ServiceProvider;
use Laravel\Fortify\Contracts\LoginResponse as LoginResponseContract;
use Laravel\Fortify\Contracts\RegisterResponse as RegisterResponseContract;
use Laravel\Fortify\Fortify;

class FortifyServiceProvider extends ServiceProvider
{
    public function register() { }

    public function boot()
    {
        Fortify::createUsersUsing(CreateNewUser::class);
        Fortify::updateUserProfileInformationUsing(UpdateUserProfileInformation::class);
        Fortify::updateUserPasswordsUsing(UpdateUserPassword::class);
        Fortify::resetUserPasswordsUsing(ResetUserPassword::class);

        Fortify::viewPrefix('auth.');

        RateLimiter::for('login', function (Request $request) {
            $email = (string) $request->email;

            return Limit::perMinute(5)->by($email.$request->ip());
        });

        RateLimiter::for('two-factor', function (Request $request) {
            return Limit::perMinute(5)->by($request->session()->get('login.id'));
        });

        $this->app->singleton(LoginResponseContract::class, LoginResponse::class);
        $this->app->singleton(RegisterResponseContract::class, RegisterResponse::class);
    }
}

そして、Actionを使用するときは、ControllerのメソッドにDIして使用しています。

https://github.com/laravel/fortify/blob/9dee9cf87c6469af1c3ab001e5a83c080efa306c/src/Http/Controllers/RegisteredUserController.php#L51

こうすることによって、基本的な処理の流れをライブラリのControllerに記述しつつも、利用者がActionを書き換えることで、処理の内容を自由にカスタマイズできるようになっています。

工夫2: インターフェースに依存しない設計

Fortify のContractを見ると、いずれも array 等の単純な型のデータを受け取り、 Responsiable インターフェースを実装したオブジェクトを返すようになっています。

特定のControllerに依存する(Requestを引数に受け取る)ようになっていないため、どのControllerから呼び出し可能なように作られています。

自分でActionを作る場合

自分で作る場合は前節で説明した工夫点を踏襲するように作れば良いとは思いますが、 Fortify のActionはライブラリとして作られているため、アプリケーションを作るときとは視点が異なります。

そこで、前節の工夫点も交えながら、自分で作る場合に気をつけるポイントを以下のように考えてみました。

ポイント1: Contractは作らない

ライブラリとして作る際はContractを作ることで利用者の実装の自由度を高められますが、アプリケーションを作成しているときは実装を一つしか作らない場合がほとんどです。そのため、Contractを作っても1つしか継承するクラスが無いため、無駄に時間を食うだけの存在になってしまいます。

Laravel のService Containerは 設定なしの依存解決 を行ってくれるので、Contractを作らず、Service Containerも登録せずに、いきなりControllerのメソッドにDIしても問題ありません。

ポイント2: なるべく小さく作る

Actionの汎用性を高めるため、なるべく一つの物事を実行するように作ったほうがいいです。

例えば、あるエンドポイントではA→B→Cの順に処理を実行するのであれば、A、B、CそれぞれをActionとして作ることで、後々A’→B→Cといった処理を別のエンドポイントで実装する必要が出てきても、A’を作るだけで簡単に対応できるようになります。

一連の処理をActionに切り分けるやり方には正解はなく、区切りすぎてもわかりにくくなったりするので判断が難しいところですが、最初はある程度まとまった処理をAction(≒Service)として作り、必要に応じて段々と分けていくやり方が無難かなと思います。

ポイント3: 疎結合に作る

Fortify のActionは特定のControllerに依存しないように作られていましたが、もう一歩進んでActionの戻り値もプリミティブな型かEloquent Modelにすることで、以下のようなメリットが生まれます。

メリット1: Actionを連続して実行できる

これはWebエンドポイントとAPIエンドポイントを両方実装するケースを考えてみた場合、APIエンドポイントで使っているActionをWebエンドポイントでも使いたくなると思います。

Webエンドポイントでは複数のAPIエンドポイントに相当するような処理を一つのエンドポイントで処理しなければいけない場面も多々あるため、Actionが Responsible を返すと複数のActionを実行した際のデータの結合がやりづらくなります。

そのため、Actionはデータを返し、Controllerで自由にレスポンスを作ることができるようにすることで、より汎用性を高めることができます。

メリット2: 完全にインターフェースから分離できる

Fortify のActionは Responsible を返す都合上、API・Webエンドポイントでの利用を想定しています。

認証処理なのでその実装で問題ありませんが、Actionがデータを返却するようにすることで、ArtisanコンソールやEvent Listener等から呼び出すことも可能になるため、インターフェースに依存しない再利用しやすいActionになります。

具体例

以下に、自分がActionを実際に実装したときのコードを具体例として載せます。

Index Action

このActionは、 Post モデルの一覧をページネーションとして取得する処理をします。

namespace App\Actions\Post;

use App\Models\Post;
use Illuminate\Contracts\Pagination\Paginator;

class IndexAction
{
    public function __invoke(
        ?int    $current_page,
        ?int    $per_page,
        ?string $sort_by,
        ?string $direction,
    ): Paginator
    {
        $current_page ??= 1;
        $per_page     ??= 20;
        $sort_by      ??= 'id';
        $direction    ??= 'asc';

        return Post::query()
                   ->orderBy($sort_by, $direction)
                   ->paginate($per_page, page: $current_page);
    }
}

Store Action

このActionでは、 Post モデルを新規保存する処理をします。

保存直後にEventを発生させることで、このStoreに付随して発生する処理を実行させることができます。

<?php

namespace App\Actions\Post;

use App\Events\Post\PostStored;
use App\Models\post;

class StoreAction
{
    public function __invoke(User $user, string $body): Post
    {
        $post = new Post(compact('body'));

                $user->posts()->save($post);
        $post->refresh();

        event(new PostStored($post));

        return $opinion;
    }
}

まとめ

本記事では Action についてどういったものなのか、どういうふうに実装すれば良いのかを解説しました。

公式ドキュメントにも載ってないので、絶対にActionを書かなければいけないという代物ではないですが、公式ライブラリの書き方に合わせたほうが何かと都合がいいと思うので、私は今後 Laravel で実装するときは、Serviceではなく、Actionで実装していこうかなと思いました。

Hirayama

Hirayama slash forward icon Engineer

業務では主にPHPやTypeScriptを使用したバックエンドアプリケーションやデスクトップアプリケーションの開発をしています。趣味は登山。

関連記事