En effet, il est parfois délicat d'injecter des objets dynamiquement, notamment lorsque leur création dépend d'informations internes à la classe qui les utilise, comme dans le cas suivant :
<?php
namespace mageekguy\atoum\phar;
class generator {
...
public function run()
{
...
$phar = new \phar($this->destinationDirectory . DIRECTORY_SEPARATOR . self::name);
$phar->buildFromIterator(new \directoryIterator($this->originDirectory));
...
}
... }
?>
Dans ce cas, la création de l'objet stocké par la variable $phar
dépend de la concaténation de $this->destinationDirectory
et de self::name
, la création de l'itérateur permettant de remplir l'archive phar dépend de $this->originDirectory
, et l'ensemble de ces informations est inaccessibles de l'extérieur de la classe.
De plus, il n'est pas possible de possible de modifier le fichier de destination d'un objet phar après sa création, et il en va de même dans le cas de l'itérateur.
Il serait bien possible de passer par des accesseurs, de la manière suivante :
<?php ... $generator->run(new \phar($generator->getPharName(), new \directoryIterator($generator->getDestinationDirectory()); ... ?>
Cependant, cette solution est de mon point de vue tirée par les cheveux, d'autant que dans le cas qui nous occupe, la récupération du répertoire de destination à partir des arguments passés en ligne de commande se fait dans la méthode run()
.
En conséquence, il semble à priori difficile de mettre en œuvre l'injection de dépendances dans ce cas, du moins avec la méthode classique.
Il faut donc se tourner vers d'autres solutions, comme Crafty, dont l'utilisation est décrite par l'ami Clochix, ou l'injecteur de dépendances de Symfony.
Cependant, ces solutions, indépendamment de leurs qualités respectives, sont relativement lourdes, ou tout du moins trop lourdes à mon goût.
J'ai donc choisi une autre voie en utilisant l'une des fonctionnalités de PHP 5.3 que j'affectionne particulièrement actuellement, à savoir les fermetures.
En effet, malgré leurs limitations, elles sont suffisamment puissantes et intéressante dans le cas présent.
Pour cela, il faut commencer par définir deux propriétés protégées au niveau de notre classe :
<?php
namespace mageekguy\atoum\phar;
class generator {
...
protected $pharInjector = null;
protected $fileIteratorInjector = null;
... }
?>
Une fois cela fait, il nous faut modifier le constructeur de notre classe afin de définir lors de l'instanciation de notre classe les injecteurs de dépendances par défaut :
<?php
namespace mageekguy\atoum\phar;
class generator {
public function __construct(...)
{
$this->pharInjector = function($name) { return new \phar($name); };
$this->fileIteratorInjector = function($directory) { return new \directoryIterator($directory); };
}
... }
?>
Il faut ensuite définir deux méthodes qui permettront au développeur de définir les injecteurs de son choix si ceux proposés par défaut ne conviennent pas :
<?php
namespace mageekguy\atoum\phar;
class generator {
public function setPharInjector(\closure $pharInjector)
{
$this->pharInjector = $pharInjector;
return $this;
}
public function setFileIteratorInjector(\closure $fileIteratorInjector)
{
$this->fileIteratorInjector = $fileIteratorInjector;
return $this;
}
... }
?>
Enfin, il faut remplacer les appels à l'opérateur new
effectués sur les classes \phar
et directoryIterator
par une invocation de nos injecteurs :
<?php
namespace mageekguy\atoum\phar;
class generator {
...
public function run()
{
...
$phar = $this->pharInjector->__invoke($this->destinationDirectory . DIRECTORY_SEPARATOR . self::name);
$phar->buildFromIterator($this->fileIteratorInjector->__invoke($this->originDirectory));
...
}
... }
?>
Grâce à ce mécanisme, il est maintenant possible d'utiliser les classes de son choix dans la méthode run()
pour construire notre archive phar.
Dans le cadre d'un test unitaire écrit avec Atoum, cela donne le code suivant :
<?php
namespace mageekguy\atoum\tests\units\phar;
use \mageekguy\atoum; use \mageekguy\atoum\mock; use \mageekguy\atoum\phar;
require_once(__DIR__ . '/../../runner.php');
/** @isolation on */ class generator extends atoum\test { public function testRun() {
...
$mockGenerator = new mock\generator();
$pharController = new mock\controller();
$pharController->injectInNextMockInstance(); $pharController->__construct = function() {}; $pharController->setStub = function() {}; $pharController->setMetadata = function() {}; $pharController->buildFromIterator = function() {}; $pharController->setSignatureAlgorithm = function() {};
$mockGenerator->generate('\phar'); $phar = new mock\phar(uniqid());
$generator->setPharInjecter(function($name) use ($phar) { return $phar; });
$fileIteratorController = new mock\controller(); $fileIteratorController->injectInNextMockInstance(); $fileIteratorController->__construct = function() {};
$mockGenerator->generate('\recursiveDirectoryIterator'); $iterator = new mock\recursiveDirectoryIterator(uniqid());
$generator->setFileIteratorInjecter(function($directory) use ($iterator) { return $iterator; });
$this->assert ->object($generator->run())->isIdenticalTo($generator) ;
...
} }
?>
Et oui, comme vous pouvez le constater dans le code ci-dessus, les fermetures peuvent servir à beaucoup de choses, puisque dans ce cas, elles permettent de définir le comportement des mocks
.
Pour en revenir à l'injection de dépendances, il est possible de renforcer le système en vérifiant le nombre d'arguments passés aux injecteurs, en faisant par exemple appel à la reflection de PHP, et en contrôlant le type de variable renvoyé par l'invocation des injecteurs à l'aide de l'opérateur instanceof
.
Et vous, vous injectez vos dépendances de quelle manière ?
4 réactions
1 De Ingolmo - 27/09/2010, 15:41
Pour le framework que je suis en train de coder, on a presque terminé d'intégrer Pimple... C'est la version "allégée" du container de Symfony :
http://github.com/fabpot/Pimple
2 De usul - 27/09/2010, 16:53
C'est intéressant dans la mesure ou on peut injecter des méthodes.
J'ai essayé l'injecteur de Symfony qui est effectivement bien fait mais que je trouve aussi un peu lourd.
Ayant fait pas mal de java, j'ai malheureusement la fâcheuse tendance à comparer et le seul manque que je trouve en php pour pouvoir faire de l'injection, c'est de ne pas pouvoir accéder à un 'vrai' conteneur dans lequel on pourrait stocker des instances réutilisables entre différentes requêtes.
3 De metagoto - 27/09/2010, 17:01
Ce qui me surprend le plus, et que j'ai vu aussi passer dans "Teasing 3", c'est injectInNextMockInstance(). Je pense comprendre ce que tu fais, et je trouve ça très spécial! Pas très intuitif.. même si le nom de la méthode est explicite. Ca me parait très propice aux side effects indésirables et difficilement détectables. Pour le coup, je trouve que cela manque d'injection de dépendance! Que se passe-t-il si une fonction s'intercale naivement entre la création du mock controller et la génération du mock et que celle-ci génère un tout autre mock?
Pour les closures dans le generator, c'est vrai que c'est pas mal ton truc. Tu peux réutiliser les paramètres par défaut ou proposer ta propre logique basée sur ces paramètres si nécessaire. Bien vu.
Je n'ai jamais été fan des containers d'injection de dépendance (disons l'abstraction au dessus de l'injection) mais je reconnais qu'ils sont utiles dans certaines architectures/projets. Comme toi, je pense qu'avec les fonctions anonymes, on a moyen de faire des choses puissantes et simples pour les problématiques d'injections (pensons à javascript).
4 De mageekguy - 27/09/2010, 17:53
@metagoto : Cette méthode
mock\controller::injectInNextInstance()
est un hack pour contourner certain bug de PHP (celui sur le constructeur de\phar
notamment) qui empêche l'injection du contrôleur via le constructeur du mock.J'ai été obligé d'être
, pour contourner le problème, et au final, le concept ne me déplais pas, même si effectivement, c'est original et qu'il faut faire attention à ce que l'on fait en l'état actuel de l'implémentation.J'ai conscience du problème que tu souléves, mais il faut bien faire avec les défauts de PHP, n'est ce pas ?
Il y a évidement une méthode beaucoup plus classique pour injecter le contrôleur, soit via le constructeur du mock (c'est le dernier argument, à
null
par défaut) ou via la méthodesetMockController()
imposée par l'interfacemock\aggregator
qu'implémente obligatoirement un mock.