Symfony Dependency Injection : Anatomie d’une révolution

Meetup Tunis Talan 2025

Imen EZZINE

Développeuse PHP Symfony

@imenezzine

  @imenezzine1

 

🤔 Qu’est-ce que l'Injection de Dépendance ?

➡️ L'Injection de Dépendance (DI) est un pattern de conception qui permet de réduire le couplage entre les différentes parties d'une application.

 

Au lieu que les classes créent leurs dépendances elles-mêmes, elles reçoivent ces dépendances de l'extérieur, généralement via leur constructeur.

🤔 Qu’est-ce que Le composant DependencyInjection?

 

➡️ Le composant DependencyInjection est un outil clé de Symfony qui permet de gérer les dépendances des services de manière centralisée et efficace.

 

➡️ Au lieu de créer et gérer manuellement les objets, Symfony s’en charge pour nous en utilisant un conteneur de services.

Pourquoi l'utiliser?

  • Séparation des préoccupations 

  • Testabilité 

  • Maintenance 

  • Compatible avec les principes SOLID

Injection de dépendances : retour sur 15 ans d’évolution

💥 Symfony : de l’instanciation directe…
à
l’injection maîtrisée

🏗️ Symfony 1 (2007) – Les fondations de l’injection de dépendances

// apps/frontend/modules/job/actions/actions.class.php 

class jobActions extends sfActions 
{ 
  private $userService;
  
  public function __construct() 
  { 
  	$this->userService = new UserService(); // ❌ Couplage fort 
  } 
  
  public function executeIndex(sfWebRequest $request)
  { 
  	$address = $this->userService->getAddress();
  	// ... 
  }
  // ... 
}

💥  Une époque où l’instanciation directe des classes était la norme

mailer:
  class: sfMailer
  param:
    logging:           %SF_LOGGING_ENABLED%
    charset:           %SF_CHARSET%
    delivery_strategy: realtime
    transport:
      class: Swift_SmtpTransport
      param:
        host:       localhost
        port:       25


// apps/backend/modules/affiliate/actions/actions.class.php
class affiliateActions extends autoAffiliateActions
{
  public function executeListActivate()
  {
    // faut passer par sfContext pour l'injection des services tiers
	$mailer = sfContext::getInstance()->getMailer(); 

    $message = $mailer->compose(.....);
 
    $mailer->send($message);
 
    $this->redirect('jobeet_affiliate');
  }
 
  // ...
}

🧙 Symfony 1 : la magie de sfContext

🚀 Symfony 2 débarque en 2011

# app/config/services.yml
services:
	app.invoice_mailer:
        class: AppBundle\Service\InvoiceMailer
        
    app.job_affiliate_controller:
        class:     AppBundle\Controller\JobAffiliateController
        arguments: ['@app.invoice_mailer']
        
    app.exception_subscriber:
        class: AppBundle\EventSubscriber\ExceptionSubscriber
        arguments:
            - '@app.invoice_mailer'
        tags:
            - { name: kernel.event_subscriber }
            
    app.twig_extension:
        class: AppBundle\Twig\AppExtension
        public: false
        tags:
            - { name: twig.extension }
    .... 

🛠️ Symfony 2 : adieu sfContext, bonjour Service Container !

 

// src/AppBundle/EventSubscriber/ExceptionSubscriber.php
namespace AppBundle\EventSubscriber;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use AppBundle\Service\InvoiceMailer;

class ExceptionSubscriber implements EventSubscriberInterface
{
	private $mailer;
    
	public function __construct(InvoiceMailer $mailer) 
    {
    	$this->mailer =$mailer;
    }
    
    public static function getSubscribedEvents()
    {
        // return the subscribed events, their methods and priorities
        return array(
           KernelEvents::EXCEPTION => array(
               array('processException', 10)
        );
    }
    
    public function processException(GetResponseForExceptionEvent $event)
    {
         //.....
    }
 }

🛠️ Symfony 2 : adieu sfContext, bonjour Service Container !

 

🧠 Symfony 3.3 : vers une DI plus intelligente avec l’autowiring
et
l'autoconfigure

✨ Symfony 3.3 : fini la déclaration manuelle, place à l’automagie

services:
    # default configuration for services in *this* file
    _defaults:
        # automatically injects dependencies in your services
        autowire: true
        # automatically registers your services as commands, event subscribers, etc.
        autoconfigure: true
        # this means you cannot fetch services directly from the container via $container->get()
        # if you need to do this, you can override this setting on individual services
        public: false

    # makes classes in src/AppBundle available to be used as services
    # this creates a service per class whose id is the fully-qualified class name
    AppBundle\:
        resource: '../../src/AppBundle/*'
        # you can exclude directories or files
        # but if a service is unused, it's removed anyway
        exclude: '../../src/AppBundle/{Entity,Repository}'

    # controllers are imported separately to make sure they're public
    # and have a tag that allows actions to type-hint services
    AppBundle\Controller\:
        resource: '../../src/AppBundle/Controller'
        public: true
        tags: ['controller.service_arguments']

✨ Symfony 3.3 : quelques cas à la marge à configurer à la main

# app/config/services.yml
parameters:
    sender_email: manager@example.com

services:
    # ... same as before
    
     _defaults:
        bind:
            # pass this value to any $adminEmail argument for any service
            # that's defined in this file (including controller arguments)
            $adminEmail: 'manager@example.com'

            # pass this service to any $requestLogger argument for any
            # service that's defined in this file
            $requestLogger: '@monolog.logger.request'

            # pass this service for any LoggerInterface type-hint for any
            # service that's defined in this file
            Psr\Log\LoggerInterface: '@monolog.logger.request'


    AppBundle\Updates\SiteUpdateManager:
        arguments:
            $senderEmail: '%sender_email%'
            
