Skip to content

12. Conclusion

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

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

[Laravel]
[Symfony]
[bootstrap/app.php], service providers,
([src/Kernel.php]), bundles, compiled container ([config/services.yaml], [#[Autowire]])
[composer.json], folder [vendor], [php artisan]
[composer.json], folder [vendor], [php bin/console]
file [.env], folder [config] (files [PHP]), functions [env] and [config]
file [.env], folder [config/packages] (files YAML), [%env(...)%], container settings
[routes/web.php], named routes, route groups
controller attributes [#[Route]], on the action and on the class
[view], [redirect], [route]
[render], [redirectToRoute], [generateUrl] ([AbstractController]); [path] in views
object [Request], constraints ([whereNumber]), model link
object [Request], [requirements] ([{id<\d+>}]), argument resolvers, [#[MapQueryString]], [#[MapRequestPayload]]
[Validator], form requests ([FormRequest]), rules, [$errors], [old()]
Validator component (attribute constraints), Form component (types, [handleRequest], [isValid])
views [Blade], [{{ }}], [@foreach], [@if]
views [Twig], [{{ }}], [{% for %}], [{% if %}]
[@extends] and sections, [@include], components ([<x-...>])
[{% extends %}] and blocks, [{% include %}], macros
view composer ([View::composer])
global variables of [Twig], extensions [Twig] (functions, filters)
session flash data ([session()->flash], [with])
flash messages ([addFlash], [app.flashes]), session in files
(global, group, route), [terminate] method
kernel event listeners ([kernel.request], [kernel.controller], [kernel.response], [kernel.terminate]...)
[withExceptions], [render], [abort]
Listeners: [kernel.exception], [HttpException], [createNotFoundException]
guards ([Auth::viaRequest]), middleware [auth], [Gate], facade [Hash]
firewall, authenticators, [access_control], [#[IsGranted]], voters, hashing ([security.yaml])
[PreventRequestForgery] middleware, [@csrf] directive (419 response)
Form component token, [csrf_token()] and [isCsrfTokenValid] (403 response, or 419 in our application)
[RateLimiter] (facade)
Component [RateLimiter] (limiters declared in [framework.yaml])
folder [lang], function [__()], [App::setLocale]
folder [translations] (YAML, ICU), filter [trans], [TranslatorInterface], query language
[Carbon] ([isoFormat]), [Number]
Extension [intl]: [IntlDateFormatter], [NumberFormatter]
Cache ([Cache], [Cache::lock])
Cache component (PSR-6 and contracts), Lock component
[Eloquent]: Active Record models, relationships ([belongsTo], [with])
[Doctrine]: Data Mapper entities, entity manager, DQL and QueryBuilder, relationships ([ManyToOne], [OneToOne])
[DB::transaction(...)]
[wrapInTransaction(...)], [flush]
[WHERE version = ?] and [version + 1], written to layer DAO
[#[ORM\Version]], managed by [Doctrine] ([OptimisticLockException])
[php artisan serve]
[php -S localhost:8000 -t public], or [symfony server:start]

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

  • [Symfony] is a set of independent components, assembled into bundles. Each component is self-contained, and [Laravel] uses several of them ([HttpFoundation], [HttpKernel], [Routing], [Console]...) : Readers of the previous lesson will recognize the same objects here: [Request] and [Response]. An application named [Symfony] installs only the components it needs: the examples started with just [framework-bundle], and then added, one by one, [Twig], validation, forms, security, and [Doctrine];
  • Everything goes through the service container and dependency injection: no facades or global functions, but services passed to the constructor and declared by type (autowiring). A class’s dependencies are always visible in its constructor. The container is compiled into code [PHP] and cached: this offsets the cost of rebuilding the application with each request (Example 01);
  • The processing of a request consists of a sequence of kernel events to which listeners respond: this is where language, security, application controls (the pass), and error pages are handled. The mechanism is more explicit than the middleware in [Laravel]: each listener chooses its event and priority, and the command [debug:event-dispatcher] displays the order of all listeners;
  • the configuration is explicit, in YAML (or [PHP]) files that describe each bundle: the firewall, access rules, limiters, and session, [Doctrine]. The commands [debug:router], [debug:container], and [debug:config] display what [Symfony] has interpreted;
  • The [PHP] attributes describe the code: routes ([#[Route]]), constraints ([#[Assert\NotBlank]]), entities ([#[ORM\Column]]), listeners ([#[AsEventListener]]), permissions ([#[IsGranted]]), and injection ([#[Autowire]]). We’ve even written some ([#[Timed]], [#[RegistrationPassRequired]]);
  • forms are objects, described by classes such as: fields, constraints, value conversion, input redispatch, error messages, and anti-CSRF tokens. This is more code than the form requests in [Laravel] for a small form, and much less for a large one: a theme ([Bootstrap] 5) styles all fields the same way;
  • security is a fully-fledged component: a firewall, authenticators that verify the user, access rules, a role hierarchy, and voters. We adapted it to our needs (a JWT token in a cookie) using an authenticator consisting of a few dozen lines, without losing any of the other features: [access_control], [#[IsGranted]], [is_granted] in the views;
  • [Doctrine] follows the Data Mapper pattern: the entities are ordinary [PHP] objects that are unaware of the database; it is the entity manager that reads them, tracks their changes, and writes them all together, in a single transaction, at the time of the [flush]. More abstract than [Eloquent]’s Active Record, it clearly separates objects from their records and supports mechanisms such as optimistic locking. The SQL order log allows you, as with [Eloquent], to verify what is sent to the database;
  • the [Twig] views have their own language, which is more limited than the [PHP] used in the [Blade] views: a view can only display the data provided to it. This is both a constraint and a guarantee: the logic remains within the classes. They are compiled into [PHP], cached, and automatically escape the content 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 limited use of [JavaScript] (confirmation windows) enhances the user experience without being necessary for operation. [Symfony] can also support more interactive applications without leaving [Twig] ([Symfony UX]: [Turbo], [Stimulus], Live Components) or with a browser-side [JavaScript] framework, to which it provides a API JSON ([API Platform]): this will be covered in other documents.

For further information, please refer to the [Symfony] documentation (symfony.com/doc), particularly the pages dedicated to the service container, kernel events, forms, security, and [Doctrine], as well as the documentation for [Twig] (twig.symfony.com), the documentation for [Doctrine] (doctrine-project.org), the documentation for [PHP] (php.net/manual/en), and compare this application with its counterparts [Laravel], [Flask], [Spring MVC], [ASP.NET Core MVC], and [NestJS].