Maîtriser le cœur de Symfony
Routes • Paramètres • Génération d'URLs • Request & Response
Formation DWWM - 2026
Le GPS de votre application
Le routing est le système qui associe une URL à un contrôleur.
Imagine un grand hôtel avec 100 chambres :
Sans réceptionniste, le client erre dans les couloirs. Sans routing, la requête HTTP est perdue !
Voici le parcours COMPLET d'une requête dans Symfony :
Une route Symfony est composée de 3 éléments essentiels :
| Composant | Description | Exemple |
|---|---|---|
| 1. Le PATH | L'URL à matcher | /blog/{id} |
| 2. Le NAME | Identifiant unique de la route | blog_show |
| 3. Le CONTROLLER | Méthode à exécuter | BlogController::show() |
#[Route('/blog/{id}', name: 'blog_show')]
public function show(int $id): Response
{
// ...
}
// PATH: /blog/{id}
// NAME: blog_show
// CONTROLLER: BlogController::show()
La méthode moderne
Depuis PHP 8 et Symfony 6, on utilise les attributs au lieu des annotations.
/**
* @Route("/blog/{id}", name="blog_show")
*/
public function show($id)
{
// ...
}
Dans des commentaires DocBlock
#[Route('/blog/{id}', name: 'blog_show')]
public function show(int $id): Response
{
// ...
}
Syntaxe native PHP
✅ Avantages des attributs :
#[Route(
path: '/blog/{id}', // L'URL
name: 'blog_show', // Nom de la route
requirements: ['id' => '\d+'], // Contraintes
methods: ['GET'], // Méthodes HTTP autorisées
defaults: ['id' => 1], // Valeurs par défaut
priority: 0, // Priorité de la route
condition: "context.getMethod() in ['GET', 'HEAD']"
)]
public function show(int $id): Response
{
// ...
}
💡 Note : Vous n'utiliserez pas tous ces paramètres à chaque fois ! Les plus courants sont : path, name, requirements et methods.
#[Route('/', name: 'home')]
public function index(): Response
{
return new Response('Page d\'accueil');
}
#[Route('/', name: 'home')]
#[Route('/accueil', name: 'home_alt')]
#[Route('/index', name: 'home_index')]
public function index(): Response
{
// Accessible par 3 URLs différentes !
return new Response('Page d\'accueil');
}
✅ Cas d'usage : Utile pour gérer plusieurs langues ou URLs héritées d'un ancien site.
On peut définir un préfixe commun pour toutes les routes d'un contrôleur :
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
#[Route('/blog')] // ← Préfixe pour tout le contrôleur
class BlogController extends AbstractController
{
#[Route('/', name: 'blog_index')]
public function index(): Response
{
// URL finale : /blog/
}
#[Route('/{id}', name: 'blog_show')]
public function show(int $id): Response
{
// URL finale : /blog/{id}
}
#[Route('/new', name: 'blog_new')]
public function new(): Response
{
// URL finale : /blog/new
}
}
Le préfixe de classe = la rue ("Rue des Lilas")
Chaque route = le numéro de maison (5, 10, 15...)
Adresse complète = "5 Rue des Lilas", "10 Rue des Lilas"...
Rendre les routes dynamiques
Les paramètres se définissent entre accolades {} dans l'URL :
#[Route('/article/{id}', name: 'article_show')]
public function show(int $id): Response
{
return new Response("Article n°" . $id);
}
// /article/5 → "Article n°5"
// /article/123 → "Article n°123"
💡 Important : Le nom du paramètre dans l'URL doit correspondre au nom de la variable dans la méthode !
On peut avoir autant de paramètres qu'on veut :
#[Route('/blog/{category}/{slug}', name: 'blog_post')]
public function post(string $category, string $slug): Response
{
return new Response(
"Catégorie: $category, Article: $slug"
);
}
// URL : /blog/tech/symfony-introduction
// Résultat : "Catégorie: tech, Article: symfony-introduction"
#[Route('/archive/{year}/{month}/{day}', name: 'archive')]
public function archive(int $year, int $month, int $day): Response
{
return new Response(
"Archives du $day/$month/$year"
);
}
// URL : /archive/2025/01/28
// Résultat : "Archives du 28/01/2025"
Un paramètre peut avoir une valeur par défaut :
#[Route('/blog/page/{page}', name: 'blog_page')]
public function page(int $page = 1): Response
{
return new Response("Page n°$page");
}
// /blog/page/5 → "Page n°5"
// /blog/page/ → "Page n°1" (valeur par défaut)
// /blog/page → Erreur 404 (URL non complète)
#[Route('/blog/{page}', name: 'blog_page', defaults: ['page' => 1])]
public function page(int $page): Response
{
return new Response("Page n°$page");
}
// /blog/5 → "Page n°5"
// /blog → "Page n°1" (valeur par défaut)
✅ Différence : La méthode 2 accepte l'URL sans le paramètre (/blog), alors que la méthode 1 nécessite le slash final (/blog/)
On peut forcer un paramètre à respecter un format avec requirements :
#[Route('/article/{id}', name: 'article_show', requirements: ['id' => '\d+'])]
public function show(int $id): Response
{
// $id sera TOUJOURS un nombre
}
// ✅ /article/123 → OK
// ❌ /article/abc → 404
#[Route('/blog/{slug}', name: 'blog_post', requirements: ['slug' => '[a-z0-9-]+'])]
public function post(string $slug): Response
{
// Accepte seulement lettres minuscules, chiffres et tirets
}
// ✅ /blog/mon-article-123 → OK
// ❌ /blog/Mon_Article! → 404
#[Route('/langue/{locale}', name: 'change_locale',
requirements: ['locale' => 'fr|en|es'])]
public function changeLocale(string $locale): Response
{
// Accepte SEULEMENT : fr, en, es
}
// ✅ /langue/fr → OK
// ✅ /langue/en → OK
// ❌ /langue/de → 404
| Type de donnée | Regex | Exemple valide |
|---|---|---|
| Nombre entier | \d+ |
123, 456 |
| Slug (URL friendly) | [a-z0-9-]+ |
mon-article-123 |
| UUID | [0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12} |
550e8400-e29b-41d4-a716-446655440000 |
| Date YYYY-MM-DD | \d{4}-\d{2}-\d{2} |
2025-01-28 |
| Code postal FR | \d{5} |
75001 |
| Langue (2 lettres) | [a-z]{2} |
fr, en |
⚠️ Important : Si aucune route ne matche, Symfony retourne une erreur 404. Les requirements protègent aussi contre les injections !
On peut limiter une route à certaines méthodes HTTP :
#[Route('/article/{id}', name: 'article_show', methods: ['GET'])]
public function show(int $id): Response
{
// Accessible SEULEMENT en GET
}
#[Route('/article/create', name: 'article_create', methods: ['POST'])]
public function create(Request $request): Response
{
// Accessible SEULEMENT en POST
}
#[Route('/contact', name: 'contact', methods: ['GET', 'POST'])]
public function contact(Request $request): Response
{
if ($request->isMethod('POST')) {
// Traiter le formulaire
}
// Afficher le formulaire
}
Ne jamais écrire d'URLs en dur
Imagine que tu as un livre de 500 pages :
❌ Mauvaise méthode : Tu écris "page 243" partout dans tes notes. Si les pages changent lors d'une réédition, toutes tes notes sont fausses !
✅ Bonne méthode : Tu notes "Chapitre 12, Section 3". Même si la pagination change, tu retrouves toujours le bon endroit.
<a href="/blog/123">Article</a>
<!-- Si tu changes la route en /article/123,
tous tes liens sont cassés ! -->
<a href="{{ path('blog_show', {id: 123}) }}">
Article
</a>
<!-- Si tu changes l'URL, les liens
se mettent à jour automatiquement ! -->
La fonction path() génère une URL relative :
{{ path('home') }}
{# Génère : / #}
<a href="{{ path('home') }}">Accueil</a>
{{ path('blog_show', {id: 123}) }}
{# Génère : /blog/123 #}
<a href="{{ path('blog_show', {id: article.id}) }}">
Lire l'article
</a>
{{ path('blog_post', {category: 'tech', slug: 'symfony-intro'}) }}
{# Génère : /blog/tech/symfony-intro #}
<a href="{{ path('blog_post', {
category: article.category,
slug: article.slug
}) }}">
{{ article.title }}
</a>
| Fonction | Résultat | Usage |
|---|---|---|
path() |
URL relative/blog/123 |
Liens internes au site |
url() |
URL absoluehttp://localhost:8000/blog/123 |
Emails, flux RSS, Open Graph |
{# Lien interne - utilise path() #}
<a href="{{ path('contact') }}">Contactez-nous</a>
{# Email - utilise url() #}
<p>Consultez cet article : {{ url('blog_show', {id: 123}) }}</p>
{# Open Graph (réseaux sociaux) - utilise url() #}
<meta property="og:url" content="{{ url('blog_show', {id: article.id}) }}">
💡 Règle : Utilise path() par défaut. N'utilise url() que si tu as besoin de l'URL complète (emails, API, métadonnées).
Dans un contrôleur, on utilise $this->generateUrl() :
$url = $this->generateUrl('blog_show', ['id' => 123]);
// Résultat : /blog/123
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
$url = $this->generateUrl(
'blog_show',
['id' => 123],
UrlGeneratorInterface::ABSOLUTE_URL
);
// Résultat : http://localhost:8000/blog/123
public function sendEmail(MailerInterface $mailer): Response
{
$articleUrl = $this->generateUrl(
'blog_show',
['id' => 123],
UrlGeneratorInterface::ABSOLUTE_URL
);
$email = (new Email())
->to('user@example.com')
->subject('Nouvel article')
->html("Lire l'article : <a href='$articleUrl'>Cliquez ici</a>");
$mailer->send($email);
return new Response('Email envoyé !');
}
Votre boîte à outils
AbstractController est la classe parent de tous vos contrôleurs. Elle fournit des méthodes utiles.
Quand tu hérites d'AbstractController, c'est comme si on te donnait une boîte à outils complète :
render() - Le marteau (rendre des templates)redirectToRoute() - Le tournevis (rediriger)json() - La règle (créer du JSON)addFlash() - Les vis (messages flash)Sans héritage, tu devrais créer tous ces outils toi-même !
| Méthode | Description | Exemple |
|---|---|---|
render() |
Rendre un template Twig | $this->render('home.html.twig') |
json() |
Retourner du JSON | $this->json(['status' => 'ok']) |
redirectToRoute() |
Rediriger vers une route | $this->redirectToRoute('home') |
redirect() |
Rediriger vers une URL | $this->redirect('https://google.com') |
file() |
Télécharger un fichier | $this->file('document.pdf') |
addFlash() |
Ajouter un message flash | $this->addFlash('success', 'OK!') |
generateUrl() |
Générer une URL | $this->generateUrl('home') |
createNotFoundException() |
Créer une erreur 404 | throw $this->createNotFoundException() |
La méthode render() génère du HTML à partir d'un template Twig.
$this->render(
string $view, // Chemin du template
array $parameters = [], // Variables à passer
?Response $response = null // Response existante (rare)
): Response
// Template simple
return $this->render('home/index.html.twig');
// Avec variables
return $this->render('blog/show.html.twig', [
'article' => $article,
'comments' => $comments
]);
// Modifier le statut HTTP
return $this->render('error.html.twig', [],
new Response('', 500)
);
💡 Note : render() retourne toujours un objet Response avec le HTML généré et un statut HTTP 200.
La méthode json() convertit automatiquement des données PHP en JSON.
$this->json(
mixed $data, // Données à convertir
int $status = 200, // Code HTTP
array $headers = [], // Headers personnalisés
array $context = [] // Options de serialization
): JsonResponse
// Tableau simple
return $this->json(['message' => 'Success', 'data' => [1, 2, 3]]);
// Objet
return $this->json($user);
// Avec statut HTTP personnalisé
return $this->json(['error' => 'Not found'], 404);
// Avec headers CORS
return $this->json($data, 200, [
'Access-Control-Allow-Origin' => '*'
]);
✅ Avantages :
Content-Type: application/jsonCommunication client-serveur
Request contient TOUTES les informations de la requête HTTP entrante.
Quand tu reçois un colis :
Request = tout ce qu'il y a sur et dans le colis !
| Propriété | Contenu | Exemple |
|---|---|---|
$request->query |
Paramètres GET (?key=value) | $request->query->get('page') |
$request->request |
Paramètres POST (formulaire) | $request->request->get('email') |
$request->files |
Fichiers uploadés | $request->files->get('photo') |
$request->cookies |
Cookies | $request->cookies->get('session_id') |
$request->headers |
Headers HTTP | $request->headers->get('User-Agent') |
$request->server |
Variables serveur | $request->server->get('REMOTE_ADDR') |
Les paramètres GET viennent de l'URL après le ?
use Symfony\Component\HttpFoundation\Request;
#[Route('/search', name: 'search')]
public function search(Request $request): Response
{
// Récupérer un paramètre
$query = $request->query->get('q');
$page = $request->query->get('page');
// Avec valeur par défaut
$limit = $request->query->get('limit', 10);
// Tous les paramètres GET
$allParams = $request->query->all();
return new Response("Recherche: $query, Page: $page");
}
// URL : /search?q=symfony&page=2
// Résultat : "Recherche: symfony, Page: 2"
💡 Note : get('key', 'default') retourne la valeur par défaut si la clé n'existe pas.
Les paramètres POST viennent d'un formulaire soumis.
#[Route('/login', name: 'login', methods: ['GET', 'POST'])]
public function login(Request $request): Response
{
if ($request->isMethod('POST')) {
// Récupérer les champs du formulaire
$email = $request->request->get('email');
$password = $request->request->get('password');
// Traiter le login...
return new Response("Login: $email");
}
// Afficher le formulaire
return $this->render('login.html.twig');
}
if ($request->isMethod('POST')) { /* ... */ }
if ($request->isMethod('GET')) { /* ... */ }
// Ou
$method = $request->getMethod(); // 'GET', 'POST', etc.
// User-Agent
$userAgent = $request->headers->get('User-Agent');
// Accept-Language
$language = $request->headers->get('Accept-Language');
// Referer (page précédente)
$referer = $request->headers->get('referer');
// Content-Type
$contentType = $request->headers->get('Content-Type');
// Tous les headers
$allHeaders = $request->headers->all();
public function detectBrowser(Request $request): Response
{
$userAgent = $request->headers->get('User-Agent');
if (str_contains($userAgent, 'Chrome')) {
$browser = 'Google Chrome';
} elseif (str_contains($userAgent, 'Firefox')) {
$browser = 'Mozilla Firefox';
} else {
$browser = 'Autre navigateur';
}
return new Response("Vous utilisez : $browser");
}
Response est ce que vous renvoyez au client (navigateur).
HTTP/1.1 200 OK ← Statut HTTP
Content-Type: text/html ← Headers
Set-Cookie: session=abc123
← Ligne vide
<html> ← Body (contenu)
<body>Hello World</body>
</html>
use Symfony\Component\HttpFoundation\Response;
return new Response(
'<html><body>Hello</body></html>', // Contenu
200, // Statut HTTP
['Content-Type' => 'text/html'] // Headers
);
| Code | Signification | Usage |
|---|---|---|
| 200 | OK | Requête réussie (par défaut) |
| 201 | Created | Ressource créée (API) |
| 204 | No Content | Succès sans contenu (API) |
| 301 | Moved Permanently | Redirection permanente |
| 302 | Found | Redirection temporaire |
| 400 | Bad Request | Requête invalide |
| 401 | Unauthorized | Non authentifié |
| 403 | Forbidden | Accès interdit |
| 404 | Not Found | Ressource introuvable |
| 500 | Internal Server Error | Erreur serveur |
return new Response('<h1>Hello</h1>');
use Symfony\Component\HttpFoundation\JsonResponse;
return new JsonResponse(['message' => 'Success', 'data' => [1, 2, 3]]);
use Symfony\Component\HttpFoundation\RedirectResponse;
return new RedirectResponse('https://google.com');
use Symfony\Component\HttpFoundation\BinaryFileResponse;
return new BinaryFileResponse('/path/to/document.pdf');
use Symfony\Component\HttpFoundation\StreamedResponse;
return new StreamedResponse(function() {
echo 'Chunk 1';
flush();
sleep(1);
echo 'Chunk 2';
});
Naviguer entre les pages
La méthode la plus courante pour rediriger :
$this->redirectToRoute(
string $route, // Nom de la route
array $parameters = [], // Paramètres de la route
int $status = 302 // Code HTTP (302 par défaut)
): RedirectResponse
// Redirection simple
return $this->redirectToRoute('home');
// Avec paramètres
return $this->redirectToRoute('blog_show', ['id' => 123]);
// Redirection permanente (301)
return $this->redirectToRoute('new_url', [], 301);
#[Route('/article/create', name: 'article_create', methods: ['POST'])]
public function create(Request $request): Response
{
// Créer l'article...
$articleId = 123;
// Rediriger vers la page de l'article créé
return $this->redirectToRoute('article_show', ['id' => $articleId]);
}
Pour rediriger vers une URL externe ou complète :
// Vers un site externe
return $this->redirect('https://www.google.com');
// Vers une URL relative
return $this->redirect('/old-page');
// Génération d'URL puis redirection
$url = $this->generateUrl('blog_show', ['id' => 123]);
return $this->redirect($url);
⚠️ Important : Préférez toujours redirectToRoute() pour les redirections internes. N'utilisez redirect() que pour les URLs externes.
| Code | Type | Comportement | Usage |
|---|---|---|---|
| 302 | Found (Temporaire) | Les moteurs de recherche gardent l'ancienne URL | Redirection après formulaire, login temporaire |
| 301 | Moved Permanently | Les moteurs de recherche indexent la nouvelle URL | Changement d'URL définitif, refonte de site |
// 302 - Temporaire (par défaut)
return $this->redirectToRoute('home');
// 301 - Permanente
return $this->redirectToRoute('new_home', [], 301);
💡 SEO : Utilisez 301 quand vous changez définitivement une URL pour conserver votre référencement Google !
Quand la ressource n'existe pas
Lance une exception qui génère une page 404 :
throw $this->createNotFoundException(string $message = 'Not Found');
#[Route('/article/{id}', name: 'article_show')]
public function show(int $id): Response
{
$article = $this->getArticleById($id);
if (!$article) {
throw $this->createNotFoundException(
'Article n°' . $id . ' introuvable'
);
}
return $this->render('article/show.html.twig', [
'article' => $article
]);
}
💡 Comportement :
Créez un template personnalisé pour vos erreurs 404 :
📄 templates/bundles/TwigBundle/Exception/error404.html.twig
{% extends 'base.html.twig' %}
{% block title %}Page non trouvée{% endblock %}
{% block body %}
<div style="text-align: center; padding: 4rem;">
<h1 style="font-size: 6rem; color: #dc3545;">404</h1>
<h2>Page non trouvée</h2>
<p>La page que vous recherchez n'existe pas.</p>
<a href="{{ path('home') }}">Retour à l'accueil</a>
</div>
{% endblock %}
⚠️ Important : Cette page personnalisée s'affiche UNIQUEMENT en mode production (APP_ENV=prod). En dev, vous voyez toujours la page de debug.
| Concept | Description | Exemple |
|---|---|---|
| Routing | Associe URL → Contrôleur | #[Route('/blog')] |
| Paramètres | Valeurs dynamiques dans l'URL | /blog/{id} |
| Requirements | Contraintes sur paramètres | ['id' => '\d+'] |
| path() | Générer une URL | path('blog_show', {id: 123}) |
| Request | Données de la requête HTTP | $request->query->get('page') |
| Response | Réponse au client | new Response('Hello') |
| redirectToRoute() | Rediriger vers une route | redirectToRoute('home') |
| 404 | Page non trouvée | createNotFoundException() |
name est obligatoire pour générer des URLs/blog/123requirements protège contre les valeurs invalidesrender(), json(), etc.createNotFoundException() + template personnalisé| Action | Commande |
|---|---|
| Lister toutes les routes | php bin/console debug:router |
| Chercher une route | php bin/console debug:router blog_show |
| Créer un contrôleur | php bin/console make:controller |
| Vérifier une URL | php bin/console router:match /blog/123 |
# Voir les détails d'une route
php bin/console debug:router blog_show
# Résultat :
# Name blog_show
# Path /blog/{id}
# Controller App\Controller\BlogController::show()
# Requirements id: \d+
# Methods GET
#[Route('/blog')] // ❌ Pas de name
public function index() { }
#[Route('/blog', name: 'blog_index')] // ✅ Avec name
public function index() { }
<a href="/blog/123">Article</a> {# ❌ #}
<a href="{{ path('blog_show', {id: 123}) }}">Article</a> {# ✅ #}
#[Route('/blog/{id}')] // ❌ Accepte /blog/abc
public function show($id) { }
#[Route('/blog/{id}', requirements: ['id' => '\d+'])] // ✅
public function show(int $id) { }
blog_index, blog_show, blog_createint $id au lieu de $id🌐 https://symfony.com/doc/current/routing.html
Guide complet sur le routing
🌐 https://symfony.com/doc/current/components/http_foundation.html
Tout sur Request et Response
🌐 https://symfony.com/doc/current/best_practices.html
Bonnes pratiques officielles Symfony
Vous savez maintenant :
✅ Créer des routes avec attributs PHP
✅ Gérer les paramètres d'URL
✅ Générer des URLs dynamiques
✅ Utiliser Request et Response
✅ Rediriger proprement
✅ Gérer les erreurs 404
Prochaine étape : Twig ! 🎨
Formation DWWM - 2026