Refactor fat controller methods by moving multi-step transactions, file storage, and third-party integrations into invokable Action classes.
When a controller handles file uploads, payment transactions, database record creation, and email notifications in a single method, testing individual business steps becomes difficult and duplicates logic across CLI commands or API endpoints.
Moving domain logic into dedicated, invokable Action classes creates modular, reusable application layers.
Fat Controller (Anti-Pattern)
public function store(StoreOrderRequest $request)
{
// ❌ Fat controller mixing validation, billing, file storage, and events
$payment = Stripe::charges()->create([...]);
$order = Order::create([...]);
$invoicePdf = PDF::generate($order);
Storage::disk('s3')->put("invoices/{$order->id}.pdf", $invoicePdf);
Mail::to($request->user())->send(new OrderConfirmation($order));
return to_route('orders.show', $order);
}
Refactored Action Class
namespace App\Actions\Orders;
use App\Models\Order;
use App\Models\User;
use Illuminate\Support\Facades\DB;
class CreateOrderAction
{
public function __construct(
protected PaymentGateway $gateway,
protected InvoiceGenerator $invoices
) {}
public function execute(User $user, array $orderData, string $paymentToken): Order
{
return DB::transaction(function () use ($user, $orderData, $paymentToken) {
$charge = $this->gateway->charge($user, $orderData['total'], $paymentToken);
$order = $user->orders()->create([...$orderData, 'transaction_id' => $charge->id]);
$this->invoices->generateAndStore($order);
return $order;
});
}
}
Skinny Controller
public function store(StoreOrderRequest $request, CreateOrderAction $action)
{
$order = $action->execute(
$request->user(),
$request->validated(),
$request->input('payment_token')
);
return to_route('orders.show', $order)->with('success', 'Order created.');
}
Summary
- Action classes isolate core business rules from HTTP transport details.
- Reusable across web controllers, API endpoints, queue workers, and Artisan commands.
- Easily testable with isolated unit test mocks.
Related Tips
View all tips →Execute Multi-Step Business Workflows with the Pipeline Facade
Use Laravel's Pipeline facade to pass objects through sequential pipe classes, keeping complex order processing and data transformations modular.
Add Fluent when() and unless() Logic to Custom Classes with Conditionable
Use the Conditionable trait to equip your custom service classes, API clients, and query builders with fluent when() and unless() branching.