PHP命令查询职责分离怎么设计

wen PHP项目 1

本文目录导读:

PHP命令查询职责分离怎么设计

  1. 核心概念分层
  2. PHP CQRS架构设计组件
  3. 基础设施与持久化
  4. 完整的使用流程示例
  5. 高级特性与最佳实践
  6. 项目结构建议
  7. 注意事项

在PHP中实现命令查询职责分离(CQRS,Command Query Responsibility Segregation)的核心思想是将“写操作(命令)”和“读操作(查询)”分离到不同的模型、服务和路径中,这样做可以提高系统的可维护性、可伸缩性,并有助于处理复杂业务逻辑。

以下是PHP中CQRS的详细设计方案,包含代码示例和最佳实践。


核心概念分层

  • Command(命令):表示一个意图(如 CreateUserCommand),改变系统状态,不返回数据。
  • Query(查询):表示一个请求(如 GetUserByIdQuery),返回数据,不改变状态。
  • Command Handler(命令处理器):执行命令逻辑。
  • Query Handler(查询处理器):执行查询逻辑。
  • Command Bus / Query Bus(消息总线):负责调度命令/查询到对应的处理器。
  • Aggregate(聚合根):在写模型中使用(DDD概念),封装业务规则。
  • Read Model(读模型):扁平化、面向查询的模型,通常存储在数据库视图或缓存中。

PHP CQRS架构设计组件

1 命令与查询

命令示例

<?php
// 命令对象:不可变,只包含数据
class CreateUserCommand
{
    public function __construct(
        public readonly string $userId,
        public readonly string $email,
        public readonly string $name
    ) {}
}

查询示例

<?php
// 查询对象
class GetUserByEmailQuery
{
    public function __construct(
        public readonly string $email
    ) {}
}

2 命令/查询处理器

命令处理器

<?php
interface CommandHandler
{
    public function handle(object $command): void;
}
class CreateUserHandler implements CommandHandler
{
    public function __construct(
        private UserRepository $userRepo,
        private EventDispatcher $eventDispatcher
    ) {}
    public function handle(object $command): void
    {
        if (!($command instanceof CreateUserCommand)) {
            throw new InvalidArgumentException();
        }
        // 业务逻辑
        $user = User::create(
            $command->userId,
            $command->email,
            $command->name
        );
        $this->userRepo->save($user);
        // 触发领域事件
        $this->eventDispatcher->dispatch(new UserCreatedEvent($command->userId));
    }
}

查询处理器

<?php
interface QueryHandler
{
    public function handle(object $query): mixed;
}
class GetUserByEmailHandler implements QueryHandler
{
    public function __construct(
        private UserReadModelRepository $readRepo
    ) {}
    public function handle(object $query): ?UserDTO
    {
        if (!($query instanceof GetUserByEmailQuery)) {
            throw new InvalidArgumentException();
        }
        return $this->readRepo->findByEmail($query->email);
    }
}

3 命令/查询总线

简单总线实现

<?php
class SimpleCommandBus
{
    private array $handlerMap = [];
    public function registerHandler(string $commandClass, CommandHandler $handler): void
    {
        $this->handlerMap[$commandClass] = $handler;
    }
    public function dispatch(object $command): void
    {
        $handler = $this->handlerMap[get_class($command)] ?? throw new RuntimeException('Handler not found');
        $handler->handle($command);
    }
}
class SimpleQueryBus
{
    private array $handlerMap = [];
    public function registerHandler(string $queryClass, QueryHandler $handler): void
    {
        $this->handlerMap[$queryClass] = $handler;
    }
    public function dispatch(object $query): mixed
    {
        $handler = $this->handlerMap[get_class($query)] ?? throw new RuntimeException('Handler not found');
        return $handler->handle($query);
    }
}

4 分离读/写模型

写模型(领域模型)

<?php
class User
{
    private string $id;
    private string $email;
    private string $name;
    private array $events = [];
    private function __construct() {}
    public static function create(string $id, string $email, string $name): self
    {
        $user = new self();
        $user->id = $id;
        $user->email = $email;
        $user->name = $name;
        // 记录领域事件
        $user->events[] = new UserCreatedEvent($id, $email, $name);
        return $user;
    }
    public function getId(): string { return $this->id; }
    public function getEvents(): array
    {
        $events = $this->events;
        $this->events = [];
        return $events;
    }
}

读模型(DTO / 只读视图)

