Layer Responsibilities
Clear layer responsibilities are key to architectural maintainability
Table of Contents
- Architecture Layer Overview
- Controller Layer
- Service Layer
- Domain Layer
- Infrastructure Layer
- Contract Layer
- Support Layer
Architecture Layer Overview
┌─────────────────────────────────────────────────────────┐
│ Controller Layer │
│ Thin Controller - Input/Output Only │
└────────────────────────┬────────────────────────────────┘
│
┌────────────────────────▼────────────────────────────────┐
│ Service Layer │
│ Use Case Orchestration - Transactions │
└────────┬────────────────────────────────────────────────┘
│
┌────────▼───────────────┐ ┌─────────────────────┐
│ Domain Layer │ │ Contract Layer │
│ Business Logic │◄───────│ Interface Defs │
│ Pure PHP │ │ │
└────────────────────────┘ └──────────┬──────────┘
▲ ▲
│ │
┌────────┴────────────────────────────────────┴──────────┐
│ Infrastructure Layer │
│ External Dependencies - DB - Services - Cache │
└─────────────────────────────────────────────────────────┘Controller Layer
Responsibilities
Single Responsibility: HTTP request entry and response exit
- Receive HTTP requests
- Validate input parameters
- Call Service layer
- Format response output
- No business logic
Characteristics
- Thin Controller
- Input/Output Only
- No direct database access
- No business rules
Code Example
php
<?php
declare(strict_types=1);
namespace app\controller\api\v1;
use app\service\order\CreateOrderService;
use app\service\order\CancelOrderService;
use app\service\order\GetOrderService;
use support\Request;
use support\Response;
final class OrderController
{
public function __construct(
private readonly CreateOrderService $createOrderService,
private readonly CancelOrderService $cancelOrderService,
private readonly GetOrderService $getOrderService
) {
}
public function create(Request $request): Response
{
// 1. Validate input
$validated = $this->validate($request, [
'items' => 'required|array|min:1',
'shipping_address' => 'required|array',
]);
// 2. Call service layer
$order = $this->createOrderService->handle(
userId: $request->user()->id,
items: $validated['items'],
shippingAddress: $validated['shipping_address']
);
// 3. Format response
return json([
'success' => true,
'data' => [
'id' => $order->id(),
'total' => $order->totalAmount()->toDollars(),
'status' => $order->status()->value,
],
], 201);
}
public function show(Request $request, int $id): Response
{
$order = $this->getOrderService->handle($id);
return json([
'success' => true,
'data' => $order->toArray(),
]);
}
public function cancel(Request $request, int $id): Response
{
$this->cancelOrderService->handle($id, $request->user()->id);
return json([
'success' => true,
'message' => 'Order cancelled successfully',
]);
}
}DO
- Validate input parameters
- Call Service layer methods
- Format JSON/HTML responses
- Handle HTTP status codes
- Convert exceptions to HTTP responses
DON'T
- Don't include business logic
- Don't directly access Model
- Don't directly access database
- Don't call external APIs
- Don't include complex calculations
Service Layer
Responsibilities
Core Responsibility: Use case orchestration and transaction management
- Orchestrate business flows
- Manage transaction boundaries
- Call Domain layer
- Call Infrastructure layer
- No business rules (rules belong in Domain layer)
Characteristics
- Use Case Driven
- Transaction Management
- Orchestration
- Depend on Interfaces
Code Example
php
<?php
declare(strict_types=1);
namespace app\service\order;
use app\contract\repository\OrderRepositoryInterface;
use app\contract\repository\UserRepositoryInterface;
use app\contract\repository\ProductRepositoryInterface;
use app\contract\gateway\PaymentGatewayInterface;
use app\contract\gateway\NotificationGatewayInterface;
use app\domain\order\entity\Order;
use app\domain\order\exception\InvalidOrderException;
use support\Db;
final class CreateOrderService
{
public function __construct(
private readonly OrderRepositoryInterface $orderRepository,
private readonly UserRepositoryInterface $userRepository,
private readonly ProductRepositoryInterface $productRepository,
private readonly PaymentGatewayInterface $paymentGateway,
private readonly NotificationGatewayInterface $notificationGateway
) {
}
public function handle(int $userId, array $items, array $shippingAddress): Order
{
// Transaction management
return Db::transaction(function () use ($userId, $items, $shippingAddress) {
// 1. Get user (call repository)
$user = $this->userRepository->findById($userId);
if ($user === null) {
throw new InvalidOrderException('User not found');
}
// 2. Validate inventory (call repository)
foreach ($items as $item) {
$product = $this->productRepository->findById($item['product_id']);
if ($product === null || $product->stock() < $item['quantity']) {
throw new InvalidOrderException('Insufficient inventory');
}
}
// 3. Create order entity (call domain layer)
$order = Order::create($userId, $items, $shippingAddress);
$order->calculateTotal();
// 4. Decrease inventory (call repository)
foreach ($items as $item) {
$product = $this->productRepository->findById($item['product_id']);
$product->decreaseStock($item['quantity']);
$this->productRepository->save($product);
}
// 5. Persist order (call repository)
$this->orderRepository->save($order);
// 6. Create payment (call external service)
$this->paymentGateway->createPaymentIntent($order);
// 7. Send notification (call external service)
$this->notificationGateway->sendOrderConfirmation($user, $order);
return $order;
});
}
}Transaction Management Example
php
<?php
declare(strict_types=1);
namespace app\service\order;
use app\contract\repository\OrderRepositoryInterface;
use app\domain\order\entity\Order;
use support\Db;
final class CancelOrderService
{
public function __construct(
private readonly OrderRepositoryInterface $orderRepository
) {
}
public function handle(int $orderId, int $userId): void
{
Db::transaction(function () use ($orderId, $userId) {
$order = $this->orderRepository->findById($orderId);
// Business rule validation (Domain layer responsibility)
$order->cancel();
// Persist
$this->orderRepository->save($order);
// Trigger domain events
foreach ($order->releaseEvents() as $event) {
event($event);
}
});
}
}DO
- Manage transaction boundaries
- Orchestrate multiple Domain objects
- Call multiple Repositories
- Call external services
- Trigger domain events
DON'T
- Don't include business rules (put in Domain layer)
- Don't directly access database (through Repository)
- Don't depend on concrete implementations (depend on interfaces)
- Don't include complex calculations (put in Domain layer)
Domain Layer
Responsibilities
Core Responsibility: Business logic and business rules
- Implement business rules
- Domain models
- Business calculations
- State transitions
- Pure PHP, no framework dependencies
Characteristics
- Pure Business Logic
- Framework Agnostic
- Independently Testable
- No Database Access
Entity Example
php
<?php
declare(strict_types=1);
namespace app\domain\order\entity;
use app\domain\order\vo\Money;
use app\domain\order\enum\OrderStatus;
use app\domain\order\event\OrderCreated;
use app\domain\order\event\OrderCancelled;
use app\domain\order\exception\InvalidOrderException;
final class Order
{
private array $domainEvents = [];
private function __construct(
private readonly int $id,
private readonly int $userId,
private array $items,
private Money $totalAmount,
private OrderStatus $status,
private readonly \DateTimeImmutable $createdAt
) {
}
public static function create(int $userId, array $items, array $shippingAddress): self
{
// Business rule: Order must have at least one item
if (empty($items)) {
throw new InvalidOrderException('Order must have at least one item');
}
// Business rule: Each item quantity must be greater than 0
foreach ($items as $item) {
if ($item['quantity'] <= 0) {
throw new InvalidOrderException('Item quantity must be greater than 0');
}
}
$order = new self(
id: 0,
userId: $userId,
items: $items,
totalAmount: Money::zero(),
status: OrderStatus::Pending,
createdAt: new \DateTimeImmutable()
);
$order->recordEvent(new OrderCreated($order));
return $order;
}
public function calculateTotal(): void
{
// Business calculation: Calculate order total amount
$total = array_reduce(
$this->items,
fn (Money $carry, array $item) => $carry->add(
Money::fromCents($item['price'] * $item['quantity'])
),
Money::zero()
);
$this->totalAmount = $total;
}
public function cancel(): void
{
// Business rule: Only pending and paid orders can be cancelled
if (!$this->status->canBeCancelled()) {
throw new InvalidOrderException(
"Order cannot be cancelled in status: {$this->status->value}"
);
}
// State transition
$this->status = OrderStatus::Cancelled;
$this->recordEvent(new OrderCancelled($this));
}
public function ship(): void
{
// Business rule: Only paid orders can be shipped
if ($this->status !== OrderStatus::Paid) {
throw new InvalidOrderException('Only paid orders can be shipped');
}
$this->status = OrderStatus::Shipped;
}
// Getters
public function id(): int
{
return $this->id;
}
public function userId(): int
{
return $this->userId;
}
public function totalAmount(): Money
{
return $this->totalAmount;
}
public function status(): OrderStatus
{
return $this->status;
}
private function recordEvent(object $event): void
{
$this->domainEvents[] = $event;
}
public function releaseEvents(): array
{
$events = $this->domainEvents;
$this->domainEvents = [];
return $events;
}
}Enum Example (OrderStatus)
enum/ - Fixed option sets like status, type, method vo/ - Value objects needing validation, math, or multiple properties
php
<?php
declare(strict_types=1);
namespace app\domain\order\enum;
enum OrderStatus: string
{
case Pending = 'pending';
case Paid = 'paid';
case Shipped = 'shipped';
case Cancelled = 'cancelled';
public function isPending(): bool
{
return $this === self::Pending;
}
public function canBeCancelled(): bool
{
return in_array($this, [self::Pending, self::Paid]);
}
public function canBeShipped(): bool
{
return $this === self::Paid;
}
}Value Object Example (Money)
php
<?php
declare(strict_types=1);
namespace app\domain\order\vo;
final class Money
{
private function __construct(
private readonly int $cents
) {
if ($cents < 0) {
throw new \InvalidArgumentException('Money cannot be negative');
}
}
public static function fromCents(int $cents): self
{
return new self($cents);
}
public static function fromDollars(float $dollars): self
{
return new self((int) round($dollars * 100));
}
public static function zero(): self
{
return new self(0);
}
public function add(self $other): self
{
return new self($this->cents + $other->cents);
}
public function subtract(self $other): self
{
return new self($this->cents - $other->cents);
}
public function multiply(int $factor): self
{
return new self($this->cents * $factor);
}
public function toCents(): int
{
return $this->cents;
}
public function toDollars(): float
{
return $this->cents / 100;
}
public function equals(self $other): bool
{
return $this->cents === $other->cents;
}
public function greaterThan(self $other): bool
{
return $this->cents > $other->cents;
}
}DO
- Implement business rules
- Business calculations and validation
- State transition logic
- Trigger domain events
- Use enums and value objects
DON'T
- Don't depend on framework classes
- Don't access database
- Don't call external APIs
- Don't depend on Infrastructure
- Don't use static methods to access global state
Infrastructure Layer
Responsibilities
Core Responsibility: Concrete implementations of external dependencies
- Implement Repository interfaces
- Implement Gateway interfaces
- Database access
- Cache operations
- Third-party service integration
Characteristics
- Implement Interfaces
- Depend on External Systems
- Data Transformation
- Technical Details
Repository Implementation Example
php
<?php
declare(strict_types=1);
namespace app\infrastructure\repository\eloquent;
use app\contract\repository\OrderRepositoryInterface;
use app\domain\order\entity\Order;
use app\domain\order\vo\Money;
use app\domain\order\enum\OrderStatus;
use app\model\eloquent\Order as OrderModel;
final class EloquentOrderRepository implements OrderRepositoryInterface
{
public function findById(int $id): ?Order
{
$model = OrderModel::find($id);
if ($model === null) {
return null;
}
return $this->toDomain($model);
}
public function findByUserId(int $userId): array
{
$models = OrderModel::where('user_id', $userId)->get();
return $models->map(fn ($model) => $this->toDomain($model))->all();
}
public function save(Order $order): void
{
$model = OrderModel::findOrNew($order->id());
$model->user_id = $order->userId();
$model->total_amount = $order->totalAmount()->toDollars();
$model->status = $order->status()->value;
$model->save();
// Trigger domain events
foreach ($order->releaseEvents() as $event) {
event($event);
}
}
public function delete(Order $order): void
{
OrderModel::destroy($order->id());
}
private function toDomain(OrderModel $model): Order
{
// Reconstruct domain entity from database model
return Order::reconstitute(
id: $model->id,
userId: $model->user_id,
items: json_decode($model->items, true),
totalAmount: Money::fromDollars($model->total_amount),
status: OrderStatus::from($model->status),
createdAt: new \DateTimeImmutable($model->created_at)
);
}
}Gateway Implementation Example
php
<?php
declare(strict_types=1);
namespace app\infrastructure\gateway\payment;
use app\contract\gateway\PaymentGatewayInterface;
use app\domain\order\entity\Order;
final class StripePaymentGateway implements PaymentGatewayInterface
{
public function __construct(
private readonly string $apiKey
) {
}
public function createPaymentIntent(Order $order): string
{
// Call Stripe API
$stripe = new \Stripe\StripeClient($this->apiKey);
$paymentIntent = $stripe->paymentIntents->create([
'amount' => $order->totalAmount()->toCents(),
'currency' => 'usd',
'metadata' => [
'order_id' => $order->id(),
],
]);
return $paymentIntent->id;
}
public function charge(Order $order, string $paymentMethodId): bool
{
$stripe = new \Stripe\StripeClient($this->apiKey);
try {
$stripe->paymentIntents->confirm($paymentMethodId);
return true;
} catch (\Stripe\Exception\CardException $e) {
return false;
}
}
}DO
- Implement Contract interfaces
- Access database
- Call third-party APIs
- Data format conversion
- Cache operations
DON'T
- Don't include business logic
- Don't be called directly by Controller
- Don't expose technical details to Domain
Contract Layer
Responsibilities
Core Responsibility: Define interface contracts
- Define Repository interfaces
- Define Gateway interfaces
- Define Service interfaces
- Only interfaces, no implementations
Characteristics
- Pure Interfaces
- Define Contracts
- Dependency Inversion
Repository Interface Example
php
<?php
declare(strict_types=1);
namespace app\contract\repository;
use app\domain\order\entity\Order;
interface OrderRepositoryInterface
{
public function findById(int $id): ?Order;
public function findByUserId(int $userId): array;
public function save(Order $order): void;
public function delete(Order $order): void;
public function nextIdentity(): int;
}Gateway Interface Example
php
<?php
declare(strict_types=1);
namespace app\contract\gateway;
use app\domain\order\entity\Order;
interface PaymentGatewayInterface
{
public function createPaymentIntent(Order $order): string;
public function charge(Order $order, string $paymentMethodId): bool;
public function refund(Order $order): bool;
}DO
- Define clear interface methods
- Use type hints
- Return Domain objects
- Documentation comments
DON'T
- Don't include implementation code
- Don't depend on concrete classes
- Don't expose technical details
Support Layer
Responsibilities
Core Responsibility: Common utilities and helper functions
- Helper functions
- Reusable Traits
- Custom exceptions
- Common utility classes
Characteristics
- Truly Generic
- No Business Logic
- Reusable
Helper Function Example
php
<?php
declare(strict_types=1);
namespace app\support\helper;
function array_get(array $array, string $key, mixed $default = null): mixed
{
return $array[$key] ?? $default;
}
function money_format(int $cents): string
{
return '$' . number_format($cents / 100, 2);
}Trait Example
php
<?php
declare(strict_types=1);
namespace app\support\trait;
trait HasTimestamps
{
private \DateTimeImmutable $createdAt;
private \DateTimeImmutable $updatedAt;
public function createdAt(): \DateTimeImmutable
{
return $this->createdAt;
}
public function updatedAt(): \DateTimeImmutable
{
return $this->updatedAt;
}
protected function initializeTimestamps(): void
{
$now = new \DateTimeImmutable();
$this->createdAt = $now;
$this->updatedAt = $now;
}
protected function touch(): void
{
$this->updatedAt = new \DateTimeImmutable();
}
}Custom Exception Example
php
<?php
declare(strict_types=1);
namespace app\support\exception;
class BusinessException extends \Exception
{
public function __construct(
string $message,
private readonly array $context = []
) {
parent::__construct($message);
}
public function context(): array
{
return $this->context;
}
}DO
- Truly generic utilities
- Helper functions without business logic
- Reusable Traits
- Base exception classes
DON'T
- Don't use as a dumping ground
- Don't include business logic
- Don't depend on specific modules
Layer Interaction Example
Complete Flow: Create Order
php
// 1. Controller Layer - Receive request
namespace app\controller\api\v1;
final class OrderController
{
public function create(Request $request): Response
{
$order = $this->createOrderService->handle(...);
return json(['data' => $order]);
}
}
// 2. Service Layer - Orchestrate flow
namespace app\service\order;
final class CreateOrderService
{
public function handle(int $userId, array $items, array $shippingAddress): Order
{
return Db::transaction(function () use ($userId, $items, $shippingAddress) {
$order = Order::create($userId, $items, $shippingAddress);
$order->calculateTotal();
$this->orderRepository->save($order);
return $order;
});
}
}
// 3. Domain Layer - Business logic
namespace app\domain\order\entity;
final class Order
{
public static function create(int $userId, array $items, array $shippingAddress): self
{
if (empty($items)) {
throw new InvalidOrderException('Order must have at least one item');
}
return new self(...);
}
public function calculateTotal(): void
{
$this->totalAmount = array_reduce(...);
}
}
// 4. Contract Layer - Interface definition
namespace app\contract\repository;
interface OrderRepositoryInterface
{
public function save(Order $order): void;
}
// 5. Infrastructure Layer - Persistence
namespace app\infrastructure\repository\eloquent;
final class EloquentOrderRepository implements OrderRepositoryInterface
{
public function save(Order $order): void
{
$model = OrderModel::findOrNew($order->id());
$model->user_id = $order->userId();
$model->save();
}
}Best Practices Summary
Controller Layer
- Keep thin controllers
- Input/output only
- No business logic
Service Layer
- Manage transaction boundaries
- Orchestrate business flows
- Depend on interfaces, not implementations
Domain Layer
- Pure business logic
- Framework agnostic
- Independently testable
- Use Enum for fixed states, Value Object for complex values
Infrastructure Layer
- Implement interfaces
- Handle technical details
- Data transformation
Contract Layer
- Define clear interfaces
- Dependency inversion principle
Support Layer
- Truly generic utilities
- No business logic