Christophe Meneses - Développeur
  • Liste des articles
  • Contact
  • Linkedin
  • Github
  • Flux RSS
Tous les articles PHP / Symfony Bonnes pratiques Symfony : les contrôleurs
PHP Symfony

Bonnes pratiques Symfony : les contrôleurs

Publié le 28/11/2015. Dernière mise à jour le 02/04/2018.

Cette quatrième partie est une traduction personnelle des recommandations officielles sur les contrôleurs.

Symfony < 4.0

Attention si vous êtes sur une version plus récente, allez directement au chapitre pour les versions >= 4.0.

Introduction

Symfony suit le principe de « contrôleurs minces et modèles imposants ». Cela signifie que les contrôleurs doivent contenir uniquement la fine « couche de colle » qui va coordonner les différentes parties de l'application.

En règle générale, vous devriez suivre la règle 5-10-20. Les contrôleurs devraient définir 5 variables ou moins, contenir 10 actions ou moins et inclure 20 lignes de code ou moins pour chaque action. Ce n'est pas une science exacte, mais cela devrait vous aider à vous rendre compte quand il est nécessaire de refactoriser le code en le déplaçant du contrôleur vers un service.

Votre contrôleur doit étendre le contrôleur de base de FrameworkBundle et utiliser si possible les annotations pour configurer le routage, le cache et la sécurité.

Coupler les contrôleurs avec le framework sous-jacent vous permet d'exploiter toutes ses fonctionnalités et d'augmenter votre productivité.

Vos contrôleurs doivent être minces et contenir uniquement une fine couche de colle. Passer des heures à essayer de les découpler du framework n'aura aucun bénéfice pour vous à long terme. La quantité de temps perdu n'en vaut pas la peine.

De plus, utiliser les annotations pour le routage, le cache et la sécurité permet de simplifier la configuration. Vous ne devez pas parcourir des dizaines de fichiers créés dans différents formats (YAML, XML, PHP) : toute la configuration est là où vous en avez besoin et utilise uniquement un format.

De manière globale, cela signifie que vous devez découpler votre code métier du framework. Dans le même temps, vous devez coupler vos contrôleurs et votre routage au framework afin d'en obtenir le meilleur.

Configuration du routage

Pour charger des routes configurées via des annotations dans vos contrôleurs, ajoutez la configuration suivante dans le fichier principal de configuration du routage :

# app/config/routing.yml
app:
    resource: "@AppBundle/Controller/"
    type:     annotation

Cette configuration charge les annotations présentes dans n'importe quel contrôleur stocké dans le répertoire src/AppBundle/Controller/ et ses sous-répertoires. Si votre application définit beaucoup de contrôleurs, il est parfaitement correct de les organiser en sous-répertoires :

/
├─ ...
└─ src/
   └─ AppBundle/
      ├─ ...
      └─ Controller/
         ├─ DefaultController.php
         ├─ ...
         ├─ Api/
         │  ├─ ...
         │  └─ ...
         └─ Backend/
            ├─ ...
            └─ ...

Configuration des templates

N'utilisez pas l'annotation @Template pour configurer le template utilisé par un contrôleur.

L'annotation @Template est utile, mais implique également de la magie en interne. Nous ne pensons pas que ses bénéfices valent la magie employée, et ainsi nous vous déconseillons de l'utiliser.

La plupart du temps, l'annotation @Template est utilisée sans aucun argument, ce qui rend plus difficile de savoir quel template est affiché. Pour les débutants, elle rend également moins évident qu'un contrôleur doit toujours retourner un objet Response (sauf si vous utilisez une couche de présentation).

À quoi ressemble le contrôleur

En prenant en compte tous ces éléments, voici un exemple de comment doit ressembler le contrôleur de la page d'accueil de votre application :

namespace AppBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;

class DefaultController extends Controller
{
    /**
     * @Route("/", name="homepage")
     */
    public function indexAction()
    {
        $posts = $this->getDoctrine()
            ->getRepository('AppBundle:Post')
            ->findLatest();

        return $this->render('default/index.html.twig', array(
            'posts' => $posts
        ));
    }
}

