Skip to content

12. Conclusion

Ce document a présenté la construction d'une application web MVC avec le framework [Django] : une application dont les pages HTML sont fabriquées par le serveur, avec les gabarits de [Django], sans framework [JavaScript] côté navigateur. Il a suivi pas à pas la progression du cours [Symfony], lui-même issu des cours [Laravel], [Flask], [Spring MVC], [ASP.NET Core MVC] et [NestJS] : des vues et du routage vers les gabarits, les formulaires, l'internationalisation, la sécurité et l'accès aux données, puis une étude de cas complète.

Les mêmes 28 exemples et la même application ont ainsi été écrits avec sept frameworks, en [TypeScript], en [C#], en [Java], deux fois en [PHP] et, deux fois, en [Python]. Les notions sont les mêmes ; seuls les noms et les mécanismes changent. Les correspondances entre [Symfony] et [Django] :

[Symfony]
[Django]
noyau ([src/Kernel.php]), bundles, conteneur de services, injection de dépendances
réglages ([config/settings.py]), applications ([INSTALLED_APPS]), modules importés, fournisseurs ([functools.cache])
[composer.json], dossier [vendor], [php bin/console]
[requirements.txt], environnement virtuel ([.venv]), [python manage.py]
fichier [.env], dossier [config/packages] (fichiers YAML), [%env(...)%]
fichier [config/settings.py] (du [Python]), variables d'environnement ([os.environ]), fichier [.env] lu par nos soins
attributs [#[Route]] des contrôleurs
listes [urlpatterns] ([path], [re_path], [include]), routes nommées, [app_name]
[render], [redirectToRoute], [generateUrl] ; [path] dans les vues
[render], [redirect], [reverse] ; [{% url %}] dans les gabarits
objet [Request], [requirements] ([{id<\d+>}]), résolveurs d'arguments, [#[MapQueryString]]
objet [HttpRequest] ([GET], [POST], [headers], [COOKIES]), convertisseurs ([<int:id>]), formulaires comme validateurs
composant Validator (contraintes en attributs), composant Form (types, [handleRequest], [isValid])
formulaires [django.forms] (champs, validateurs, [clean], [error_messages], [is_valid], [cleaned_data])
thème de formulaire [Bootstrap] 5
moteur de rendu ([FORM_RENDERER]), gabarit de champ, [as_field_group]
vues [Twig], [{{ }}], [{% for %}], [{% if %}]
gabarits du DTL, [{{ }}], [{% for %}] ... [{% empty %}], [{% if %}]
[{% extends %}] et blocs, [{% include %}], macros
[{% extends %}] et blocs, [{% include %}], fragments nommés ([{% partialdef %}])
variables globales, extensions [Twig] (fonctions, filtres)
processeurs de contexte, bibliothèques de balises ([templatetags] : filtres, balises simples, balises d'inclusion)
messages flash ([addFlash], [app.flashes]), session en fichiers
application [messages] (cookie signé), sessions (fichiers, cookie signé, base...)
écouteurs des événements du noyau ([kernel.request], [kernel.controller], [kernel.response]...)
middlewares ([__call__], [process_view], [process_exception]), signaux ([request_started], [request_finished])
écouteurs de [kernel.exception], [HttpException], [createNotFoundException]
[Http404], [PermissionDenied], [BadRequest], vues [handler404]..., [process_exception]
pare-feu, authentificateurs, [access_control], [#[IsGranted]], voteurs
middlewares d'authentification et de contrôle d'accès écrits par nos soins, décorateurs ; ou l'application [django.contrib.auth]
jeton anti-CSRF du composant Form, [csrf_token()], [isCsrfTokenValid]
middleware [CsrfViewMiddleware], balise [{% csrf_token %}], [CSRF_FAILURE_VIEW] (réponse 403, ou 419 dans notre application)
composant [RateLimiter]
un limiteur écrit sur le cache
dossier [translations] (YAML, ICU), filtre [trans], [TranslatorInterface]
dossier [locale] ([gettext] : [.po], [.mo]), [gettext], [gettext_lazy], [ngettext], [{% translate %}], [{% blocktranslate %}]
extension [intl] : [IntlDateFormatter], [NumberFormatter]
mise en forme selon la langue active (filtre [date], [floatformat]), [Babel]
composant Cache (PSR-6 et contrats), composant Lock
cache de [Django] ([LocMemCache], [FileBasedCache]...), [get_or_set]
[Doctrine] : entités Data Mapper, gestionnaire d'entités, DQL et QueryBuilder
ORM de [Django] : modèles Active Record, gestionnaires ([objects]), [QuerySet], [select_related], [F()]
[wrapInTransaction(...)], [flush]
[transaction.atomic()], [select_for_update()]
[#[ORM\Version]], géré par [Doctrine] ([OptimisticLockException])
[update()] conditionné par la version ([filter(version=...)]), écrit dans la couche DAO
[php -S localhost:8000 -t public], ou [symfony server:start]
[python manage.py runserver]

Par rapport à [Symfony], on retiendra surtout :

  • l'application [Django] est chargée une fois, au démarrage du serveur, puis reste en mémoire et traite toutes les requêtes (exemple 01) : les objets construits au démarrage (réglages, routes, middlewares, services) sont partagés par toutes les requêtes. C'est simple et rapide, mais cela oblige à distinguer ce qui appartient à l'application (partagé, et donc sans état propre à une requête) de ce qui appartient à une requête (l'objet [request], les variables locales des vues), et à se souvenir qu'un serveur de production lance plusieurs processus, qui ne partagent que ce qui est rangé hors d'eux (la base, le cache en fichiers) ;
  • pas de conteneur de services : les services sont des modules [Python] que l'on importe, et des fonctions fournisseurs qui créent un objet unique à la première demande. Les dépendances d'un module sont visibles dans ses [import] ; les remplacer, pour un essai, se fait par un réglage (exemple 06) ;
  • la configuration est un fichier [Python], [config/settings.py] : une suite d'affectations, que l'on peut calculer, commenter, découper. La commande [python manage.py diffsettings] affiche ce qui diffère des valeurs par défaut ;
  • le traitement d'une requête traverse une liste de middlewares, dans l'ordre de la liste [MIDDLEWARE], puis en sens inverse pour la réponse : c'est là que se placent la langue, la protection anti-CSRF, l'authentification, le contrôle d'accès et les pages d'erreur. Le mécanisme est plus simple que les événements du noyau de [Symfony] : l'ordre de la liste est l'ordre d'exécution ;
  • les vues sont de simples fonctions, qui reçoivent une requête et rendent une réponse ; les décorateurs ([@require_POST], [@passe_inscription_exige]) les enveloppent pour ajouter un contrôle. Nous en avons écrit plusieurs, et même une classe dont les méthodes sont des vues ([GestionPersonnes]), sans rien demander de particulier à [Django] ;
  • les formulaires sont des objets, décrits par des classes : champs, conversion des valeurs, validateurs, réaffichage de la saisie, messages d'erreur. Leur rendu passe par des gabarits que l'on peut remplacer (un moteur de rendu, un gabarit de champ) : les champs de toute l'application sont dessinés de la même façon, avec les classes de [Bootstrap] et les attributs d'accessibilité ;
  • [Django] est livré « piles comprises » : sessions, messages, traductions, cache, protection anti-CSRF, fichiers statiques, ORM, tout est là, documenté au même endroit, sans rien installer de plus. Nous n'avons ajouté que trois bibliothèques : le pilote [MySQL], [PyJWT] et [bcrypt]. L'application d'authentification [django.contrib.auth], très complète, a été laissée de côté : la base de l'étude de cas a ses propres utilisateurs, et nous voulions des jetons JWT, comme les autres versions de l'application. Écrire l'authentification soi-même, en une centaine de lignes, a montré ce qu'elle fait réellement ;
  • l'ORM de [Django] suit le modèle Active Record : un modèle est une classe liée à une table, dont chaque objet sait s'enregistrer. Plus direct que le Data Mapper de [Doctrine], il se prête aussi très bien à une base existante ([managed = False], les noms de tables et de colonnes imposés). Ses [QuerySet] paresseux, [select_related], les expressions [F()] et les [update()] conditionnels permettent d'écrire des requêtes efficaces et sûres, et la console du mode développement montre chaque ordre SQL envoyé à la base ;
  • les gabarits du DTL ont un langage volontairement limité : pas d'arithmétique, pas d'appel de fonction avec paramètres. Un gabarit ne peut qu'afficher les données qu'on lui donne : c'est une contrainte, et une garantie, qui pousse la logique dans les vues et les modèles de vue. Ce qui manque s'ajoute par des filtres et des balises écrits en [Python]. Les gabarits échappent automatiquement ce qu'ils affichent.

L'étude de cas montre qu'une application HTML classique, sans [JavaScript] indispensable, reste une solution simple et robuste : chaque page est une URL, le bouton « précédent » du navigateur fonctionne, et le serveur contrôle tout. Le peu de [JavaScript] utilisé (les fenêtres de confirmation) améliore le confort sans être nécessaire au fonctionnement. [Django] sait aussi servir des applications plus interactives, sans quitter ses gabarits (avec la bibliothèque [htmx], par exemple, que les fragments nommés de [Django] 6 servent très bien) ou avec un framework [JavaScript] côté navigateur, auquel il fournit une API JSON ([Django REST framework], [Django Ninja]) : ce sera l'objet d'autres documents.

Pour aller plus loin, on pourra consulter la documentation de [Django] (docs.djangoproject.com, traduite en français), en particulier les pages consacrées aux vues et aux URL, aux formulaires, aux gabarits, aux middlewares, à l'internationalisation, à l'ORM et à la sécurité, son tutoriel (qui présente l'interface d'administration et les migrations, laissées de côté ici), celle de [Python] (docs.python.org/fr), et comparer cette application à ses sœurs [Symfony], [Laravel], [Flask], [Spring MVC], [ASP.NET Core MVC] et [NestJS].