Apprendre le composant Dependency Injection du framework PHP Symfony

Nicolas Claverie

Le but de ce cours est de vous apprendre le composant Dependency Injection, qui est un composant majeur utilisé dans Symfony.

Merci d'apporter vos commentaires.
Commentez

- Titre : Apprendre le composant Dependency Injection du framework PHP Symfony

- Auteur : Nicolas Claverie

- Parution : 8 février 2016

- Public visé :

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2016 Nicolas Claverie. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Introduction

- II. Qu'est-ce que l'injection de dépendances (dependency injection en anglais)

- II-A. Le phénomène de dépendance entre les classes

- II-B. Répondre au problème de dépendance des classes : l'injection de dépendances

- III. L'injection de dépendances dans Symfony, une approche par l'exemple

- III-A. Le conteneur de services (service container en anglais)

- III-B. Configurer notre conteneur de services dans Symfony

- IV. De l'importance du dossier DependencyInjection de nos bundles

- IV-A. Configurer votre bundle

- IV-A-1. La classe Extension

- IV-B. Configurer les services de mon bundle

- V. Conclusion

- VI. Sources

- VII. Remerciements

I - Introduction

La documentation officielle de Symfony nous présente le composant Dependency Injection comme suit :

« Le composant DependecyInjection vous permet de standardiser et de centraliser la façon dont les objets sont construits dans votre application. »

Nous allons dans ce cours tenter de comprendre ce qui se cache vraiment derrière ce composant. Dans un premier temps, nous nous pencherons sur ce qu'est l'injection de dépendances (dependency injection en anglais) indépendamment de Symfony, puis nous verrons comment l'injection de dépendances est mise en œuvre dans Symfony. Nous finirons par ses implications dans l'écriture d'un bundle.

Ce cours est à destination des développeurs désireux d'apprendre plus en détail le fonctionnement de Symfony, mais aussi pour comprendre ce qu'il faut faire pour créer ses propres composants Symfony et peut-être un jour en proposer à la communauté !

II - Qu'est-ce que l'injection de dépendances (dependency injection en anglais)

II-A - Le phénomène de dépendance entre les classes

Pour expliquer ce phénomène, reprenons l'exemple de la documentation de Symfony : la création d'un Mailer.

Ce Mailer s'occupe de l'envoi des e-mails de votre application. La classe que l'on veut créer est composée d'un attribut $transport comportant le nom du programme à utiliser sur le serveur pour l'envoi des e-mails, dans l'exemple qui suit, le serveur étant sous Linux.

Exemple d'une classe Mailer

1. namespace Dvlp\Mailer;

3. class Mailer

4. {

5. private $transport;

7. public function __construct()

8. {

9. $this->transport = 'sendmail';

10. }

11. }

Pour plus de flexibilité, nous aimerions que l'attribut $transport de notre classe soit modifiable. La solution la plus simple qui s'offre à nous est de le passer en paramètre du constructeur :

Classe Mailer

1. class Mailer

2. {

3. private $transport;

5. public function __construct($transport)

6. {

7. $this->transport = $transport;

8. }

9. }

Très bien, il semble que nous ayons répondu au problème. Dans un contrôleur de notre application, lorsque je voudrai envoyer un e-mail, je ferai appel à mon Mailer. Prenons l'exemple de l'envoi d'un e-mail pour la création d'un compte utilisateur :

Contrôleur AccountController appelant la classe Mailer

1. namespace Dvlp\DemoBundle\Controller;

3. use Dvlp\Mailer;

5. class AccountController

6. {

7. public function CreateAccountAction()

8. {

9. // [...]

10. $mailer = new Mailer('sendmail');

11. // [...]

12. }

13. }

Ah oui, mais à côté de cela, j'ai besoin d'envoyer par e-mail le contenu d'un formulaire de contact. Pas de problème :

Contrôleur WebsiteController appelant la Classe Mailer

1. namespace Dvlp\DemoBundle\Controller;

3. use Dvlp\Mailer;

5. class WebsiteController

6. {

7. public function ContactFormAction()

8. {

9. // [...]

10. $mailer = new Mailer('sendmail');

11. // [...]

12. }

13. }

Maintenant, je complexifie la classe, lors de l'appel à la méthode $mailer->prepare(), je dois enregistrer dans un fichier de logs des informations seulement si l'utilisateur connecté est un « administrateur ».

Classe Mailer

1. namespace Dvlp\Mailer;

3. use Monolog\Logger;

4. use Symfony\Component\HttpFoundation\Session\Session;

6. class Mailer