Utiliser le ParamConverter

Si vous utilisez Doctrine, vous pouvez utiliser optionnellement le ParamConverter pour récupérer automatiquement une entité et la passer en argument au contrôleur.

Utilisez le ParamConverter pour récupérer automatiquement une entité Doctrine quand c'est simple et pratique.

Par exemple :

use AppBundle\Entity\Post;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;

/**
 * @Route("/{id}", name="admin_post_show")
 */
public function showAction(Post $post)
{
    $deleteForm = $this->createDeleteForm($post);

    return $this->render('admin/post/show.html.twig', array(
        'post'        => $post,
        'delete_form' => $deleteForm->createView(),
    ));
}

Normalement, vous attendez un argument $id dans l'action showAction. À la place de cela, en créant un nouvel argument $post et en le typant avec la classe Post (qui est une entité Doctrine), le ParamConverter va automatiquement récupérer un objet dont l'attribut id correspond à la valeur de {id}. Il montrera également une page 404 si aucun objet Post n'a pu être trouvé.

Quand les choses deviennent plus compliqué

Cela fonctionne sans aucune configuration parce que le nom du joker {id} correspond au nom de l'attribut de l'entité. Si ce n'est pas le cas, ou si vous avez une logique plus compliqué, la meilleure chose à faire est de faire manuellement une requête pour récupérer l'entité. Dans notre application, nous avons cette situation dans le contrôleur CommentController :

/**
 * @Route("/comment/{postSlug}/new", name = "comment_new")
 */
public function newAction(Request $request, $postSlug)
{
    $post = $this->getDoctrine()
        ->getRepository('AppBundle:Post')
        ->findOneBy(array('slug' => $postSlug));

    if (!$post) {
        throw $this->createNotFoundException();
    }

    // ...
}

Vous pouvez aussi utiliser la configuration de ParamConverter, qui est très flexible :

use AppBundle\Entity\Post;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Symfony\Component\HttpFoundation\Request;

/**
 * @Route("/comment/{postSlug}/new", name = "comment_new")
 * @ParamConverter("post", options={"mapping": {"postSlug": "slug"}})
 */
public function newAction(Request $request, Post $post)
{
    // ...
}

En conclusion, le raccourci ParamConverter est idéal pour des situations simples. Mais vous ne devriez pas oublier que récupérer directement des entités est toujours très facile.

Pre et Post Hooks

Si vous avez besoin d'exécuter du code avant ou après l'exécution de vos contrôleurs, vous pouvez utiliser le composant EventDispatcher pour mettre en place des pré et post filtres.


Symfony >= 4.0

Introduction

Symfony suit le principe de « contrôleurs minces et modèles imposants ». Cela signifie que les contrôleurs doivent contenir uniquement la fine « couche de colle » qui va coordonner les différentes parties de l'application.

Les méthodes de vos contrôleurs doivent seulement appeler d'autres services, lancer des événements si besoin, retourner une réponse mais elles ne doivent pas contenir de code métier. Si c'est le cas, il faut refactoriser en externalisant ce code dans des services.

Vos contrôleurs doivent étendre le contrôleur de base AbstractController fourni par Symfony et utiliser à chaque fois que c'est possible les annotations pour configurer le routage, le cache et la sécurité.

Coupler les contrôleurs avec le framework sous-jacent vous permet d'exploiter toutes ses fonctionnalités et d'augmenter votre productivité.

Vos contrôleurs doivent être minces et contenir uniquement une fine couche de colle. Passer des heures à essayer de les découpler du framework n'aura aucun bénéfice pour vous à long terme. La quantité de temps perdu n'en vaut pas la peine.

De plus, utiliser les annotations pour le routage, le cache et la sécurité permet de simplifier la configuration. Vous ne devez pas parcourir des dizaines de fichiers créés dans différents formats (YAML, XML, PHP) : toute la configuration est là où vous en avez besoin et utilise uniquement un format.

