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.

<?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();
   }
}

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 new est autorisée.

Ces méthodes, regroupées sous la dénomination de setter sont autant de points d'entrée disponibles dans le code de la classe lors de son exécution.

<?php
class foo
{
   protected $bar = null;

   public function setBar(bar $bar)
   {
       $this->bar = $bar;
       return $this;
   }

   public function doSomething()
   {
       $this->bar->doOtherThing();
   }
}

Les plus attentifs auront remarqué que la propriété bar 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 foo::doSomething() est appelée alors que bar est indéfinie, le développeur soit averti du problème.

Cela signifie également que l'utilisateur de la classe devra systématiquement penser à appeler la méthode foo::setBar() avec l'argument adéquat avant d'utiliser foo::doSomething(), 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.

Pour remédier à cela, il suffit de définir un constructeur jouant également le rôle de setter, mais avec une gestion de la dépendance suffisamment intelligente pour ne pas nécessiter le recours systématique à foo::setBar(), et pour cela, l'opérateur elvis, aka ?: et apparu avec PHP 5.3, est l'outil idéal.

<?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();
   }
}

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.

De cette façon, toute classe implémentant l'interface requise pourra être utilisée par votre méthode.

<?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();
   }
}

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 :

<?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)
     ;
   }
}

L'objectif est donc atteint, mais la méthode a cependant un inconvénient majeur.

En effet, la longueur de la signature du constructeur et le nombre de setter vont évoluer proportionnellement au nombre de dépendances nécessaires au fonctionnement de la classe.

De plus, tout ajout de dépendance nécessitera, outre l'ajout du setter correspondant, la modification de la signature du constructeur et donc potentiellement la mise à jour de l'ensemble des tests unitaires y faisant appel.

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 atoum, mon framework de test unitaire simple, moderne et intuitif pour PHP 5.3+.

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 atoum encore plus souple et plus facile à maintenir et à faire évoluer.

Grâce à cet injecteur, le code de notre classe foo deviendrait le suivant :

<?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();
   }
}

De cette façon, si à l'avenir j'ai besoin d'ajouter une dépendance, je ne dois modifier que la méthode foo::setDependenciesInjector() 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.

<?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)
     ;
   }
}

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.

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.