Apprendre Symfony 6 : Contrôleurs

En réceptionnant une requête HTTP, les contrôleurs de Symfony sont chargés de coordonner la réponse HTTP adaptée.

Icône de calendrier
Intermédiaire
13 chapitres

Rôle d’un contrôleur

Symfony est basé sur le protocole HTTP, développé pour le World Wide Web. L’acronyme HTTP signifie HyperText Transfer Protocol.

Ce protocole définit la communication entre un client (navigateur) et un serveur.

  1. L’ordinateur de l’internaute utilise le navigateur pour envoyer une requête HTTP à un serveur web.
  2. Le serveur génère puis retourne une réponse HTTP au client.

Dans Symfony, c’est le contrôleur, situé sur le serveur, qui oeuvre à la création de cette réponse.

Schéma - Protocole HTTP

Le contrôleur est le chef d’orchestre qui implémente la logique métier de notre application via les modèles et vues (cela va encore plus loin avec la notion de services).

Son rôle est simple : il reçoit une requête de la part du routeur, puis coordonne la construction d’une réponse HTTP adaptée.

Si par réponse on pense la plupart du temps à une page .html, ce n’est pas systématique. Cela peut aussi être un fichier .json (API), un fichier .xml, un téléchargement de fichier, une redirection, une page 404, etc.

Squelette d’un contrôleur

Voici la structure élémentaire d’un contrôleur dans Symfony :

DemoController.php
copié !
namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

class DemoController extends AbstractController {

	#[Route('/demo', name: 'app_demo')]
	public function index(): Response {
		return $this->render('demo/index.html.twig', [
			'controller_name' => 'DemoController',
		]);
	}

}

Namespace

namespace App\Controller;

Un framework contient beaucoup de bibliothèques !

Une bibliothèque est une sorte de boîte noire : on ne sait pas ce qu’elle contient, mais on sait ce qu’elle fait. Il est alors possible d’avoir plusieurs fichiers, plusieurs classes qui ont le même nom.

Il n’est pas impossible que vous nommiez votre classe de contrôleur de la même manière qu’une classe qui existe déjà dans une de vos bibliothèques. Par exemple UserController, MainController, AdminController

Le rôle du namespace est de dire à Symfony : « cette classe appartient à cette famille, à ce répertoire, à cet espace de nom ». Un namespace permet donc d’identifier un fichier (ici notre contrôleur) de manière précise. Il est déclaré avec le mot-clé namespace.

Import de classe

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

Pour exploiter des ressources qui se trouvent dans des fichiers de code distincts, il faut les mettre en relation. C’est le principe de l’inclusion de fichiers (cf: require et include).

C’est le rôle de ce use, qui va spécifier que l’on exploite une classe provenant d’un espace de nom particulier.

Vous voyez que notre contrôleur basique importe les classes AbstractController, Response et Route, qui proviennent de la bibliothèque du Framework Symfony.

  • AbstractController est une classe dont hérite notre contrôleur.
  • Response est une classe facilitant la création d’une réponse HTTP.
  • Route nous permet de déclarer notre système de routage dans notre contrôleur via des annotations / attributs PHP.

Classe du contrôleur

class DemoController extends AbstractController { ... }

La classe du contrôleurs est déclarée avec le mot-clé class.

Elle étend, hérite (extends) de AbstractController, ce qui lui confère tout un tas de propriétés et méthodes extrêmement utiles. Cette classe paraît vide mais est en réalité déjà très riche.

Au sein de nos contrôleurs, on fera appel à cette classe parente dans de nombreuses situations avec $this.

Attributs PHP

#[Route('/demo', name: 'app_demo')]

Les attributs PHP sont des commentaires interprétés (ou métadonnées), qui nous permettent de spécifier le comportement d’un élément (ici classe ou méthode). Nous les avons utilisés pour créer notre système de routage, mais leurs pouvoirs ne s’arrêtent pas là ! ⚡

Méthodes

public function index(): Response {
	return $this->render('demo/index.html.twig', [
		'controller_name' => 'DemoController',
	]);
}

Une méthode est une fonction qui appartient à une classe.

Dans un contrôleur, elles sont la plupart du temps rattachées à une route qui va permettre de les déclencher, de les appeler. Ce sont les méthodes qui vont définir la logique d’une fonctionnalité, et ce sont aussi elles qui vont retourner une réponse HTTP.

Construire ses contrôleurs

Un contrôleur :

  1. Reçoit une requête HTTP
  2. Effectue éventuellement des opérations intermédiaires
  3. Renvoie une réponse HTTP

Requêtes

Requêtes simples

Nos requêtes HTTP contiennent des informations à destination de notre contrôleur. Par « requêtes simples », on entend des requêtes qui ne récupèrent pas de paramètres, ou qui récupèrent des paramètres de routes.

