Clean architecture determines long-term maintainability. Laravel 10 enables modern patterns with full type declarations. At ZIRA Software, these patterns keep our large applications manageable and testable.
Project Structure
app/
├── Actions/ # Single-purpose action classes
├── DTOs/ # Data Transfer Objects
├── Enums/ # PHP 8.1 enums
├── Events/ # Event classes
├── Exceptions/ # Custom exceptions
├── Http/
│ ├── Controllers/ # Thin controllers
│ ├── Middleware/
│ ├── Requests/ # Form requests
│ └── Resources/ # API resources
├── Jobs/ # Queue jobs
├── Listeners/ # Event listeners
├── Models/ # Eloquent models
├── Policies/ # Authorization policies
├── Providers/
├── Repositories/ # Data access layer
├── Services/ # Business logic
└── Support/ # Helpers, traits
Thin Controllers
// app/Http/Controllers/OrderController.php
class OrderController extends Controller
{
public function __construct(
private CreateOrderAction $createOrder,
private GetUserOrdersAction $getUserOrders,
) {}
public function index(Request $request): OrderCollection
{
$orders = $this->getUserOrders->execute(
$request->user(),
OrderFiltersDTO::fromRequest($request)
);
return new OrderCollection($orders);
}
public function store(CreateOrderRequest $request): OrderResource
{
$order = $this->createOrder->execute(
CreateOrderDTO::fromRequest($request)
);
return new OrderResource($order);
}
}
Data Transfer Objects
// app/DTOs/CreateOrderDTO.php
readonly class CreateOrderDTO
{
public function __construct(
public int $userId,
public array $items,
public ?string $couponCode,
public Address $shippingAddress,
public Address $billingAddress,
) {}
public static function fromRequest(CreateOrderRequest $request): self
{
return new self(
userId: $request->user()->id,
items: $request->validated('items'),
couponCode: $request->validated('coupon_code'),
shippingAddress: Address::from($request->validated('shipping_address')),
billingAddress: Address::from($request->validated('billing_address')),
);
}
}
// app/DTOs/Address.php
readonly class Address
{
public function __construct(
public string $line1,
public ?string $line2,
public string $city,
public string $state,
public string $postalCode,
public string $country,
) {}
public static function from(array $data): self
{
return new self(
line1: $data['line1'],
line2: $data['line2'] ?? null,
city: $data['city'],
state: $data['state'],
postalCode: $data['postal_code'],
country: $data['country'],
);
}
}
Action Classes
// app/Actions/CreateOrderAction.php
class CreateOrderAction
{
public function __construct(
private OrderRepository $orders,
private InventoryService $inventory,
private PaymentService $payments,
) {}
public function execute(CreateOrderDTO $data): Order
{
return DB::transaction(function () use ($data) {
// Validate inventory
$this->inventory->validateAvailability($data->items);
// Create order
$order = $this->orders->create([
'user_id' => $data->userId,
'status' => OrderStatus::Pending,
'shipping_address' => $data->shippingAddress,
'billing_address' => $data->billingAddress,
]);
// Add items
foreach ($data->items as $item) {
$order->items()->create($item);
}
// Apply coupon
if ($data->couponCode) {
$this->applyCoupon($order, $data->couponCode);
}
// Calculate totals
$order->calculateTotals();
// Reserve inventory
$this->inventory->reserve($order);
event(new OrderCreated($order));
return $order->fresh(['items', 'user']);
});
}
}
Repository Pattern
// app/Repositories/OrderRepository.php
interface OrderRepositoryInterface
{
public function find(int $id): ?Order;
public function create(array $data): Order;
public function getForUser(User $user, OrderFiltersDTO $filters): LengthAwarePaginator;
}
class OrderRepository implements OrderRepositoryInterface
{
public function __construct(
private Order $model,
) {}
public function find(int $id): ?Order
{
return $this->model->find($id);
}
public function create(array $data): Order
{
return $this->model->create($data);
}
public function getForUser(User $user, OrderFiltersDTO $filters): LengthAwarePaginator
{
return $this->model
->where('user_id', $user->id)
->when($filters->status, fn($q, $s) => $q->where('status', $s))
->when($filters->dateFrom, fn($q, $d) => $q->where('created_at', '>=', $d))
->when($filters->dateTo, fn($q, $d) => $q->where('created_at', '<=', $d))
->with(['items.product'])
->orderBy($filters->sortBy, $filters->sortDirection)
->paginate($filters->perPage);
}
}
// Register in AppServiceProvider
$this->app->bind(OrderRepositoryInterface::class, OrderRepository::class);
Service Classes
// app/Services/InventoryService.php
class InventoryService
{
public function validateAvailability(array $items): void
{
foreach ($items as $item) {
$product = Product::find($item['product_id']);
if ($product->stock < $item['quantity']) {
throw new InsufficientInventoryException(
"Insufficient stock for {$product->name}"
);
}
}
}
public function reserve(Order $order): void
{
foreach ($order->items as $item) {
$item->product->decrement('stock', $item->quantity);
InventoryReservation::create([
'order_id' => $order->id,
'product_id' => $item->product_id,
'quantity' => $item->quantity,
]);
}
}
public function release(Order $order): void
{
foreach ($order->items as $item) {
$item->product->increment('stock', $item->quantity);
}
$order->inventoryReservations()->delete();
}
}
Enums for Status
// app/Enums/OrderStatus.php
enum OrderStatus: string
{
case Pending = 'pending';
case Processing = 'processing';
case Shipped = 'shipped';
case Delivered = 'delivered';
case Cancelled = 'cancelled';
public function label(): string
{
return match($this) {
self::Pending => 'Pending Payment',
self::Processing => 'Processing',
self::Shipped => 'Shipped',
self::Delivered => 'Delivered',
self::Cancelled => 'Cancelled',
};
}
public function canTransitionTo(self $status): bool
{
return match($this) {
self::Pending => in_array($status, [self::Processing, self::Cancelled]),
self::Processing => in_array($status, [self::Shipped, self::Cancelled]),
self::Shipped => $status === self::Delivered,
default => false,
};
}
}
Form Requests
// app/Http/Requests/CreateOrderRequest.php
class CreateOrderRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'items' => ['required', 'array', 'min:1'],
'items.*.product_id' => ['required', 'exists:products,id'],
'items.*.quantity' => ['required', 'integer', 'min:1'],
'coupon_code' => ['nullable', 'string', 'exists:coupons,code'],
'shipping_address' => ['required', 'array'],
'shipping_address.line1' => ['required', 'string'],
'shipping_address.city' => ['required', 'string'],
'shipping_address.state' => ['required', 'string'],
'shipping_address.postal_code' => ['required', 'string'],
'shipping_address.country' => ['required', 'string', 'size:2'],
];
}
}
Conclusion
Clean architecture with Laravel 10 ensures maintainable, testable applications. DTOs, actions, and repositories separate concerns while leveraging PHP 8.1 features.
Need architecture consulting? Contact ZIRA Software for Laravel best practices.