    # explicitly configure the wanted service for monolog
    AppBundle\Service\MessageGenerator:
        arguments:
            $logger: '@monolog.logger.request'

⚡ Symfony 5 (>=2020) : l’injection de dépendances

boostée par les attributs

 

🧩 Promoted Properties et Attributs PHP 8 : une syntaxe plus claire et moins de cas particuliers à gérer

 

namespace App\Controller;

use Psr\Log\LoggerInterface;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\HttpFoundation\Response;

class UserController
{
    public function __construct(
        private LoggerInterface $logger, // Promoted property
        #[Autowire('%kernel.project_dir%')] private string $projectDir
    ) {}

    #[Route('/user', name: 'user_index')]
    public function __invoke(): Response
    {
        $this->logger->info('Contrôleur invocable avec propriétés promues');
        
        return new Response();
    }
}

🧩 Promoted Properties et Attributs PHP 8 : une syntaxe plus claire et moins de cas particuliers à gérer

 

namespace App\EventListener;

use Symfony\Component\EventDispatcher\Attribute\AsEventListener;

#[AsEventListener(event: CustomEvent::class, method: 'onCustomEvent', priority: 42)]
final class RegisterUserListener
{
    public function onRegister(CustomEvent $event): void
    {
        // ...
    }

}

🧩 Promoted Properties et Attributs PHP 8 : une syntaxe plus claire et moins de cas particuliers à gérer

 

// src/Command/RegisterUserCommand.php
namespace App\Command;

use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;

// the "name" and "description" arguments of AsCommand replace the
// static $defaultName and $defaultDescription properties
#[AsCommand(
    name: 'app:register-user',
    description: 'Creates a new user.',
    hidden: false,
    aliases: ['app:add-user']
)]
class RegisterUserCommand extends Command
{
    // ...
}

📦 Symfony 6 : plus d’attributs et une meilleure gestion des cas non couverts

 

Symfony 6.1 : #[Autowire]

use Symfony\Component\DependencyInjection\Attribute\Autowire;

class MyService {
    public function __construct(
        #[Autowire(service: 'my_dependency')] private MyDependency $dependency
    ) {}
}

Symfony 6.1 : #[Autoconfigure]

use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;

#[Autoconfigure]
class MyService {}
namespace App\Controller;

use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Monolog\Attribute\WithMonologChannel;

#[Route('/user', name: 'user_index')]
#[WithMonologChannel('security')] // Configuration via attribut
class UserController
{
    public function __construct(
        #[Autowire(service: 'monolog.logger.custom')] // Ciblage précis,
        private LoggerInterface $customLogger,
        #[Autowire(env: 'APP_SECRET')] 
        private string $appSecret,
    ) {}

    public function __invoke(): Response
    {
        $this->customLogger->info('Log dans un canal spécifique');
        return new Response();
    }
}

Symfony 6.3 : nouvelles fonctionnalités pour #[Autowire]

Symfony 6.3 : #[AsAlias]

  • AsAlias : Il permet de déclarer un alias pour un service directement dans le code, sans avoir besoin d’une définition YAML.

 

use Symfony\Component\DependencyInjection\Attribute\AsAlias;

#[AsAlias('my_service')]
class MyService {}

Symfony 6.3 : #[AutowireServiceClosure]

namespace App\Service;

use App\Service\Remote\MessageFormatter;
use Symfony\Component\DependencyInjection\Attribute\AutowireServiceClosure;

class MessageGenerator
{
    public function __construct(
        #[AutowireServiceClosure('third_party.remote_message_formatter')]
        private \Closure $messageFormatterResolver,
    ) {
    }

    public function generate(string $message): void
    {
        $formattedMessage = ($this->messageFormatterResolver)()->format($message);

        // ...
    }
}

Symfony 6.3 : #[AutowireCallable]

// src/Service/MessageGenerator.php
namespace App\Service;

use Symfony\Component\DependencyInjection\Attribute\AutowireCallable;

class MessageGenerator
{
    public function __construct(
        #[AutowireCallable(service: 'third_party.remote_message_formatter', method: 'format')]
        private \Closure $formatCallable,
    ) {
    }

    public function generate(string $message): void
    {
        $formattedMessage = ($this->formatCallable)($message);

        // ...
    }
}

 Symfony 7.1 : #[AutowireMethodOf]

// src/Service/MessageGenerator.php
namespace App\Service;

use Symfony\Component\DependencyInjection\Attribute\AutowireMethodOf;

class MessageGenerator
{
    public function __construct(
        #[AutowireMethodOf('third_party.remote_message_formatter')]
        private \Closure $format,
    ) {
    }

    public function generate(string $message): void
    {
        $formattedMessage = ($this->format)($message);

        // ...
    }
}

📋 Quelques autres attributs

et changements

 

  • AsDecorator

  • AsTaggedItem

  • AutoconfigureTag

  • AutowireDecorated

  • Exclude

  • Lazy

  • TaggedIterator AutowireIterator

  • TaggedLocator AutowireLocator

  • Target

  • When

  • WhenNot

📝 L'injection de dépendances, un parcours évolutif – Place au résumé !

 

✅ Moins de configuration YAML/XML

✅ Plus de flexibilité grâce aux attributs PHP
✅ Meilleure lisibilité et maintenabilité du code

 

➡️ Depuis Symfony 6, on observe une nette simplification de l’injection de dépendances :

🗂️ Ne pas mélanger attributs et fichiers de configuration YAML/XML

  ⚖️ Utilisation mesurée des attributs

  🔄 Prudence avec les dépendances circulaires

   🧩Testabilité

⚠️ Points d'attention à ne pas négliger :

Merci !

Made with Slides.com