Injection de dépendances et services - Symfony 6.4
Un service est un objet qui exécute une tâche spécifique dans votre application.
Imaginez un atelier de réparation automobile. Chaque mécanicien est spécialisé dans un domaine :
• Le mécanicien moteur s'occupe uniquement des moteurs
• Le carrossier répare la carrosserie
• L'électricien auto gère l'électronique
Chaque mécanicien = un service !
🔧 Principe : Une responsabilité = un service
• Un gestionnaire d'emails (MailerService)
• Un système de logs (Logger)
• Un validateur de données (Validator)
• Un accès à la base de données (EntityManager)
• Votre logique métier personnalisée
🎯 Objectif : Découpler et réutiliser le code
⚠️ Voyons pourquoi créer des objets directement pose problème
class UserController
{
public function register(Request $request)
{
// ❌ Création directe des dépendances
$mailer = new MailerService('smtp.gmail.com', 'user', 'pass');
$logger = new Logger('/var/log/app.log');
$validator = new UserValidator();
$user = new User();
$user->setEmail($request->get('email'));
if ($validator->isValid($user)) {
// Sauvegarder l'utilisateur...
$mailer->sendWelcomeEmail($user);
$logger->info('Nouvel utilisateur créé');
}
}
}
• Couplage fort : Difficile à modifier
• Tests impossibles : Pas de mocks
• Configuration en dur : Pas flexible
• Duplication : Même code partout
• Erreurs : Oubli de dépendances
• Flexibilité : Changer facilement
• Testabilité : Mocker les services
• Réutilisabilité : Partager les services
• Configuration : Centralisée
• Maintenabilité : Code propre
Au lieu de créer ses dépendances, un objet les reçoit de l'extérieur.
Plutôt que "J'ai besoin d'un marteau, je vais le fabriquer", on dit "Donne-moi un marteau".
Le container DI devient notre magasin d'outils !
Le container crée tous les services
Une seule fois, au bon momentLes services sont fournis automatiquement
Via le constructeur ou méthodesMême instance partagée partout
Économie mémoire et cohérence• Constructor injection : Via le constructeur (recommandé)
• Setter injection : Via des méthodes set
• Property injection : Directement dans les propriétés
🎯 Symfony privilégie l'injection par constructeur
✅ Voici le même code, mais avec l'injection de dépendances
class UserController extends AbstractController
{
public function __construct(
private MailerService $mailer,
private LoggerInterface $logger,
private UserValidator $validator
) {}
public function register(Request $request): Response
{
// ✅ Services déjà injectés et prêts !
$user = new User();
$user->setEmail($request->get('email'));
if ($this->validator->isValid($user)) {
// Sauvegarder l'utilisateur...
$this->mailer->sendWelcomeEmail($user);
$this->logger->info('Nouvel utilisateur créé');
}
return $this->json(['status' => 'created']);
}
}
• 6 lignes de création manuelle
• Configuration en dur
• Impossible à tester
• Couplage fort
• 0 ligne de création manuelle
• Configuration externalisée
• Facilement testable
• Couplage faible
• Code plus lisible et focalisé sur la logique métier
• Symfony gère automatiquement les dépendances
• Tests unitaires possibles avec des mocks
• Configuration centralisée dans les fichiers YAML/PHP
🎼 Le container est le cerveau qui coordonne tous vos services
1. Analyse : Symfony scanne vos classes
2. Enregistrement : Les services sont référencés
3. Résolution : Les dépendances sont calculées
4. Injection : Les services sont fournis automatiquement
💡 Tout cela se passe de manière transparente !
# Lister tous les services php bin/console debug:container # Chercher un service spécifique php bin/console debug:container mailer # Voir les détails d'un service php bin/console debug:container App\Service\UserService
• logger : Pour les logs
• mailer : Pour envoyer des emails
• doctrine.orm.entity_manager : Pour la base de données
• security.token_storage : Pour l'authentification
• validator : Pour valider les données
🎯 Plus de 200 services disponibles par défaut !
🛠️ Créons un service pour gérer la logique métier
// src/Service/UserService.php
namespace App\Service;
use App\Entity\User;
use App\Repository\UserRepository;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\Mailer\MailerInterface;
class UserService
{
public function __construct(
private EntityManagerInterface $entityManager,
private MailerInterface $mailer,
private LoggerInterface $logger,
private UserRepository $userRepository
) {}
public function createUser(string $email, string $name): User
{
// Vérifier si l'utilisateur existe déjà
if ($this->userRepository->findOneBy(['email' => $email])) {
throw new \Exception('Un utilisateur avec cet email existe déjà');
}
// Créer le nouvel utilisateur
$user = new User();
$user->setEmail($email);
$user->setName($name);
$user->setCreatedAt(new \DateTime());
// Sauvegarder en base
$this->entityManager->persist($user);
$this->entityManager->flush();
// Envoyer un email de bienvenue
$this->mailer->send(/* ... */);
// Logger l'événement
$this->logger->info('Nouvel utilisateur créé', ['email' => $email]);
return $user;
}
}
• Namespace : Placer dans src/Service/
• Responsabilité unique : Un service = un domaine
• Injection par constructeur : Plus sûr et lisible
• Interfaces : Utiliser les interfaces quand possible
💡 Symfony détecte automatiquement votre service !
🎮 Injectons notre service dans un controller
// src/Controller/UserController.php
namespace App\Controller;
use App\Service\UserService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class UserController extends AbstractController
{
public function __construct(
private UserService $userService
) {}
#[Route('/register', name: 'user_register', methods: ['POST'])]
public function register(Request $request): Response
{
$email = $request->request->get('email');
$name = $request->request->get('name');
try {
$user = $this->userService->createUser($email, $name);
return $this->json([
'message' => 'Utilisateur créé avec succès',
'user_id' => $user->getId()
]);
} catch (\Exception $e) {
return $this->json([
'error' => $e->getMessage()
], 400);
}
}
}
#[Route('/register', name: 'user_register', methods: ['POST'])]
public function register(Request $request, UserService $userService): Response
{
// $userService est automatiquement injecté !
$user = $userService->createUser(
$request->request->get('email'),
$request->request->get('name')
);
return $this->json(['status' => 'created']);
}
• Constructeur : Si vous utilisez le service dans plusieurs méthodes
• Méthode : Si vous l'utilisez dans une seule méthode
🎯 Les deux fonctionnent parfaitement !
⚙️ Personnaliser le comportement de vos services
# config/services.yaml
services:
# Configuration par défaut
_defaults:
autowire: true # Injection automatique
autoconfigure: true # Configuration automatique
public: false # Services privés par défaut
# Tous les services dans src/ sont auto-enregistrés
App\:
resource: '../src/'
exclude:
- '../src/DependencyInjection/'
- '../src/Entity/'
- '../src/Kernel.php'
# config/services.yaml
services:
# Service avec paramètres personnalisés
App\Service\UserService:
arguments:
$maxUsersPerDay: 100
$adminEmail: '%env(ADMIN_EMAIL)%'
# Alias pour une interface
App\Service\NotificationServiceInterface: '@App\Service\EmailNotificationService'
# Service avec configuration spéciale
App\Service\PaymentService:
calls:
- setApiKey: ['%env(PAYMENT_API_KEY)%']
• autowire: true : Injection automatique des dépendances
• autoconfigure: true : Configuration automatique des tags
• public: false : Service privé, plus performant
• shared: true : Une seule instance partagée (défaut)
💡 Symfony fait le maximum automatiquement !
🌍 Adapter vos services selon l'environnement (dev, test, prod)
# config/services.yaml (configuration globale)
services:
App\Service\MailerService: ~
# config/services_dev.yaml (spécifique au dev)
services:
App\Service\MailerService:
class: App\Service\FakeMailerService # Mailer de dev
# config/services_test.yaml (pour les tests)
services:
App\Service\MailerService:
class: App\Service\MockMailerService # Mailer de test
# config/services_prod.yaml (production)
services:
App\Service\MailerService:
arguments:
$smtpHost: '%env(SMTP_HOST)%'
$apiKey: '%env(MAILER_API_KEY)%'
• Développement : Services de debug, logs verbeux
• Test : Mocks, services factices
• Production : Services optimisés, cache activé
💡 Un même code, différents comportements !
// Service qui s'adapte à l'environnement
class NotificationService
{
public function __construct(
private string $environment,
private MailerInterface $mailer,
private LoggerInterface $logger
) {}
public function notify(User $user, string $message): void
{
if ($this->environment === 'dev') {
// En dev : juste un log
$this->logger->info("Email simulé pour {$user->getEmail()}: $message");
} else {
// En prod : vrai email
$this->mailer->send(/* ... */);
}
}
}
🧪 L'injection de dépendances rend les tests très faciles
// tests/Service/UserServiceTest.php
use App\Service\UserService;
use App\Repository\UserRepository;
use Doctrine\ORM\EntityManagerInterface;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
use Symfony\Component\Mailer\MailerInterface;
class UserServiceTest extends TestCase
{
public function testCreateUser(): void
{
// Créer des mocks pour toutes les dépendances
$entityManager = $this->createMock(EntityManagerInterface::class);
$mailer = $this->createMock(MailerInterface::class);
$logger = $this->createMock(LoggerInterface::class);
$userRepository = $this->createMock(UserRepository::class);
// Configurer le comportement des mocks
$userRepository->expects($this->once())
->method('findOneBy')
->willReturn(null); // Aucun utilisateur existant
$entityManager->expects($this->once())
->method('persist');
$entityManager->expects($this->once())
->method('flush');
// Créer le service avec les mocks
$userService = new UserService(
$entityManager,
$mailer,
$logger,
$userRepository
);
// Tester la méthode
$user = $userService->createUser('test@example.com', 'John Doe');
// Vérifications
$this->assertEquals('test@example.com', $user->getEmail());
$this->assertEquals('John Doe', $user->getName());
}
}
• Isolation : Tester uniquement la logique du service
• Mocks : Simuler les dépendances complexes
• Rapidité : Pas de base de données ou d'emails réels
• Fiabilité : Tests reproductibles
🎯 Sans DI, ces tests seraient impossibles !
🏷️ Les tags permettent de catégoriser et d'utiliser vos services automatiquement
// Service qui écoute les événements automatiquement
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;
class RequestLoggerService implements EventSubscriberInterface
{
public function __construct(
private LoggerInterface $logger
) {}
public static function getSubscribedEvents(): array
{
return [
KernelEvents::REQUEST => 'onKernelRequest',
];
}
public function onKernelRequest(RequestEvent $event): void
{
$request = $event->getRequest();
$this->logger->info('Nouvelle requête', [
'method' => $request->getMethod(),
'uri' => $request->getUri(),
]);
}
}
# config/services.yaml
services:
App\Service\EmailNotificationHandler:
tags:
- { name: 'app.notification_handler', type: 'email' }
App\Service\SmsNotificationHandler:
tags:
- { name: 'app.notification_handler', type: 'sms' }
# Service qui collecte tous les handlers
App\Service\NotificationManager:
arguments:
$handlers: !tagged_iterator app.notification_handler
• kernel.event_listener : Écouteurs d'événements
• doctrine.event_listener : Événements Doctrine
• twig.extension : Extensions Twig
• validator.constraint_validator : Validateurs personnalisés
• serializer.normalizer : Normaliseurs de sérialisation
💡 Les tags automatisent l'enregistrement des services !
✅ Guide des meilleures pratiques pour vos services
• Un service = une responsabilité
• Éviter les "god services" qui font tout
✅ Bon : EmailService, UserService, PaymentService
❌ Éviter : ApplicationService, HelperService
• Dépendances obligatoires dans le constructeur
• Services toujours dans un état valide
• Plus facile à tester et comprendre
// Interface pour découpler
interface NotificationServiceInterface
{
public function send(User $user, string $message): void;
}
// Implémentations concrètes
class EmailNotificationService implements NotificationServiceInterface { /* ... */ }
class SmsNotificationService implements NotificationServiceInterface { /* ... */ }
💡 Permet de changer d'implémentation facilement
• Services stateless : Pas d'état interne
• Noms explicites : UserRegistrationService
• Interfaces : Pour le découplage
• Tests unitaires : Toujours !
• Documentation : PHPDoc détaillé
• Services statiques : Utilisez l'injection
• Dépendances circulaires : A dépend de B qui dépend de A
• Services publics : Sauf cas spéciaux
• new dans les services : Injectez plutôt
• Services trop gros : Divisez !
🔍 Outils pour comprendre et déboguer vos services
# Lister tous les services php bin/console debug:container # Rechercher un service php bin/console debug:container user # Détails d'un service spécifique php bin/console debug:container App\Service\UserService # Voir l'arbre des dépendances php bin/console debug:container --show-arguments App\Service\UserService # Services avec un tag spécifique php bin/console debug:container --tag=doctrine.event_listener # Voir tous les paramètres php bin/console debug:container --parameters
• Nombre de services : Total dans le container
• Services instanciés : Ceux réellement utilisés
• Temps de compilation : Performance du container
• Graphique des dépendances : Visualisation des liens
🎯 Accès via /_profiler en mode dev
Cause : Interface sans implémentation configurée
Solution : Créer un alias dans services.yaml
Cause : Deux services se dépendent mutuellement
Solution : Extraire la logique commune dans un 3e service
Cause : Service non enregistré ou mal nommé
Solution : Vérifier le namespace et la configuration
🚀 Techniques avancées pour maîtriser les services
// Créer des objets complexes
class PaymentServiceFactory
{
public function createPaymentService(string $provider): PaymentServiceInterface
{
return match($provider) {
'stripe' => new StripePaymentService($this->stripeApiKey),
'paypal' => new PaypalPaymentService($this->paypalCredentials),
default => throw new \InvalidArgumentException("Provider $provider not supported")
};
}
}
# Configuration
services:
App\Factory\PaymentServiceFactory:
arguments:
$stripeApiKey: '%env(STRIPE_API_KEY)%'
$paypalCredentials: '%env(PAYPAL_CREDENTIALS)%'
// Ajouter des fonctionnalités à un service existant
class CacheableUserService implements UserServiceInterface
{
public function __construct(
private UserServiceInterface $userService, // Service original
private CacheInterface $cache
) {}
public function findUser(int $id): ?User
{
$cacheKey = "user_$id";
if ($this->cache->hasItem($cacheKey)) {
return $this->cache->getItem($cacheKey)->get();
}
$user = $this->userService->findUser($id); // Délégation
if ($user) {
$this->cache->save($this->cache->getItem($cacheKey)->set($user));
}
return $user;
}
}
• Service Locator : Pour l'injection optionnelle
• Chain of Responsibility : Traitement en chaîne
• Strategy Pattern : Algorithmes interchangeables
• Observer Pattern : Via les événements Symfony
⚡ Optimiser vos services pour de meilleures performances
# config/services.yaml
services:
App\Service\HeavyService:
lazy: true # Le service n'est créé qu'au premier usage
App\Service\DatabaseService:
lazy: true
arguments:
$connection: '@doctrine.dbal.default_connection'
# Compiler le container pour la production php bin/console cache:warmup --env=prod # Dumper le container optimisé php bin/console debug:container --env=prod > container_prod.txt
• Services privés : Plus rapides
• Lazy loading : Création à la demande
• Container compilé : En production
• Services stateless : Partageables
• Cache OpCode : APCu, Redis
• Services lourds : Initialisation coûteuse
• Dépendances circulaires : Performance dégradée
• Trop de services : Container gonflé
• Services publics : Moins optimisés
• Injection excessive : Complexité inutile
• Temps de compilation : Container build time
• Mémoire utilisée : Services en mémoire
• Services instanciés : Ratio utilisés/totaux
• Dépendances par service : Complexité
🎯 Profiler : votre meilleur ami pour l'optimisation !
• Les services encapsulent la logique métier réutilisable
• L'injection de dépendances découple votre code
• Le container Symfony gère automatiquement vos services
• Une responsabilité = un service (Single Responsibility)
• L'injection par constructeur est recommandée
• Les tests deviennent faciles avec l'injection
💡 Conseil : Commencez simple, complexifiez au besoin
1. Pratique : Créer vos premiers services personnalisés
2. Tests : Écrire des tests unitaires avec mocks
3. Patterns : Implémenter Factory, Decorator, Strategy
4. Events : Utiliser le système d'événements
5. Performance : Optimiser avec lazy loading et cache
📚 Ressources : Documentation Symfony DI + Design Patterns
• Compiler Passes : Modifier le container au build
• Service Locator : Injection optionnelle avancée
• Bundle Development : Créer vos propres bundles
• Microservices : Architecture distribuée
🔮 L'injection de dépendances est la base de tout framework moderne !
N'hésitez pas à poser vos questions !
💡 Sujets d'approfondissement :
• Configuration avancée des services
• Patterns de conception avec DI
• Tests et mocks
• Performance et optimisation
Formation DWWM - Services & DI avec Symfony 6.4
Bonne continuation dans vos projets ! 🚀
Utilisez les boutons de navigation ou les touches ← → pour naviguer