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.