3min.

Désactiver des routes Symfony en production

Dans certains cas, il peut être nécessaire d’avoir des actions qui ne sont disponibles que lorsqu’on développe en local, mais pas en production. On peut penser au styleguide ou à des pages de debug par exemple. Voyons ensemble comment implémenter cela.

Section intitulée la-solution-rapideLa solution rapide

Pour désactiver une route rapidement, vous pourriez être tenté d’utiliser l’attribut When('dev') au niveau du contrôleur :

use Symfony\Component\DependencyInjection\Attribute\When;
// …

#[When('dev')]
class StyleguideController extends AbstractController
{

Bien que cela permette que l’action ne soit plus exécutable en environnement de prod, elle a plusieurs inconvénients. Déjà, cela désactive complètement tout le controleur, et pas juste une action (ce qui n’est peut-être pas un souci si vous utilisez le pattern ADR). Mais surtout, cela désactive le service attaché au contrôleur, sans supprimer la route associée à l’action. Lorsque que l’on accède à notre styleguide en env de prod, cela va donc déclencher l’exception suivante :

Uncaught PHP Exception InvalidArgumentException: "The controller for URI "/styleguide/" is not callable: Controller "App\Controller\StyleguideController" cannot be fetched from the container because it is private. Did you forget to tag the service with "controller.service_arguments"?

Ce qui provoque une erreur 500. Je vous propose donc une autre solution qui a le mérite d’être plus granulaire et de provoquer une erreur 404 bien plus adaptée.

Section intitulée une-solution-plus-eleganteUne solution plus élégante

Pour ce faire, nous allons utiliser le système de condition au niveau de nos routes. Si la condition n’est pas respectée, alors la route ne sera pas active et une erreur 404 sera retournée.

Ces conditions vont utiliser le moteur de l’Expression Language et permettent de faire des conditions assez poussées, comme le montre la documentation :

#[Route('/action', name: action, condition: "context.getMethod() in ['GET', 'HEAD'] and request.headers.get('User-Agent') matches '/firefox/i'",
    // expressions can also include config parameters:
    // condition: "request.headers.get('User-Agent') matches '%app.allowed_browsers%'"
)]

Par défaut, il est possible d’accéder à des paramètres de la requête ou la route (variables request, params et context), à des vars d’env (env('XXX')) ou encore à des services (service('xxx')).

Dans notre cas, on souhaite que la condition se base sur l’env Symfony, et plus précisément, quand le mode debug est activé (ce qui est le cas par défaut pour les environnements dev et test). Mais il semblerait qu’il ne soit pas possible d’accéder aux paramètres enregistrés dans le container (peut-être une idée de contribution dans Symfony ?), donc pas possible de faire une condition à base de %kernel.debug%.

Pour palier ça, on va chercher à appeler la méthode isDebug() du kernel directement :

#[Route(path: '/styleguide', name: 'styleguide', condition: "service('kernel').isDebug()")]

Mais si on fait ça, on se retrouve avec l’erreur suivante :

Uncaught PHP Exception Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException: "Service "kernel" not found: the container inside "Symfony\Component\DependencyInjection\Argument\ServiceLocator" is a smaller service locator that is empty…" at ServiceLocator.php line 133"

En effet, la fonction service() des conditions n’a pas accès au conteneur principal de l’application Symfony, mais à un container différent et plus petit dans lequel il faut explicitement définir les services présents. Heureusement, cela se fait facilement avec l’attribut AsRoutingConditionService à ajouter sur le service qui doit être mis dans ce conteneur dédié. On modifie donc notre kernel, en définissant au passage l’alias (kernel dans notre cas) que prendra le service dans le conteneur :

use Symfony\Component\HttpKernel\Kernel as BaseKernel;

+#[AsRoutingConditionService('kernel')]
class Kernel extends BaseKernel
{
    use MicroKernelTrait;

Voilà, on est maintenant capable de désactiver certaines routes facilement en fonction de l’environnement Symfony actuel.

À bientôt pour de nouvelles astuces 😉


Edit du 04/10/2024 :

Section intitulée la-vraie-bonne-solutionLa vraie bonne solution 😅

L’attribut Route de Symfony dispose en fait d’une propriété env depuis Symfony 5.3 qui permet de faire plus ou moins la même chose avec encore moins de code à écrire.

Commentaires et discussions

Nos formations sur ce sujet

Notre expertise est aussi disponible sous forme de formations professionnelles !

Voir toutes nos formations

Ces clients ont profité de notre expertise