+z
Un mode maintenance sur Symfony 6
Un mode maintenance sur Symfony 6
Sur un site e-commerce avec une partie administrateur, je souhaite mettre en place un mode maintenance qui redirige tous les utilisateurs sur une page dédiée, mais qui permet aux admins de pouvoir tout de même se connecter.
J'ai d'abord parcouru tous les bundles existants, mais j'ai finalement décidé d'opter pour ma propre approche.
Dans mon cas, la partie administrateur du site n'a pas été réalisée à l'aide d'un bundle (EasyAdminBundle ou SonataAdmin) mais cela ne devrait pas poser problème si vous avez fait le choix d'utiliser l'un d'eux dans votre application.
Vue d'ensemble
- Entity : Entité très simple qui va permettre de stocker la valeur de la maintenance dans la base de données.
- Controller : Mise en place du simple controller permettant de définir la route ainsi que de renvoyer la view concernant la page de maintenance.
- View : On crée la vue retournée pour la page de maintenance.
- Event Subscriber : Écoute l'évènement
kernel.request
et vérifie le statut de la maintenance.
Assurez-vous d'avoir le maker-bundle
pour faciliter les opérations.
composer require --dev symfony/maker-bundle
Les différentes parties
Entity
Personnellement, j'ai choisi d'appeler cette entité GlobalOption
.
Ce n'est qu'à titre d'exemple, car je me dis que je pourrais aussi stocker d'autres informations globales concernant certaines fonctionnalités de mon application.
Par exemple : Autoriser les commentaires sur les produits ?
On commence par créer l'entité qui va simplement être composée d'une clé et d'une valeur.
C'est-à-dire 2 champs, un champ name
et un champ value
.
name
sera de typestring
value
sera de typeboolean
(traduit en tinyint pour la base MySQL)
php bin/console make:entity GlobalOption
On n'oublie pas de générer la migration.
php bin/console make:migration
Ainsi que de l'exécuter pour mettre à jour notre base de données.
php bin/console d:m:m
On se retrouve avec le fichier GlobalOption.php
que le maker-bundle a généré pour nous.
Controller
Grâce au maker bundle, on va pouvoir créer le controller rapidement.
Je l'ai appelé MaintenanceController
mais adoptez le nom qui vous plaît !
php bin/console make:controller MaintenanceController
Dans ce controller, on crée la route qui correspondra à la page de maintenance.
namespace App\Controller; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; class MaintenanceController extends AbstractController { #[Route('/maintenance', name: 'maintenance')] public function maintenance(): Response { return $this->render('maintenance/index.html.twig'); } }
#[Route('/maintenance', name: 'maintenance')]
J'utilise ici les attributs de PHP 8 pour définir mes annotations.
Donc en pratique, lorsque le mode maintenance sera activé, l'utilisateur sera redirigé vers la route portant le nom maintenance
, vers l'URL $HOST/maintenance
.
View
Je ne vais pas trop rentrer dans les détails pour la vue, vous faites comme bon vous semble...
Ici, simplement expliquer à l'utilisateur que le site est momentanément indisponible !
{% extends "base.html.twig" %} {% block title %}Site en maintenance{% endblock %} {% block body %} <div class="bg-beige rounded-lg"> <div class="py-6"> <img class="mx-auto" src="{{ asset('images/brand/logo.svg') }}" width="300" alt=""> <div class="mx-auto max-w-prose"> <p>Le site est actuellement en maintenance. Cela ne devrait pas durer trop longtemps !</p> <p>Veuillez nous excuser pour la gêne occasionnée... Merci.</p> </div> </div> </div> {% endblock %}
Event Subscriber
L'Event Subscriber, n'est rien d'autre que, par définition, une classe qui contient une ou plusieurs méthodes qui vont écouter un ou plusieurs évènements.
Je commence donc par créer un dossier EventListener
dans le dossier src
de mon application.
Puis je crée ma classe MaintenanceStatusSubscriber
à l'intérieur.
Ensuite, je réfléchis à ce dont j'ai besoin...
- J'ai besoin d'accéder au repository de mon entité (en l'occurrence, GlobalOptionRepository) pour savoir si la maintenance est activée ou non
- J'aimerais pouvoir savoir si l'utilisateur est connecté ou non
- J'aimerais pouvoir savoir si l'utilisateur à le ROLE_ADMIN ou non
- J'aimerais pouvoir émettre une RedirectResponse vers ma route
maintenance
à un moment donné
Parfait ! Je commence à écrire dans mon fichier MaintenanceStatusSubscriber
, et passe dans mon constructeur les dépendances nécessaires aux 4 besoins que j'ai cités juste au-dessus. J'en profite également pour créer les fonctions onKernelRequest()
et getSubscribedEvents()
que je remplirai après.
namespace App\EventListener; class MaintenanceStatusSubscriber implements EventSubscriberInterface { protected AuthorizationCheckerInterface $authorizationChecker; protected GlobalOptionRepository $globalOptionRepository; protected RouterInterface $router; public function __construct( AuthorizationCheckerInterface $authorizationChecker, GlobalOptionRepository $globalOptionRepository, RouterInterface $router) { $this->authorizationChecker = $authorizationChecker; $this->globalOptionRepository = $globalOptionRepository; $this->router = $router; } public function onKernelRequest() { // [...] } public static function getSubscribedEvents() { // [...] } }
Dans la fonction getSubscribedEvents()
je vais retourner pour chaque requête la fonction onKernelRequest().
namespace App\EventListener; class MaintenanceStatusSubscriber implements EventSubscriberInterface { public function __construct() { // [OK] } public function onKernelRequest() { // [...] } public static function getSubscribedEvents() { return [ RequestEvent::class => 'onKernelRequest' ]; } }
Toute ma logique va s'appliquer dans la fonction onKernelRequest()
.
Je n'oublie pas ne passer l'évènement RequestEvent en paramètre.
Je souhaite permettre aux administrateurs authentifiés de pouvoir accéder à l'ensemble du site.
Aussi, je définis dans un tableau les routes autorisées par leur nom.
Cela va me permettre de dire par exemple que la route contact sera accessible en mode maintenance même pour les visiteurs !
Il est important d'ajouter dans le tableau la route maintenance elle-même pour éviter une erreur de type TOO_MANY_REDIRECTS.
Également la route qui permet de s'authentifier sur mon application. Dans mon cas elle s'appelle security_login.
namespace App\EventListener; class MaintenanceStatusSubscriber implements EventSubscriberInterface { public function __construct() { // [OK] } public function onKernelRequest(RequestEvent $event) { // Obtenir la route actuelle $currentRoute = $event->getRequest()->get('_route'); // Vérifier le statut de la maintenance $maintenanceStatus = $this->globalOptionRepository->findOneBy(['name' => 'maintenance'])->getValue(); // Définir la redirection vers la page de maintenance $maintenanceResponse = new RedirectResponse($this->router->generate('maintenance')); // Routes autorisées durant le mode maintenance $allowedPublicMaintenanceRoutes = ['maintenance', 'contact', 'security_login']; // Si maintenance ON if ($maintenanceStatus === true && !in_array($currentRoute, $allowedPublicMaintenanceRoutes)) { // Vérifier si user est connecté ou non switch ($this->authorizationChecker->isGranted('IS_AUTHENTICATED_FULLY')) { // Si user connecté case true: // Vérifier si user est admin ou non switch ($this->authorizationChecker->isGranted('ROLE_ADMIN')) { // Si user est admin case true: break; // Si user n'est pas admin -> redirection case false: $event->setResponse($maintenanceResponse); } break; // Si user non connecté -> redirection case false: $event->setResponse($maintenanceResponse); } } } public static function getSubscribedEvents() { // [OK] } }
Conclusion
Super, ça fonctionne !
Je me suis amusé à mettre en place ce mode maintenance, qui peut facilement être amené à évoluer vers de plus amples fonctionnalités.
Il n'y avait pas de bundle, à ma connaissance, permettant de créer un mode maintenance tout en donnant l'accès à certaines parties de son application.