SYS://VISION.ACTIVE
VIEWPORT.01
LAT 28.0222° N
SIGNAL.NOMINAL
VISION Loading
Back to Blog

The Actions Pattern in Laravel: Clean, Testable Business Logic

Shane Barron

Shane Barron

Laravel Developer & AI Integration Specialist

Beyond Fat Controllers

As applications grow, controllers become bloated with business logic. Services help, but often become dumping grounds themselves. Actions offer a more focused approach: one class, one action, one responsibility.

The Action Pattern

class CreateOrder
{
    public function __construct(
        private PaymentGateway $payments,
        private InventoryService $inventory,
        private Notifier $notifier
    ) {}

    public function execute(User $user, array $items): Order
    {
        // Validate inventory
        $this->inventory->reserve($items);

        // Create order
        $order = DB::transaction(function () use ($user, $items) {
            $order = Order::create([
                'user_id' => $user->id,
                'total' => $this->calculateTotal($items),
            ]);

            foreach ($items as $item) {
                $order->items()->create($item);
            }

            return $order;
        });

        // Process payment
        $this->payments->charge($user, $order->total);

        // Send notifications
        $this->notifier->orderCreated($order);

        return $order;
    }

    private function calculateTotal(array $items): float
    {
        return collect($items)->sum(fn ($item) =>
            $item['price'] * $item['quantity']
        );
    }
}

Using Actions

class OrderController extends Controller
{
    public function store(
        OrderRequest $request,
        CreateOrder $createOrder
    ): RedirectResponse {
        $order = $createOrder->execute(
            $request->user(),
            $request->validated()['items']
        );

        return redirect()->route('orders.show', $order);
    }
}

Testing Actions

class CreateOrderTest extends TestCase
{
    public function test_creates_order_with_items(): void
    {
        $user = User::factory()->create();
        $product = Product::factory()->create(['price' => 10]);

        $action = app(CreateOrder::class);
        $order = $action->execute($user, [
            ['product_id' => $product->id, 'quantity' => 2, 'price' => 10],
        ]);

        $this->assertDatabaseHas('orders', [
            'user_id' => $user->id,
            'total' => 20,
        ]);
        $this->assertCount(1, $order->items);
    }

    public function test_charges_payment(): void
    {
        $mock = $this->mock(PaymentGateway::class);
        $mock->shouldReceive('charge')->once()->with(
            Mockery::type(User::class),
            100
        );

        $action = app(CreateOrder::class);
        $action->execute($this->user, $this->items);
    }
}

Actions as Jobs

class ProcessRefund implements ShouldQueue
{
    use AsAction;

    public function handle(Order $order): void
    {
        $this->refundPayment($order);
        $this->restoreInventory($order);
        $this->notifyCustomer($order);
    }

    public function asJob(Order $order): void
    {
        $this->handle($order);
    }
}

// Dispatch as job
ProcessRefund::dispatch($order);

When to Use Actions

  • Complex business operations with multiple steps
  • Logic that needs to be reused across controllers
  • Operations that require transaction handling
  • Logic that benefits from isolated testing

Conclusion

Actions provide a clean, focused approach to organizing business logic. One class, one responsibility, easy to test, easy to understand. Start using them for your next complex feature.

Share this article
Shane Barron

Shane Barron

Strategic Technology Architect with 40 years of experience building production systems. Specializing in Laravel, AI integration, and enterprise architecture.

Need Help With Your Project?

I respond to all inquiries within 24 hours. Let's discuss how I can help build your production-ready system.

Get In Touch