Laravel DDD怎么落地实践

wen PHP项目 1

本文目录导读:

Laravel DDD怎么落地实践

  1. 核心理念:分层与适配,而非对抗
  2. 第一步:项目结构设计(关键中的关键)
  3. 第二步:战术设计关键模式落地
  4. 第三步:实际落地中的坑与最佳实践
  5. 总结:Laravel DDD 落地路径

这是一个很有价值的问题,Laravel DDD(领域驱动设计)的落地实践,核心挑战在于如何将DDD的战略设计(限界上下文、通用语言)和战术设计(实体、值对象、领域服务等)与Laravel的MVC架构及Eloquent ORM的Active Record模式进行平衡和适配

完全照搬Java/Spring的DDD写法,在Laravel中会非常别扭且低效,下面是一套经过验证、可落地的实践指南。

核心理念:分层与适配,而非对抗

不要试图完全摒弃Eloquent,Eloquent非常强大,我们要做的是在边界内部(领域层)使用纯净的PHP对象,在边界外部(基础设施层)利用Eloquent


第一步:项目结构设计(关键中的关键)

推荐的目录结构是 Module-based(模块化),每个模块就是一个独立的限界上下文。

app/
├── Modules/
│   ├── Ordering/          # 订单上下文(BC)
│   │   ├── Domain/        # 领域层(核心,零依赖外部框架)
│   │   │   ├── Models/    # 实体(Entity)和 值对象(Value Object)
│   │   │   ├── Events/    # 领域事件
│   │   │   ├── Exceptions/
│   │   │   └── Services/  # 领域服务
│   │   ├── Application/   # 应用层(用例/服务)
│   │   │   ├── Commands/  # 命令/请求对象
│   │   │   ├── Handlers/  # 命令处理器
│   │   │   └── DTOs/      # 数据传输对象
│   │   ├── Infrastructure/ # 基础设施层
│   │   │   ├── Eloquent/  # Eloquent模型、Repository实现
│   │   │   ├── Providers/ # 服务提供者(绑定接口和实现)
│   │   │   ├── Listeners/ # 监听器(处理领域事件)
│   │   │   └── Database/  # 迁移、工厂、种子
│   │   └── Presentation/  # 表现层(或直接放在 Http/Controllers)
│   │       └── Http/
│   │           └── Controllers/
│   └── Shared/            # 共享内核
│       └── Domain/
│           └── ValueObjects/
├── Http/
└── ...

关键点

  • Domain 层是“孤岛”:不 import 任何 Laravel 类(如 Model, DB, Auth),只使用标准 PHP 对象和自定义接口。
  • Infrastructure 层是“适配器”:负责实现 Domain 层的接口,这里可以尽情使用 Eloquent, Cache, Queue 等 Laravel 特性。

第二步:战术设计关键模式落地

实体(Entity)与值对象(Value Object)

不要用 Eloquent Model 充当领域实体。 创建一个纯净的 Plain PHP Class。

// Modules/Ordering/Domain/Models/Order.php (Domain Entity)
namespace Modules\Ordering\Domain\Models;
use Modules\Ordering\Domain\ValueObjects\OrderId;
use Modules\Ordering\Domain\ValueObjects\Money;
use Modules\Ordering\Domain\Events\OrderPlaced;
class Order
{
    public function __construct(
        private OrderId $orderId,
        private Money $totalAmount,
        private string $customerId,
        private array $lineItems = []
    ) {
        // 构造函数中执行创建时的业务规则
        if ($totalAmount->isLessThan(Money::fromFloat(0.01))) {
            throw new \InvalidArgumentException("Order total must be positive");
        }
    }
    public function place(): void
    {
        // ... 业务逻辑 ...
        // 触发领域事件(通过EventDispatcher或简单的事件集合)
        // $this->events[] = new OrderPlaced($this->orderId);
    }
    // Getters 暴露必要信息,但避免暴露修改内部状态的方法
    public function getOrderId(): OrderId { return $this->orderId; }
    public function getTotalAmount(): Money { return $this->totalAmount; }
}
// Modules/Ordering/Domain/ValueObjects/Money.php (Value Object)
namespace Modules\Ordering\Domain\ValueObjects;
class Money
{
    public function __construct(
        private int $amount, // 以分为单位,避免浮点数
        private string $currency = 'CNY'
    ) {}
    public static function fromFloat(float $amount, string $currency = 'CNY'): self
    {
        return new self((int) round($amount * 100), $currency);
    }
    public function add(self $other): self
    {
        if ($this->currency !== $other->currency) {
            throw new \InvalidArgumentException('Currency mismatch');
        }
        return new self($this->amount + $other->amount, $this->currency);
    }
    // ... equals(), isLessThan() 等方法
}

资源库(Repository)

Repository 是通往数据库的唯一桥梁。 Domain 层定义接口,Infrastructure 层用 Eloquent 实现。

