
Un mode maintenance sur Symfony 6
Mettre en place un mode maintenance sur son application Symfony
27 avril 2022
Un mode maintenance sur Symfony 6
Sur un site ecommerce 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.
Voir le contenu
namespace App\Entity;
use App\Repository\GlobalOptionRepository;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity(repositoryClass=GlobalOptionRepository::class)
*/
class GlobalOption
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\Column(type="string", length=255)
*/
private $name;
/**
* @ORM\Column(type="boolean", nullable=true)
*/
private $value;
public function getId(): ?int
{
return $this->id;
}
public function getName(): ?string
{
return $this->name;
}
public function setName(string $name): self
{
$this->name = $name;
return $this;
}
public function getValue(): ?bool
{
return $this->value;
}
public function setValue(?bool $value): self
{
$this->value = $value;
return $this;
}
}
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');
}
}
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.
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 !
É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]
}
}
Voir le contenu
namespace App\EventListener;
use App\Repository\GlobalOptionRepository;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
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(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
case false:
$event->setResponse($maintenanceResponse);
}
break;
// Si user non connecté
case false:
$event->setResponse($maintenanceResponse);
}
}
}
public static function getSubscribedEvents()
{
return [
RequestEvent::class => 'onKernelRequest'
];
}
}
Conclusion
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.
Mis à jour le : 27 avril 2022