CQRS(Command Query Responsibility Segregation)是一种将数据读写操作分离的架构模式。在传统CRUD架构中,我们通常使用同一个数据模型来处理读写操作,而CQRS则明确区分了命令(写操作)和查询(读操作)的边界。
这种分离带来的直接好处是:
在PHP生态中实现CQRS,我们需要理解几个核心概念:
重要提示:CQRS不是银弹,它最适合于写操作和读操作有显著不同需求的场景。如果你的应用读写模式相似,传统CRUD可能更合适。
一个典型的PHP CQRS实现通常包含以下层次:
code复制应用层(HTTP/CLI入口)
├── 表现层(Controllers/Console Commands)
│ ├── 命令总线(Command Bus)
│ └── 查询总线(Query Bus)
├── 领域层(Domain)
│ ├── 命令处理器(Command Handlers)
│ ├── 领域模型(Domain Models)
│ └── 领域事件(Domain Events)
└── 基础设施层(Infrastructure)
├── 写模型存储(通常用ORM如Doctrine)
└── 读模型存储(可能用Elasticsearch、Redis等)
在PHP生态中,我们可以选择以下工具构建CQRS:
命令/查询总线:
事件系统:
读模型存储:
写模型存储:
php复制// 定义创建订单命令
class CreateOrderCommand {
private $userId;
private $items;
private $totalAmount;
public function __construct(int $userId, array $items, float $totalAmount) {
$this->userId = $userId;
$this->items = $items;
$this->totalAmount = $totalAmount;
}
// getters...
}
// 对应的命令处理器
class CreateOrderHandler {
private $orderRepository;
private $eventDispatcher;
public function __construct(OrderRepository $repository, EventDispatcher $dispatcher) {
$this->orderRepository = $repository;
$this->eventDispatcher = $dispatcher;
}
public function handle(CreateOrderCommand $command) {
// 验证业务规则
if ($command->getTotalAmount() <= 0) {
throw new \InvalidArgumentException('订单金额必须大于0');
}
// 创建领域对象
$order = Order::create(
$command->getUserId(),
$command->getItems(),
$command->getTotalAmount()
);
// 持久化
$this->orderRepository->save($order);
// 发布领域事件
$this->eventDispatcher->dispatch(
new OrderCreated($order->getId(), $order->getUserId())
);
return $order->getId();
}
}
php复制// 定义订单详情查询
class GetOrderDetailsQuery {
private $orderId;
public function __construct(int $orderId) {
$this->orderId = $orderId;
}
public function getOrderId(): int {
return $this->orderId;
}
}
// 对应的查询处理器
class GetOrderDetailsHandler {
private $orderReadRepository;
public function __construct(OrderReadRepository $repository) {
$this->orderReadRepository = $repository;
}
public function handle(GetOrderDetailsQuery $query): OrderDetailsDto {
return $this->orderReadRepository->findById($query->getOrderId());
}
}
使用Tactician配置示例:
php复制use League\Tactician\Setup\QuickStart;
// 命令总线配置
$commandHandlerMiddleware = [
// ...其他中间件
new CommandHandlerMiddleware($commandHandlerMap)
];
$commandBus = QuickStart::create($commandHandlerMiddleware);
// 查询总线配置(可以复用Tactician或单独实现)
$queryBus = new SimpleQueryBus($queryHandlerMap);
在CQRS中,写模型和读模型通常是最终一致的。常见同步方式:
php复制// 订单创建事件处理器
class OrderCreatedListener {
private $orderReadRepository;
public function __construct(OrderReadRepository $repository) {
$this->orderReadRepository = $repository;
}
public function onOrderCreated(OrderCreated $event) {
// 从写模型获取最新数据
$order = $this->writeModel->find($event->getOrderId());
// 转换为读模型DTO
$dto = new OrderDetailsDto(
$order->getId(),
$order->getUser()->getName(),
// ...其他字段
);
// 更新读模型
$this->orderReadRepository->update($dto);
}
}
读模型更新通常是异步的,需要考虑:
数据去规范化:
分页缓存策略:
php复制class CachedOrderListQueryHandler implements QueryHandlerInterface {
private $decorated;
private $cache;
public function __construct(OrderListQueryHandler $handler, CacheInterface $cache) {
$this->decorated = $handler;
$this->cache = $cache;
}
public function handle(OrderListQuery $query): PaginatedResult {
$cacheKey = sprintf('order_list_%d_%d', $query->getPage(), $query->getPerPage());
return $this->cache->get($cacheKey, function() use ($query) {
return $this->decorated->handle($query);
});
}
}
不同粒度的读模型:
批量命令处理:
php复制class BatchOrderUpdateHandler {
public function handle(BatchOrderUpdateCommand $command) {
foreach ($command->getUpdates() as $update) {
$this->commandBus->handle($update);
}
}
}
事件批量发布:
命令队列处理:
读模型不同步:
命令验证复杂:
php复制class ValidationMiddleware implements Middleware {
private $validator;
public function __construct(ValidatorInterface $validator) {
$this->validator = $validator;
}
public function execute($command, callable $next) {
$errors = $this->validator->validate($command);
if (count($errors) > 0) {
throw new ValidationFailedException($errors);
}
return $next($command);
}
}
事务管理:
php复制class TransactionalMiddleware implements Middleware {
private $connection;
public function __construct(Connection $connection) {
$this->connection = $connection;
}
public function execute($command, callable $next) {
$this->connection->beginTransaction();
try {
$result = $next($command);
$this->connection->commit();
return $result;
} catch (\Exception $e) {
$this->connection->rollBack();
throw $e;
}
}
}
命令处理器测试:
php复制class CreateOrderHandlerTest extends TestCase {
public function testHandleValidCommand() {
$repository = $this->createMock(OrderRepository::class);
$repository->expects($this->once())->method('save');
$dispatcher = $this->createMock(EventDispatcher::class);
$dispatcher->expects($this->once())->method('dispatch');
$handler = new CreateOrderHandler($repository, $dispatcher);
$command = new CreateOrderCommand(1, [['id' => 1, 'qty' => 2]], 100.00);
$orderId = $handler->handle($command);
$this->assertIsInt($orderId);
}
}
读一致性测试:
性能测试:
CQRS常与Event Sourcing配合使用:
php复制class Order {
private $events = [];
private $id;
private $status;
public static function create(int $userId, array $items): self {
$order = new self();
$order->record(new OrderCreated(Uuid::uuid4(), $userId, $items));
return $order;
}
private function record(object $event): void {
$this->events[] = $event;
$this->apply($event);
}
private function apply(object $event): void {
switch (get_class($event)) {
case OrderCreated::class:
$this->id = $event->getId();
$this->status = 'created';
break;
// 其他事件处理...
}
}
public function getUncommittedEvents(): array {
return $this->events;
}
}
在微服务场景下:
php复制class OrderSaga {
private $commandBus;
public function __construct(CommandBus $commandBus) {
$this->commandBus = $commandBus;
}
public function handlePaymentCompleted(PaymentCompleted $event) {
$this->commandBus->handle(
new ApproveOrder($event->getOrderId())
);
}
public function handlePaymentFailed(PaymentFailed $event) {
$this->commandBus->handle(
new CancelOrder($event->getOrderId())
);
}
}
前端架构也需要相应调整:
javascript复制// 前端命令调用示例
async function createOrder(items) {
// 乐观更新
const fakeOrderId = generateTempId();
updateUIWithPendingOrder(fakeOrderId, items);
try {
const response = await fetch('/api/commands/create-order', {
method: 'POST',
body: JSON.stringify({ items })
});
const realOrderId = await response.json();
replaceTempOrderInUI(fakeOrderId, realOrderId);
} catch (error) {
revertUIOnError(fakeOrderId);
}
}
Broadway:
Ecotone:
Prooph:
Laravel CQRS:
框架选择建议:对于新项目,可以从Broadway或Ecotone开始;如果是Laravel项目,可以考虑Laravel CQRS包;对于需要高度定制化的场景,可以基于Tactician等基础库自行构建。