Maison >développement back-end >tutoriel php >Code légèrement plus avancé que l'exemple de la documentation des frameworks.

Code légèrement plus avancé que l'exemple de la documentation des frameworks.

Patricia Arquette
Patricia Arquetteoriginal
2024-12-27 19:06:12390parcourir

Slightly more advanced code than the example in the frameworks documentation.

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.

Problèmes avec une structure de code non optimale

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).

  1. Demande HTTP -> Contrôleur/Présentateur
  2. Validation des entrées -> Convertir en demande
  3. Demande -> Commande
  4. Commande -> Rendu/Réponse

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!

Déclaration:
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn