当常规CRUD不再适用:认识CQRS和事件溯源
\n\n您是否曾遇到过这样的情况:您的PHP应用不断增长……直到有一天,一个普通的UserController变成了一个2000行的怪物?熟悉吗?Bug滋生,性能下降,添加新功能让整个团队神经紧张。恭喜您,您已经成长到需要引入严肃架构模式的时候了。
今天我们将讨论两个强大的工具——CQRS(命令查询职责分离)和事件溯源。不要被复杂的术语吓到。实际上,它们只是优雅地表达:“让我们停止将读写数据混为一谈”和“我们将存储变更历史,而不是当前状态”。
\n\n本文面向已经掌握OOP基础并希望将项目提升到新水平的PHP开发者。您将学习如何在实际中应用CQRS和事件溯源,看到真实代码,并理解何时这些模式真正必要,何时最好避免使用它们。
\n\n什么是CQRS?分离命令和查询
\n\n想象您的代码是一家餐厅。通常服务员(控制器)接受订单(写请求),跑到厨房(数据库),准备菜肴(处理数据),然后立即上菜(返回响应)。当餐厅只有10个人时,一切都很完美。但当有1000位客人时,厨房开始不堪重负。
\n\nCQRS(命令查询职责分离)建议分离职责:一些厨师(命令)只负责烹饪,其他服务员(查询)只负责上菜。从技术上讲,这意味着您使用不同的模型来读写数据。
\n\nPHP中CQRS的基本实现
\n\n让我们看看这在代码中是如何实现的。从一个没有框架的简单示例开始,以理解核心概念。
\n\n// 命令——包含用于改变状态的数据的DTO\nclass CreateUserCommand {\n public function __construct(\n public readonly string $email,\n public readonly string $name\n ) {}\n}\n\n// 命令处理器——业务逻辑在此发生\nclass CreateUserHandler {\n public function __construct(\n private UserRepository $repository,\n private EventDispatcher $dispatcher\n ) {}\n\n public function handle(CreateUserCommand $command): void {\n $user = new User($command->email, $command->name);\n $this->repository->save($user);\n $this->dispatcher->dispatch(new UserCreatedEvent($user->getId()));\n }\n}\n\n// 查询——用于获取数据的DTO\nclass GetUserQuery {\n public function __construct(\n public readonly int $userId\n ) {}\n}\n\n// 查询处理器——只读,不做任何修改\nclass GetUserHandler {\n public function __construct(\n private UserReadRepository $readRepository\n ) {}\n\n public function handle(GetUserQuery $query): ?UserDTO {\n return $this->readRepository->findById($query->userId);\n }\n}\n\n注意:我们有两个不同的仓库。UserRepository负责写入(可以使用复杂的业务逻辑、验证),而UserReadRepository负责读取(可以是简单的SQL查询,甚至是反规范化的表)。
命令总线(Command Bus)
\n\n在实际项目中,命令和查询通过总线发送——一个中央调度器,它自动找到相应的处理器。
\n\n// 最简单的命令总线实现\nclass SimpleCommandBus {\n public function __construct(\n private array $handlers = []\n ) {}\n\n public function registerHandler(string $commandClass, object $handler): void {\n $this->handlers[$commandClass] = $handler;\n }\n\n public function dispatch(object $command): void {\n $handler = $this->handlers[$co