저는 프로그래머로서 지난 수년간 수많은 프로젝트를 접했고, 이를 상속받고, 관리하고, 업그레이드하고, 개발하고, 넘겨받았습니다. 그 중 다수는 스파게티 코드 또는 "큰 진흙 공"이라고도 불리는 것과 관련되어 있습니다. 이 문제는 코드가 프레임워크 문서의 예제와 유사하게 구성되어 있는 일부 프레임워크를 기반으로 구축된 프로젝트에 영향을 미치는 경우가 많습니다.
안타깝게도 MVC 프레임워크 문서에는 코드 예제가 주로 기능을 설명하기 위한 것이며 실제 애플리케이션에는 적합하지 않다는 경고가 표시되지 않는 경우가 많습니다. 결과적으로 실제 프로젝트에서는 요청(일반적으로 HTTP 요청)을 처리하는 컨트롤러 또는 프리젠터 메서드(MVP의 경우)에 모든 레이어를 통합하는 경우가 많습니다. 프레임워크에 Nette와 같은 구성 요소 개체 모델이 포함된 경우 구성 요소가 컨트롤러나 프리젠터의 일부인 경우가 많아 상황이 더욱 복잡해집니다.
이러한 프로젝트의 코드는 길이와 복잡성이 빠르게 증가합니다. 단일 스크립트에는 데이터베이스 작업, 데이터 조작, 구성 요소 초기화, 템플릿 설정 및 비즈니스 논리가 혼합되어 있습니다. 작성자는 때때로 기능의 일부를 독립형 서비스(일반적으로 싱글톤)로 추출하지만 이것이 큰 도움이 되는 경우는 거의 없습니다. 이러한 프로젝트는 읽기도 어렵고 유지 관리도 어려워집니다.
내 경험에 따르면 표준화된 디자인 패턴은 특히 관리 단순화를 원하는 중소기업을 위한 간단한 CRUD 애플리케이션과 같은 소규모 프로젝트(코드 5~5만 줄)에서는 거의 사용되지 않습니다. 그러나 이러한 프로젝트는 CQRS(Command Query Responsibility Segregation) 및 DDD(Domain-Driven Design)와 같은 패턴을 통해 큰 이점을 얻을 수 있습니다.
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<mixed> $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를 사용하여 해당 데이터를 검색해야 하며, 이것이 요청과 엔터티 모두에 해당 데이터를 포함하는 이유입니다. 또 다른 단점은 데이터베이스 스키마를 변경하려면 모든 요청을 새 스키마와 일치하도록 업데이트해야 하므로 시간이 많이 걸릴 수 있다는 것입니다.
위 내용은 프레임워크 문서의 예제보다 약간 더 고급 코드입니다.의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!