7. {

8. private $transport;

10. private $session;

12. public function __construct($transport, $session)

13. {

14. $this->transport = $transport;

15. $this->session = $session;

16. }

18. public function prepare()

19. {

20. if ( $this->session->getUser()->getGroup() == 'administrateur' ) {

21. $logger = new Logger();

22. $logger->setTitle('...');

23. [...]

24. }

25. }

26. }

Là, ça commence à devenir intéressant. Ma classe Mailer maintenant contient un attribut $session qui est une instance d'un objet Symfony\Component\HttpFoundation\Session\Session. De plus, ma classe Mailer utilise une autre classe Monolog\Logger. Et on peut en rajouter encore et encore… des dépendances. Nous venons de créer une classe dépendante d'objets ou de paramètres extérieurs à cette dernière.

Ce phénomène s'amplifie au fur et à mesure que notre application grossit. Vous pouvez ajouter à cela que dans des cadres de développement et d'exécution (frameworks en anglais), organisés en briques logicielles (bundles sous Symfony) activables ou non, vous obtenez un casse-tête pour gérer tout cela.

II-B - Répondre au problème de dépendance des classes : l'injection de dépendances

« L'injection de dépendances (dependency injection en anglais) est un mécanisme qui permet d'implémenter le principe de l'inversion de contrôle.

Il consiste à créer dynamiquement (injecter) les dépendances entre les différentes classes en s'appuyant sur une description (fichier de configuration ou métadonnées) ou de manière programmatique. Ainsi les dépendances entre composants logiciels ne sont plus exprimées dans le code de manière statique, mais déterminées dynamiquement à l'exécution. »

Source : Injection de dépendances - Wikipédia

Tout est dit dans ces quelques lignes : l'injection de dépendances crée dynamiquement les dépendances entre les différentes classes. Les dépendances entre composants logiciels ne sont plus exprimées dans le code de manière statique, mais elles sont déterminées dynamiquement à l'exécution.

Je vous présente donc : le conteneur (Dependency Injection Container, ou DIC en anglais). Le conteneur est un objet qui sait comment construire d'autres objets. Ce conteneur est un élément central dans votre application. Via le conteneur, nous allons pouvoir normaliser, centraliser et simplifier l'appel à notre Mailer et de tous les autres objets de votre application.

Exemple de conteneur

1. use Dvlp\Mailer;

2. use Symfony\Component\HttpFoundation\Session\Session;

4. class Container

5. {

6. public function getMailer()

7. {

8. return new Mailer('sendmail', $this->getSession());

9. }

11. public function getSession()

12. {

13. return new Session();

14. }

15. }

17. $container = new Container();

De plus, les objets peuvent aussi recevoir des paramètres. Imaginons que ces paramètres soient configurables via des fichiers de configuration ou tout autre moyen permettant au développeur de centraliser un peu tout cela. Améliorons notre conteneur précédemment créé afin de lui donner une liste de paramètres.

Exemple de conteneur

1. use Dvlp\Mailer;

2. use Symfony\Component\HttpFoundation\Session\Session;

4. class Container

5. {

6. /**

7. * @var array

8. */

9. protected $parameters;

11. public function __construct(array $parameters)

12. {

13. $this->parameters = $parameters;

14. }

16. public function setParameter($name, $value)

17. {

18. $this->parameters[$name] = $value;

19. }

21. public function getParameter($name)

22. {

23. return $this->parameter[$name];

24. }

26. public function getMailer()

27. {

28. return new Mailer($this->parameters['mailer.transport'], $this->getSession());

29. }

31. public function getSession()

32. {

33. return new Session();

34. }

35. }

37. $configs = array('mailer.transport' => 'sendmail');

39. $container = new Container($configs);

40. $container->getMailer();

Ce conteneur est maintenant en mesure d'injecter les dépendances de chaque objet et de retourner une instance de cet objet. Ces objets sont communément appelés des services.

Vous savez maintenant ce qui se trame derrière le design pattern « injection de dépendances » (ou dependency injection en anglais). L'injection de dépendances est un mécanisme permettant de ne plus exprimer les dépendances dans le code de manière statique, mais de les déterminer dynamiquement à l'exécution.

Un dernier petit exemple pour vous montrer tout l'intérêt de ce patron de programmation (design pattern en anglais). Lorsque nous développons des applications, nous définissons régulièrement des environnements : « dev », « prod » ou « test ». De plus, il peut nous arriver de travailler à plusieurs sur ce projet.

Rappelons que l'injection de dépendances permet de centraliser dans un conteneur les déclarations des différents objets qui composent notre application. Mon projet est composé de trois objets : MonBundle\Newsletter, MonBundle\Sender et LeBundleDunAutre\User.

Classe Newsletter

1. namespace MonBundle\Mailer;

3. class Newsletter

