本文目录导读:

在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架构,分离命令与查询的职责,提高系统的可维护性和扩展性。