// 1. Domain 层定义接口
// Modules/Ordering/Domain/Repositories/OrderRepositoryInterface.php
namespace Modules\Ordering\Domain\Repositories;
use Modules\Ordering\Domain\Models\Order;
use Modules\Ordering\Domain\ValueObjects\OrderId;
interface OrderRepositoryInterface
{
    public function save(Order $order): void;
    public function findById(OrderId $orderId): ?Order;
    public function findByCustomerId(string $customerId): array;
}
// 2. Infrastructure 层实现
// Modules/Ordering/Infrastructure/Eloquent/EloquentOrderRepository.php
namespace Modules\Ordering\Infrastructure\Eloqeunt;
use Modules\Ordering\Domain\Models\Order;
use Modules\Ordering\Domain\Repositories\OrderRepositoryInterface;
use Modules\Ordering\Infrastructure\Eloqeunt\Models\Order as EloquentOrderModel; // 注意命名冲突
use Modules\Ordering\Infrastructure\Factories\OrderFactory; // 用于将Eloquent模型转换为领域实体
class EloquentOrderRepository implements OrderRepositoryInterface
{
    public function save(Order $order): void
    {
        // 将领域实体转换为Eloquent数据
        $eloquentModel = EloquentOrderModel::findOrNew($order->getOrderId()->getId());
        $eloquentModel->customer_id = $order->getCustomerId();
        $eloquentModel->total_amount = $order->getTotalAmount()->getAmount(); // 存分
        // ... 保存关联商品
        $eloquentModel->save();
    }
    public function findById(OrderId $orderId): ?Order
    {
        $eloquentModel = EloquentOrderModel::with('items')->find($orderId->getId());
        if (!$eloquentModel) {
            return null;
        }
        // 将Eloquent模型转换回领域实体
        return OrderFactory::fromEloquent($eloquentModel);
    }
    // ...
}
// 3. 在 ServiceProvider 中绑定
// Modules/Ordering/Infrastructure/Providers/OrderingServiceProvider.php
class OrderingServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->app->bind(
            \Modules\Ordering\Domain\Repositories\OrderRepositoryInterface::class,
            \Modules\Ordering\Infrastructure\Eloqeunt\EloquentOrderRepository::class
        );
    }
}

应用服务(Application Service)与命令(Command)

Controller 不应该直接调用 Repository,而是通过 Application Service。

// Modules/Ordering/Application/Commands/PlaceOrderCommand.php
namespace Modules\Ordering\Application\Commands;
class PlaceOrderCommand
{
    public function __construct(
        public readonly string $customerId,
        public readonly array $items // [{productId, quantity, price}]
    ) {}
}
// Modules/Ordering/Application/Handlers/PlaceOrderHandler.php
namespace Modules\Ordering\Application\Handlers;
use Modules\Ordering\Domain\Repositories\OrderRepositoryInterface;
use Modules\Ordering\Domain\Models\Order;
// ... 其他依赖
class PlaceOrderHandler
{
    public function __construct(
        private OrderRepositoryInterface $orderRepository,
        private ProductRepositoryInterface $productRepository
    ) {}
    public function handle(PlaceOrderCommand $command): void
    {
        // 1. 从数据库查询(通过Repository)
        $customer = $this->customerRepository->findById($command->customerId);
        // 2. 构建领域实体(这里是核心业务逻辑所在)
        $order = new Order(
            new OrderId(Uuid::generate()),
            // ... 从command和商品信息构建lineItems并计算总价
        );
        // 3. 执行业务方法
        $order->place(); // 可能会抛出异常或触发领域事件
        // 4. 持久化
        $this->orderRepository->save($order);
    }
}

领域事件(Domain Events)

使用 Laravel 的 Event 机制来分发领域事件,但事件类定义在 Domain 层。

// Modules/Ordering/Domain/Events/OrderPlaced.php
namespace Modules\Ordering\Domain\Events;
use Modules\Ordering\Domain\ValueObjects\OrderId;
class OrderPlaced
{
    public function __construct(
        public OrderId $orderId,
        public string $customerId
    ) {}
}
// 在 Application/Infrastructure 层监听
// Modules/Ordering/Infrastructure/Listeners/SendOrderConfirmation.php
class SendOrderConfirmation
{
    public function handle(OrderPlaced $event): void
    {
        // 使用 Laravel Mail 发送邮件
    }
}

第三步:实际落地中的坑与最佳实践

  1. 不要一开始就所有模块都DDD,选择核心业务逻辑复杂、规则频繁变化的模块(如订单、库存、支付)先落地,普通CRUD模块(如文章管理、配置管理)依然用Laravel传统方式。

  2. 聚合(Aggregate)是重要概念但容易用错,在Laravel中,可以简单理解为:一个聚合根就是一个Repository的入口点Order(订单)是聚合根,OrderLineItem(订单行)是内部实体,你只能通过 Order 来操作 OrderLineItem,不能有 OrderLineItemRepository,这有助于维护数据一致性。

  3. Eloquent 模型放置的位置:放在 Infrastructure/Eloquent/Models/ 下,而不是 app/Models/,它们只负责数据映射和数据访问,不包含业务规则。

  4. 处理事务:事务边界通常在应用服务层,可以使用 DB::transaction() 包裹 $handler->handle($command)

  5. 性能优化:如果领域实体构建开销过大(比如加载大量关联数据),可以考虑使用 CQRS(命令查询职责分离)

    • Command 端:完整DDD流程,保证数据一致性。
    • Query 端:直接使用 Eloquent/DB Facade 进行查询,返回 DTO(数据传输对象)或数组,跳过领域实体,追求极致读取性能,这是 Laravel DDD 中最常见也最有效的优化。
  6. 使用 Laravel 自带 IoC 容器:Service Provider 是绑定接口和实现的绝佳场所,每个 Module 注册自己的 Provider。

Laravel DDD 落地路径

  1. 模块化:按业务切分目录。
  2. 纯净领域:Domain 层只定义接口、实体、值对象和领域事件。
  3. 适配器模式:Infrastructure 层用 Eloquent 实现 Repository 接口,用 Laravel Mail/Queue 实现领域事件监听。
  4. 应用层调度:Controller -> Application Service (Command/Handler) -> Domain (Entity/Service) -> Infrastructure (Repository)。
  5. 关键取舍:聚合根、跨模块通信、性能优化(CQRS)。

DDD 是一个持续演进的过程,从一个小模块开始,让代码自然生长出清晰的边界,比一次性设计出完美的架构更重要。

抱歉,评论功能暂时关闭!