Laravel 12 中实践领域驱动设计(DDD)的完整指南
在 Laravel 12 中实施领域驱动设计可以帮助您创建更可维护、可扩展且业务专注的应用程序。本指南将带您了解核心概念、实现步骤和最佳实践。
项目结构与核心概念
目录结构
src/
├── Domain/ # 核心业务逻辑
│ ├── Models/ # 聚合根和实体
│ ├── ValueObjects/ # 值对象
│ ├── Events/ # 领域事件
│ ├── Repositories/ # 仓储接口
│ └── Services/ # 领域服务
├── Application/ # 用例协调
│ ├── Services/ # 应用服务
│ ├── Commands/ # CQRS命令
│ ├── Queries/ # CQRS查询
│ └── DTOs/ # 数据传输对象
├── Infrastructure/ # 技术实现
│ ├── Repositories/ # 仓储实现
│ ├── Providers/ # 服务绑定
│ └── EventListeners/ # 事件监听器
└── UI/ # 用户界面
├── Http/ # Web接口
│ ├── Controllers/
│ ├── Requests/
│ └── Resources/
└── Console/ # CLI命令
核心DDD组件
- 聚合根(Aggregate Roots):业务边界,封装业务规则
- 实体(Entities):具有唯一标识的对象
- 值对象(Value Objects):不可变的属性容器
- 领域事件(Domain Events):业务状态变化通知
- 仓储(Repositories):数据访问抽象
- 领域服务(Domain Services):不自然属于聚合的逻辑
实现步骤
1. 配置项目自动加载
修改 composer.json
:
"autoload": {
"psr-4": {
"App\\": "app/",
"Domain\\": "src/Domain/",
"Application\\": "src/Application/",
"Infrastructure\\": "src/Infrastructure/",
"UI\\": "src/UI/"
}
}
运行 composer dump-autoload
2. 创建领域模型(聚合根)
// src/Domain/Models/Order.php
namespace Domain\Models;
use Domain\Events\OrderCreated;
use Domain\ValueObjects\OrderId;
use Domain\ValueObjects\CustomerId;
use Domain\ValueObjects\Money;
use Domain\Exceptions\DomainException;
class Order {
private array $items = [];
private \DateTimeImmutable $createdAt;
public function __construct(
private OrderId $id,
private CustomerId $customerId,
private OrderStatus $status = OrderStatus::PENDING
) {
$this->createdAt = new \DateTimeImmutable();
}
public function addItem(ProductId $productId, int $quantity, Money $price): void {
if ($quantity <= 0) {
throw new DomainException('Quantity must be positive');
}
$this->items[] = new OrderItem(
$productId,
$quantity,
$price
);
}
public function confirm(): void {
if (count($this->items) === 0) {
throw new DomainException('Cannot confirm empty order');
}
$this->status = OrderStatus::CONFIRMED;
$this->recordEvent(new OrderConfirmed($this->id));
}
// 获取聚合根ID
public function getId(): OrderId {
return $this->id;
}
}
3. 创建值对象
// src/Domain/ValueObjects/Money.php
namespace Domain\ValueObjects;
class Money {
public function __construct(
private float $amount,
private string $currency = 'USD'
) {
if ($amount < 0) {
throw new \InvalidArgumentException('Money amount cannot be negative');
}
}
public function add(Money $other): Money {
if ($this->currency !== $other->currency) {
throw new \InvalidArgumentException('Currencies must match');
}
return new Money($this->amount + $other->amount, $this->currency);
}
public function equals(Money $other): bool {
return $this->amount === $other->amount && $this->currency === $other->currency;
}
public function __toString(): string {
return sprintf('%s %.2f', $this->currency, $this->amount);
}
}
4. 定义仓储接口
// src/Domain/Repositories/OrderRepositoryInterface.php
namespace Domain\Repositories;
use Domain\Models\Order;
use Domain\ValueObjects\OrderId;
interface OrderRepositoryInterface {
public function save(Order $order): void;
public function findById(OrderId $id): ?Order;
public function nextIdentity(): OrderId;
}
5. 实现基础设施仓储
// src/Infrastructure/Repositories/EloquentOrderRepository.php
namespace Infrastructure\Repositories;
use Domain\Models\Order;
use Domain\Repositories\OrderRepositoryInterface;
use Domain\ValueObjects\OrderId;
use App\Models\EloquentOrder as OrderModel;
class EloquentOrderRepository implements OrderRepositoryInterface {
public function save(Order $order): void {
OrderModel::updateOrCreate(
['uuid' => $order->getId()->toString()],
$this->mapToEloquent($order)
);
}
private function mapToEloquent(Order $order): array {
return [
'customer_id' => $order->getCustomerId()->toString(),
'status' => $order->getStatus()->value,
'items' => array_map(fn($item) => [
'product_id' => $item->getProductId()->toString(),
'quantity' => $item->getQuantity(),
'price' => $item->getPrice()->getAmount(),
'currency' => $item->getPrice()->getCurrency()
], $order->getItems())
];
}
public function findById(OrderId $id): ?Order {
$model = OrderModel::where('uuid', $id->toString())->first();
return $model ? $this->mapToDomain($model) : null;
}
public function nextIdentity(): OrderId {
return OrderId::generate();
}
}
6. 创建应用服务
// src/Application/Services/OrderService.php
namespace Application\Services;
use Domain\Models\Order;
use Domain\Repositories\OrderRepositoryInterface;
use Domain\ValueObjects\CustomerId;
use Application\DTOs\CreateOrderDTO;
use Illuminate\Events\Dispatcher;
class OrderService {
public function __construct(
private OrderRepositoryInterface $orderRepository,
private Dispatcher $dispatcher
) {}
public function createOrder(CreateOrderDTO $dto): Order {
$order = new Order(
$this->orderRepository->nextIdentity(),
new CustomerId($dto->customerId)
);
foreach ($dto->items as $item) {
$order->addItem(
$item->productId,
$item->quantity,
new Money($item->price, $item->currency)
);
}
$this->orderRepository->save($order);
$this->dispatcher->dispatch(new OrderCreated($order->getId()));
return $order;
}
}
7. 创建控制器
// src/UI/Http/Controllers/OrderController.php
namespace UI\Http\Controllers;
use Application\Services\OrderService;
use UI\Http\Requests\CreateOrderRequest;
use UI\Http\Resources\OrderResource;
use Illuminate\Http\JsonResponse;
class OrderController extends Controller {
public function __construct(private OrderService $orderService) {}
public function store(CreateOrderRequest $request): JsonResponse {
$order = $this->orderService->createOrder($request->toDTO());
return response()->json([
'data' => new OrderResource($order),
'message' => 'Order created successfully'
], 201);
}
}
8. 配置服务提供者
// src/Infrastructure/Providers/DomainServiceProvider.php
namespace Infrastructure\Providers;
use Illuminate\Support\ServiceProvider;
use Domain\Repositories\OrderRepositoryInterface;
use Infrastructure\Repositories\EloquentOrderRepository;
class DomainServiceProvider extends ServiceProvider {
public function register() {
$this->app->bind(
OrderRepositoryInterface::class,
EloquentOrderRepository::class
);
}
}
在 config/app.php
中注册提供者:
'providers' => [
// ...
Infrastructure\Providers\DomainServiceProvider::class,
],
领域事件处理
1. 定义领域事件
// src/Domain/Events/OrderCreated.php
namespace Domain\Events;
use Domain\ValueObjects\OrderId;
class OrderCreated {
public function __construct(
public readonly OrderId $orderId
) {}
}
2. 创建事件监听器
// src/Infrastructure/EventListeners/SendOrderConfirmation.php
namespace Infrastructure\EventListeners;
use Domain\Events\OrderCreated;
use App\Services\NotificationService;
class SendOrderConfirmation {
public function __construct(private NotificationService $notifier) {}
public function handle(OrderCreated $event): void {
$this->notifier->sendOrderConfirmation($event->orderId);
}
}
3. 注册事件监听
在 EventServiceProvider
中:
protected $listen = [
OrderCreated::class => [
SendOrderConfirmation::class,
UpdateInventory::class
]
];
CQRS实现
命令端(写操作)
// src/Application/Commands/CancelOrderHandler.php
namespace Application\Commands;
use Domain\Repositories\OrderRepositoryInterface;
use Domain\ValueObjects\OrderId;
class CancelOrderHandler {
public function __construct(private OrderRepositoryInterface $orderRepo) {}
public function handle(CancelOrderCommand $command): void {
$order = $this->orderRepo->findById(
OrderId::fromString($command->orderId)
);
if (!$order) {
throw new \Exception('Order not found');
}
$order->cancel();
$this->orderRepo->save($order);
}
}
查询端(读操作)
// src/Application/Queries/GetOrderStatusQuery.php
namespace Application\Queries;
use App\Models\OrderStatusViewModel;
class GetOrderStatusQuery {
public function execute(string $orderId): ?OrderStatusViewModel {
return OrderStatusViewModel::where('uuid', $orderId)->first();
}
}
测试策略
单元测试(领域模型)
// tests/Unit/Domain/Models/OrderTest.php
use Domain\Models\Order;
use Domain\ValueObjects\OrderId;
use Domain\ValueObjects\CustomerId;
use Domain\ValueObjects\Money;
use Domain\ValueObjects\ProductId;
use Domain\Exceptions\DomainException;
test('order requires at least one item for confirmation', function () {
$order = new Order(OrderId::generate(), new CustomerId('cust-123'));
$this->expectException(DomainException::class);
$order->confirm();
});
test('adding item with negative quantity throws exception', function () {
$order = new Order(OrderId::generate(), new CustomerId('cust-123'));
$this->expectException(DomainException::class);
$order->addItem(
new ProductId('prod-456'),
-1,
new Money(10.99)
);
});
集成测试(应用服务)
// tests/Integration/Application/Services/OrderServiceTest.php
use Application\Services\OrderService;
use Domain\Repositories\InMemoryOrderRepository;
use Illuminate\Events\Dispatcher;
use Application\DTOs\CreateOrderDTO;
test('order service creates order with items', function () {
$repo = new InMemoryOrderRepository();
$service = new OrderService($repo, new Dispatcher());
$dto = new CreateOrderDTO('cust-123', [
['product_id' => 'prod-1', 'quantity' => 2, 'price' => 9.99, 'currency' => 'USD'],
['product_id' => 'prod-2', 'quantity' => 1, 'price' => 19.99, 'currency' => 'USD']
]);
$order = $service->createOrder($dto);
expect($order->getId())->toBeInstanceOf(OrderId::class);
expect($order->getItems())->toHaveCount(2);
});
性能优化策略
-
批量操作:
// 在仓储实现中 public function saveMultiple(array $orders): void { $this->entityManager->transactional(function() use ($orders) { foreach ($orders as $order) { $this->save($order); } }); }
-
缓存常见查询:
// 在查询处理中 public function getOrderStatus(string $orderId): OrderStatusView { return Cache::remember("order_status_{$orderId}", 300, function() use ($orderId) { return $this->query->execute($orderId); }); }
-
异步事件处理:
// 在事件服务提供者中 protected $listen = [ OrderCreated::class => [ SendOrderConfirmation::class => ['queue' => 'notifications'], UpdateInventory::class => ['queue' => 'inventory'] ] ];
常见问题解决
问题:如何处理复杂事务?
解决方案:使用工作单元模式
class UnitOfWork {
private array $aggregates = [];
public function register(AggregateRoot $aggregate): void {
$this->aggregates[] = $aggregate;
}
public function commit(): void {
foreach ($this->aggregates as $aggregate) {
$this->repository->save($aggregate);
}
$this->aggregates = [];
}
}
问题:如何避免贫血领域模型?
解决方案:
- 将所有业务规则封装在聚合根中
- 避免在应用服务中实现业务逻辑
- 使用领域事件通知状态变化
问题:如何管理聚合之间的依赖?
解决方案:
- 通过ID引用而不是直接对象引用
- 在应用服务中协调多个聚合
- 使用领域事件进行跨聚合通信
最佳实践总结
-
严格分层:确保领域层不依赖基础设施
-
聚合设计:
- 保持聚合小巧(最多5-10个实体)
- 通过ID引用其他聚合
- 封装不变条件
-
测试策略:
- 单元测试覆盖领域模型
- 集成测试验证应用服务
- E2E测试API端点
-
持续重构:
- 定期与领域专家沟通
- 根据业务变化调整模型
- 重构代码以反映领域语言
-
性能考虑:
- 读/写模型分离(CQRS)
- 事件溯源处理复杂业务流
- 异步处理非关键操作
-
团队协作:
- 使用通用语言(Ubiquitous Language)
- 创建上下文映射图
- 定期进行事件风暴会议
通过遵循这些实践,您可以在 Laravel 12 中成功实现领域驱动设计,创建出灵活、可维护且与业务需求紧密对齐的应用程序。