Sans paramètres de route

C’est le cas des URLs statiques.

copié !
#[Route('/mentions-legales', name: 'cgu')]
public function cgu(): Response {
	// On se contente de renvoyer une page HTML statique
}

monsite.fr/mentions-legales : cette page sera identique pour tous mes visiteurs, elle n’a pas de paramètres de route.

Une requête de ce type ne nécessite pas à notre contrôleur de récupérer dans notre requête des informations particulières.

Avec paramètres de route

C’est le cas des URLs dynamiques.

copié !
#[Route('/blog/{slug}', name: 'article_show')]
public function showArticle(string $slug): Response { ... }

L’argument {slug} de la requête est récupéré par la route, qui le transmettra à la méthode de contrôleur via son paramètre $slug.

Mais si nous souhaitons désormais récupérer des paramètres d’un formulaire soumis en POST, ou encore des paramètres d’URL qui ne font pas partie de la route comme les fameux GET (monsite.com/blog/tous?tag=symfony), nous aurons besoin de l’objet Request.

Requêtes complexes avec l’objet Request

Charger l’objet Request
Importation de la classe Request

On explicite avec use dans quel namespace se trouve la classe Request.

copié !
// Importation de la classe Request
use Symfony\Component\HttpFoundation\Request;
Instanciation de la classe Request en typant la variable $request
DemoController.php
copié !
namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\HttpFoundation\Request;

class DemoController extends AbstractController {

	#[Route('/', name: 'home')]
	public function home(Request $request): Response {  
		// Je peux utiliser ici l'objet $request
	}

}

Systématiquement, le Kernel de Symfony vérifie si l’un des paramètres est typé avec la classe Request. Si c’est le cas, il va transmettre à ce paramètre toutes les informations contenues dans la requête : c’est le principe de l’autowiring.

Récupérer des paramètres GET

Un moteur de recherche pourrait par exemple exploiter des paramètres GET pour rechercher des contenus par tag : monsite.com/blog?tag=dev

copié !
#[Route('/blog', name: 'article_list')]
public function listArticles(Request $request): Response {
	// On récupère les paramètres GET
	$tag = $request->query->get('tag');
	// On va ensuite pouvoir aller chercher en base de données les articles correspondant à ce tag...
}

$request->query->get('tag') est l’alternative Symfony de $_GET['tag'], en PHP traditionnel.

Récupérer des paramètres POST

Sur un site dynamique, nous avons souvent besoin d’insérer du contenu en base de données. Et qui dit enregistrement, dit formulaire… Nous allons donc difficilement pouvoir nous passer de la méthode POST pour récupérer les informations soumises.

copié !
#[Route('/admin/articles/ajouter', name: 'admin_article_add')]
public function addArticle(Request $request): Response {
	// On récupère les paramètres POST
	$title = $request->request->get('title');
	$content = $request->request->get('content');
	// On va ensuite pouvoir enregistrer notre article en base de données...
}

$request->request->get('title') et $request->request->get('content') sont les alternatives Symfony de $_POST['title'] et $_POST['content'], en PHP traditionnel.

Les autres paramètres (cookies, server…)

Il est aussi possible d’accéder aux cookies via $request->cookies, aux variables de serveurs via $request->server, etc.

Connaître la méthode HTTP

Request ne permet pas uniquement de récupérer les paramètres qui transitent via nos requêtes HTTP. On l’utilise également pour obtenir d’autres types d’informations comme la méthode HTTP (POST ou GET) de la requête en cours.

Certaines routes vont ainsi pouvoir effectuer des traitements différents en fonction de la méthode HTTP qui les appelle.

C’est le cas de la route qui permet d’ajouter un article :

  • Si on y accède avec une requête GET, c’est que l’on souhaite récupérer son contenu et donc afficher le formulaire de création d’article.
  • Si on y accède avec une requête POST, c’est que l’on souhaite envoyer du contenu au serveur et donc enregistrer le nouvel article.
copié !
#[Route('/admin/articles/ajouter', name: 'admin_article_add')]
public function addArticle(Request $request): Response {
	if ($request->isMethod('POST')) {
		// Le formulaire est soumis en POST, on enregistre l'article en base de données
	} else {
		// On affiche la page avec le formulaire de création d'article
	}
}

$request->isMethod() est une méthode de l’objet Request qui renvoie un booléen :

  • true : lorsque la méthode renseignée en paramètre correspond à la méthode HTTP de la requête.
  • false : lorsque la méthode renseignée en paramètre ne correspond pas.

Opérations intermédiaires

Le rôle d’un contrôleur est certes de récupérer une requête afin de retourner une réponse, mais rien n’interdit d’effectuer des traitements intermédiaires, bien au contraire !

