I. Introduction▲
La documentation officielle de SymfonyDocumentation officielle de Symfony sur le composant DependencyInjection 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.
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 :
2.
3.
4.
5.
6.
7.
8.
9.
class Mailer
{
private
$transport
;
public
function
__construct
($transport
)
{
$this
->
transport =
$transport
;
}
}
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 :
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 :
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 ».
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
namespace Dvlp\Mailer;
use Monolog\Logger;
use Symfony\Component\HttpFoundation\Session\Session;
class Mailer
{
private
$transport
;
private
$session
;
public
function
__construct
($transport
,
$session
)
{
$this
->
transport =
$transport
;
$this
->
session =
$session
;
}
public
function
prepare()
{
if
( $this
->
session->
getUser()->
getGroup() ==
'administrateur'
) {
$logger
=
new
Logger();
$logger
->
setTitle('...'
);
[...]
}
}
}
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. »
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.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
use Dvlp\Mailer;
use Symfony\Component\HttpFoundation\Session\Session;
class Container
{
public
function
getMailer()
{
return
new
Mailer('sendmail'
,
$this
->
getSession());
}
public
function
getSession()
{
return
new
Session();
}
}
$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.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
use Dvlp\Mailer;
use Symfony\Component\HttpFoundation\Session\Session;
class Container
{
/**
*
@var
array
*/
protected
$parameters
;
public
function
__construct
(array
$parameters
)
{
$this
->
parameters =
$parameters
;
}
public
function
setParameter($name
,
$value
)
{
$this
->
parameters[
$name
]
=
$value
;
}
public
function
getParameter($name
)
{
return
$this
->
parameter[
$name
];
}
public
function
getMailer()
{
return
new
Mailer($this
->
parameters[
'mailer.transport'
],
$this
->
getSession());
}
public
function
getSession()
{
return
new
Session();
}
}
$configs
=
array
('mailer.transport'
=>
'sendmail'
);
$container
=
new
Container($configs
);
$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.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
namespace MonBundle\Mailer;
interface SenderInterface
{
public
function
getName();
public
function
getEmail();
}
class
Sender implements
SenderInterface
{
protected
$name
;
protected
$email
;
public
function
getName()
{
return
$this
->
name;
}
public
function
getEmail()
{
return
$this
->
email;
}
}
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.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
class Container
{
/**
*
@var
array
*/
protected
$parameters
;
public
function
__construct
(array
$parameters
)
{
$this
->
parameters =
$parameters
;
}
public
function
getNewsletter()
{
return
new
Newsletter($this
->
getSender());
}
public
function
getSender()
{
$class
=
$this
->
parameters[
'sender.class'
];
return
new
$class
();
}
}
$configs
=
array
('sender.class'
=>
'MonBundle\Sender'
);
$container
=
new
Container($configs
);
var_dump( get_class($container
->
getSender()) ) ;
// Nous donnera : MonBundle\Sender
$configs
=
array
('sender.class'
=>
'LeBundleDunAutre\User'
);
$container
=
new
Container($configs
);
var_dump( get_class($container
->
getSender()) ) ;
// 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.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
// /path/to/project/web/dvlp/app.php
$loader
=
require __DIR__.
'
/../../app/autoload.php
'
;
use Symfony\Component\HttpKernel\Kernel;
use Symfony\Component\Config\Loader\LoaderInterface;
class AppKernel extends
Kernel
{
public
function
registerBundles()
{
$bundles
=
[
new
Symfony\Bundle\FrameworkBundle\FrameworkBundle()
];
return
$bundles
;
}
public
function
getRootDir()
{
return
__DIR__;
}
public
function
getCacheDir()
{
return
$this
->
getRootDir().
'/var/cache/'
.
$this
->
getEnvironment();
}
public
function
getLogDir()
{
return
$this
->
getRootDir().
'/var/logs'
;
}
public
function
registerContainerConfiguration(LoaderInterface $loader
)
{
$loader
->
load($this
->
getRootDir().
'/config/config_'
.
$this
->
getEnvironment().
'.yml'
);
}
}
$kernel
=
new
\AppKernel('test'
,
true
);
$kernel
->
boot();
N'oubliez pas de créer un fichier de configuration dans ce dossier, nous verrons plus tard pourquoi :
2.
3.
// /path/to/project/web/dvlp/config/config_test.yml
framework:
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() :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
// /path/to/project/vendor/symfony/symfony/src/Symfony/Component/HttpKernel/Kernel.php
abstract class Kernel implements
KernelInterface, TerminableInterface
{
[...]
/**
* Boots the current kernel.
*/
public
function
boot()
{
if
(true
===
$this
->
booted) {
return
;
}
if
($this
->
loadClassCache) {
$this
->
doLoadClassCache($this
->
loadClassCache[
0
],
$this
->
loadClassCache[
1
]
);
}
// init bundles
$this
->
initializeBundles();
// init container
$this
->
initializeContainer();
foreach
($this
->
getBundles() as
$bundle
) {
$bundle
->
setContainer($this
->
container);
$bundle
->
boot();
}
$this
->
booted =
true
;
}
[...]
}
La ligne qui nous intéresse est $this->initializeContainer. Allons donc voir cette méthode :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
// /path/to/project/vendor/symfony/symfony/src/Symfony/Component/HttpKernel/Kernel.php
abstract class Kernel implements
KernelInterface, TerminableInterface
{
[...]
/**
* Initializes the service container.
*
* The cached version of the service container is used when fresh, otherwise the
* container is built.
*/
protected
function
initializeContainer()
{
$class
=
$this
->
getContainerClass();
$cache
=
new
ConfigCache($this
->
getCacheDir().
'/'
.
$class
.
'.php'
,
$this
->
debug
);
$fresh
=
true
;
if
(!
$cache
->
isFresh()) {
$container
=
$this
->
buildContainer();
$container
->
compile();
$this
->
dumpContainer($cache
,
$container
,
$class
,
$this
->
getContainerBaseClass());
$fresh
=
false
;
}
require_once $cache
->
getPath();
$this
->
container =
new
$class
();
$this
->
container->
set('kernel'
,
$this
);
if
(!
$fresh
&&
$this
->
container->
has('cache_warmer'
)) {
$this
->
container->
get('cache_warmer'
)->
warmUp($this
->
container->
getParameter('kernel.cache_dir'
));
}
}
[...]
}
Le travail de initializeContainer est de construire le conteneur et de le mettre à disposition de notre application.
2.
3.
4.
5.
[...]
require_once $cache
->
getPath();
$this
->
container =
new $class
();
[...]
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 :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Exception\LogicException;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
use Symfony\Component\DependencyInjection\ParameterBag\FrozenParameterBag;
/**
* dvlpTestDebugProjectContainer.
*
* This class has been auto-generated
* by the Symfony Dependency Injection Component.
*/
class dvlpTestDebugProjectContainer extends
Container
{
private
$parameters
;
[...]
/**
* Constructor.
*/
public
function
__construct
()
{
[...]
$this
->
parameters =
$this
->
getDefaultParameters();
$this
->
services =
array
();
$this
->
methodMap =
array
(
'annotation_reader'
=>
'getAnnotationReaderService'
,
[...]
);
}
/**
* Gets the 'annotation_reader' service.
*
* This service is shared.
* This method always returns the same instance of the service.
*
*
@return
\Doctrine\Common\Annotations\CachedReader A Doctrine\Common\Annotations\CachedReader instance.
*/
protected
function
getAnnotationReaderService()
{
return
$this
->
services[
'annotation_reader'
]
=
new
\Doctrine\Common\Annotations\CachedReader(new
\Doctrine\Common\Annotations\AnnotationReader(),
new
\Doctrine\Common\Cache\FilesystemCache((__DIR__.
'/annotations'
)),
true
);
}
/**
* {@inheritdoc}
*/
public
function
getParameter($name
)
{
$name
=
strtolower($name
);
if
(!
(isset($this
->
parameters[
$name
]
) ||
array_key_exists($name
,
$this
->
parameters))) {
throw
new
InvalidArgumentException(sprintf('The parameter "%s" must be defined.'
,
$name
));
}
return
$this
->
parameters[
$name
];
}
/**
* {@inheritdoc}
*/
public
function
hasParameter($name
)
{
$name
=
strtolower($name
);
return
isset($this
->
parameters[
$name
]
) ||
array_key_exists($name
,
$this
->
parameters);
}
[...]
}
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.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
class AppKernel extends
Kernel
{
public
function
registerBundles()
{
$bundles
=
[
new
Symfony\Bundle\FrameworkBundle\FrameworkBundle(),
[...]
];
return
$bundles
;
}
}
$kernel
=
new
\AppKernel('test'
,
true
);
$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 :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
// /path/to/project/vendor/symfony/symfony/src/Symfony/Component/HttpKernel/Kernel.php
abstract class Kernel implements
KernelInterface, TerminableInterface
{
[...]
/**
* Initializes the service container.
*
* The cached version of the service container is used when fresh, otherwise the
* container is built.
*/
protected
function
initializeContainer()
{
$class
=
$this
->
getContainerClass();
$cache
=
new
ConfigCache($this
->
getCacheDir().
'/'
.
$class
.
'.php'
,
$this
->
debug
);
$fresh
=
true
;
if
(!
$cache
->
isFresh()) {
$container
=
$this
->
buildContainer();
$container
->
compile();
$this
->
dumpContainer($cache
,
$container
,
$class
,
$this
->
getContainerBaseClass());
$fresh
=
false
;
}
require_once $cache
->
getPath();
$this
->
container =
new
$class
();
$this
->
container->
set('kernel'
,
$this
);
if
(!
$fresh
&&
$this
->
container->
has('cache_warmer'
)) {
$this
->
container->
get('cache_warmer'
)->
warmUp($this
->
container->
getParameter('kernel.cache_dir'
));
}
}
[...]
}
La ligne qui va nous intéresser maintenant est $container = $this->buildContainer().
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
/**
* Builds the service container.
*
*
@return
ContainerBuilder The compiled service container
*
* @throws \RuntimeException
*/
protected function buildContainer()
{
foreach (array('
cache
'
=>
$this
->
getCacheDir(),
'
logs
'
=>
$this
->
getLogDir()) as $name
=>
$dir
) {
if (!
is_dir($dir
)) {
if (false ===
@
mkdir($dir
,
0777
,
true) &&
!
is_dir($dir
)) {
throw new \RuntimeException(sprintf("
Unable to create the %s directory (%s)
\n
"
,
$name
,
$dir
));
}
}
elseif (!
is_writable($dir
)) {
throw new \RuntimeException(sprintf("
Unable to write in the %s directory (%s)
\n
"
,
$name
,
$dir
));
}
}
$container
=
$this
->
getContainerBuilder();
$container
->
addObjectResource($this
);
$this
->
prepareContainer($container
);
if (null !==
$cont
=
$this
->
registerContainerConfiguration($this
->
getContainerLoader($container
))) {
$container
->
merge($cont
);
}
$container
->
addCompilerPass(new AddClassesToCachePass($this
));
$container
->
addResource(new EnvParametersResource('
SYMFONY__
'
));
return $container
;
}
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).
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
/**
* Prepares the ContainerBuilder before it is compiled.
*
*
@param
ContainerBuilder
$container
A ContainerBuilder instance
*/
protected function prepareContainer(ContainerBuilder $container
)
{
$extensions
=
array();
foreach ($this
->
bundles as $bundle
) {
if ($extension
=
$bundle
->
getContainerExtension()) {
$container
->
registerExtension($extension
);
$extensions
[]
=
$extension
->
getAlias();
}
if ($this
->
debug
) {
$container
->
addObjectResource($bundle
);
}
}
foreach ($this
->
bundles as $bundle
) {
$bundle
->
build($container
);
}
// ensure these extensions are implicitly loaded
$container
->
getCompilerPassConfig()->
setMergePass(new MergeExtensionConfigurationPass($extensions
));
}
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 :
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 SymfonyConfigurer 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 :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
// src/Dvlp/DemoBundle/DependencyInjection/DvlpDemoExtension.php
namespace Dvlp\DemoBundle\DependencyInjection;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
class DvlpDemoExtension extends
Extension implements
PrependExtensionInterface
{
// ...
public
function
prepend(ContainerBuilder $container
)
{
$bundles
=
$container
->
getParameter('kernel.bundles'
);
$configs
=
$container
->
getExtensionConfig($this
->
getAlias());
$config
=
$this
->
processConfiguration(new
Configuration(),
$configs
);
foreach
(array_keys($container
->
getExtensions()) as
$name
) {
switch
($name
) {
case
'twig'
:
// Add supports assets list in twig variables
$container
->
prependExtensionConfig($name
,
array
(
'form_themes'
=>
'DvlpDemoBundle::form_themes_layout.html.twig'
));
break
;
}
}
}
}
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 SymfonyDocumentation 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 :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
<?xml version="1.0" encoding="UTF-8" ?>
<container
xmlns
=
"http://symfony.com/schema/dic/services"
xmlns
:
xsi
=
"http://www.w3.org/2001/XMLSchema-instance"
xsi
:
schemaLocation
=
"http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"
>
<parameters>
<!-- ... -->
<parameter
key
=
"newsletter.sender"
>
SenderManager</parameter>
</parameters>
<services>
<service
id
=
"newsletter_manager"
class
=
"NewsletterManager"
>
<argument
type
=
"service"
id
=
"newsletter.SenderManager"
/>
</service>
</services>
</container>
Voici ce qu'il ne faut pas faire :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
<?xml version="1.0" encoding="UTF-8" ?>
<container
xmlns
=
"http://symfony.com/schema/dic/services"
xmlns
:
xsi
=
"http://www.w3.org/2001/XMLSchema-instance"
xsi
:
schemaLocation
=
"http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"
>
<parameters>
<!-- ... -->
<parameter
key
=
"newsletter.sender"
>
SenderManager</parameter>
</parameters>
<services>
<service
id
=
"newsletter_manager"
class
=
"NewsletterManager"
>
<argument
type
=
"service"
id
=
"service_container"
/>
</service>
</services>
</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.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
class NewsletterManager
{
protected
$mailer
;
public
function
__construct
(\Mailer $mailer
)
{
$this
->
mailer =
$mailer
;
}
// ...
}
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
<?xml version="1.0" encoding="UTF-8" ?>
<container
xmlns
=
"http://symfony.com/schema/dic/services"
xmlns
:
xsi
=
"http://www.w3.org/2001/XMLSchema-instance"
xsi
:
schemaLocation
=
"http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"
>
<services>
<service
id
=
"my_mailer"
>
<!-- ... -->
</service>
<service
id
=
"newsletter_manager"
class
=
"NewsletterManager"
>
<argument
type
=
"service"
id
=
"my_mailer"
/>
</service>
</services>
</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 :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
class NewsletterManager
{
protected
$mailer
;
public
function
setMailer(\Mailer $mailer
)
{
$this
->
mailer =
$mailer
;
}
// ...
}
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
<?xml version="1.0" encoding="UTF-8" ?>
<container
xmlns
=
"http://symfony.com/schema/dic/services"
xmlns
:
xsi
=
"http://www.w3.org/2001/XMLSchema-instance"
xsi
:
schemaLocation
=
"http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"
>
<services>
<service
id
=
"my_mailer"
>
<!-- ... -->
</service>
<service
id
=
"newsletter_manager"
class
=
"NewsletterManager"
>
<call
method
=
"setMailer"
>
<argument
type
=
"service"
id
=
"my_mailer"
/>
</call>
</service>
</services>
</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.
2.
3.
4.
5.
6.
class NewsletterManager
{
public
$mailer
;
// ...
}
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
<?xml version="1.0" encoding="UTF-8" ?>
<container
xmlns
=
"http://symfony.com/schema/dic/services"
xmlns
:
xsi
=
"http://www.w3.org/2001/XMLSchema-instance"
xsi
:
schemaLocation
=
"http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"
>
<services>
<service
id
=
"my_mailer"
>
<!-- ... -->
</service>
<service
id
=
"newsletter_manager"
class
=
"NewsletterManager"
>
<property
name
=
"mailer"
type
=
"service"
id
=
"my_mailer"
/>
</service>
</services>
</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 :
VI. Sources▲
VII. Remerciements▲
Merci à Guillaume SIGUI et Claude Leloup pour leur aide lors de la rédaction de cet article !