比框架文档中的示例代码稍微高级一些。
作为一名程序员,多年来我接手、管理、升级、开发和移交过无数项目。其中许多项目都涉及意大利面条式代码,或者说是“大泥球”。这个问题经常影响基于某些框架构建的项目,这些框架中的代码组织方式与框架文档中的示例类似。
不幸的是,MVC 框架的文档通常没有警告说,代码示例主要用于说明功能,并不适合实际应用。因此,实际项目通常会将所有层集成到控制器或演示器方法中(就 MVP 而言),这些方法处理请求(通常是 HTTP 请求)。如果框架包含组件对象模型(例如 Nette),则组件通常是控制器或演示器的一部分,这会使情况更加复杂。
非最优代码结构的问题
此类项目的代码长度和复杂性会迅速增加。在单个脚本中,数据库操作、数据操作、组件初始化、模板设置和业务逻辑混杂在一起。虽然作者偶尔会将部分功能提取到独立服务(通常是单例)中,但这几乎没有什么帮助。这样的项目变得难以阅读和维护。
根据我的经验,标准化设计模式很少使用,尤其是在较小的项目(5-5 万行代码)中,例如希望简化管理的小型企业的简单 CRUD 应用程序。然而,这些项目可以从 CQRS(命令查询职责分离)和 DDD(领域驱动设计)等模式中受益匪浅。
我将演示如何使用 Nette 堆栈、Contributte(与 Symfony Event Dispatcher 集成)和 Nextras ORM 来实现这种方法。
// Command definition final class ItemSaveCommand implements Command { private ItemSaveRequest $request; public function __construct(private Orm $orm) { // } /** @param ItemSaveRequest $request */ public function setRequest(Request $request): void { $this->request = $request; } public function execute(): void { $request = $this->request; if ($request->id) { $entity = $this->orm->items->getById($request->id); } else { $entity = new Item(); $entity->uuid = $request->uuid; } $entity->data = $request->data; $this->orm->persist($entity); } } // Command Factory interface ItemSaveCommandFactory extends FactoryService { public function create(): ItemSaveCommand; } // Request #[RequestCommandFactory(ItemSaveCommandFactory::class)] final class ItemSaveRequest implements Request { public int|null $id = null; public string $uuid; public string $data; } /** * Command execution service * Supports transactions and request logging */ final class CommandExecutionService implements Service { private \DateTimeImmutable $requestedAt; public function __construct( private Orm $orm, private Container $container, private Connection $connection, ) { $this->requestedAt = new \DateTimeImmutable(); } /** @throws \Throwable */ public function execute(AbstractCommandRequest $request, bool $logRequest = true, bool $transaction = true): mixed { $factoryClass = RequestHelper::getCommandFactory($request); $factory = $this->container->getByType($factoryClass); $cmd = $factory->create(); $clonedRequest = clone $request; $cmd->setRequest($request); try { $cmd->execute(); if (!$transaction) { $this->orm->flush(); } if (!$logRequest) { return; } $logEntity = RequestHelper::createRequestLog( $clonedRequest, $this->requestedAt, RequestLogConstants::StateSuccess ); if ($transaction) { $this->orm->persistAndFlush($logEntity); } else { $this->orm->persist($logEntity); } return; } catch (\Throwable $e) { if ($transaction) { $this->connection->rollbackTransaction(); } if (!$logRequest) { throw $e; } $logEntity = RequestHelper::createRequestLog( $clonedRequest, $this->requestedAt, RequestLogConstants::StateFailed ); if ($transaction) { $this->orm->persistAndFlush($logEntity); } else { $this->orm->persist($logEntity); } throw $e; } } } // Listener for executing commands via Event Dispatcher final class RequestExecutionListener implements EventSubscriberInterface { public function __construct( private CommandExecutionService $commandExecutionService ) { // } public static function getSubscribedEvents(): array { return [ RequestExecuteEvent::class => 'onRequest' ]; } /** @param ExecuteRequestEvent$ev */ public function onRequest(ExecuteRequestEvent $ev): void { $this->commandExecutionService->execute($ev->request, $ev->logRequest, $ev->transaction); } } // Event definition for command execution final class ExecuteRequestEvent extends Event { public function __construct( public Request $request, public bool $logRequest = true, public bool $transaction = true, ) { // Constructor } } // Event Dispatcher Facade final class EventDispatcherFacade { public static EventDispatcherInterface $dispatcher; public static function set(EventDispatcherInterface $dispatcher): void { self::$dispatcher = $dispatcher; } } // Helper function for simple event dispatching function dispatch(Event $event): object { return EventDispatcherFacade::$dispatcher->dispatch($event); } // Usage in Presenter (e.g., in response to a component event) final class ItemPresenter extends Presenter { public function createComponentItem(): Component { $component = new Component(); $component->onSave[] = function (ItemSaveRequest $request) { dispatch(new ExecuteRequestEvent($request)); }; return $component; } }
该解决方案有几个优点。与数据库相关的逻辑与 MVC/P 分离,有助于提高可读性和更易于维护。请求对象充当数据载体,非常适合记录到数据库中,例如事件日志。这可确保所有修改数据的用户输入都按时间顺序保存。如果出现错误,可以查看这些日志并在必要时重播。
这种方法的缺点包括命令不应返回任何数据。因此,如果我需要进一步处理新创建的数据(例如,将其传递给模板),我必须使用其 UUID 检索它,这就是请求和实体都包含它的原因。另一个缺点是,对数据库架构的任何更改都需要更新所有请求以匹配新架构,这可能非常耗时。