Nous aurons souvent besoin d’effectuer des opérations supplémentaires dans nos contrôleurs, sinon nos réponses HTTP seront peut intéressantes. Il serait par exemple utile de :

  • Récupérer des articles en base de données
  • Envoyer un mail
  • Enregistrer un article
  • Définir des variables de session
  • Poser un cookie

Nous détaillerons ces opérations au chapitre suivant, mais retenez que nous les mettrons en place dans nos méthodes, après avoir reçu une requête, et avant de retourner une réponse.

Réponses

Le rôle d’un contrôleur est de retourner une réponse. Il s’agit généralement une page web, mais parfois aussi un fichier JSON (API), XML, un téléchargement de fichier, une redirection, une page 404, etc.

Dans Symfony, retourner une réponse signifie tout simplement instancier la classe Response.

Ici, on crée un objet $response (son nom par convention, comme pour $request), on définit son contenu, puis on le retourne.

Charger l’objet Response
Importation de la classe Response
copié !
// Importation de la classe Response
use Symfony\Component\HttpFoundation\Response;
Instanciation de la classe Response
DemoController.php
copié !
namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

class DemoController extends AbstractController {

	#[Route('/', name: 'home')]
	public function home(): Response {
		// On instancie notre classe Response
		$response = new Response();
		// On lui ajoute un contenu
		$response->setContent('<html><body><h1>Accueil</h1></body></html>');
		// On lui ajoute un code de statut (200 = OK, 404 = NOT FOUND, etc)
		$response->setStatusCode(Response::HTTP_OK);
		// On lui ajoute une entête
		$response->headers->set('Content-Type','text/html');
		// On retourne la réponse
		return $response;
	}

}

Ça, c’était pour la beauté du geste. Dans les faits… ce code est extrêmement lourd à écrire. Notre contrôleur héritant de AbstractController, il va pouvoir faire appel aux méthodes de cette super-classe.

Détaillons les méthodes correspondantes aux réponse HTTP les plus fréquentes lors du développement d’un site web : le retour de pages et les redirections.

Retourner une vue

Non, nous n’allons pas utiliser la méthode setContent() pour retourner tout le code HTML de la page.

La méthode héritée $this->render() s’occupe toute seule de créer la réponse, lui passer le contenu du template, et la retourner. Quand on retourne une vue on a également la possibilité de lui faire passer des variables en paramètre.

copié !
#[Route('/', name: 'home')]
public function home(): Response {  
	// On retourne un template sans paramètre
	return $this->render('blog/home.html.twig');
}

#[Route('/blog/{slug}', name: 'article_show')]
public function showArticle(string $slug): Response {
	// À partir du slug passé en paramètre, je vais pouvoir aller récupérer l'article correspondant en base de données dans la variable $article puis on retourne un template en lui faisant passer l'objet $article
	// ...
	return $this->render('blog/article.html.twig', ['article' => $article]);
}

$this est une pseudo-variable qui fait référence à l’objet en question.

La méthode render() prend en paramètre :

  • Le chemin vers le template (elle se place par défaut dans le dossier 📁 templates)
  • Des variables à lui transmettre (s’il y en a).

Finalement, l’objet Response est utilisé en coulisses.

.html.twig… Quelle est cette extension transgénique ?!

Twig est le moteur de templates pour le langage PHP, utilisé par défaut dans Symfony. Il va nous permettre de dynamiser nos pages HTML statiques en bénéficiant de nombreuses fonctionnalités, que nous aurions normalement dû écrire en PHP (affichage de variables, conditions, boucles, inclusions, etc.).

Faire une redirection

Il peut aussi être très intéressant pour un contrôleur d’appeler une autre route lorsqu’il a fini son traitement, au lieu de renvoyer une page web. Cela est par exemple utile après :

  • L’ajout, la modification ou la suppression de ressources
  • L’envoi de mails
  • La connexion (selon le rôle d’un utilisateur, il pourrait être redirigé vers des pages variables)
  • La déconnexion

On fait alors appel à la méthode redirectToRoute().

copié !
#[Route('admin/articles/{id}/supprimer', name: 'admin_article_delete')]
public function deleteArticle(int $id): Response {
	// Ici, on imagine qu'on supprime l'article qui a l'id $id... puis on redirige
	return $this->redirectToRoute('article_list');
}

La méthode redirectToRoute() prend en paramètre :

  • Le nom de la route
  • Des variables à lui transmettre (s’il y en a). $this->redirectToRoute('nom_route', ['id' => 1]) appellerait une route avec un paramètre de route {id} en le remplaçant par 1.

Autres types de retours

Il est également possible de retourner du JSON, du XML, etc.

Nous n’entrerons pas en détail là-dessus mais sachez que c’est évidemment possible. Vous trouverez votre bonheur sur ce chapitre de la doc Symfony.