4. {

5. protected $sender;

7. public function __construct(SenderInterface $sender)

8. {

9. $this->sender = $sender;

10. }

11. }

Classe Sender

1. namespace MonBundle\Mailer;

3. interface SenderInterface

4. {

5. public function getName();

7. public function getEmail();

8. }

10. class Sender implements SenderInterface

11. {

12. protected $name;

14. protected $email;

16. public function getName()

17. {

18. return $this->name;

19. }

21. public function getEmail()

22. {

23. return $this->email;

24. }

25. }

Classe User

1. namespace LeBundleDunAutre\User;

3. use MonBundle\Mailer;

5. class User implements SenderInterface

6. {

7. protected $username;

9. public function getUsername()

10. {

11. return $this->username;

12. }

13. }

Je crée maintenant mon conteneur dans lequel chaque développeur du projet pourra puiser les objets disponibles de mon projet. Pour rappel ces objets sont appelés des services.

Classe Container

1. class Container

2. {

3. /**

4. * @var array

5. */

6. protected $parameters;

8. public function __construct(array $parameters)

9. {

10. $this->parameters = $parameters;

11. }

13. public function getNewsletter()

14. {

15. return new Newsletter($this->getSender());

16. }

18. public function getSender()

19. {

20. $class = $this->parameters['sender.class'];

21. return new $class();

22. }

24. }

26. $configs = array('sender.class' => 'MonBundle\Sender');

27. $container = new Container($configs);

28. var_dump( get_class($container->getSender()) ) ;

29. // Nous donnera : MonBundle\Sender

31. $configs = array('sender.class' => 'LeBundleDunAutre\User');

32. $container = new Container($configs);

33. var_dump( get_class($container->getSender()) ) ;

34. // Nous donnera :  LeBundleDunAutre\User

Mon conteneur peut aussi charger des classes via une variable dynamique pour peu que cette classe implémente la bonne interface. Ce design pattern peut donc être utilisé pour apporter plus de rigueur dans nos projets tout en gardant une grande flexibilité.

Bien entendu, les exemples vus jusqu'à présent sont volontairement simples, mais permettent de comprendre le mécanisme. Ce mécanisme fait donc appel à un conteneur principal (ou conteneur de services) centralisant des objets (les services) et des paramètres accessibles par la suite dans toute votre application. Ce conteneur rassemble les services et injecte les dépendances dans chaque service.

III - L'injection de dépendances dans Symfony, une approche par l'exemple

Maintenant que nous savons ce qu'est le principe d'injection de dépendances, voyons comment celui-ci est géré dans Symfony.

Note : si vous le désirez, vous pouvez suivre cette partie du cours et pratiquer dans le même temps. Pour ce faire, installez-vous une copie de Symfony.

Si vous êtes un habitué de Symfony, vous avez dû déjà reconnaître certains termes dans le chapitre précédent. Rappelez-vous, nous avons parlé de conteneur (container en anglais), de services et de paramètres de configuration. Eh bien, allons voir dans Symfony comment tout cela est géré !

III-A - Le conteneur de services (service container en anglais)

On l'utilise sans arrêt dans Symfony ! Dans nos contrôleurs, dans nos services, dans les extensions, etc. Le conteneur est partout ! Voyons un peu quelle tête il a. Dans un premier temps, créez dans le dossier web un fichier dvlp/app.php. Ce fichier sera notre amorce.

Fichier d'amorçage

1. // /path/to/project/web/dvlp/app.php

3. $loader = require __DIR__.'/../../app/autoload.php';

5. use Symfony\Component\HttpKernel\Kernel;

6. use Symfony\Component\Config\Loader\LoaderInterface;

8. class AppKernel extends Kernel

9. {

10. public function registerBundles()

11. {

12. $bundles = [

13. new Symfony\Bundle\FrameworkBundle\FrameworkBundle()

14. ];

16. return $bundles;

17. }

19. public function getRootDir()

20. {

21. return __DIR__;

22. }

24. public function getCacheDir()

25. {

26. return $this->getRootDir().'/var/cache/'.$this->getEnvironment();

27. }

29. public function getLogDir()

30. {

31. return $this->getRootDir().'/var/logs';

32. }

34. public function registerContainerConfiguration(LoaderInterface $loader)

35. {

36. $loader->load($this->getRootDir().'/config/config_'.$this->getEnvironment().'.yml');

37. }

38. }

40. $kernel = new \AppKernel('test', true);

41. $kernel->boot();

N'oubliez pas de créer un fichier de configuration dans ce dossier, nous verrons plus tard pourquoi :

Fichier de configuration

1. // /path/to/project/web/dvlp/config/config_test.yml

