Qu’est-ce que Symfony ?
Symfony est deux choses à la fois. C’est un ensemble de composants PHP réutilisables — HttpFoundation, Routing, Console, Mailer, Serializer et des dizaines d’autres — et c’est un framework web complet assemblé à partir de ces composants. Cette double nature explique presque tout : le framework est stable parce que ses parties sont stables, et ses composants sont utiles même utilisés seuls.
Créé par Fabien Potencier en 2005 et maintenu par SensioLabs, Symfony est devenu le moteur du PHP moderne. Laravel est bâti sur des composants Symfony, Drupal et phpBB en dépendent, et des milliers de bibliothèques les utilisent sans jamais toucher au framework complet.
Symfony est plus exigeant qu’un framework « batteries-included ». En retour, il vous offre un système cohérent, extensible et évolutif sur le long terme.
Composants : le véritable produit
Les composants sont le cœur du projet. Chacun d’entre eux résout un problème unique et ne possède aucune dépendance vis-à-vis du framework.
- HttpFoundation — classes
RequestetResponseorientées objet. - Routing — association d’URLs à des contrôleurs via des attributs, YAML ou PHP.
- Console — création d’outils en ligne de commande avec arguments et options.
- Mailer — envoi d’e-mails via des transports et des messages templatés.
- Serializer — conversion d’objets en JSON, XML et inversement.
- EventDispatcher — découplage du code grâce aux événements et aux listeners.
Vous pouvez installer n’importe lequel d’entre eux directement :
composer require symfony/http-foundation
use Symfony\Component\HttpFoundation\Request;
$request = Request::createFromGlobals();
$page = $request->query->getInt('page', 1);
C’est cette portabilité qui explique pourquoi les composants Symfony se retrouvent partout, même dans des projets qui n’adopteraient jamais le framework complet.
Le noyau HTTP et les contrôleurs
Dans une application complète, chaque requête passe par le HttpKernel. Celui-ci déclenche un événement, fait correspondre une route, appelle un contrôleur et transforme le Response retourné en sortie. Les contrôleurs étendent AbstractController pour bénéficier d’helpers pratiques.
class BlogController extends AbstractController
{
#[Route('/blog/{slug}', name: 'blog_show')]
public function show(string $slug, PostRepository $posts): Response
{
$post = $posts->findOneBy(['slug' => $slug]);
if (!$post) {
throw $this->createNotFoundException('Post not found');
}
return $this->render('blog/show.html.twig', ['post' => $post]);
}
}
Un contrôleur doit retourner une Response. $this->render() en construit une à partir d’un template Twig, $this->json() retourne du JSON, et $this->redirectToRoute() retourne une redirection. Comme l’objet de réponse est explicite, les tests et le middleware sont simplifiés.
Les templates Twig étendent une mise en page de base et remplissent des blocs, et la sortie est échappée sauf si vous choisissez le contraire :
{# templates/blog/show.html.twig #}
{% extends 'base.html.twig' %}
{% block body %}
<article>
<h1>{{ post.title }}</h1>
<p>{{ post.publishedAt|date('F j, Y') }}</p>
{{ post.body|raw }}
</article>
{% endblock %}
Routage par attributs
Les routes sont déclarées à côté de l’action qui les gère en utilisant les attributs de PHP 8.
#[Route('/blog', name: 'blog_index', methods: ['GET'])]
#[Route('/blog/{slug}', name: 'blog_show', methods: ['GET'])]
Le routage peut également être défini dans config/routes.yaml si vous préférez le garder séparé. Les attributs sont la norme dans Symfony moderne car la route et la méthode restent liées. Vous pouvez inspecter les routes enregistrées à tout moment avec :
php bin/console debug:router
Le container de services et l’autowiring
Presque tout dans Symfony est un service, et le container se charge de les construire et de les connecter entre eux. Lorsque l’autowiring est activé, il vous suffit de typer les arguments de votre constructeur.
class PostPublisher
{
public function __construct(
private EntityManagerInterface $em,
private MailerInterface $mailer,
) {}
}
config/services.yaml indique au container d’effectuer l’autowiring de vos classes :
services:
_defaults:
autowire: true
autoconfigure: true
App\:
resource: '../src/'
exclude:
- '../src/Entity/'
- '../src/Kernel.php'
Si deux implémentations d’une même interface existent, liez explicitement la bonne version. Le container est compilé en PHP pur pour la production, ce qui explique sa rapidité malgré la flexibilité dont vous disposez lors du développement.
Bundles et recettes Flex
Un bundle regroupe des fonctionnalités dans une unité distribuable. Les fonctionnalités tierces — Doctrine, la sécurité, le mailer, les panels d’administration — arrivent sous forme de bundle, et le framework lui-même est composé de bundles cœurs.
L’installation d’un bundle se fait via une seule commande :
composer require symfony/orm-pack
Symfony Flex est le plugin Composer qui rend ce processus agréable. Lorsqu’un package possède une recette, Flex crée les fichiers de configuration, enregistre le bundle, et ajoute souvent des variables d’environnement ou un service docker-compose. C’est pourquoi les applications Symfony modernes utilisent une configuration concise et explicite plutôt que des pages entières de code répétitif.
Doctrine ORM
Doctrine est la couche de persistance par défaut. Les entités sont de simples classes PHP annotées avec des attributs de mapping, et les repositories encapsulent les requêtes.
#[ORM\Entity(repositoryClass: PostRepository::class)]
class Post
{
#[ORM\Id, ORM\GeneratedValue, ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 255)]
private string $title;
#[ORM\Column(type: 'text')]
private string $body;
#[ORM\Column(type: 'datetime_immutable', nullable: true)]
private ?\DateTimeImmutable $publishedAt = null;
}
Les requêtes sont généralement exprimées via l’API du repository ou en DQL plutôt qu’avec des chaînes SQL :
$latest = $posts->findBy([], ['publishedAt' => 'DESC'], limit: 10);
Les modifications de schéma sont gérées par Doctrine Migrations, qui compare votre mapping à la base de données et génère des fichiers de migration versionnés que vous pouvez examiner avant de les exécuter.
Configuration et environnements
Symfony sépare la configuration du code et l’applique par environnement. APP_ENV sélectionne l’environnement, et les fichiers sous config/ se superposent les uns aux autres.
.envcontient les valeurs par défaut locales et les variables d’environnement.config/packages/configure les bundles pour tous les environnements.config/packages/dev/etconfig/packages/prod/permettent de surcharger la configuration par environnement.config/routes.yamletconfig/services.yamlcorrespondent aux paramètres propres à l’application.
L’environnement dev active le profiler et les outils de débogage, test utilise une base de données distincte, et prod compile tout pour optimiser les performances. Vider le cache après des modifications de configuration fait partie intégrante du flux de travail :
php bin/console cache:clear
Messenger et travail asynchrone
Le composant Messenger permet de sortir certaines tâches du cycle de la requête. Vous envoyez un message à un bus et un handler le traite, soit immédiatement, soit via un worker.
final class UserRegistered
{
public function __construct(public readonly int $userId) {}
}
#[AsMessageHandler]
final class SendWelcomeEmail
{
public function __invoke(UserRegistered $message): void
{
// send the email here
}
}
Envoyez-le depuis n’importe où, et consommez la file d’attente dans un processus séparé :
$bus->dispatch(new UserRegistered($user->getId()));
php bin/console messenger:consume async
Les transports peuvent être Doctrine, Redis, AMQP ou un service tiers. Les messages en échec peuvent être retentés ou stockés, ce qui fait du traitement en arrière-plan une fonctionnalité native et robuste.
Tests avec PHPUnit
Symfony est livré avec PHPUnit et une classe de base WebTestCase qui démarre le kernel et effectue de réelles requêtes HTTP.
class BlogControllerTest extends WebTestCase
{
public function testShowPost(): void
{
$client = static::createClient();
$client->request('GET', '/blog/hello-world');
$this->assertResponseIsSuccessful();
$this->assertSelectorTextContains('h1', 'Hello world');
}
}
Comme les services sont injectés, les tests unitaires peuvent remplacer une dépendance par un stub ou un mock sans modifier le container. Réservez les tests d’intégration pour la couche HTTP et les tests unitaires pour les services qui contiennent les règles métier.
Bonnes pratiques
- Privilégiez l’injection par constructeur plutôt que de récupérer les services depuis le container.
- Gardez vos controllers légers ; déplacez la logique métier dans des services.
- Utilisez des repositories pour vos requêtes et ne laissez pas de SQL dans les controllers.
- Déclarez vos routes via des attributs juste à côté de l’action qu’elles appellent.
- Laissez Flex gérer la configuration du bundle pour éviter d’écrire du boilerplate.
- Séparez la configuration par environnement et conservez vos secrets dans des variables d’environnement.
- Déléguez les tâches lourdes à Messenger plutôt que de les traiter durant la requête.
- Consultez le profiler lorsque vous constatez des lenteurs ou des comportements inattendus.
Erreurs courantes
- Appeler
$container->get()au lieu d’injecter les dépendances. - Placer la logique métier dans les contrôleurs et les entités.
- Oublier d’enregistrer ou de lier une interface, puis s’interroger sur ce que le container a résolu.
- Committer des secrets
.envau lieu de les exclure du contrôle de version. - Modifier des migrations générées après leur exécution.
- Mélanger aléatoirement les styles de configuration entre YAML, XML et PHP.
- Ignorer les avis de dépréciation jusqu’à la prochaine mise à jour majeure.
Et après ?
Symfony vous apporte les composants et la rigueur nécessaires pour bâtir des systèmes pérennes, et ses concepts se retrouvent dans de nombreux frameworks de l’écosystème. Consultez le guide Laravel pour voir comment ces composants sont utilisés pour le développement rapide d’applications, ou comparez cette philosophie avec l’approche minimaliste d’Express. Pour concevoir et faire évoluer vos API, poursuivez avec les sections sur le REST et le versionnage d’API.