De manière globale, cela signifie que vous devez découpler votre code métier du framework. Dans le même temps, vous devez coupler vos contrôleurs et votre routage au framework afin d'en obtenir le meilleur.

Nommage des contrôleurs

Ne rajoutez pas le suffixe Action aux méthodes des actions du contrôleur.

Le première version de Symfony demandait que le nom des actions finissent par Action (par exemple newAction(), showAction()). Ce suffixe est devenu facultatif quand les annotations ont été introduites dans les contrôleurs. Dans les applications Symfony modernes, ce suffixe n'est plus requis ni recommandé, par conséquent vous pouvez le supprimer sans problème.

Configuration du routage

Pour charger des routes configurées via des annotations dans vos contrôleurs, ajoutez la configuration suivante dans le fichier principal de configuration du routage :

# config/routes.yaml
controllers:
    resource: '../src/Controller/'
    type:     annotation

Cette configuration charge les annotations présentes dans n'importe quel contrôleur stocké dans le répertoire src/Controller/ et ses sous-répertoires. Si votre application définit beaucoup de contrôleurs, il est parfaitement correct de les organiser en sous-répertoires :

/
├─ ...
└─ src/
   ├─ ...
   └─ Controller/
      ├─ DefaultController.php
      ├─ ...
      ├─ Api/
      │  ├─ ...
      │  └─ ...
      └─ Backend/		 
         ├─ ...
         └─ ...

Configuration des templates

N'utilisez pas l'annotation @Template pour configurer le template utilisé par un contrôleur.

L'annotation @Template est utile, mais implique également de la magie en interne. Nous ne pensons pas que ses bénéfices valent la magie employée, et ainsi nous vous déconseillons de l'utiliser.

La plupart du temps, l'annotation @Template est utilisée sans aucun argument, ce qui rend plus difficile de savoir quel template est affiché. Pour les débutants, elle rend également moins évident qu'un contrôleur doit toujours retourner un objet Response (sauf si vous utilisez une couche de présentation).

À quoi ressemble le contrôleur

En prenant en compte tous ces éléments, voici un exemple de comment doit ressembler le contrôleur de la page d'accueil de votre application :

namespace App\Controller;

use App\Entity\Post;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;

class DefaultController extends AbstractController
{
    /**
     * @Route("/", name="homepage")
     */
    public function index()
    {
        $posts = $this->getDoctrine()
            ->getRepository(Post::class)
            ->findLatest();

        return $this->render('default/index.html.twig', [
            'posts' => $posts,
        ]);
    }
}

Récupération de service

Si vous étendez le contrôleur de base AbstractController, vous ne pouvez pas accéder aux services via le conteneur de service $this->container->get() ou $this->get(). À la place, vous devez utiliser l'injection de dépendance pour récupérer les services. Le plus facile est de typer les arguments des méthodes.

N'utilisez pas $this->container->get() ou $this->get() pour récupérer des services. À la place, utilisez l'injection de dépendance.

En évitant de récupérer les services via le conteneur, vous pouvez configurer vos services en privé, ce qui a plusieurs avantages.

Utiliser le ParamConverter

Si vous utilisez Doctrine, vous pouvez utiliser optionnellement le ParamConverter pour récupérer automatiquement une entité et la passer en argument au contrôleur.

Utilisez le ParamConverter pour récupérer automatiquement une entité Doctrine quand c'est simple et pratique.

Par exemple :

use App\Entity\Post;
use Symfony\Component\Routing\Annotation\Route;

/**
 * @Route("/{id}", name="admin_post_show")
 */
public function show(Post $post)
{
    $deleteForm = $this->createDeleteForm($post);

    return $this->render('admin/post/show.html.twig', [
        'post' => $post,
        'delete_form' => $deleteForm->createView(),
    ]);
}

