L'injection de dépendance, ou comment être fainéant avec élégance

9 min de lecture

La fainéantise organisée

Avant d'aborder plus en détail l'architecture du site, je vais parler de notions qu'il vous faudra acquérir. C'est parti !

Vous savez, dans le monde merveilleux de Symfony, il y a un truc assez magique qu'on appelle « l'injection de dépendance ». Déjà rien que le nom, ça sonne comme une maladie contractée lors d'une soirée un peu trop festive à Berlin. Mais rassurez-vous, rien à voir avec votre carnet de santé.

En gros, l'injection de dépendance, c'est comme avoir un pote super serviable, toujours prêt à vous apporter votre boisson préférée sans que vous ayez à lever votre arrière-train du canapé. Bah oui, c'est tout simplement Symfony qui vous ramène gentiment les services dont vous avez besoin dans votre classe, histoire que vous puissiez rester tranquille, à coder comme un gros fainéant.

Fini le temps des "new" à tout va

Alors attention, c'est subtil hein ! Certains s'emmêlent les pinceaux à coup de new Service() par-ci, par-là, comme si on était encore en 2012. Non mais sérieusement les gars, il est temps d'arrêter le massacre. Quand Symfony vous file un framework, c'est pas pour que vous fassiez du bricolage façon Ikea avec des pièces qui restent en trop à la fin.

L'autowiring, la livraison de service à domicile

Grâce à l'injection de dépendance et à l'autowiring, plus besoin de se demander comment se débrouiller pour appeler ce foutu service que vous avez créé. Non, maintenant, c'est propre, c'est net. On injecte directement dans le constructeur :

use App\Service\SuperService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;

class HomeController extends AbstractController
{
    public function __construct(private SuperService $service) {}
    
    public function index(): Response
    {
        $data = $this->service->getLesBonnesInfos();
        return $this->render('page/home.html.twig', [
            'data' => $data
        ]);
    }
}

Et voilà, magie de l'autowiring : Symfony reconnaît automatiquement vos services grâce aux types hints, pas besoin de tout déclarer dans un fichier de config.

Configuration minimale, résultats maximaux

D'ailleurs, en parlant de fainéantise, Symfony pousse encore plus loin avec les services automatiques. Plus besoin de déclarer explicitement votre service, tout se fait par convention et par type. Le truc vous lit littéralement dans les pensées. Même votre psychologue ne fait pas mieux !

Un petit exemple ? Allez, pour le plaisir :

# config/services.yaml
services:
    App\:
        resource: '../src/'
        autowire: true
        autoconfigure: true

Hop là ! C'est réglé, on ne touche plus à rien. Symfony sait quoi faire, il instancie vos services, les configure, et les injecte là où il faut. C'est comme un Uber Eats, mais pour votre code : ça arrive chaud et tout prêt dans votre assiette, vous avez juste à déguster.

Mais attendez, y'a encore plus de fainéantise possible !

Les services tagués, ou comment devenir collectionneur sans effort

Vous savez ce qui est encore plus cool que l'injection de dépendance de base ? Les services tagués ! Imaginez que vous voulez collecter tous les services d'un certain type, genre comme si vous faisiez une razzia sur les petits fours lors d'un mariage.

// Votre super interface
interface TraitementDonneeInterface
{
    public function traiter(array $data): array;
}

// Vos implémentations
class TraitementExcel implements TraitementDonneeInterface 
{
    public function traiter(array $data): array { /* ... */ }
}

class TraitementJson implements TraitementDonneeInterface 
{
    public function traiter(array $data): array { /* ... */ }
}

// Et maintenant le collector qui ramasse tout ce petit monde
class TraitementRegistry
{
    /** @var TraitementDonneeInterface[] */
    private array $traitements = [];
    
    public function addTraitement(TraitementDonneeInterface $traitement, string $name): void
    {
        $this->traitements[$name] = $traitement;
    }
    
    public function getTraitement(string $name): TraitementDonneeInterface
    {
        return $this->traitements[$name] ?? throw new \Exception("Traitement introuvable, t'as trop bu ou quoi ?");
    }
}

