Builder

laravel-service-builder

Creates services for orchestrating operations and actions

Overview

The laravel-service-builder agent creates service classes and actions that encapsulate business logic. It follows the single responsibility principle, creating focused classes that handle one specific operation or orchestrate multiple steps.

Responsibilities

  • Service Classes - Multi-method services for related operations
  • Action Classes - Single-purpose invokable classes
  • Dependency Injection - Proper constructor injection
  • Interface Binding - Contract-based service registration
  • Error Handling - Custom exceptions and error states
  • Unit Tests - Tests with mocked dependencies

What It Creates

Component Location Purpose
Services app/Services/ Multi-method service classes
Actions app/Actions/ Single-purpose invokable classes
Contracts app/Contracts/ Service interfaces
Tests tests/Unit/ Service/action tests

Generated Service Example

<?php

namespace App\Services;

use App\Models\Order;
use App\Models\User;
use App\Contracts\PaymentGatewayInterface;
use App\Contracts\InventoryServiceInterface;
use App\Events\OrderPlaced;
use App\Exceptions\InsufficientStockException;
use Illuminate\Support\Facades\DB;

class OrderService
{
    public function __construct(
        private PaymentGatewayInterface $paymentGateway,
        private InventoryServiceInterface $inventory,
    ) {}

    /**
     * Create a new order and process payment.
     *
     * @throws InsufficientStockException
     * @throws PaymentFailedException
     */
    public function createOrder(User $user, array $items, string $paymentMethodId): Order
    {
        // Validate stock
        foreach ($items as $item) {
            if (!$this->inventory->hasStock($item['product_id'], $item['quantity'])) {
                throw new InsufficientStockException($item['product_id']);
            }
        }

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

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

            // Process payment
            $this->paymentGateway->charge(
                amount: $order->total,
                paymentMethodId: $paymentMethodId,
                metadata: ['order_id' => $order->id],
            );

            // Reserve inventory
            $this->inventory->reserve($items);

            $order->update(['status' => 'paid']);

            event(new OrderPlaced($order));

            return $order;
        });
    }

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

Generated Action Example

<?php

namespace App\Actions;

use App\Models\User;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\DB;

class CreateUserAction
{
    /**
     * Create a new user with profile.
     */
    public function __invoke(array $data): User
    {
        return DB::transaction(function () use ($data) {
            $user = User::create([
                'name' => $data['name'],
                'email' => $data['email'],
                'password' => Hash::make($data['password']),
            ]);

            if (isset($data['profile'])) {
                $user->profile()->create($data['profile']);
            }

            if (isset($data['roles'])) {
                $user->roles()->attach($data['roles']);
            }

            return $user->load(['profile', 'roles']);
        });
    }
}

// Usage in controller
public function store(Request $request, CreateUserAction $createUser)
{
    $user = $createUser($request->validated());

    return redirect()->route('users.show', $user);
}

Service vs Action Pattern

Use Service When Use Action When
Related operations grouped together Single operation with clear purpose
e.g., OrderService (create, cancel, refund) e.g., CreateOrderAction
Shared state/dependencies Stateless, composable
Complex domain with many operations Simple, focused operations

Testing Services

<?php

use App\Services\OrderService;
use App\Contracts\PaymentGatewayInterface;
use App\Contracts\InventoryServiceInterface;

it('creates an order with payment', function () {
    $paymentGateway = Mockery::mock(PaymentGatewayInterface::class);
    $paymentGateway->shouldReceive('charge')->once()->andReturn(true);

    $inventory = Mockery::mock(InventoryServiceInterface::class);
    $inventory->shouldReceive('hasStock')->andReturn(true);
    $inventory->shouldReceive('reserve')->once();

    $service = new OrderService($paymentGateway, $inventory);

    $user = User::factory()->create();
    $items = [['product_id' => 1, 'quantity' => 2, 'unit_price' => 1000]];

    $order = $service->createOrder($user, $items, 'pm_test');

    expect($order->status)->toBe('paid');
    expect($order->total)->toBe(2000);
});

Invoked By Commands

Called By

Guardrails

The service builder follows strict rules:

  • ALWAYS use constructor injection for dependencies
  • ALWAYS define return types and exceptions in docblocks
  • ALWAYS wrap multi-step operations in transactions
  • NEVER inject Request objects into services
  • NEVER create services with more than 7 methods

See Also