Retour au blog
Space
+
z

Un mode maintenance sur Symfony 6

Un mode maintenance sur Symfony 6

Publié le 1er Fév 2023 Mis à jour le 24 Fév 2024

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 type string
  • value sera de type boolean (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.