Fabien à publié aujourd'hui un nouveau composant Symfony : le Dependency Injection Container. J'en profite donc pour publier un petit billet sur l'utilisation de ce composant dans un projet Symfony. Je pars du principe que vous avez déjà un projet Symfony installé, et si ce n'est pas le cas, commencez par la page d'installation.
Dans ce billet, je vais reprendre directement les exemples présents dans la http://components.symfony-project.org/dependency-injection/documentation, à savoir créer un service permettant d'envoyer un mail.
Nous allons donc avoir besoin de deux librairies, à savoir:
- Dependency injection Container bien évidemment
- Zend Mail
Prérequis
Pour faire les choses rapidement, on va juste faire un export des librairies dans le repertoire lib/vendor de votre application. Si vous utilisez déjà Subversion avec votre projet, la meilleure façon sera d'ajouter ces deux repository en externals.
$> svn export http://svn.symfony-project.com/components/dependency_injection/trunk/ lib/vendor/dependency_injection $> svn export http://framework.zend.com/svn/framework/standard/trunk/library/Zend lib/vendor/Zend
Création du Service Container
Tout d'abord, il faut initialiser l'autoloading pour pouvoir utiliser le composant. Ajoutez ces deux lignes dans le fichier
<?php
require_once dirname(__FILE__).'/../lib/vendor/dependency_injection/lib/sfServiceContainerAutoloader.php';
sfServiceContainerAutoloader::register();
Toujours dans ce fichier, nous allons ajouter une méthode initializeServiceContainer() pour initialiser le conteneur de services:
<?php
class ProjectConfiguration extends sfProjectConfiguration
{
protected
$serviceContainer;
/* ... */
protected function initializeServiceContainer()
{
$this->serviceContainer = new sfServiceContainerBuilder();
$loader = new sfServiceContainerLoaderFileYaml($this->serviceContainer);
if ($this instanceof sfApplicationConfiguration && file_exists($file = sfConfig::get('sf_config_dir').'/container_'.$this->environment.'.yml'))
{
$loader->load($file);
}
else
{
$loader->load(sfConfig::get('sf_config_dir').'/container.yml');
}
}
Le composant nous offre la possibilité de charger la configuration en XML ou YAML à travers les classes sfServiceContainerLoaderFileXml ou sfServiceContainerLoaderFileYaml. Dans cet exemple, nous utilisons le format YAML, pour rester cohérent avec les autres fichiers de configuration de Symfony.
La classe sfServiceContainerBuilder sera notre conteneur de services. C'est lui qui sera chargé de d'instancier et de configurer les différents services. Pour charger la configuration, nous demandons ensuite au loader de charger les fichier Yaml pour l'environnement actuel si il y en a un, ou la configuration par défaut le cas échéant (par exemple en mode cli).
Le fichier container.yml est exactement le même que celui de la documentation:
parameters:
mailer.username: foo
mailer.password: bar
mailer.class: Zend_Mail
services:
mail.transport:
class: Zend_Mail_Transport_Smtp
arguments: [smtp.gmail.com, { auth: login, username: %mailer.username%, password: %mailer.password%, ssl: ssl, port: 465 }]
shared: false
mailer:
class: %mailer.class%
calls:
- [setDefaultTransport, [@mail.transport]]
Ces fichiers ne font rien de plus qu'importer le fichier de configuration par défaut.
C'est à peut prêt tout ce que nous avons besoin pour utiliser l'injection de dépendance. Nous avons juste besoin d'initialiser automatiquement le service container, et d'avoir une méthode permettant d'y accéder.
Dans votre fichier config/ProjectConfiguration.class.php, nous ajoutons une méthode getServiceContainer(), et nous ajoutons l'appel à la méthode initializeServiceContainer() dans la méthode setup() :
<?php
class ProjectConfiguration extends sfProjectConfiguration
{
/* ... */
public function setup()
{
/* ... */
$this->initializeServiceContainer();
}
public function getServiceContainer()
{
return $this->serviceContainer;
}
/* ... */
}
Pour l'utiliser, c'est très simple. Si vous utiliser le service mail dans une action, c'est désormais assez simple:
<?php
public function executeSendMail(sfWebRequest $request)
{
$this->mailer = sfApplicationConfiguration::getActive()->getServiceContainer()->mailer;
$this->mailer->
setBodyText('This is the text of the mail.')->
setFrom('somebody@example.com', 'Some Sender')->
addTo('somebody_else@example.com', 'Some Recipient')->
setSubject('TestSubject')->
send();
}
Oui, et alors?
C'est un petit peu ce que je me suis dis la 1ère fois que j'ai entendu parler d'injection de dépendance. Le gros avantage, c'est que désormais on va pouvoir configurer notre service en fonction de chaque environnement. Ça a un gros intérêt lors de l'écriture de tests unitaire par exemple, parce que vous ne voulez peut-être pas recevoir un mail à chaque fois que les tests sont lancés. Pour les tests, il va donc falloir désactiver l'envoi de mail. Sans l'injection de dépendance, il faudrait tester si on est en environnement de test directement dans l'action. Maintenant, il suffira de modifier la configuration, sans avoir à modifier le code.
Customiser la configuration pour l'environnement de test
En général, lors des tests, on écrit des classes qui simulent le comportement de la classe utilisées par l'application (les "Mocks"). C'est assez facile à mettre en place pour les tests unitaires, mais pas toujours évidents lors des tests fonctionnels. Ici, c'est très simple, il suffit d'ajouter un fichier de configuration pour l'environnement de test (config/container_test.yml) :
parameters:
mailer.class: Mail_Mock
services:
mailer:
class: %mailer.class%
La classe Mail_Mock ici est très simpliste, elle reprend juste les méthodes utilisés dans notre action, et ne fait aucun traitement :
lib/test/Mail_Mock.class.php
<?php
class Mail_Mock
{
public function setFrom()
{
return $this;
}
public function addTo()
{
return $this;
}
public function setBodyText()
{
return $this;
}
public function setSubject()
{
return $this;
}
public function send()
{
return true;
}
}
Conclusion
Ce composant est l'une des pièces maitresse de Symfony 2, et fait entrer le framework dans la cour des grands comme Spring. L'autre gros avantage, c'est qu'il est complétement découplé de Symfony, et donc réutilisable sans avoir besoin du framework.

