本文目录导读:

在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架构,分离命令与查询的职责,提高系统的可维护性和扩展性。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播