Aujourd'hui nous allons parler d'un pattern assez particulier : Le conteneur d'injecteur de dépendance. Le but de ce pattern et d'être capable de résoudre les dépendances d'un objet simplement.
Le problème
Afin d'avoir un code bien organisé et testable, on utilise l'injection de dépendance mais cette méthodologie peut parfois rendre les objets difficiles à instancier.
$d = new D(new C(new B(new A())));
// L'objet D à besoin de C pour fonctionner mais C à besoin de B et B de A...
Lorsque notre code va grandir ce type de cas va se produire assez souvent rendant les objets beaucoup trop difficile à utiliser.
La solution : le conteneur
La solution pour remédier à ce problème est l'utilisation d'un conteneur. Le principe est d'expliquer à PHP comment instancier une class quand on en a besoin. Pour cela, on peut profiter des Closures.
// J'explique à mon conteneur comment résoudre B
$container = new DIC();
// J'explique à mon container comment obtenir une instance de A
$container->set('A', function($container){
return new A();
});
// J'explique à mon container comment obtenir une instance de B
$container->set('B', function($container){
// Je peux utiliser le container pour résoudre A
return new B($container->get('A'));
});
// Maintenant si je veux une instance de B
$container->get('B');
Pour que ce code fonctionne il suffit de créer un singleton qui va sauvegarder nos différentes instances.
class DIC{
private $registry = [];
private $instances= [];
public function set($key, Callable $resolver){
$this->registry[$key] = $resolver;
}
public function get($key){
if(!isset($this->instances[$key])){
if(isset($this->registry[$key])){
$this->instances[$key] = $this->registry[$key]($this);
} else {
throw new Exception($key . " n'est pas dans mon conteneur :(");
}
}
return $this->instances[$key];
}
}
Reflection & Automatisation
Le problème de ce système c'est que l'on doit penser à enregistrer les manières d'instancier nos objets dans notre conteneur alors que dans la pluspart des cas la construction peut être résolue de manière automatique. On peut donc améliorer notre injecteur de dépendance pour résoudre de manière automatique nos objets.
class A{
}
$container->get('A');
Ici par exemple il suffit de vérifier si A est une classe instanciable et alors on peut résoudre le problème en l'instanciant de manière automatique. De la même manière.
class B{
public function __construct(A $a){
$this->a = $a;
}
}
$container->get('B');
Ce cas est un petit peu plus complexe car on doit analyser le constructeur de notre objet pour déterminer les dépendances et essayer des les résoudre automatiquement. Pour cela on va s'aider des réflection.
/**
* Instancie la classe à partir de son nom
* @param $key
* @return object
* @throws Exception
*/
private function resolve($key){
$reflected_class = new ReflectionClass($key); // On récupère la class depuis la chaine de caractère
if($reflected_class->isInstantiable()){ // On a bien une class instanciable (et pas une interface)
$constructor = $reflected_class->getConstructor(); // On récupère le constructeur
if($constructor){
// Si le constructeur existe alors on va analyser ses paramètres
$parameters = $constructor->getParameters();
$constructor_parameters = [];
foreach($parameters as $parameter){
if( $parameter->getClass() ){
$constructor_parameters[] = $this->get($parameter->getClass()->getName());
} else {
$constructor_parameters[] = $parameter->getDefaultValue();
}
}
return $reflected_class->newInstanceArgs($constructor_parameters);
} else {
// sinon on peut directement instancier notre objet à vide.
return $reflected_class->newInstance();
}
} else {
throw new Exception($key . " is not an instanciable Class");
}
}
Conclusion
Le but ici est de vous montrer que l'on peut très rapidement se construire un conteneur d'injecteur de dépendance, et lui donner en plus la capacité de résoudre les choses automatiquement gràce au principe de réflexivité.
Si vous souhaitez utiliser un conteneur pour votre application il existe des librairies qui propose des conteneurs clefs en main.
- PHP-DI intégrable dans SF2 et Zend
- Pimple, créé par SensioLabs
- DICE