Skip to Content

Chapitre 5.4 — Compiler passes & bundles

Partie 5 : Services & DI — Chapitre 4/5 Version de référence : Symfony 7.4.

⏱️ TL;DR

  • Le conteneur est construit puis compilé (optimisé et mis en cache) avant l’exécution. Une compiler pass est un point d’extension qui peut modifier les définitions de services à ce moment-là (une fois que toutes sont connues).
  • Cas d’usage historiques : collecter des services taggés, remplacer/décorer une définition. Aujourd’hui, autoconfiguration + #[AutowireIterator] couvrent la plupart des besoins → on écrit rarement une compiler pass à la main.
  • Un bundle est un paquet qui étend le framework (services, config, routes, commandes). Votre application n’est pas un bundle (Partie 1) ; les bundles servent au code réutilisable/partagé.
  • Un bundle moderne étend AbstractBundle : config simplifiée, chargement de services, sans le boilerplate d’antan.
  • On crée un bundle quand du code doit être partagé entre projets ou publié (Packagist — Partie 15).
  • Pour un dev Next.js : un bundle ≈ un package npm réutilisable qui, en plus, sait se configurer dans l’app hôte (services, options) via Flex.

🎯 Objectifs

  • Comprendre la compilation du conteneur et le rôle d’une compiler pass.
  • Savoir quand une compiler pass est (encore) nécessaire.
  • Connaître l’anatomie d’un bundle moderne.
  • Distinguer code applicatif et code de bundle.

1. La compilation du conteneur

Le conteneur ne reste pas « dynamique » à l’exécution : Symfony le compile en une classe PHP optimisée (dans var/cache/). Le processus :

À l’étape 2, toutes les définitions de services sont connues. C’est le seul moment où l’on peut agir sur l’ensemble (ex. « trouver tous les services taggés X et les passer à Y »). Après compilation, plus rien ne bouge : d’où les excellentes performances en prod.

2. Qu’est-ce qu’une compiler pass

Une compiler pass implémente CompilerPassInterface et reçoit le ContainerBuilder pour le modifier :

