Laravelアクションの異なる使用方法

Laravelのアクションに関する詳しい情報はこちらこちらで見ることができます。この記事では、アクションを使う二つの異なるアプローチと、それぞれの良い点と悪い点を示したいと思います。

例として、Order モデルと関連する OrderLine モデルがあり、オーダーを作成するAPIエンドポイントを追加する必要がある状況を考えてみましょう。

最初の古典的なアプローチは以下のようになるかもしれません。

  • セキュリティチェックを行う OrderPolicy クラスの作成メソッド。
final class OrderPolicy
{
    use HandlesAuthorization;

    public function store(User $user, OrderStoreRequest $request): bool
    {
        //...
    }
}
  • ポリシーを呼び出し、入力データのバリデーションルールを含む OrderStoreRequest クラス。
final class OrderStoreRequest extends FormRequest
{
    public function authorize(): bool
    {
        return $this->user()->can('store', [
            Order::class,
            $this,
        ]);
    }

    public function rules(): array
    {
        return [
            //...
        ];
    }
}
  • OrderOrderLine モデルを単一トランザクションで保存する CreateOrderAction クラス。
final class CreateOrderAction
{
    public function __invoke(CreateOrderData $createOrderData): int
    {
        return DB::transaction(function () use ($createOrderData) {
            //...
        });
    }
}
  • 上記のクラスすべてを組み合わせた OrderController のstoreメソッド。
final class OrderController extends Controller
{
    public function store(OrderStoreRequest $request, CreateOrderAction $createOrder): Response
    {
        $orderId = $createOrder(CreateOrderData::fromRequest($request));

        return $this->response()->created(content: $orderId);
    }
}

このアプローチは従来のLaravelの方法論に従っており、いくつかの利点を提供します。

  • Laravelのドキュメントを詳しく参照できます。
  • ほとんどのLaravel開発者はこのアプローチに慣れています。
  • フレームワークのツールは簡潔で読みやすいコードを促進します。
  • アクションはアプリケーションの他の部分で再利用することができます。

しかし、データの変更やトランザクション内での保存だけでなく、セキュリティやバリデーションチェックまでも再利用したい場合、いくつかの課題に直面するかもしれません。

次に、別のアプローチを見てみましょう。それは以下のような形になるかもしれません。

  • 必要なセキュリティチェックを含む CreateOrderSecurityAction クラス。
final class CreateOrderSecurityAction
{
    public function __invoke(int $userId, CreateOrderData $createOrderData): bool
    {
        //...
    }
}
  • データバリデーションを担当する CreateOrderValidationAction クラス。
final class CreateOrderValidationAction
{
    public function __invoke(CreateOrderData $createOrderData): array
    {
        $errors = [];

        //...

        return $errors;
    }
}
  • 上記二つのクラスを結合し、OrderOrderLine モデルを単一トランザクションで保存する CreateOrderAction クラス。
final readonly class CreateOrderAction
{
    public function __construct(
        private CreateOrderSecurityAction $createOrderSecurity,
        private CreateOrderValidationAction $createOrderValidation,
    ) {
    }

    /**
     * @throws OrderSecurityException
     * @throws OrderValidationException
     */
    public function __invoke(int $userId, CreateOrderData $createOrderData): int
    {
        if (! ($this->createOrderSecurity)($userId, $createOrderData)) {
            throw new OrderSecurityException();
        }

        $validationErrors = ($this->createOrderValidation)($createOrderData);
        if (! empty($validationErrors)) {
            throw new OrderValidationException($validationErrors);
        }

        return DB::transaction(function () use ($createOrderData) {
            //...
        });
    }
}
  • 簡単に CreateOrderAction を呼び出す OrderController
final class OrderController extends ApiController
{
    public function store(Request $request, CreateOrderAction $createOrder): ?Response
    {
        try {
            $orderId = $createOrder(auth()->id(), $CreateOrderData::fromRequest($request));
        } catch (OrderSecurityException) {
            $this->response()->errorForbidden();
            return null;
        } catch (OrderValidationException $exception) {
            return response()->json(['errors' => $exception->getErrors()], 400);
        }

        return $this->response()->created(content: $orderId);
    }
}

このアプローチの主な利点は、アクションをビジネス要件(セキュリティ、バリデーション、エンティティの不変条件を強制する)と共に再利用する能力です。しかし、この場合、Laravelフレームワークの全能力を駆使してコードを効率的に書くことはそれほど簡単ではないかもしれません。

どちらのアプローチも、プロジェクトの具体的なニーズに応じて妥当な選択です。もしプロジェクト全体で完全なビジネスアクションを再利用する必要がなければ、古典的なLaravelアプローチがストレートな選択です。しかし、同じ完全なビジネスアクションを呼び出す必要があるより大きく複雑なプロジェクトにおいては、二番目のアプローチの方が適しているかもしれません。

選択にかかわらず、どちらの方法も知っていることは有益で、より情報に基づいた決定を下すことができます。

こちらの記事はdev.toの良い記事を日本人向けに翻訳しています。
https://dev.to/ayacaste/different-ways-to-use-laravel-actions-138k