2. framework:

3. secret: "aSecret"

Eh oui, le conteneur commence à montrer son nez dès notre amorce ! Lorsque l'on crée le noyau (kernel en anglais) de notre application, on va construire la liste des bundles nécessaires que l'on veut utiliser, définir quelques path et donner le chemin vers un fichier de configuration qui sera utilisé par le conteneur.

Rentrons dans le détail de $kernel->boot() :

Kernel.php

1. // /path/to/project/vendor/symfony/symfony/src/Symfony/Component/HttpKernel/Kernel.php

3. abstract class Kernel implements KernelInterface, TerminableInterface

4. {

5. [...]

6. /**

7. * Boots the current kernel.

8. */

9. public function boot()

10. {

11. if (true === $this->booted) {

12. return;

13. }

15. if ($this->loadClassCache) {

16. $this->doLoadClassCache($this->loadClassCache[0], $this->loadClassCache[1]);

17. }

19. // init bundles

20. $this->initializeBundles();

22. // init container

23. $this->initializeContainer();

25. foreach ($this->getBundles() as $bundle) {

26. $bundle->setContainer($this->container);

27. $bundle->boot();

28. }

30. $this->booted = true;

31. }

32. [...]

33. }

La ligne qui nous intéresse est $this->initializeContainer. Allons donc voir cette méthode :

Kernel.php

1. // /path/to/project/vendor/symfony/symfony/src/Symfony/Component/HttpKernel/Kernel.php

3. abstract class Kernel implements KernelInterface, TerminableInterface

4. {

5. [...]

6. /**

7. * Initializes the service container.

8. *

9. * The cached version of the service container is used when fresh, otherwise the

10. * container is built.

11. */

12. protected function initializeContainer()

13. {

14. $class = $this->getContainerClass();

15. $cache = new ConfigCache($this->getCacheDir().'/'.$class.'.php', $this->debug);

16. $fresh = true;

17. if (!$cache->isFresh()) {

18. $container = $this->buildContainer();

19. $container->compile();

20. $this->dumpContainer($cache, $container, $class, $this->getContainerBaseClass());

21. $fresh = false;

22. }

24. require_once $cache->getPath();

26. $this->container = new $class();

27. $this->container->set('kernel', $this);

29. if (!$fresh && $this->container->has('cache_warmer')) {

30. $this->container->get('cache_warmer')->warmUp($this->container->getParameter('kernel.cache_dir'));

31. }

32. }

33. [...]

34. }

Le travail de initializeContainer est de construire le conteneur et de le mettre à disposition de notre application.

Kernel.php

1. [...]

2. require_once $cache->getPath();

4. $this->container = new $class();

5. [...]

Le reste des lignes permet une optimisation dans la construction du conteneur. Pour améliorer les performances, Symfony construit un conteneur et le met en cache. La première requête reçue, Symfony va construire le cache de zéro puis créer dans votre dossier cache le fichier de la classe container. Pour les prochaines requêtes, il ne fera qu'un require_once d'un fichier déjà existant, donc d'une classe conteneur déjà générée. Pour preuve, si vous allez dans votre dossier de tests, vous pouvez le voir.

Le dossier /path/to/project/web/dvlp/var/cache/test/ contient dvlpTestDebugProjectContainer.php. Si vous l'ouvrez, il contiendra cela :

Conteneur Symfony

1. use Symfony\Component\DependencyInjection\ContainerInterface;

2. use Symfony\Component\DependencyInjection\Container;

3. use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;

4. use Symfony\Component\DependencyInjection\Exception\LogicException;

5. use Symfony\Component\DependencyInjection\Exception\RuntimeException;

6. use Symfony\Component\DependencyInjection\ParameterBag\FrozenParameterBag;

8. /**

9. * dvlpTestDebugProjectContainer.

10. *

11. * This class has been auto-generated

12. * by the Symfony Dependency Injection Component.

13. */

14. class dvlpTestDebugProjectContainer extends Container