<?php
class UserDTO
{
    public function __construct(
        public readonly string $id,
        public readonly string $email,
        public readonly string $name,
        public readonly \DateTimeImmutable $createdAt
    ) {}
}

基础设施与持久化

写仓库

<?php
interface UserRepository
{
    public function save(User $user): void;
    public function ofId(string $id): ?User;
}
class DoctrineUserRepository implements UserRepository
{
    private EntityManager $em;
    public function save(User $user): void
    {
        $this->em->persist($user);
        $this->em->flush();
        // 触发领域事件(通常在聚合根/事件发布者中)
        foreach ($user->getEvents() as $event) {
            $this->eventBus->dispatch($event);
        }
    }
}

读仓库

<?php
interface UserReadModelRepository
{
    public function findByEmail(string $email): ?UserDTO;
}
class SqlUserReadModelRepository implements UserReadModelRepository
{
    private PDO $pdo;
    public function findByEmail(string $email): ?UserDTO
    {
        $stmt = $this->pdo->prepare('SELECT id, email, name, created_at FROM user_view WHERE email = ?');
        $stmt->execute([$email]);
        $row = $stmt->fetch();
        if (!$row) return null;
        return new UserDTO($row['id'], $row['email'], $row['name'], new \DateTimeImmutable($row['created_at']));
    }
}

完整的使用流程示例

<?php
// --- 初始化 ---
$commandBus = new SimpleCommandBus();
$queryBus = new SimpleQueryBus();
// 注册处理器
$commandBus->registerHandler(CreateUserCommand::class, new CreateUserHandler($userRepo, $eventDispatcher));
$queryBus->registerHandler(GetUserByEmailQuery::class, new GetUserByEmailHandler($readRepo));
// --- 写操作 ---
$command = new CreateUserCommand(
    userId: Uuid::v4()->toString(),
    email: 'test@example.com',
    name: 'Alice'
);
$commandBus->dispatch($command); 
// 用户保存到写数据库,发出 UserCreatedEvent
// --- 读操作 (可能来自异步更新的读库) ---
$query = new GetUserByEmailQuery('test@example.com');
$userDTO = $queryBus->dispatch($query);
echo $userDTO->name; // 'Alice'

高级特性与最佳实践

特性 说明
事件溯源 写模型存储事件流而非当前状态,读模型通过事件投影更新
异步投影 使用消息队列(RabbitMQ/Kafka)将事件发布到读模型更新器
最终一致性 写后立即读可能读到旧数据,通过缓存/版本号处理
命令验证 在处理器内部或使用单独验证器(如 Symfony Validator)
命令/查询包装 每个命令/查询都实现一个 __invoke 方法,方便统一调用
队列支持 通过 CommandBus 中间件支持队列执行(写操作异步化)

中间件示例(用于日志、事务、验证)

<?php
interface CommandBusMiddleware
{
    public function dispatch(object $command, callable $next): void;
}
class LoggingMiddleware implements CommandBusMiddleware
{
    public function dispatch(object $command, callable $next): void
    {
        $start = microtime(true);
        $next($command);
        Logger::info('Command processed in ' . (microtime(true) - $start));
    }
}

项目结构建议

src/
├─ Application/
│  ├─ Command/
│  │  ├─ CreateUserCommand.php
│  │  ├─ CreateUserHandler.php
│  │  └─ CommandBus.php
│  └─ Query/
│     ├─ GetUserByEmailQuery.php
│     ├─ GetUserByEmailHandler.php
│     └─ QueryBus.php
├─ Domain/
│  ├─ Model/
│  │  ├─ User.php
│  │  └─ UserRepositoryInterface.php
│  └─ Event/
│     └─ UserCreatedEvent.php
└─ Infrastructure/
   ├─ Persistence/
   │  ├─ DoctrineUserRepository.php
   │  └─ SqlUserReadModelRepository.php
   └─ View/
      └─ UserDTO.php

注意事项

  • 不要过度设计:如果读写场景差异不大,CQRS可能带来不必要的复杂性,仅当读写模型、性能或扩展性需求显著不同时使用。
  • 最终一致性:读模型更新可能存在延迟,业务上需要接受短暂不一致。
  • 事务边界:写操作通常在一个事务中完成(包括事件存储),读操作独立查询。
  • 测试策略:命令/查询处理器容易独立测试,读/写模型也可以分开验证。

通过上述设计,你可以在PHP项目中实现清晰的CQRS架构,分离命令与查询的职责,提高系统的可维护性和扩展性。

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