Et dans votre config, vous tagguez comme un artiste urbain en pleine nuit :

# config/services.yaml
services:
    _defaults:
        autowire: true
        autoconfigure: true
    
    App\TraitementExcel:
        tags:
            - { name: 'app.traitement', key: 'excel' }
    
    App\TraitementJson:
        tags:
            - { name: 'app.traitement', key: 'json' }

Et la partie magique, c'est lorsque vous configurez votre registry pour collecter automatiquement tous ces services tagués :

// Dans un CompilerPass, parce que oui, on est des pros
namespace App\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Reference;
use App\TraitementRegistry;

class TraitementPass implements CompilerPassInterface
{
    public function process(ContainerBuilder $container): void
    {
        $registry = $container->findDefinition(TraitementRegistry::class);
        
        foreach ($container->findTaggedServiceIds('app.traitement') as $id => $tags) {
            foreach ($tags as $tag) {
                $registry->addMethodCall('addTraitement', [
                    new Reference($id),
                    $tag['key']
                ]);
            }
        }
    }
}

Tadaaaaam ! Symfony va maintenant collecter tous vos traitements comme on ramasse des pokémons. Vous ajoutez un nouveau traitement ? Ajoutez juste le tag et le tour est joué, pas besoin de toucher à votre registry. C'est comme avoir un assistant personnel qui range votre appart pendant que vous jouez à la PS5.

L'injection par setters, pour les relations compliquées

Même si l'injection par constructeur, c'est le Saint Graal de la propreté, parfois vous avez des relations un peu... disons... compliquées avec certains services. Genre quand ils sont optionnels ou quand vous ne voulez pas alourdir votre constructeur qui ressemble déjà à l'annuaire téléphonique.

class ServiceComplique
{
    private ?LoggerInterface $logger = null;
    
    // D'autres services injectés dans le constructeur...
    
    #[Required]
    public function setLogger(LoggerInterface $logger): void
    {
        $this->logger = $logger;
    }
    
    public function faireUnTrucComplique(): void
    {
        if ($this->logger) {
            $this->logger->info('Je fais un truc compliqué, mais au moins c\'est loggé !');
        }
        
        // Faire le truc compliqué...
    }
}

Avec l'attribut #[Required], Symfony va automatiquement appeler cette méthode après avoir instancié votre service. C'est comme dire "apporte-moi ma bière, mais seulement si t'en as une sous la main, sinon c'est pas grave".

Le cycle de vie des services, ou comment gérer vos relations à long terme

Par défaut, Symfony considère tous vos services comme des singletons, c'est-à-dire qu'ils sont créés une seule fois puis réutilisés partout. C'est comme votre collègue qui réutilise la même tasse à café depuis 3 ans sans jamais la laver.

Mais parfois, vous voulez un service tout frais à chaque fois, comme un croissant qui sort du four :

# config/services.yaml
services:
    App\Service\GenerateurDeDocuments:
        shared: false  # Nouvel objet à chaque injection

Maintenant, chaque controller ou service qui demande ce générateur en recevra une instance flambant neuve. C'est comme commander un Uber à chaque fois au lieu de partager le même avec vos collègues.

Interfaces vs Implémentations : l'art d'être infidèle élégamment

L'un des avantages majeurs de l'injection de dépendance, c'est de pouvoir injecter des interfaces plutôt que des implémentations concrètes. C'est comme dire "j'ai besoin d'un moyen de transport" plutôt que "j'ai besoin d'une Ferrari rouge modèle 2025 avec siège chauffant".

interface NotificationInterface
{
    public function envoyer(string $message, string $destinataire): void;
}

class EmailNotification implements NotificationInterface { /* ... */ }
class SMSNotification implements NotificationInterface { /* ... */ }
class PigeonVoyageurNotification implements NotificationInterface { /* ... */ }

class CommandeService
{
    public function __construct(private NotificationInterface $notification) {}
    
    public function confirmerCommande(Commande $commande): void
    {
        // Traitement de la commande...
        
        $this->notification->envoyer(
            "Votre commande n°{$commande->getNumero()} est confirmée !",
            $commande->getClient()->getContact()
        );
    }
}