15. {

16. private $parameters;

17. [...]

19. /**

20. * Constructor.

21. */

22. public function __construct()

23. {

24. [...]

25. $this->parameters = $this->getDefaultParameters();

27. $this->services = array();

28. $this->methodMap = array(

29. 'annotation_reader' => 'getAnnotationReaderService',

30. [...]

31. );

32. }

34. /**

35. * Gets the 'annotation_reader' service.

36. *

37. * This service is shared.

38. * This method always returns the same instance of the service.

39. *

40. * @return \Doctrine\Common\Annotations\CachedReader A Doctrine\Common\Annotations\CachedReader instance.

41. */

42. protected function getAnnotationReaderService()

43. {

44. return $this->services['annotation_reader'] = new \Doctrine\Common\Annotations\CachedReader(new \Doctrine\Common\Annotations\AnnotationReader(), new \Doctrine\Common\Cache\FilesystemCache((__DIR__.'/annotations')), true);

45. }

47. /**

48. * {@inheritdoc}

49. */

50. public function getParameter($name)

51. {

52. $name = strtolower($name);

54. if (!(isset($this->parameters[$name]) || array_key_exists($name, $this->parameters))) {

55. throw new InvalidArgumentException(sprintf('The parameter "%s" must be defined.', $name));

56. }

58. return $this->parameters[$name];

59. }

61. /**

62. * {@inheritdoc}

63. */

64. public function hasParameter($name)

65. {

66. $name = strtolower($name);

68. return isset($this->parameters[$name]) || array_key_exists($name, $this->parameters);

69. }

71. [...]

72. }

Tout cela ressemble furieusement à ce que l'on a déjà vu dans le chapitre précédent ! Nous retrouvons :

- le tableau des paramètres : private $parameters ;

- une méthode par service : getAnnotationReaderService. Service qui est donc une dépendance qui pourra être injectée dans d'autres services ;

- des méthodes pour accéder aux paramètres : getParameter, hasParameter, etc.

Maintenant que nous savons comment l'injection de dépendances est mise en place dans Symfony, il faut savoir comment on la paramètre. Eh oui, le conteneur met à disposition des services, des paramètres à l'application, mais il faut bien que ce dernier sache ce qu'il doit mettre à disposition !

III-B - Configurer notre conteneur de services dans Symfony

Nous avons donc vu ce qu'est le conteneur de services et comment il met à notre disposition les services. Par contre, d'où sortent ces services ? Tout simplement de nos bundles.

Lorsque l'on installe un bundle dans notre projet Symfony, nous renseignons un tableau (array) $bundles via la classe Kernel dans la méthode registerBundles.

AppKernel.php

1. class AppKernel extends Kernel

2. {

3. public function registerBundles()

4. {

5. $bundles = [

6. new Symfony\Bundle\FrameworkBundle\FrameworkBundle(),

7. [...]

8. ];

10. return $bundles;

11. }

12. }

14. $kernel = new \AppKernel('test', true);

15. $kernel->boot();

Lorsque nous faisons appel à $kernel->boot(), nous avons vu que nous lancions l'initialisation du bundle via la méthode initializeContainer(). Retournons dedans :

Kernel.php

1. // /path/to/project/vendor/symfony/symfony/src/Symfony/Component/HttpKernel/Kernel.php

3. abstract class Kernel implements KernelInterface, TerminableInterface

4. {

5. [...]

6. /**

7. * Initializes the service container.

8. *

9. * The cached version of the service container is used when fresh, otherwise the

10. * container is built.

11. */

12. protected function initializeContainer()

13. {

14. $class = $this->getContainerClass();

15. $cache = new ConfigCache($this->getCacheDir().'/'.$class.'.php', $this->debug);

16. $fresh = true;

17. if (!$cache->isFresh()) {

18. $container = $this->buildContainer();

19. $container->compile();

20. $this->dumpContainer($cache, $container, $class, $this->getContainerBaseClass());

21. $fresh = false;

22. }

24. require_once $cache->getPath();

26. $this->container = new $class();

27. $this->container->set('kernel', $this);

29. if (!$fresh && $this->container->has('cache_warmer')) {

30. $this->container->get('cache_warmer')->warmUp($this->container->getParameter('kernel.cache_dir'));

31. }

32. }

33. [...]

34. }

La ligne qui va nous intéresser maintenant est $container = $this->buildContainer().

Kernel.php

1. /**

2. * Builds the service container.

3. *

4. * @return ContainerBuilder The compiled service container

5. *

6. * @throws \RuntimeException

7. */

8. protected function buildContainer()

9. {

10. foreach (array('cache' => $this->getCacheDir(), 'logs' => $this->getLogDir()) as $name => $dir) {

11. if (!is_dir($dir)) {

12. if (false === @mkdir($dir, 0777, true) && !is_dir($dir)) {

13. throw new \RuntimeException(sprintf("Unable to create the %s directory (%s)\n", $name, $dir));

14. }

15. } elseif (!is_writable($dir)) {

16. throw new \RuntimeException(sprintf("Unable to write in the %s directory (%s)\n", $name, $dir));

17. }

18. }

20. $container = $this->getContainerBuilder();

21. $container->addObjectResource($this);

22. $this->prepareContainer($container);

24. if (null !== $cont = $this->registerContainerConfiguration($this->getContainerLoader($container))) {

25. $container->merge($cont);

26. }

28. $container->addCompilerPass(new AddClassesToCachePass($this));

29. $container->addResource(new EnvParametersResource('SYMFONY__'));

31. return $container;

32. }