use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; final class NotificationChannelPass implements CompilerPassInterface { public function process(ContainerBuilder $container): void { if (!$container->has(Notifier::class)) { return; } $notifier = $container->findDefinition(Notifier::class); // trouver tous les services taggés et les injecter dans le Notifier $channels = $container->findTaggedServiceIds('app.notification_channel'); foreach (array_keys($channels) as $id) { $notifier->addMethodCall('addChannel', [new Reference($id)]); } } }

On l’enregistre dans le Kernel (ou un bundle) via build() :

// src/Kernel.php protected function build(ContainerBuilder $container): void { $container->addCompilerPass(new NotificationChannelPass()); }

Depuis Symfony 5 : écrire ce genre de passe « collecter les services taggés » était courant. Aujourd’hui, l’attribut #[AutowireIterator('app.notification_channel')] (chapitre 5.3) fait la même chose sans compiler pass. Résultat : vous n’écrirez une compiler pass que pour des besoins avancés (modifier/décorer dynamiquement des définitions, conditionner selon d’autres bundles). Pour 95 % des cas, les attributs suffisent.

Autres usages (rares) : remplacer un service du cœur, décorer conditionnellement, retirer des services selon la présence d’un autre bundle. À connaître, à utiliser avec parcimonie.

3. Anatomie d’un bundle

Un bundle empaquette une fonctionnalité réutilisable. Il peut fournir : des services, de la configuration (avec des options validées), des routes, des commandes, des templates, des assets, des migrations. Structure typique d’un bundle moderne :

AcmePaymentBundle/ ├── src/ │ ├── AcmePaymentBundle.php ← la classe du bundle (étend AbstractBundle) │ ├── Controller/ │ ├── Service/ │ └── DependencyInjection/ ← (optionnel) config avancée ├── config/ │ └── services.php ← services du bundle ├── composer.json ← type: "symfony-bundle" └── ...

La classe du bundle, avec AbstractBundle (Symfony 6.1+), est concise — elle déclare sa config et charge ses services :

use Symfony\Component\Config\Definition\Configurator\DefinitionConfigurator; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; use Symfony\Component\HttpKernel\Bundle\AbstractBundle; final class AcmePaymentBundle extends AbstractBundle { // options de configuration exposées à l'app hôte (validées) public function configure(DefinitionConfigurator $definition): void { $definition->rootNode() ->children() ->scalarNode('api_key')->isRequired()->end() ->booleanNode('sandbox')->defaultTrue()->end() ->end(); } // charge les services du bundle + applique la config public function loadExtension(array $config, ContainerConfigurator $container, ContainerBuilder $builder): void { $container->import('../config/services.php'); $builder->getDefinition('acme_payment.client') ->setArgument('$apiKey', $config['api_key']) ->setArgument('$sandbox', $config['sandbox']); } }

L’application hôte configure le bundle dans config/packages/acme_payment.yaml :

acme_payment: api_key: '%env(ACME_PAYMENT_KEY)%' sandbox: true

Depuis Symfony 5 : AbstractBundle simplifie énormément la création de bundles. Avant, il fallait une classe Extension, une classe Configuration, un DependencyInjection/ verbeux. Désormais, configure() + loadExtension() dans la classe du bundle suffisent pour la plupart des cas. Si vous aviez le souvenir « créer un bundle, c’est lourd », c’est révolu.

4. Quand créer un bundle ?

  • Ne créez pas de bundle pour organiser votre application : le code applicatif vit dans src/ (namespace App\), organisé en dossiers. Un bundle pour ça est un anti-pattern hérité de Symfony 2/3.
  • Créez un bundle quand vous voulez partager du code : entre plusieurs projets (une base commune d’entreprise), ou le publier publiquement sur Packagist (un produit réutilisable — angle monétisation, Partie 15/17).

💡 Pour un dev Next.js : un bundle ≈ extraire une fonctionnalité en package npm partagé, mais avec un superpouvoir : le bundle se câble tout seul dans l’app hôte (services, config validée, recipe Flex). C’est plus intégré qu’un simple package. On construit et publie un bundle complet en Partie 15.

✏️ Exercices

Exercice 1. Vrai ou faux : « Pour collecter tous les services taggés app.exporter et les injecter dans un registre, il faut écrire une compiler pass. » Justifiez.

✅ Solution

Faux (dans le cas courant). Depuis Symfony moderne, on utilise l’attribut #[AutowireIterator('app.exporter')] (ou #[AutowireLocator]) pour collecter les services taggés, sans compiler pass. Les compiler passes restent utiles pour des besoins avancés (remplacer/décorer dynamiquement des définitions, agir selon d’autres bundles), pas pour la simple collecte.

Exercice 2. À quel moment précis une compiler pass s’exécute-t-elle, et pourquoi est-ce important ?

✅ Solution

Lors de la compilation du conteneur, après que toutes les définitions de services sont enregistrées et avant l’optimisation/mise en cache. C’est important car c’est le seul moment où l’on connaît l’ensemble des services — donc où l’on peut agir globalement (collecter tous les taggés, remplacer un service). Après compilation, tout est figé.

Exercice 3. Un collègue veut créer un UserBundle pour ranger les entités et contrôleurs « utilisateur » de l’application. Bon ou mauvais réflexe ?

✅ Solution

Mauvais réflexe (anti-pattern hérité de Symfony 2/3). Le code applicatif vit dans src/ (namespace App\), organisé en dossiers (src/Entity/, src/Controller/…), pas dans un bundle. On ne crée un bundle que pour du code réutilisable entre projets ou publié. Pour organiser une app, on utilise des sous-dossiers/namespaces, éventuellement une architecture par domaine (Partie 15).

🧠 Quiz de révision

1. Que se passe-t-il lors de la « compilation » du conteneur ?

Les définitions sont enregistrées, les compiler passes s’exécutent (modifications globales), puis le conteneur est optimisé et mis en cache en une classe PHP figée — d’où la rapidité en prod.

2. Que peut faire une compiler pass ?

Modifier les définitions de services une fois toutes connues (collecter des taggés, remplacer/décorer, conditionner). Rarement nécessaire aujourd’hui grâce aux attributs.

3. Qu’est-ce qu’un bundle ?

Un paquet qui étend le framework (services, config, routes, commandes), destiné au code réutilisable/partagé — pas à organiser sa propre application.

4. Quelle classe simplifie la création de bundles depuis Symfony 6.1 ?

AbstractBundle (avec configure() et loadExtension()), qui remplace le trio Extension/Configuration/DependencyInjection verbeux.

5. Quand crée-t-on un bundle ?

Pour partager du code entre projets ou le publier (Packagist). Jamais pour organiser une application (le code applicatif reste dans src/).


Prochain chapitre : 5.5 — EventDispatcher — réagir à des événements métier sans coupler, avec listeners et subscribers.