Le cycle requête / réponse
Symfony suit le pattern Front Controller : toutes les requêtes passent par
public/index.php → Kernel → Router → Controller → Response.
HTTP Request
↓
public/index.php (front controller)
↓
Kernel::handle()
↓
Router → trouve la route correspondante
↓
Controller::action() → construit une Response
↓
HTTP Response (HTML, JSON, redirect…)
Déclarer des routes
Symfony accepte 4 formats : Attributs PHP (recommandé), YAML, XML, PHP.
Format YAML (config/routes.yaml)
blog_index:
path: /blog
controller: App\Controller\BlogController::index
methods: [GET]
blog_show:
path: /blog/{slug}
controller: App\Controller\BlogController::show
Format Attribut (recommandé Symfony 6+)
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
#[Route('/blog', name: 'blog_')] // préfixe sur la classe
class BlogController extends AbstractController
{
#[Route('/', name: 'index', methods: ['GET'])]
public function index(): Response
{
return $this->render('blog/index.html.twig');
}
#[Route('/{slug}', name: 'show', methods: ['GET'])]
public function show(string $slug): Response
{
return $this->render('blog/show.html.twig', ['slug' => $slug]);
}
}
Options de l'attribut #[Route]
<?php
#[Route(
path: '/article/{id}',
name: 'article_show',
methods: ['GET', 'POST'],
requirements: ['id' => '\d+'], // regex sur le paramètre
defaults: ['id' => 1], // valeur par défaut
host: '{locale}.example.com', // contrainte de domaine
schemes: ['https'], // HTTPS uniquement
priority: 10, // priorité de matching
)]
public function show(int $id): Response { /* … */ }
Déboguer les routes
php bin/console debug:router
php bin/console debug:router blog_index # détail d'une route
php bin/console router:match /blog/hello # tester une URL
Paramètres & Value Resolvers
Paramètres de route
<?php
// Paramètre simple
#[Route('/user/{id}', requirements: ['id' => '\d+'])]
public function show(int $id): Response { /* … */ }
// Paramètre optionnel
#[Route('/articles/{page}', defaults: ['page' => 1])]
public function list(int $page): Response { /* … */ }
// EntityValueResolver (Symfony 6.2+) — inject directement l'entité
use App\Entity\Post;
#[Route('/post/{id}')]
public function showPost(Post $post): Response
{
// Symfony fait automatiquement EntityManager::find(Post::class, $id)
return $this->render('post/show.html.twig', ['post' => $post]);
}
Query string & paramètres de requête
<?php
use Symfony\Component\HttpFoundation\Request;
#[Route('/search')]
public function search(Request $request): Response
{
$q = $request->query->get('q', ''); // ?q=symfony
$page = $request->query->getInt('page', 1); // ?page=2
$sort = $request->query->getString('sort', 'date');
// Sécurisé : get() retourne null si absent
return $this->render('search/results.html.twig', compact('q', 'page'));
}
AbstractController — services utiles
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
class DemoController extends AbstractController
{
public function demo(): Response
{
// Rendu Twig
return $this->render('demo/index.html.twig', ['key' => 'value']);
// Redirection
return $this->redirectToRoute('blog_index');
return $this->redirect('https://example.com');
// 404 / exceptions HTTP
throw $this->createNotFoundException('Article introuvable');
throw $this->createAccessDeniedException();
// Sérialisation JSON directe
return $this->json(['status' => 'ok', 'data' => []]);
// Vérifier les droits
$this->denyAccessUnlessGranted('ROLE_ADMIN');
$this->denyAccessUnlessGranted('edit', $article); // Voter
}
}
L'objet Request
<?php
use Symfony\Component\HttpFoundation\Request;
public function create(Request $request): Response
{
// Méthode HTTP
$request->getMethod(); // 'POST'
$request->isMethod('POST'); // true
// Paramètres POST (formulaire)
$name = $request->request->get('name');
$data = $request->request->all(); // tableau complet
// Paramètres GET
$page = $request->query->getInt('page', 1);
// Headers
$contentType = $request->headers->get('Content-Type');
// Cookies
$token = $request->cookies->get('session_token');
// Corps JSON (API)
$payload = json_decode($request->getContent(), true);
// Ou avec getAttribute si content-type: application/json
$data = $request->toArray(); // Symfony 5.2+
// Fichiers uploadés
$file = $request->files->get('avatar');
// IP client
$ip = $request->getClientIp();
// Requête AJAX/Fetch
$isXhr = $request->isXmlHttpRequest();
$wantsJson = $request->getPreferredFormat() === 'json';
return new Response('ok');
}
Réponses HTTP
<?php
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\StreamedResponse;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
// Réponse HTML simple
return new Response('<h1>Bonjour</h1>', 200, ['Content-Type' => 'text/html']);
// Réponse JSON
return new JsonResponse(['status' => 'ok', 'count' => 42]);
return $this->json(['items' => $items], 201); // statut 201 Created
// Téléchargement de fichier
return new BinaryFileResponse('/path/to/file.pdf');
// Réponse streamée (gros fichiers)
return new StreamedResponse(function () {
echo 'ligne 1'.PHP_EOL;
flush();
});
// Headers personnalisés
$response = new JsonResponse($data);
$response->headers->set('X-Custom-Header', 'value');
$response->setMaxAge(3600); // cache HTTP
return $response;
Flash messages & Redirects
<?php
// Dans le controller
public function store(Request $request): Response
{
// traitement...
$this->addFlash('success', 'Article créé avec succès !');
$this->addFlash('warning', 'Image manquante.');
return $this->redirectToRoute('blog_index');
}
// Dans Twig — templates/base.html.twig
{% for type, messages in app.flashes %}
{% for message in messages %}
<div class="alert alert-{{ type }}">{{ message }}</div>
{% endfor %}
{% endfor %}