La première partie de cette méthode vérifie si le cache peut être créé physiquement. Ensuite vient la partie qui nous intéresse : le chargement des informations provenant des bundles, la ligne $this->prepareContainer($container).

Kernel.php

1. /**

2. * Prepares the ContainerBuilder before it is compiled.

3. *

4. * @param ContainerBuilder $container A ContainerBuilder instance

5. */

6. protected function prepareContainer(ContainerBuilder $container)

7. {

8. $extensions = array();

9. foreach ($this->bundles as $bundle) {

10. if ($extension = $bundle->getContainerExtension()) {

11. $container->registerExtension($extension);

12. $extensions[] = $extension->getAlias();

13. }

15. if ($this->debug) {

16. $container->addObjectResource($bundle);

17. }

18. }

19. foreach ($this->bundles as $bundle) {

20. $bundle->build($container);

21. }

23. // ensure these extensions are implicitly loaded

24. $container->getCompilerPassConfig()->setMergePass(new MergeExtensionConfigurationPass($extensions));

25. }

C'est ici que Symfony recherche les fichiers de configuration de nos bundles et les charge dans le conteneur. La boucle foreach parcourt le tableau $bundles que nous remplissons lors de l'installation d'un bundle et charge ensuite son extension : le $container->registerExtension($extension). Ajouter dans la boucle foreach un var_dump comme suit :

Kernel.php

1. foreach ($this->bundles as $bundle) {

2. if ($extension = $bundle->getContainerExtension()) {

3. var_dump( get_class($extension) );

4. $container->registerExtension($extension);

5. $extensions[] = $extension->getAlias();

6. }

8. if ($this->debug) {

9. $container->addObjectResource($bundle);

10. }

11. }

L'affichage vous sortira :

- Symfony\Bundle\FrameworkBundle\DependencyInjection\FrameworkExtension=> pour le bundle FrameworkBundle de Symfony

Nous arrivons donc au bout de cette route sur ce que nous connaissons le mieux lorsque nous développons sous Symfony : les bundles. La prochaine étape est donc de comprendre l'importance de ce qu'il y a dans le dossier DependencyInjection de nos bundles et comment bien l'utiliser.

IV - De l'importance du dossier DependencyInjection de nos bundles

Un projet Symfony est composé de bundles. Ces petites unités nous permettent d'isoler notre code par « blocs logiques ». Ces bundles que l'on crée ont une vie, une utilité, qui dans un premier temps n'est limitée que pour notre projet. Puis vient le jour où ce bundle représente un ensemble logique réutilisable dans différents projets. Le bundle n'est plus un simple bundle, il devient un composant.

Pourquoi vous parler des composants ? Parce que, à mon humble avis, ce sont des exemples de bundles qui utilisent le plein potentiel de l'injection de dépendances (dependency injection en anglais).

Nous avons deux types de fichiers de configuration à notre disposition pour piloter la construction de notre conteneur de services : les fichiers de configuration de l'application : config.yml, config_test.yml, etc. et les fichiers de configuration des services : services.yml (ou .xml ou .php).

IV-A - Configurer votre bundle

Le dossier DependencyInjection de votre bundle est composé de deux fichiers :

- Configuration.php ;

- AcmeExtension.php (Acme étant le nom de votre bundle).

On a coutume de lire que la configuration (dans le dossier DependencyInjection) n'est qu'un moyen de donner la possibilité de modifier des paramètres de votre bundle via les fichiers config.yml de l'application. Après la démonstration de ce cours, je trouve cette affirmation peut-être un peu réductrice.

N'oublions pas que le développement d'un bundle doit se faire dans le but d'être utilisé dans un environnement centralisé, mais découplé (centralisé dans Symfony, découplé en bundles). Le dossier DependencyInjection permet justement de configurer finement votre bundle pour lui permettre d'activer ou non tel ou tel service en fonction du projet.

De plus, bien réfléchir sur la configuration du bundle impose au développeur de correctement structurer ce dernier et de ne pas par exemple coder en dur, mais de proposer un bundle dynamique permettant une bonne intégration dans l'environnement Symfony et en utilisant les conteneurs de services à bon escient.

La classe Configuration de votre bundle va permettre de définir des paramètres nécessaires au bon fonctionnement de votre bundle, mais aussi donner la possibilité de configurer les services de ce dernier. Il consiste donc à construire un arbre de configuration. Il a donc un rôle dans l'injection de dépendances.

