Schéma architecture modulaire Symfony + API Platform

API Platform

Imen EZZINE

Développeuse PHP Symfony

@imenezzine

  @imenezzine1

 

Plan

  • Les principes de l'Injection de Dépendance (DI)

  • La configuration du container de services Symfony

  • L'évolution du composant dans le temps

  • L’utilisation des Attributs pour configurer les services

🤔 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

Container de services ?

Configuration du conteneur de services 

 

  • YAML
  • XML
  • PHP
  • Annotations et Attributs PHP

Configuration via YAML

services:
    app.my_service:
        class: App\Service\MyService
        arguments:
            $dependency: '@app.dependency'

Arrivé du Symfony 1 en 2007

Class UserController extends sfActions
{
    public function executeIndex(sfWebRequest $request)
    {
        $logger = $this->getContext()->getLogger();
        $logger->info('Chargement de la page utilisateur.');

        return sfView::SUCCESS;
    }
}

💥  Avant l’injection de dépendance — quels problèmes ?

Arrivée du Symfony 2  en 2011

services:
    app.user_controller:
        class: App\Controller\UserController
        arguments: ['@logger', '%kernel.project_dir%']

Un vrai tournant dans l'histoire du framework !

✅  Après l’injection de dépendance 

namespace App\Controller;

use Psr\Log\LoggerInterface;

class UserController
{
    private LoggerInterface $logger;
    private string $projectDir;

    public function __construct(LoggerInterface $logger, string $projectDir)
    {
        $this->logger = $logger;
        $this->projectDir = $projectDir;
    }

    public function indexAction()
    {
        $this->logger->info('Chargement de la page utilisateur.');

        return new Response('Page utilisateur chargée');
    }
}

Arrivée du Symfony 3.3 en 2017

Service Locator ?

  • Introduit une refonte du container DI avec le support du PSR-11 (ContainerInterface)
  • La performance (on ne crée les services que lorsqu’ils sont utilisés)

  • La modularité (on injecte un sous-ensemble de services)

Service Locator ?

# app/config/services.yml

services:
    app.command_handler_locator:
        class: Symfony\Component\DependencyInjection\ServiceLocator
        tags: ['container.service_locator']
        arguments:
            AppBundle\FooCommand: '@app.command_handler.foo'
            AppBundle\BarCommand: '@app.command_handler.bar'

    AppBundle\CommandBus:
        arguments: ['@app.command_handler_locator']
# app/config/services.yml
services:
    _defaults:
        autowire: true
        autoconfigure: true

    App\: '../src/'
namespace App\Controller;

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

class UserController
{
    private $logger;

    public function __construct(LoggerInterface $logger) {
        $this->logger = $logger; // Injection automatique
    }

    /**
     * @Route("/user", name="user_index")
     */
    public function index() {
        $this->logger->info('Autowiring Symfony 3.3');
        return new Response();
    }
}

Introduction du l'autowiring et l'autoconfiguration

Arrivée du Symfony 5 en 2020

Promoted properties + attributs PHP 8

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

Arrivé de la version Symfony 6

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
    ) {}

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

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 {}

Arrivé de la version 6.3

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 : Améliorations de #[Autowire]

  • Nouveaux paramètres : env, param et service

class MyService {
    public function __construct(
        #[Autowire(env: 'APP_SECRET')] private string $appSecret
    ) {}
}

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 3.3+
services:
    _defaults:
        autowire: true    # Active l'inférence des dépendances
        autoconfigure: true # Auto-tagging des services
        public: false

    App\:
        resource: '../src/*'
        exclude: '../src/{Entity,Migrations,Tests,Kernel.php}'
# Symfony 6.1+
when@dev:
    monolog:
        channels: ['custom'] # Déclaration de canal

Évolution des fichiers de configuration

L’utilisation des Attributs pour configurer les services

Liste des tous les attributs 

  • AsDecorator
  • AsTaggedItem
  • AutoconfigureTag
  • AutowireDecorated
  • Exclude
  • Lazy
  • TaggedIterator AutowireIterator
  • TaggedLocator AutowireLocator
  • Target
  • When
  • WhenNot

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

        // ...
    }
}

 Impact des nouveautés

 

✅ 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 :

Conclusion

Le composant DependencyInjection continue d’évoluer vers plus de simplicité et d’efficacité.

 

Grâce aux nouvelles fonctionnalités, il est plus facile de structurer ses services et d’écrire du code maintenable et performant.

Merci !

Copy of Symfony et l'Injection de Dépendance : Une approche simplifiée

By Imen TROUDI

Copy of Symfony et l'Injection de Dépendance : Une approche simplifiée

  • 96