Slide 1

Services & DI

Injection de dépendances et services - Symfony 6.4

Slide 2

Qu'est-ce qu'un Service ?

Définition

Un service est un objet qui exécute une tâche spécifique dans votre application.

💡 Analogie de l'atelier

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

Dans Symfony, un service peut être :

• 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

Slide 3

Le problème sans Injection de Dépendances

⚠️ Voyons pourquoi créer des objets directement pose problème

Exemple problématique :

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éé');
        }
    }
}

❌ Problèmes

• Couplage fort : Difficile à modifier

• Tests impossibles : Pas de mocks

• Configuration en dur : Pas flexible

• Duplication : Même code partout

• Erreurs : Oubli de dépendances

✅ Ce qu'on veut

• Flexibilité : Changer facilement

• Testabilité : Mocker les services

• Réutilisabilité : Partager les services

• Configuration : Centralisée

• Maintenabilité : Code propre

Slide 4

L'Injection de Dépendances (DI)

Principe fondamental

Au lieu de créer ses dépendances, un objet les reçoit de l'extérieur.

🎯 Inversion de contrôle

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 !

🏗️ Construction

Le container crée tous les services

Une seule fois, au bon moment

🔗 Injection

Les services sont fournis automatiquement

Via le constructeur ou méthodes

♻️ Réutilisation

Même instance partagée partout

Économie mémoire et cohérence

💭 Types d'injection :

• 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

Slide 5

Avec l'Injection de Dépendances

✅ 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']);
    }
}

❌ Avant (sans DI)

• 6 lignes de création manuelle

• Configuration en dur

• Impossible à tester

• Couplage fort

✅ Après (avec DI)

• 0 ligne de création manuelle

• Configuration externalisée

• Facilement testable

• Couplage faible

🚀 Avantages immédiats :

• 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

Slide 6

Le Container de Services Symfony

Le chef d'orchestre de vos services

🎼 Le container est le cerveau qui coordonne tous vos services

🏭 Fonctionnement du container :

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 !

Voir les services disponibles :

# 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

🔍 Services Symfony par défaut :

• 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 !

Slide 7

Créer un Service Personnalisé

🛠️ Créons un service pour gérer la logique métier

1. Créer la classe du service :

// 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;
    }
}

🎯 Bonnes pratiques :

• 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 !

Slide 8

Utiliser votre Service dans un Controller

🎮 Injectons notre service dans un controller

Méthode 1 : Injection par constructeur (recommandée)

// 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);
        }
    }
}

Méthode 2 : Injection par méthode

#[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']);
}

💡 Quelle méthode choisir ?

• 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 !

Slide 9

Configuration des Services

⚙️ Personnaliser le comportement de vos services

Auto-configuration par défaut :

# 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'

Configuration personnalisée :

# 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)%']

🔧 Options importantes :

• 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 !

Slide 10

Services selon l'Environnement

🌍 Adapter vos services selon l'environnement (dev, test, prod)

Configuration par environnement :

# 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)%'

🎯 Stratégies par environnement :

• Développement : Services de debug, logs verbeux

• Test : Mocks, services factices

• Production : Services optimisés, cache activé

💡 Un même code, différents comportements !

Exemple de service adaptatif :

// 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(/* ... */);
        }
    }
}
Slide 11

Tester avec l'Injection de Dépendances

🧪 L'injection de dépendances rend les tests très faciles

Test unitaire avec mocks :

// 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());
    }
}

✅ Avantages pour les tests :

• 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 !

Slide 12

Tags et Services Spécialisés

🏷️ Les tags permettent de catégoriser et d'utiliser vos services automatiquement

Event Listeners et Subscribers :

// 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(),
        ]);
    }
}

Services avec tags personnalisés :

# 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

🎯 Tags courants dans Symfony :

• 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 !

Slide 13

Patterns et Bonnes Pratiques

✅ Guide des meilleures pratiques pour vos services

🎯 Règles d'or des services :

1. Single Responsibility Principle

• Un service = une responsabilité

• Éviter les "god services" qui font tout

✅ Bon : EmailService, UserService, PaymentService

❌ Éviter : ApplicationService, HelperService

2. Injection par constructeur

• Dépendances obligatoires dans le constructeur

• Services toujours dans un état valide

• Plus facile à tester et comprendre

3. Utiliser les interfaces

// 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

✅ À faire

• Services stateless : Pas d'état interne

• Noms explicites : UserRegistrationService

• Interfaces : Pour le découplage

• Tests unitaires : Toujours !

• Documentation : PHPDoc détaillé

❌ À éviter

• 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 !

Slide 14

Debugging et Outils de Développement

🔍 Outils pour comprendre et déboguer vos services

Commandes utiles :

# 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

Web Profiler - Onglet Services :

• 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

Erreurs courantes et solutions :

❌ "Cannot autowire service"

Cause : Interface sans implémentation configurée

Solution : Créer un alias dans services.yaml

❌ "Circular reference detected"

Cause : Deux services se dépendent mutuellement

Solution : Extraire la logique commune dans un 3e service

❌ "Service not found"

Cause : Service non enregistré ou mal nommé

Solution : Vérifier le namespace et la configuration

Slide 15

Cas d'Usage Avancés

🚀 Techniques avancées pour maîtriser les services

Factory 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)%'

Decorateur Pattern :

// 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;
    }
}

💡 Autres patterns utiles :

• Service Locator : Pour l'injection optionnelle

• Chain of Responsibility : Traitement en chaîne

• Strategy Pattern : Algorithmes interchangeables

• Observer Pattern : Via les événements Symfony

Slide 16

Performance et Optimisation

⚡ Optimiser vos services pour de meilleures performances

Services Lazy :

# 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'

Compilation du Container :

# 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

✅ Optimisations

• Services privés : Plus rapides

• Lazy loading : Création à la demande

• Container compilé : En production

• Services stateless : Partageables

• Cache OpCode : APCu, Redis

⚠️ À surveiller

• 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

📊 Métriques à surveiller :

• 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 !

Slide 17

À retenir

🎯 Points clés :

• 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

🚀 Prochaines étapes :

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

🎓 Pour aller plus loin :

• 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 !

Slide 18

Questions ?

Des questions sur les Services & DI ?

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