Als Programmierer bin ich im Laufe der Jahre auf zahlreiche Projekte gestoßen, die ich übernommen, verwaltet, erweitert, weiterentwickelt und übergeben habe. Bei vielen handelte es sich um Spaghetti-Code oder, wie es auch genannt wird, um einen „großen Schlammball“. Dieses Problem betrifft häufig Projekte, die auf einem Framework basieren und bei denen der Code ähnlich wie Beispiele in der Dokumentation des Frameworks organisiert ist.
Leider wird in der Dokumentation von MVC-Frameworks häufig nicht darauf hingewiesen, dass Codebeispiele in erster Linie der Veranschaulichung der Funktionalität dienen und nicht für reale Anwendungen geeignet sind. Daher integrieren reale Projekte oft alle Schichten in Controller- oder Presenter-Methoden (im Fall von MVP), die Anfragen (typischerweise HTTP-Anfragen) verarbeiten. Wenn das Framework ein Komponentenobjektmodell wie Nette enthält, sind Komponenten häufig Teil des Controllers oder Präsentators, was die Situation weiter verkompliziert.
Der Code solcher Projekte nimmt schnell an Länge und Komplexität zu. In einem einzigen Skript werden Datenbankoperationen, Datenbearbeitung, Komponenteninitialisierung, Vorlageneinstellungen und Geschäftslogik gemischt. Während Autoren gelegentlich Teile der Funktionalität in eigenständige Dienste (normalerweise Singletons) extrahieren, hilft dies selten viel. Ein solches Projekt wird schwer zu lesen und schwer zu warten.
Meiner Erfahrung nach werden standardisierte Entwurfsmuster selten verwendet, insbesondere in kleineren Projekten (5–50.000 Codezeilen), wie beispielsweise einfachen CRUD-Anwendungen für kleine Unternehmen, die die Verwaltung vereinfachen möchten. Dennoch könnten diese Projekte stark von Mustern wie CQRS (Command Query Responsibility Segregation) und DDD (Domain-Driven Design) profitieren.
Ich werde zeigen, wie dieser Ansatz mithilfe des Nette-Stacks, Contributte (mit Symfony Event Dispatcher-Integration) und Nextras ORM aussieht.
// 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; } }
Diese Lösung hat mehrere Vorteile. Die datenbankbezogene Logik ist von MVC/P getrennt, was zu einer besseren Lesbarkeit und einfacheren Wartung beiträgt. Das Request-Objekt, das als Datenträger fungiert, eignet sich ideal für die Protokollierung in der Datenbank, beispielsweise einem Ereignisprotokoll. Dadurch wird sichergestellt, dass alle datenverändernden Benutzereingaben in chronologischer Reihenfolge gespeichert werden. Im Fehlerfall können diese Protokolle überprüft und bei Bedarf erneut abgespielt werden.
Zu den Nachteilen dieses Ansatzes gehört die Tatsache, dass der Befehl keine Daten zurückgeben sollte. Wenn ich also weiter mit den neu erstellten Daten arbeiten muss (z. B. sie an eine Vorlage übergeben muss), muss ich sie mithilfe ihrer UUID abrufen, weshalb sie sowohl in der Anfrage als auch in der Entität enthalten ist. Ein weiterer Nachteil besteht darin, dass bei Änderungen am Datenbankschema alle Anforderungen aktualisiert werden müssen, damit sie mit dem neuen Schema übereinstimmen, was zeitaufwändig sein kann.
Das obige ist der detaillierte Inhalt vonEtwas fortgeschrittenerer Code als das Beispiel in der Framework-Dokumentation.. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!