Cette configuration par défaut est ensuite fusionnée avec les fichiers config.yml de votre application. Je n'expliquerai pas son mécanisme dans ce cours. Je vous renvoie à la documentation officielle de Symfony.

IV-A-1 - La classe Extension

La classe Extension de votre bundle quant à elle mérite que l'on s'y arrête. Vous ne l'avez peut-être plus en tête, mais nous avons déjà eu affaire à elle dans ce cours. Revenez au chapitre précédent « L'injection de dépendances dans Symfony, une approche par l'exemple » dans la sous-partie III.A « Configurer notre conteneur de services dans Symfony».

Pour construire le conteneur, Symfony parcourt tous les bundles et charge notamment toutes les extensions de ces derniers.

Deux méthodes sont importantes dans cette classe : load et prepend.

- La méthode load permet de charger tous les services et les paramètres de votre bundle. C'est ici donc que vous pouvez lire la configuration de votre bundle après la fusion de cette dernière entre la configuration de base (la classe Configuration), et les fichiers de configuration de votre application (config.yml, config_dev.yml, etc.). Vous pourrez à loisir charger un ou plusieurs services.

- La méthode prepend vous donne accès au conteneur de services avant qu'il soit passé à la méthode load de chaque bundle. En d'autres termes, cette méthode permet de surcharger des paramètres de configuration des bundles autres que celui sur lequel vous travaillez. Par exemple, vous souhaitez que votre bundle définisse le paramètre du bundle Twig form_themes dans la configuration.

Votre extension implémentera alors l'interface prependExtensionInterface dont la méthode prepend sera automatiquement appelée :

Classe Extension d'un bundle

1. // src/Dvlp/DemoBundle/DependencyInjection/DvlpDemoExtension.php

2. namespace Dvlp\DemoBundle\DependencyInjection;

4. use Symfony\Component\HttpKernel\DependencyInjection\Extension;

5. use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;

6. use Symfony\Component\DependencyInjection\ContainerBuilder;

8. class DvlpDemoExtension extends Extension implements PrependExtensionInterface

9. {

10. // ...

12. public function prepend(ContainerBuilder $container)

13. {

14. $bundles = $container->getParameter('kernel.bundles');

16. $configs = $container->getExtensionConfig($this->getAlias());

17. $config = $this->processConfiguration(new Configuration(), $configs);

19. foreach(array_keys($container->getExtensions()) as $name) {

20. switch($name) {

21. case 'twig':

22. // Add supports assets list in twig variables

23. $container->prependExtensionConfig($name, array(

24. 'form_themes' => 'DvlpDemoBundle::form_themes_layout.html.twig'

25. ));

26. break;

27. }

28. }

29. }

30. }

Lorsque le conteneur de services chargera l'extension de Twig (la classe TwigExtension), le paramètre de form_themes sera donc DvlpDemoBundle::form_themes_layout.html.twig.

IV-B - Configurer les services de mon bundle

Pour créer un service, vous devez le déclarer via un fichier de configuration services.yml, services.xml ou services.php selon votre habitude.

Symfony préconise de ne pas injecter tout le conteneur dans un service.

« Avoiding your Code Becoming Dependent on the Container¶

Whilst you can retrieve services from the container directly it is best to minimize this. For example, in the NewsletterManager you injected the mailer service in rather than asking for it from the container. You could have injected the container in and retrieved the mailer service from it but it would then be tied to this particular container making it difficult to reuse the class elsewhere.

You will need to get a service from the container at some point but this should be as few times as possible at the entry point to your application. »
Source : Documentation officielle du composant DependencyInjection de Symfony

Maintenant, nous comprenons mieux pourquoi. Rappelez-vous, l'injection de dépendances est le fait d'injecter des paramètres définis dynamiquement, non pas injecter le conteneur dans sa globalité !

Pour faire une analogie, si vous avez besoin de donner en argument une seule valeur d'un tableau (array), vous n'allez pas donner tout le tableau ; vous ne donnerez que la valeur souhaitée. Pour l'injection de dépendances en argument d'un autre service, c'est la même chose.

De plus, comme l'explique la documentation, le fait de donner tout le conteneur en argument d'un service, augmente les risques de dépendance de ce service avec ce type de conteneur. Si plus tard Symfony décide de modifier son conteneur, il vous faudra réécrire votre service. Sans parler des problèmes d'optimisation.

Voici donc un exemple correct d'injection de dépendances dans un service :

Exemple de déclaration d'un service

1. <?xml version="1.0" encoding="UTF-8" ?>

2. <container xmlns="http://symfony.com/schema/dic/services"

3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

4. xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">

6. <parameters>

7. <!-- ... -->

8. <parameter key="newsletter.sender">SenderManager</parameter>