Normalement, vous attendez un argument $id dans l'action show. À la place de cela, en créant un nouvel argument $post et en le typant avec la classe Post (qui est une entité Doctrine), le ParamConverter va automatiquement récupérer un objet dont l'attribut id correspond à la valeur de {id}. Il montrera également une page 404 si aucun objet Post n'a pu être trouvé.

Quand les choses deviennent plus compliqué

Cela fonctionne sans aucune configuration parce que le nom du joker {id} correspond au nom de l'attribut de l'entité. Si ce n'est pas le cas, ou si vous avez une logique plus compliqué, la meilleure chose à faire est de faire manuellement une requête pour récupérer l'entité. Dans notre application, nous avons cette situation dans le contrôleur CommentController :

/**
 * @Route("/comment/{postSlug}/new", name="comment_new")
 */
public function new(Request $request, $postSlug)
{
    $post = $this->getDoctrine()
        ->getRepository(Post::class)
        ->findOneBy(['slug' => $postSlug]);

    if (!$post) {
        throw $this->createNotFoundException();
    }

    // ...
}

Vous pouvez aussi utiliser la configuration de ParamConverter, qui est très flexible :

use App\Entity\Post;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;

/**
 * @Route("/comment/{postSlug}/new", name="comment_new")
 * @ParamConverter("post", options={"mapping"={"postSlug"="slug"}})
 */
public function new(Request $request, Post $post)
{
    // ...
}

En conclusion, le raccourci ParamConverter est idéal pour des situations simples. Mais vous ne devriez pas oublier que récupérer directement des entités est toujours très facile.

Pre et Post Hooks

Si vous avez besoin d'exécuter du code avant ou après l'exécution de vos contrôleurs, vous pouvez utiliser le composant EventDispatcher pour mettre en place des pré et post filtres.

Autres articles sur les bonnes pratiques

  1. Création d'un projet
  2. La configuration
  3. Organiser sa logique métier
  4. Les contrôleurs
  5. Les templates
  6. Les formulaires
  7. L'internationalisation
  8. La sécurité
  9. Les ressources web
  10. Les tests
Début de l'article

Tous les articles PHP / Symfony Bonnes pratiques Symfony : les contrôleurs

Liste des articles par catégorie

  1. Tous 124
  2. Apache2
  3. APC1
  4. Assetic2
  5. Bash2
  6. CentOS9
  7. Composer6
  8. CSS1
  9. Debian1
  10. Deployer1
  11. Design Pattern11
  12. Docker6
  13. Doctrine14
  14. Elasticsearch3
  15. Git6
  16. Google Charts1
  17. Hardware1
  18. Hébergement1
  19. JavaScript1
  20. jQuery4
  21. Kibana2
  22. Logstash1
  23. Machine Learning1
  24. MariaDB2
  25. Memcached2
  26. MySQL3
  27. Nginx2
  28. Panther3
  29. PHP59
  30. PHP_CodeSniffer1
  31. PHP-FPM2
  32. PhpMyAdmin1
  33. PhpStorm3
  34. PHPUnit6
  35. PostgreSQL2
  36. RabbitMQ2
  37. SQL1
  38. SVN4
  39. Sybase ASE1
  40. Symfony56
  41. Twig3
  42. Ubuntu14
  43. Vue.js2
  44. Vuex1
  45. Webpack Encore1
  46. Xdebug5

Derniers articles publiés

PHP
Différence entre les mots clés self et static dans le langage PHP

Symfony / Docker / Nginx
Migrer un projet Symfony du protocole HTTP 1.1 au protocole HTTP 2

Vue.js
Utiliser un filtre Vue.js directement dans le JavaScript

PHP / Symfony
Supprimer les messages de dépréciation de type : The "sensio_framework_extra.routing.loader.annot_*" service is deprecated since version 5.2

Symfony / RabbitMQ
Exemple d'utilisation du composant Messenger pour envoyer des emails en asynchrone avec RabbitMQ

PHP Symfony MariaDB HTML5 CSS3 JavaScript Bootstrap

© 2014 - 2019 réalisé par Christophe MENESES