DEV Community

Anton
Anton

Posted on

Different Ways to Use Laravel Actions

You can find more information about Laravel actions here and here. In this article, I aim to demonstrate two different approaches to using these actions, along with their respective pros and cons.

Let's consider a scenario where we have an Order model and a related OrderLine model, and we need to add an API endpoint to create an order.

The first classic approach may look like this:

  • OrderPolicy class with a create method where we perform security checks.
final class OrderPolicy
{
    use HandlesAuthorization;

    public function store(User $user, OrderStoreRequest $request): bool
    {
        //...
    }
}
Enter fullscreen mode Exit fullscreen mode
  • OrderStoreRequest class, responsible for invoking the policy and containing validation rules for input data.
final class OrderStoreRequest extends FormRequest
{
    public function authorize(): bool
    {
        return $this->user()->can('store', [
            Order::class,
            $this,
        ]);
    }

    public function rules(): array
    {
        return [
            //...
        ];
    }
}
Enter fullscreen mode Exit fullscreen mode
  • CreateOrderAction class, where we save the Order and OrderLine models in a single transaction.
final class CreateOrderAction
{
    public function __invoke(CreateOrderData $createOrderData): int
    {
        return DB::transaction(function () use ($createOrderData) {
            //...
        });
    }
}
Enter fullscreen mode Exit fullscreen mode
  • OrderController with a store method that combines all the previous classes.
final class OrderController extends Controller
{
    public function store(OrderStoreRequest $request, CreateOrderAction $createOrder): Response
    {
        $orderId = $createOrder(CreateOrderData::fromRequest($request));

        return $this->response()->created(content: $orderId);
    }
}
Enter fullscreen mode Exit fullscreen mode

This approach follows the traditional Laravel methodology and offers its own set of advantages:

  • You can refer to the Laravel documentation for detailed information.
  • Most Laravel developers are familiar with this approach.
  • Framework tools facilitate concise and readable code.
  • Actions can be reused in other parts of the application.

However, if you find the need to reuse not only data mutation and saving within a transaction but also security and validation checks, you might encounter some challenges.

Now, let's explore an alternative approach that may look like this:

  • CreateOrderSecurityAction class, containing all necessary security checks.
final class CreateOrderSecurityAction
{
    public function __invoke(int $userId, CreateOrderData $createOrderData): bool
    {
        //...
    }
}
Enter fullscreen mode Exit fullscreen mode
  • CreateOrderValidationAction class, responsible for data validation.
final class CreateOrderValidationAction
{
    public function __invoke(CreateOrderData $createOrderData): array
    {
        $errors = [];

        //...

        return $errors;
    }
}
Enter fullscreen mode Exit fullscreen mode
  • CreateOrderAction class, which combines the previous two classes and saves the Order and OrderLine models in a single transaction.
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) {
            //...
        });
    }
}
Enter fullscreen mode Exit fullscreen mode
  • OrderController, where we simply call the CreateOrderAction.
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);
    }
}
Enter fullscreen mode Exit fullscreen mode

The primary advantage of this approach is the ability to reuse actions in conjunction with all business requirements (security, validation, enforcing entity invariants). However, in this case, it may not be as straightforward to leverage the full power of the Laravel framework to streamline your code writing process.

Both approaches are valid choices, depending on the project's specific needs. If you don't require the reuse of entire business actions across multiple parts of your project, the classic Laravel approach is a straightforward choice. However, for larger and more complex projects, where you need to call the same entire business action, the second approach may be more suitable.

Regardless of your choice, having knowledge of both methods is valuable to make informed decisions.

Top comments (0)