9. </parameters>

11. <services>

12. <service id="newsletter_manager" class="NewsletterManager">

13. <argument type="service" id="newsletter.SenderManager" />

14. </service>

15. </services>

16. </container>

Voici ce qu'il ne faut pas faire :

Exemple incorrect d'injection de dépendances

1. <?xml version="1.0" encoding="UTF-8" ?>

2. <container xmlns="http://symfony.com/schema/dic/services"

3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

4. xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">

6. <parameters>

7. <!-- ... -->

8. <parameter key="newsletter.sender">SenderManager</parameter>

9. </parameters>

11. <services>

12. <service id="newsletter_manager" class="NewsletterManager">

13. <argument type="service" id="service_container" />

14. </service>

15. </services>

16. </container>

Vous avez plusieurs manières d'injecter vos dépendances dans un service.

- L'injection via le constructeur : c'est la méthode le plus utilisée, elle permet d'injecter via le constructeur de la classe les dépendances nécessaires.

Classe mailer

1. class NewsletterManager

2. {

3. protected $mailer;

5. public function __construct(\Mailer $mailer)

6. {

7. $this->mailer = $mailer;

8. }

10. // ...

11. }

Claude Leloup 2016-02-10T17:16:13correct ?Configurer de service

1. <?xml version="1.0" encoding="UTF-8" ?>

2. <container xmlns="http://symfony.com/schema/dic/services"

3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

4. xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">

6. <services>

7. <service id="my_mailer">

8. <!-- ... -->

9. </service>

11. <service id="newsletter_manager" class="NewsletterManager">

12. <argument type="service" id="my_mailer"/>

13. </service>

14. </services>

15. </container>

Le typage de l'objet injecté permet d'être sûr de la nature de la dépendance qui est injectée. De plus, si vous utilisez une interface plutôt qu'une classe, vous donnerez encore plus de flexibilité à votre service.

- L'injection via un setter : ici, vous injectez une dépendance non dans le constructeur, mais en faisant appel à une méthode de votre classe :

Classe Mailer

1. class NewsletterManager

2. {

3. protected $mailer;

5. public function setMailer(\Mailer $mailer)

6. {

7. $this->mailer = $mailer;

8. }

10. // ...

11. }

Service

1. <?xml version="1.0" encoding="UTF-8" ?>

2. <container xmlns="http://symfony.com/schema/dic/services"

3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

4. xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">

6. <services>

7. <service id="my_mailer">

8. <!-- ... -->

9. </service>

11. <service id="newsletter_manager" class="NewsletterManager">

12. <call method="setMailer">

13. <argument type="service" id="my_mailer" />

14. </call>

15. </service>

16. </services>

17. </container>

Cette méthode peut être utile si la dépendance à injecter dans votre service est optionnelle. Si vous n'en avez pas besoin, il vous suffit de ne pas appeler la méthode. Cependant, étant donné que le setter peut être appelé n'importe quand, il faudra ajouter des contrôles sur la dépendance.

- L'injection directement dans un attribut public de la classe : cette technique n'est pas très recommandée, car vous ne savez pas trop ce qui est injecté. Sachez qu'elle existe.

1. class NewsletterManager

2. {

3. public $mailer;

5. // ...

6. }

1. <?xml version="1.0" encoding="UTF-8" ?>

2. <container xmlns="http://symfony.com/schema/dic/services"

3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

4. xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">

6. <services>

7. <service id="my_mailer">

8. <!-- ... -->

9. </service>

11. <service id="newsletter_manager" class="NewsletterManager">

12. <property name="mailer" type="service" id="my_mailer" />

13. </service>

14. </services>

15. </container>

V - Conclusion

Comme nous avons pu le voir dans ce cours, l'injection de dépendances est un mécanisme de base très important dans le fonctionnement de Symfony. Il a un impact sur notre manière de développer et structurer nos applications et nos bundles. Il est un point central de votre application répertoriant tous les services utilisables et donc permet de gérer les dépendances entre ces différents services… en les injectant.

La bonne compréhension de l'injection de dépendances vous permettra de développer des bundles structurés, facilement réutilisables, et pourquoi pas, de les proposer par la suite à la communauté !

Voici quelques pistes pour aller plus loin :

- How to Work with Compiler Passes in Bundles

- Using a Factory to Create Services

- Configuring Services with a Service Configurator

VI - Sources

Documentation officielle Symfony - DependencyInjection Component

Documentation officielle Symfony - Types of injection

Documentation officielle de Symfony - How to Create Friendly Configuration for a Bundle

VII - Remerciements

Merci à Guillaume SIGUI et Claude Leloup pour leur aide lors de la rédaction de cet article !