Et dans votre config, vous décidez quelle implémentation utiliser :

# config/services.yaml
services:
    App\NotificationInterface: '@App\EmailNotification'  # En prod
    # App\NotificationInterface: '@App\PigeonVoyageurNotification'  # Pour les hipsters

Changez d'avis ? Modifiez une ligne, et toute votre application envoie maintenant des notifications par pigeon voyageur. C'est magique !

Les services lazy, parce que la procrastination, c'est un art

Certains de vos services sont lourds, comme vraiment lourds. Genre, le genre de service qui fait un sleep(5) dans son constructeur (si vous faites ça, on doit avoir une conversation sérieuse). Heureusement, Symfony offre le lazy loading :

# config/services.yaml
services:
    App\Service\ServiceHyperLourd:
        lazy: true

Maintenant, Symfony va créer un proxy qui ne chargera le vrai service que lorsque vous appellerez une méthode dessus. C'est comme commander une pizza mais ne pas la payer avant qu'elle n'arrive réellement chez vous.

Les tests unitaires, ou comment faire semblant que tout marche

L'un des avantages les plus cool de l'injection de dépendance, c'est la facilité à mocker vos services pour les tests :

public function testConfirmerCommande(): void
{
    // On crée un mock de notre service de notification
    $notificationMock = $this->createMock(NotificationInterface::class);
    
    // On s'attend à ce que la méthode envoyer soit appelée avec ces paramètres précis
    $notificationMock
        ->expects($this->once())
        ->method('envoyer')
        ->with(
            $this->stringContains('est confirmée'),
            'client@example.com'
        );
    
    // On crée notre service avec le mock
    $commandeService = new CommandeService($notificationMock);
    
    // On exécute la méthode à tester
    $commande = new Commande();
    // ... setup de la commande
    $commandeService->confirmerCommande($commande);
    
    // Pas besoin d'assertions supplémentaires, le mock vérifie que envoyer() a été appelé correctement
}

Comme ça, vous testez votre service sans vraiment envoyer d'email ou de SMS. C'est comme simuler une conversation avec votre belle-mère sans avoir à la supporter pour de vrai.

La compilation du container, ou comment gagner en performances sans effort

Saviez-vous que Symfony compile tout son container de services en PHP pur pour les environnements de production ? C'est comme si votre code était pré-mâché avant même que vous ne l'exécutiez :

# Pour voir la magie
bin/console debug:container --env=prod

En prod, Symfony génère un énorme fichier PHP avec toutes les définitions de services pré-résolues. Plus besoin de parcourir les fichiers YAML ou de faire de la réflexion PHP à chaque requête. C'est comme si vous aviez un assistant qui prépare tous vos repas pour la semaine chaque dimanche.

Conclusion : la fainéantise élevée au rang d'art

Certains esprits mal placés diront : « Ouais mais c'est pas un peu de la fainéantise tout ça ? ». Bah, bien sûr que oui ! Mais c'est justement l'intérêt : déléguer au maximum à Symfony, parce qu'on a clairement autre chose à faire que de taper 50 lignes inutiles pour appeler un service qu'on a déjà codé il y a 3 semaines et dont on ne se souvient même plus pourquoi il existe.

En plus, Symfony, dans sa grande bonté, vérifie tout comme un videur à l'entrée de boîte : si un service manque ou n'est pas déclaré, il vous refuse l'entrée direct. Pas moyen de venir squatter avec un mauvais service dans la classe VIP.

Donc, si vous bossez encore sans injection de dépendance et autowiring en 2025, c'est comme aller bosser en charrette à bœufs pendant que vos collègues déboulent en voiture électrique. C'est possible, mais bon, il est temps d'arrêter le masochisme.

Allez, injectez joyeusement, déléguez massivement, et surtout, arrêtez d'être radins : Symfony est là pour ça. Santé !

Confidentialité

Ce site utilise Umami pour analyser le trafic de manière anonyme. Acceptez-vous la collecte de données anonymes ?