mageekblog - Mot-clé - injection de dépendancesLe blog personnel de Frédéric Hardy. Au menu, PHP, agilité, FreeBSD, cuisine et photographies.2021-12-02T08:20:54+01:00Frédéric Hardyurn:md5:26874ca5b8cd4cac8d08b0e68e64f63aDotclearÀ propos de l'injection de dépendanceurn:md5:59f6b15da58583f5f4dbc5e761dd155e2012-06-28T14:00:00+02:002012-06-28T16:42:08+02:00mageekguyPHPatouminjection de dépendancesPHP<p><a href="http://fr.wikipedia.org/wiki/Injection_de_dépendances">L'injection de dépendance</a> est un concept qui commence à être relativement connu par les développeurs, ne serait-ce que parce que la plupart des frameworks modernes y font massivement appel.</p>
<p>Pour mémoire, elle permet de définir les objets nécessaires au fonctionnement d'un objet non plus statiquement, c'est à dire explicitement dans le code, mais dynamiquement, soit lors de l'exécution de ce dernier.</p>
<p>Ce mécanisme permet donc au développeur d'injecter dans une instance de classe les instances de son choix afin de modifier, améliorer ou bien encore tester son fonctionnement.</p>
<p>L'injection de dépendance permet donc de modifier très simplement, la plupart du temps via un simple fichier de configuration, le comportement d'un programme et il permet également d'utiliser des bouchons, <abbr title="Also Known As">aka</abbr> mock, dans le cadre d'un test unitaire.</p>
<p>C'est donc un concept très puissant et il est de plus très simple à mettre en œuvre, puisque même si cela peut sembler très caricatural au premier abord, utiliser l'injection de dépendance revient à bannir du code d'une classe tout appel explicite à l'opérateur <code>new</code>.</p> <p>En effet, un tel appel suppose que le nom de la classe devant être instanciée est codé en dur dans le code, et il est donc alors impossible de redéfinir dynamiquement la classe à postériori de l'écriture du code.</p>
<blockquote><pre><code><?php
class foo
{
public function doSomething()
{
$bar = new bar(); // il est impossible de remplacer "bar" par une classe qui en hérite, par exemple
$bar->doOtherThing();
}
}
</code></pre></blockquote>
<p>Une première solution pour mettre en œuvre l'injection de dépendance est donc de définir des méthodes au niveau de la classe permettant d'injecter des objets qui auront été créés en dehors de la classe, là où l'utilisation de l'opérateur <code>new</code> est autorisée.</p>
<p>Ces méthodes, regroupées sous la dénomination de <q>setter</q> sont autant de points d'entrée disponibles dans le code de la classe lors de son exécution.</p>
<blockquote><pre><code><?php
class foo
{
protected $bar = null;
public function setBar(bar $bar)
{
$this->bar = $bar;
return $this;
}
public function doSomething()
{
$this->bar->doOtherThing();
}
}
</code></pre></blockquote>
<p>Les plus attentifs auront remarqué que la propriété <code>bar</code> n'est jamais initialisée dans le code précédent, et en toute logique, il faudrait ajouter une gestion d'erreurs pour que si la méthode <code>foo::doSomething()</code> est appelée alors que <code>bar</code> est indéfinie, le développeur soit averti du problème.</p>
<p>Cela signifie également que l'utilisateur de la classe devra systématiquement penser à appeler la méthode <code>foo::setBar()</code> avec l'argument adéquat avant d'utiliser <code>foo::doSomething()</code>, et l'on peut être certain que cela ne sera pas systématiquement le cas, ce qui provoquera des bugs et l'exaspération de l'utilisateur.</p>
<p>Pour remédier à cela, il suffit de définir un constructeur jouant également le rôle de <q>setter</q>, mais avec une gestion de la dépendance suffisamment intelligente pour ne pas nécessiter le recours systématique à <code>foo::setBar()</code>, et pour cela, l'opérateur elvis, aka ?: et apparu avec PHP 5.3, est l'outil idéal.</p>
<blockquote><pre><code><?php
class foo
{
protected $bar = null;
public function __construct(bar $bar = null)
{
$this->setBar($bar ?: new bar());
}
public function setBar(bar $bar)
{
$this->bar = $bar;
return $this;
}
public function doSomething()
{
$this->bar->doOtherThing();
}
}
</code></pre></blockquote>
<p>Pour rendre votre code encore plus modulaire, vous pouvez de plus recourir à une interface pour typer le type d'objet géré par l'objet.</p>
<p>De cette façon, toute classe implémentant l'interface requise pourra être utilisée par votre méthode.</p>
<blockquote><pre><code><?php
interface barInterface
{
public function doOtherThing(...);
}
class foo
{
protected $bar = null;
public function __construct(barInterface $bar = null)
{
$this->setBar($bar ?: new bar());
}
public function setBar(barInterface $bar)
{
$this->bar = $bar;
return $this;
}
public function doSomething()
{
$this->bar->doOtherThing();
}
}
</code></pre></blockquote>
<p>Une fois cette stratégie mise en place au niveau de toute les classes de votre programme, vous êtes virtuellement à même d'injecter les classes de votre choix à tous les niveaux de votre code, et vous êtes de plus capable de le tester facilement :</p>
<blockquote><pre><code><?php
class foo extends atoum\test
{
public function testDoSomething()
{
$this
->if($foo = new foo(new \mock\bar()))
->and($bar->getMockController()->doOtherThing->throw = $exception = new \exception('Houston, we have a problem !')
->then
->exception(function() use ($foo) { $foo->doSomething(); })
->isIdenticalTo($exception)
;
}
}
</code></pre></blockquote>
<p>L'objectif est donc atteint, mais la méthode a cependant un inconvénient majeur.</p>
<p>En effet, la longueur de la signature du constructeur et le nombre de <q>setter</q> vont évoluer proportionnellement au nombre de dépendances nécessaires au fonctionnement de la classe.</p>
<p>De plus, tout ajout de dépendance nécessitera, outre l'ajout du <q>setter</q> correspondant, la modification de la signature du constructeur et donc potentiellement la mise à jour de l'ensemble des tests unitaires y faisant appel.</p>
<p>Cela peut donc vite devenir très lourd à concevoir et à maintenir, surtout dans le cadre d'un projet conséquent, et je parle d'expérience car le principe énoncé ci-dessus est utilisé abondamment dans <a href="http://www.atoum.org">atoum</a>, mon framework de test unitaire simple, moderne et intuitif pour PHP 5.3+.</p>
<p>C'est d'ailleurs la raison pour laquelle j'ai décidé de modifier totalement mon approche et de faire dorénavant appel à un injecteur de dépendance pour rendre le code de <a href="http://www.atoum.org">atoum</a> encore plus souple et plus facile à maintenir et à faire évoluer.</p>
<p>Grâce à cet injecteur, le code de notre classe <code>foo</code> deviendrait le suivant :</p>
<blockquote><pre><code><?php
class foo
{
protected $bar = null;
public function __construct(dependenciesInjector $injector = null)
{
$this->setDependencies($injector ?: new dependenciesInjector());
}
public function setDependencies(dependenciesInjector $injector)
{
$injector['bar'] = $injector['bar'] ?: function() { return new bar(); };
$this->bar = $injector['bar']();
return $this;
}
public function doSomething()
{
$this->bar->doOtherThing();
}
}
</code></pre></blockquote>
<p>De cette façon, si à l'avenir j'ai besoin d'ajouter une dépendance, je ne dois modifier que la méthode <code>foo::setDependenciesInjector()</code> et je n'ai plus besoin de mettre à jour tous les appels au constructeur dans mes tests unitaires, puisque je n'ai plus qu'à enrichir mon injecteur de dépendance avec les bouchons nécessaires pour qu'ils soient automatiquement utilisés.</p>
<blockquote><pre><code><?php
class foo extends atoum\test
{
public function testDoSomething()
{
$this
->if($injector = new injector())
->and($injector['bar'] = new \mock\bar())
->and($foo = new foo($injector))
->and($bar->getMockController()->doOtherThing->trow = $exception = new \exception('Houston, we have a problem !')
->then
->exception(function() use ($foo) { $foo->doSomething(); })
->isIdenticalTo($exception)
;
}
}
</code></pre></blockquote>
<p>L'injection de dépendance est donc un concept puissant que je trouve indispensable à mettre en œuvre pour obtenir un code de qualité modulable et facile à tester et à maintenir.</p>
<p>Sa mise en œuvre peut être très simple, mais dans le cadre d'un projet important et/ou qui évolue de manière importante et récurrente, les solutions les plus simples ne sont pas forcément les plus adaptées à moyen terme, et il peut se révéler plus rentable d'utiliser un injecteur de dépendance qui apportera encore plus de souplesse à la fois lors de la conception du code et lors de sa mise en œuvre.</p>http://blog.mageekbox.net/?post/2012/06/28/%C3%80-propos-de-l-injection-de-d%C3%A9pendance#comment-formhttp://blog.mageekbox.net/?feed/atom/comments/354Vous êtes dépendant ? ce n'est pas un problème !urn:md5:6db273ff91ce3cac14ec800c86d861fd2010-09-27T14:30:00+02:002010-09-27T14:30:00+02:00mageekguyPHPAtoumfermetureinjection de dépendancesmockphartest unitaire<p>L'injection de dépendances est un patron de conception que tout bon développeur qui fait de la programmation orientée objet, en <a href="http://www.php.net">PHP</a> ou avec tout autre langage, devrait mettre en œuvre.</p>
<p>Pour rappel, il consiste à ne pas instancier d'objet dans le code des classes lors de la conception de ces dernières, mais à les injecter lors de exécution du code dans les instances de classes, soit à l'aide d'un passage par argument sur la méthode appelée, soit à l'aide d'une méthode dédiée.</p>
<p>Ainsi, le découplage entre les classes est augmenté, et le code est alors plus modulaire et réutilisable.</p>
<p>De plus, l'injection de dépendances facilite l'écriture des tests unitaires puisqu'elle permet de <q>bouchonner</q> facilement à l'aide de <q><a href="http://en.wikipedia.org/wiki/Mock_object">mock</a></q>.</p>
<p>Pour autant, sa mise en œuvre n'est pas toujours forcément aisée.</p> <p>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 :</p>
<blockquote><pre><code><?php
<br />namespace mageekguy\atoum\phar;
<br />class generator
{<br /> ... <br /> public function run() <br /> {<br /> ...<br /> $phar = new \phar($this->destinationDirectory . DIRECTORY_SEPARATOR . self::name); <br /> $phar->buildFromIterator(new \directoryIterator($this->originDirectory));<br /> ...<br /> }<br /> ...
}
<br />?></code></pre></blockquote>
<p>Dans ce cas, la création de l'objet stocké par la variable <code>$phar</code> dépend de la concaténation de <code>$this->destinationDirectory</code> et de <code>self::name</code>, la création de l'itérateur permettant de remplir l'archive <a href="http://fr.php.net/phar">phar</a> dépend de <code>$this->originDirectory</code>, et l'ensemble de ces informations est inaccessibles de l'extérieur de la classe.</p>
<p>De plus, il n'est pas possible de possible de modifier le fichier de destination d'un objet <a href="http://fr.php.net/phar">phar</a> après sa création, et il en va de même dans le cas de l'itérateur.</p>
<p>Il serait bien possible de passer par des accesseurs, de la manière suivante :</p>
<blockquote><pre><code><?php
...
$generator->run(new \phar($generator->getPharName(), new \directoryIterator($generator->getDestinationDirectory());
...
?></code></pre></blockquote>
<p>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 <code>run()</code>.</p>
<p>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.</p>
<p>Il faut donc se tourner vers d'autres solutions, comme <a href="http://phpcrafty.sourceforge.net/">Crafty</a>, dont l'utilisation est décrite par l'ami <a href="http://www.clochix.net/post/2008/07/07/Injection-de-dependances-en-PHP-avec-Crafty">Clochix</a>, ou l'injecteur de dépendances de <a href="http://components.symfony-project.org/dependency-injection/">Symfony</a>.</p>
<p>Cependant, ces solutions, indépendamment de leurs qualités respectives, sont relativement lourdes, ou tout du moins trop lourdes à mon goût.</p>
<p>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 <a href="http://php.net/manual/en/functions.anonymous.php">fermetures</a>.</p>
<p>En effet, malgré leurs <a href="http://blog.mageekbox.net/?post/2010/07/08/Les-fermetures%2C-c-est-trop...-ferm%C3%A9-%21">limitations</a>, elles sont suffisamment puissantes et intéressante dans le cas présent.</p>
<p>Pour cela, il faut commencer par définir deux propriétés protégées au niveau de notre classe :</p>
<blockquote><pre><code><?php
<br />namespace mageekguy\atoum\phar;
<br />class generator
{ <br /> ...<br /> protected $pharInjector = null;<br /> protected $fileIteratorInjector = null;<br /> ...
}
<br />?></code></pre></blockquote>
<p>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 :</p>
<blockquote><pre><code><?php
<br />namespace mageekguy\atoum\phar;
<br />class generator
{ <br /> public function __construct(...)<br /> {<br /> $this->pharInjector = function($name) { return new \phar($name); };<br /> $this->fileIteratorInjector = function($directory) { return new \directoryIterator($directory); };<br /> } <br /> ...
}
<br />?></code></pre></blockquote>
<p>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 :</p>
<blockquote><pre><code><?php
<br />namespace mageekguy\atoum\phar;
<br />class generator
{ <br /> public function setPharInjector(\closure $pharInjector)<br /> {<br /> $this->pharInjector = $pharInjector;<br /> return $this;<br /> } <br /><br /> public function setFileIteratorInjector(\closure $fileIteratorInjector)<br /> {<br /> $this->fileIteratorInjector = $fileIteratorInjector;<br /> return $this;<br /> }<br /> ...
}
<br />?></code></pre></blockquote>
<p>Enfin, il faut remplacer les appels à l'opérateur <code>new </code> effectués sur les classes <code>\phar</code> et <code>directoryIterator</code> par une invocation de nos injecteurs :</p>
<blockquote><pre><code>
<?php
<br />namespace mageekguy\atoum\phar;
<br />class generator
{<br /> ... <br /> public function run() <br /> {<br /> ...<br /> $phar = $this->pharInjector->__invoke($this->destinationDirectory . DIRECTORY_SEPARATOR . self::name); <br /> $phar->buildFromIterator($this->fileIteratorInjector->__invoke($this->originDirectory));<br /> ...<br /> }<br /> ...
}
<br />?>
</code></pre>
</blockquote>
<p>Grâce à ce mécanisme, il est maintenant possible d'utiliser les classes de son choix dans la méthode <code>run()</code> pour construire notre archive <a href="http://fr.php.net/phar">phar</a>.</p>
<p>Dans le cadre d'un test unitaire écrit avec Atoum, cela donne le code suivant :</p>
<blockquote><pre><code><?php<br />
namespace mageekguy\atoum\tests\units\phar;<br /><br />use \mageekguy\atoum;
use \mageekguy\atoum\mock;
use \mageekguy\atoum\phar;<br />
require_once(__DIR__ . '/../../runner.php');<br />
/** @isolation on */
class generator extends atoum\test
{
public function testRun()
{ <br /> ...<br /> $mockGenerator = new mock\generator();
<br /> $pharController = new mock\controller();<br /> $pharController->injectInNextMockInstance();
$pharController->__construct = function() {};
$pharController->setStub = function() {};
$pharController->setMetadata = function() {};
$pharController->buildFromIterator = function() {};
$pharController->setSignatureAlgorithm = function() {};
<br /> $mockGenerator->generate('\phar');
$phar = new mock\phar(uniqid());
<br /> $generator->setPharInjecter(function($name) use ($phar) { return $phar; });
<br /> $fileIteratorController = new mock\controller();
$fileIteratorController->injectInNextMockInstance();
$fileIteratorController->__construct = function() {};
<br /> $mockGenerator->generate('\recursiveDirectoryIterator');
$iterator = new mock\recursiveDirectoryIterator(uniqid());
<br /> $generator->setFileIteratorInjecter(function($directory) use ($iterator) { return $iterator; });
<br /> $this->assert
->object($generator->run())->isIdenticalTo($generator)
;<br /> ...<br /> }
}
<br />?></code></pre></blockquote>
<p>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 <q><a href="http://en.wikipedia.org/wiki/Mock_object">mocks</a></q>.</p>
<p>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 <a href="http://blog.pascal-martin.fr/post/php-5.3-2-closures-et-lambdas">reflection</a> de <a href="http://www.php.net">PHP</a>, et en contrôlant le type de variable renvoyé par l'invocation des injecteurs à l'aide de l'opérateur <code>instanceof</code>.</p>
<p>Et vous, vous injectez vos dépendances de quelle manière ?</p>http://blog.mageekbox.net/?post/2010/09/27/Vous-%C3%AAtes-d%C3%A9pendant-ce-n-est-pas-un-probl%C3%A8me-%21#comment-formhttp://blog.mageekbox.net/?feed/atom/comments/192