Chapitre 5.2 — Config des services, paramètres & environnements
Partie 5 : Services & DI — Chapitre 2/5 Version de référence : Symfony 7.4.
⏱️ TL;DR
- Les paramètres sont des valeurs de config nommées (
%param%), déclarées dansservices.yamlou dérivées de variables d’environnement.#[Autowire]injecte ce que l’autowiring ne devine pas : un paramètre (#[Autowire('%kernel.project_dir%')]), une variable d’env (#[Autowire(env: 'STRIPE_KEY')]), un service précis (#[Autowire(service: 'app.mailer.transactional')]) ou une expression.- Quand une interface a plusieurs implémentations, on lève l’ambiguïté avec un alias par défaut,
#[Autowire(service: ...)]ou l’attribut#[Target].- Tous les services sont privés par défaut (on les injecte, on ne les « récupère » pas du conteneur).
- Des services peuvent n’exister que dans certains environnements (
#[When(env: 'dev')],when@deven YAML).- Pour un dev Next.js :
#[Autowire(env: ...)]≈ lireprocess.env.Xmais injecté et typé au bon endroit, pas lu globalement.
🎯 Objectifs
- Déclarer et injecter des paramètres.
- Utiliser
#[Autowire]pour les cas que l’autowiring ne couvre pas. - Résoudre l’ambiguïté de plusieurs implémentations d’une interface.
- Adapter des services par environnement.
1. Les paramètres
Un paramètre est une valeur de configuration nommée, réutilisable :
# config/services.yaml
parameters:
app.booking.max_seats: 12
app.upload_dir: '%kernel.project_dir%/var/uploads'
services:
_defaults:
autowire: true
autoconfigure: true
App\:
resource: '../src/'
exclude: ['../src/Entity/', '../src/Kernel.php']Les paramètres %kernel.*% sont fournis par Symfony (%kernel.project_dir%, %kernel.environment%…). On référence un paramètre par %nom%. Pour l’injecter dans un service, on utilise #[Autowire] (§2).
⚠️ Piège : ne mettez pas de secrets dans
parameters(c’est committé). Les valeurs sensibles viennent des variables d’environnement / du coffre de secrets (chapitre 3.4). Les paramètres servent aux constantes de config (limites, chemins, options), pas aux secrets.
2. #[Autowire] : injecter ce qui n’est pas un service
L’autowiring par type couvre les services. Pour le reste (scalaires, params, env, service nommé), on utilise l’attribut #[Autowire] sur l’argument :
use Symfony\Component\DependencyInjection\Attribute\Autowire;
final class AvatarUploader
{
public function __construct(
// un paramètre
#[Autowire('%app.upload_dir%')]
private readonly string $uploadDir,
// une variable d'environnement
#[Autowire(env: 'MAX_UPLOAD_MB')]
private readonly int $maxUploadMb,
// un service précis (quand plusieurs candidats existent)
#[Autowire(service: 'app.storage.s3')]
private readonly StorageInterface $storage,
// une expression (ex. récupérer une valeur d'un autre service)
#[Autowire(expression: 'service("App\\\\Service\\\\Clock").now()')]
private readonly \DateTimeImmutable $bootTime,
) {}
}C’est l’outil de finition de l’injection : 95 % du câblage est automatique (par type), et #[Autowire] gère les 5 % restants, au plus près de l’argument concerné.
💡 Pour un dev Next.js :
#[Autowire(env: 'MAX_UPLOAD_MB')]remplace unprocess.env.MAX_UPLOAD_MBdisséminé. La différence : la valeur est injectée dans le service qui en a besoin (localisée, typée, testable), pas lue depuis un objet global. Vos services ne « connaissent » pas l’environnement ; ils reçoivent leurs valeurs.
3. Lier des arguments globalement (bind)
Si un même argument revient dans plusieurs services, on peut le lier une fois pour toutes dans _defaults :
services:
_defaults:
autowire: true
autoconfigure: true
bind:
string $uploadDir: '%app.upload_dir%'
int $bookingMaxSeats: '%app.booking.max_seats%'
$adminEmail: '%env(ADMIN_EMAIL)%'Désormais, tout service dont le constructeur a un argument string $uploadDir reçoit automatiquement la valeur — sans #[Autowire] répété. On lie par nom ($uploadDir), par type + nom (string $uploadDir), ou par type.
4. Plusieurs implémentations d’une interface
L’autowiring d’une interface fonctionne s’il n’y a qu’une implémentation. Avec plusieurs (ex. StorageInterface → LocalStorage, S3Storage), il faut désambiguïser :
Option A — un alias par défaut (l’implémentation « par défaut » de l’interface) :
services:
App\Storage\StorageInterface: '@App\Storage\S3Storage' # défaut = S3Désormais StorageInterface $storage injecte S3Storage partout, sauf indication contraire.
Option B — nommer explicitement avec #[Autowire(service: ...)] (vu au §2).
Option C — l’attribut #[Target] (le plus élégant) : on nomme les implémentations et on cible par nom.
use Symfony\Component\DependencyInjection\Attribute\Target;
public function __construct(
#[Target('localStorage')] private readonly StorageInterface $local,
#[Target('s3Storage')] private readonly StorageInterface $remote,
) {}⚠️ Piège fréquent : injecter une interface qui a plusieurs implémentations sans désambiguïser → erreur au conteneur (« multiple candidate services »). Le message est explicite et vous liste les candidats. La solution : alias par défaut,
#[Autowire(service:)]ou#[Target].
5. Services publics vs privés
Depuis Symfony 4, tous les services sont privés par défaut : on les injecte (on les reçoit), on ne les récupère pas manuellement depuis le conteneur ($container->get(...) est proscrit dans le code applicatif). C’est voulu : ça garantit le découplage et permet au conteneur d’optimiser (supprimer les services inutilisés, inliner…).
⚠️ Piège : chercher à faire
$this->container->get('mon_service')dans un service. C’est un anti-pattern (« service locator » caché) : injectez le service par constructeur à la place. Si vous avez vraiment besoin de récupérer dynamiquement l’un parmi plusieurs services, utilisez un service locator explicite (chapitre 5.3), pas le conteneur global.
6. Services par environnement
Certains services ne doivent exister qu’en dev (un faux paiement, un logger verbeux) ou qu’en prod. Deux mécanismes :
use Symfony\Component\DependencyInjection\Attribute\When;
#[When(env: 'dev')]
final class FakePaymentGateway implements PaymentGatewayInterface { /* ... */ }
#[When(env: 'prod')]
final class StripePaymentGateway implements PaymentGatewayInterface { /* ... */ }En YAML, l’équivalent est la syntaxe when@dev: / when@prod: dans services.yaml ou des fichiers config/services_dev.yaml. Ainsi, PaymentGatewayInterface est câblé sur le faux en dev, sur Stripe en prod — sans changer le code qui l’utilise.
💡 Pour un dev Next.js : c’est une façon propre de faire du « feature flag par environnement » au niveau de l’injection : le même point d’usage reçoit une implémentation différente selon l’env, décidé par le conteneur — plus robuste que des
if (process.env.NODE_ENV === ...)éparpillés.
✏️ Exercices
Exercice 1. Injectez dans un service InvoiceGenerator : le paramètre %app.upload_dir% (comme string $dir) et la variable d’environnement COMPANY_NAME (comme string $company).
✅ Solution
public function __construct(
#[Autowire('%app.upload_dir%')] private readonly string $dir,
#[Autowire(env: 'COMPANY_NAME')] private readonly string $company,
) {}(Alternative : lier ces arguments dans _defaults.bind si plusieurs services les partagent.)
Exercice 2. PaymentGatewayInterface a deux implémentations. L’injection échoue avec « multiple candidate services ». Donnez deux façons de résoudre.
✅ Solution
(1) Déclarer un alias par défaut dans services.yaml : App\Payment\PaymentGatewayInterface: '@App\Payment\StripePaymentGateway'. (2) Cibler l’implémentation à l’injection avec #[Autowire(service: StripePaymentGateway::class)] ou l’attribut #[Target('stripe')]. (Bonus : si le choix dépend de l’env, utiliser #[When(env: 'prod')]/#[When(env: 'dev')] sur les implémentations.)
Exercice 3. Pourquoi $this->container->get('app.mailer') dans un service est-il déconseillé ?
✅ Solution
C’est un service locator caché : le service se couple au conteneur global et masque ses dépendances réelles (on ne voit plus ce dont il a besoin dans son constructeur), ce qui nuit à la testabilité et empêche les optimisations du conteneur. Bonne pratique : injecter le service par constructeur. Pour un choix dynamique parmi plusieurs, utiliser un service locator explicite (chapitre 5.3), pas le conteneur global.
🧠 Quiz de révision
1. Qu’est-ce qu’un paramètre et comment le référence-t-on ?
Une valeur de config nommée (dans parameters:), référencée par %nom% ; injectée via #[Autowire('%nom%')] ou bind.
2. Comment injecter une variable d’environnement dans un service ?
Avec #[Autowire(env: 'NOM')] sur l’argument (ou via bind avec %env(NOM)%).
3. Une interface a deux implémentations : comment l’injecter sans erreur ?
Désambiguïser via un alias par défaut, #[Autowire(service: ...)], ou l’attribut #[Target('nom')].
4. Les services sont-ils publics ou privés par défaut ?
Privés : on les injecte, on ne les récupère pas via $container->get() (anti-pattern).
5. Comment fournir une implémentation différente selon l’environnement ?
Avec #[When(env: 'dev')] / #[When(env: 'prod')] sur les classes (ou when@dev/when@prod en YAML).
Prochain chapitre : 5.3 — Tags, autoconfiguration & service locators — collecter des familles de services (le pattern stratégie) et les charger paresseusement.