Maison >développement back-end >tutoriel php >Code légèrement plus avancé que l'exemple de la documentation des frameworks.
En tant que programmeur, j'ai rencontré de nombreux projets au fil des années, dont j'ai hérité, géré, mis à niveau, développé et transmis. Beaucoup d’entre eux impliquaient du code spaghetti ou, comme on l’appelle aussi, une « grosse boule de boue ». Ce problème affecte souvent les projets construits sur un framework, où le code est organisé de la même manière que les exemples de la documentation du framework.
Malheureusement, la documentation des frameworks MVC omet souvent d'avertir que les exemples de code sont principalement destinés à illustrer les fonctionnalités et ne sont pas adaptés aux applications du monde réel. En conséquence, les projets réels intègrent souvent toutes les couches dans les méthodes du contrôleur ou du présentateur (dans le cas de MVP), qui traitent les requêtes (généralement les requêtes HTTP). Si le framework inclut un modèle d'objet de composant, tel que Nette, les composants font souvent partie du contrôleur ou du présentateur, ce qui complique encore la situation.
Le code de tels projets devient rapidement long et complexe. Dans un seul script, les opérations de base de données, la manipulation des données, l'initialisation des composants, les paramètres de modèle et la logique métier sont mélangés. Bien que les auteurs extraient occasionnellement des parties des fonctionnalités dans des services autonomes (généralement des singletons), cela aide rarement beaucoup. Un tel projet devient difficile à lire et difficile à maintenir.
D'après mon expérience, les modèles de conception standardisés sont rarement utilisés, en particulier dans les petits projets (5 à 50 000 lignes de code), tels que les applications CRUD simples pour les petites entreprises cherchant à simplifier l'administration. Pourtant, ces projets pourraient grandement bénéficier de modèles tels que CQRS (Command Query Responsibility Segregation) et DDD (Domain-Driven Design).
Je vais démontrer à quoi ressemble cette approche en utilisant la pile Nette, Contributte (avec l'intégration de Symfony Event Dispatcher) et 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; } }
Cette solution présente plusieurs avantages. La logique liée à la base de données est séparée de MVC/P, contribuant ainsi à une meilleure lisibilité et à une maintenance plus facile. L'objet de requête, qui fait office de support de données, est idéal pour se connecter à la base de données, comme par exemple un journal d'événements. Cela garantit que toutes les entrées utilisateur modifiant les données sont enregistrées avec leur ordre chronologique. En cas d'erreur, ces journaux peuvent être consultés et rejoués si nécessaire.
Les inconvénients de cette approche incluent le fait que la commande ne doit renvoyer aucune donnée. Par conséquent, si je dois travailler davantage avec les données nouvellement créées (par exemple, les transmettre à un modèle), je dois les récupérer en utilisant leur UUID, c'est pourquoi la requête et l'entité les contiennent. Un autre inconvénient est que toute modification apportée au schéma de la base de données nécessite la mise à jour de toutes les requêtes pour qu'elles correspondent au nouveau schéma, ce qui peut prendre du temps.
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!