Skip to content

12. Conclusion

This document described the development of a MVC web application using the [Django] framework: an application whose HTML pages are generated by the server using [Django] templates, without a browser-side [JavaScript] framework. He followed the [Symfony] course step by step, which itself builds upon the courses [Laravel], [Flask], [Spring MVC], [ASP.NET Core MVC], and [NestJS]: views and routing to templates, forms, internationalization, security, and data access, followed by a comprehensive case study.

The same 28 examples and the same application were thus written using seven frameworks: in [TypeScript], in [C#], in [Java], twice in [PHP], and twice in [Python]. The concepts are the same; only the names and mechanisms differ. The correspondences between [Symfony] and [Django]:

[Symfony]
[Django]
core ([src/Kernel.php]), bundles, service container, dependency injection
settings ([config/settings.py]), applications ([INSTALLED_APPS]), imported modules, providers ([functools.cache])
[composer.json], folder [vendor], [php bin/console]
[requirements.txt], virtual environment ([.venv]), [python manage.py]
file [.env], folder [config/packages] (files YAML), [%env(...)%]
file [config/settings.py] (from [Python]), environment variables ([os.environ]), file [.env] read by us
[#[Route]] attributes of the controllers
lists [urlpatterns] ([path], [re_path], [include]), named routes, [app_name]
[render], [redirectToRoute], [generateUrl]; [path] in the views
[render], [redirect], [reverse]; [{% url %}] in the templates
object [Request], [requirements] ([{id<\d+>}]), argument resolvers, [#[MapQueryString]]
object [HttpRequest] ([GET], [POST], [headers], [COOKIES]), converters ([<int:id>]), forms as validators
Validator component (attribute constraints), Form component (types, [handleRequest], [isValid])
[django.forms] forms (fields, validators, [clean], [error_messages], [is_valid], [cleaned_data])
form theme [Bootstrap] 5
rendering engine ([FORM_RENDERER]), field template, [as_field_group]
views [Twig], [{{ }}], [{% for %}], [{% if %}]
Templates for DTL, [{{ }}], [{% for %}] ... [{% empty %}], [{% if %}]
[{% extends %}] and blocks, [{% include %}], macros
[{% extends %}] and blocks, [{% include %}], named fragments ([{% partialdef %}])
global variables, extensions [Twig] (functions, filters)
context processors, tag libraries ([templatetags]: filters, simple tags, inclusion tags)
flash messages ([addFlash], [app.flashes]), file-based sessions
[messages] application (signed cookie), sessions (files, signed cookie, database...)
kernel event listeners ([kernel.request], [kernel.controller], [kernel.response]...)
middleware ([__call__], [process_view], [process_exception]), signals ([request_started], [request_finished])
headphones: [kernel.exception], [HttpException], [createNotFoundException]
[Http404], [PermissionDenied], [BadRequest], views [handler404]..., [process_exception]
firewalls, authenticators, [access_control], [#[IsGranted]], voters
authentication and access control middleware written by us, decorators; or the [django.contrib.auth] application
anti-CSRF token for the Form component, [csrf_token()], [isCsrfTokenValid]
[CsrfViewMiddleware] middleware, [{% csrf_token %}] tag, [CSRF_FAILURE_VIEW] (403 response, or 419 in our application)
component [RateLimiter]
a limiter written to the cache
File [translations] (YAML, ICU), filter [trans], [TranslatorInterface]
file [locale] ([gettext]: [.po], [.mo]), [gettext], [gettext_lazy], [ngettext], [{% translate %}], [{% blocktranslate %}]
[intl] extension: [IntlDateFormatter], [NumberFormatter]
Formatting based on the active language (filter [date], [floatformat]), [Babel]
Cache component (PSR-6 and contracts), Lock component
cache for [Django] ([LocMemCache], [FileBasedCache]...), [get_or_set]
[Doctrine]: Data Mapper entities, entity manager, DQL, and QueryBuilder
ORM from [Django]: Active Record models, managers ([objects]), [QuerySet], [select_related], [F()]
[wrapInTransaction(...)], [flush]
[transaction.atomic()], [select_for_update()]
[#[ORM\Version]], managed by [Doctrine] ([OptimisticLockException])
[update()], version-dependent ([filter(version=...)]), written to the DAO layer
[php -S localhost:8000 -t public], or [symfony server:start]
[python manage.py runserver]

Regarding [Symfony], the key points to note are:

  • the [Django] application is loaded once, when the server starts up, then remains in memory and processes all requests (example 01): the objects created at startup (settings, routes, middleware, services) are shared by all requests. It’s simple and fast, but it requires distinguishing between what belongs to the application (shared, and therefore without state specific to a request) and what belongs to a request (the [request] object, the local variables of the views), and to remember that a production server runs multiple processes, which share only what is stored outside of them (the database, the file cache);
  • no service container: services are [Python] modules that are imported, and provider functions that create a unique object on the first request. A module’s dependencies are visible in its [import] files; replacing them for testing purposes is done via a setting (example 06);
  • The configuration is a [Python] or [config/settings.py] file: a sequence of assignments that can be calculated, commented on, or split up. The [python manage.py diffsettings] command displays what differs from the default values;
  • The processing of a request passes through a list of middleware components in the order specified in the [MIDDLEWARE] list, and then in reverse order for the response: this is where language settings, anti-CSRF protection, authentication, access control, and error pages are handled. The mechanism is simpler than the kernel events in [Symfony]: the order of the list is the order of execution;
  • Views are simple functions that receive a request and return a response; decorators ([@require_POST], [@registration_pass_required]) wrap them to add control. We have written several of them, and even a class whose methods are views ([PersonManagement]), without requiring anything specific from [Django];
  • Forms are objects, described by classes: fields, value conversion, validators, input redisply, and error messages. They are rendered using templates that can be replaced (a rendering engine, a field template): fields throughout the application are rendered in the same way, using the classes from [Bootstrap] and accessibility attributes;
  • [Django] comes “ready to use”: sessions, messages, translations, cache, anti-CSRF protection, static files, ORM—everything is included, documented in one place, with no additional installation required. We’ve added only three libraries: the [MySQL] driver, [PyJWT], and [bcrypt]. The very comprehensive [django.contrib.auth] authentication application was left out: the case study database has its own users, and we wanted JWT tokens, just like the other versions of the application. Writing the authentication code from scratch, in about a hundred lines, demonstrated exactly what it does;
  • the ORM from [Django] follows the Active Record pattern: a model is a class linked to a table, where each object knows how to save itself. More straightforward than the Data Mapper in [Doctrine], it also works very well with an existing database ([managed = False], where table and column names are predefined). Its lazy [QuerySet], [select_related], [F()] expressions, and the conditional [update()] statements allow you to write efficient and safe queries, and the development mode console displays every SQL command sent to the database;
  • DTL templates use a deliberately limited language: no arithmetic, no function calls with parameters. A template can only display the data provided to it: this is both a constraint and a guarantee that drives logic into views and view models. Any missing information is added via filters and tags written in [Python]. Templates automatically escape whatever they display.

The case study shows that a classic HTML application, without the essential [JavaScript], remains a simple and robust solution: each page is a URL, the browser’s “Back” button works, and the server controls everything. The minimal use of [JavaScript] (confirmation windows) enhances the user experience without being necessary for functionality. [Django] can also support more interactive applications without straying from its templates (using the [htmx] library, for example, the named fragments from [Django] 6 work very well) or with a browser-side [JavaScript] framework, to which it provides a API JSON ([Django REST framework], [Django Ninja]): this will be covered in other documents.

For further information, please refer to the documentation for [Django] (docs.djangoproject.com, translated into French), particularly the pages dedicated to views and URL, forms, templates, middleware, internationalization, ORM, and security, its tutorial (which covers the administration interface and migrations, which are not covered here), the [Python] tutorial (docs.python.org/fr), and compare this application to its sister applications [Symfony], [Laravel], [Flask], [Spring MVC], [ASP.NET Core MVC], and [NestJS].