Different Ways to Use Laravel Actions
Tags: laravel, php, ddd
Published: 2023-09-23 12:12:49

You can find more information about Laravel actions [here](https://laravel-beyond-crud.com) and [here](https://stitcher.io/blog/laravel-beyond-crud-03-actions). 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.
```php
final class OrderPolicy
{
    use HandlesAuthorization;

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

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

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

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.
```php
final class CreateOrderSecurityAction
{
    public function __invoke(int $userId, CreateOrderData $createOrderData): bool
    {
        //...
    }
}
```
* `CreateOrderValidationAction` class, responsible for data validation.
```php
final class CreateOrderValidationAction
{
    public function __invoke(CreateOrderData $createOrderData): array
    {
        $errors = [];

        //...

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

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.