Conteneur d'Injection de Dépendances (DIC) avec surcharge magique
Posté : 15 févr. 2016, 20:28
Bonjour tout le monde, c'est mon premier post sur le forum, je suis un "grand débutant" en PHP et je développe actuellement un micro-framework. Un des composant principal de mon framework est un Conteneur d'Injection de Dépendances, j'ai voulus en faire un facilement utilisable et ergonomique. Le conteneur fonctionne comme une collection ou toutes les valeurs sont gérées de la même manière. Il est possible d'y mapper des variables et de les retourner par la suite grâce aux méthode magiques __get et __set, le conteneur permet aussi de mapper des méthodes qui seront accessibles grâce à __call. Pour permettre des manipulations plus avancé et créer des automatismes, il est possible d'appliquer des "filtres" aux getters et aux setters dynamiques, par exemple, on peut créer un filtre pour les getters qui vérifie si la valeur est une chaine qui est le nom d'une classe valide, et si oui, le getter retournera une instance de celle-ci. Je souhaiterais avoir des retours et avis quant à la qualités et à la conception de ma classe, voilà donc le code (Il est fonctionnel et documenté):
Et voici le code du test unitaire:
Utiliser le code est assez simple, pour mapper un variable, il suffit de faire ça:
Pour retourner une variable, mapper, il suffit de faire cela:
Un filtre par défaut permet aussi de mapper des classes comme montré si-dessous:
Il est aussi possible de faire ce que j'ai appellé de "l'injection en chaine", c'est à dire injecté une dépendance dans une autre dépendance. C'est pratique par exemple si on a besoin d'un dispatcher ou d'une classe qui n'est ni statique ou ni un singleton et don la même instance doit être accessible par tout un système. Il est aussi très simple de définir les dépendances à injecter, a noter qu'il faut que la dépendance qui va ce voir injecter les autres dépendances doit être une classe fille de Container:
Que pensez vous de ce conteneur, est il bien conceptualisé, des problèmes majeurs sont ils notables? Que puis-je améliorer ou dois-je refaire certaines choses? Tous les opinions et avis son le bienvenues, merci d'avance!
Code : Tout sélectionner
<?php
// DIC Class WIP by TheKitsuneWithATie
class Container
{
/**
* @var array Filters.
*/
private $_filters = array('set' => array(), 'get' => array());
/**
* @var array Mapped variables.
*/
private $_map = array();
public function __construct()
{
// Adding default classes get filter
$this->addGetFilter('*', function($container, &$value, &$output) {
if (is_array($value) && isset($value['class'])) {
// If an instance is stored, the return it
if (isset($value['instance'])) {
$output = $value['instance'];
return;
}
// Fixing parameters
$args = isset($value['args']) ? $value['args'] : array();
$shared = isset($value['shared']) ? $value['shared'] : true;
$inject = isset($value['inject']) ? $value['inject'] : array();
$reflection = new \ReflectionClass($value['class']);
$instance = $reflection->newInstanceArgs($args);
// Storing the instance if the class is shared
if ($shared)
$value['instance'] = $instance;
if (is_subclass_of($instance, __CLASS__)) {
foreach ($inject as $dependency)
$instance->{$dependency} = $this->{$dependency};
}
$output = $instance;
}
});
}
public function __set($name, $value)
{
// Calling filters
foreach ($this->_filters['set'] as $filter) {
if (preg_match($filter['pattern'], $name)) {
$filter['filter']($this, $value);
}
}
$index = &$this->_goto($name, true);
$index = $value;
}
public function __get($name)
{
$index = &$this->_goto($name);
$return = $index;
// The isset function should be used beforehand to avoid this exception
if ($index === null)
throw new \Exception("Cannot get unset variable '$name'.");
// Calling filters
foreach ($this->_filters['get'] as $filter) {
if (preg_match($filter['pattern'], $name))
$filter['filter']($this, $index, $return);
}
return $return;
}
public function __call($method, $args)
{
$index = &$this->_goto($method);
if ($index === null)
throw new \Exception("Cannot call unset function '$method'.");
if (!is_callable($index))
throw new \Exception("Cannot call non-callable '$method'.");
return call_user_func_array($index, $args);
}
public function __isset($name)
{
return ($this->_goto($name) !== null);
}
public function __unset($name)
{
$index = &$this->_goto($name);
$index = null;
}
/**
* Adds a filter called when setting a variable.
*
* @param string $pattern Regex pattern of the variables to filter
* @param callable $filter Filter
*
* @return $this
*/
public function addSetFilter($pattern, $filter)
{
return $this->_addFilter('set', $pattern, $filter);
}
/**
* Adds a filter called when getting a variable.
*
* @param string $pattern Regex pattern of the variables to filter
* @param callable $filter Filter
*
* @return $this
*/
public function addGetFilter($pattern, $filter)
{
return $this->_addFilter('get', $pattern, $filter);
}
/**
* Adds a filter called when getting or setting a variable.
*
* @param string $type Either 'get' or 'set'
* @param string $pattern Regex pattern of the variables to filter
* @param callable $filter Filter
*
* @return $this
*/
private function _addFilter($type, $pattern, $filter)
{
$pattern = '#' . str_replace('*', '.*', $pattern) . '#';
$this->_filters[$type][] = array(
'pattern' => $pattern,
'filter' => $filter
);
return $this;
}
/**
* Returns a reference of mapped array index according to the path.
*
* @param string $path Path to go to
* @param boolean $fix Will it create missing indexes from the path
*
* @return mixed|null Reference to the index or null if nothing matches the path
*/
private function &_goto($path, $fix = false)
{
$path = explode('_', $path);
$pointer = &$this->_map; // Initializing pointer
$return = $pointer; // Return value
// Going throught the path
foreach ($path as $index) {
if (!isset($pointer[$index])) {
// Create missing indexes if the path needs to be fixed
if ($fix) {
$pointer[$index] = null;
}
// Stop if the path doesn't continue
else {
$return = null;
break;
}
}
// Updating the pointer
$pointer = &$pointer[$index];
}
// Updating return value
if ($return !== null)
$return = &$pointer;
return $return;
}
}Code : Tout sélectionner
<?php
require 'classes/ContainerChild.php';
class ContainerTest extends PHPUnit_Framework_TestCase
{
private $container;
public function setUp()
{
$this->container = new \Bonzai\core\di\Container;
}
/**
* Setting and getting a variable.
*/
public function testVariable()
{
$container = $this->container;
$container->testVar = true;
$retreived = $container->testVar;
$this->assertTrue($retreived);
}
/**
* Checking if a variable is set.
*/
public function testIssetVariable()
{
$container = $this->container;
$container->testIssetVar = true;
$isset = isset($container->testIssetVar);
$this->assertTrue($isset);
}
/**
* Unsetting a variable.
*/
public function testUnsetVariable()
{
$container = $this->container;
$container->testUnsetVar = true;
unset($container->testUnsetVar);
$isset = isset($container->testUnsetVar);
$this->assertFalse($isset);
}
/**
* Mapping a function.
*/
public function testMapFunction()
{
$container = $this->container;
$container->testFunction = function($int) { return $int * $int; };
$square = $container->testFunction(3);
$this->assertEquals(9, $square);
}
/**
* Mapping a class.
*/
public function testMapClass()
{
$container = $this->container;
$container->testMap_class = '\ContainerChild';
$instance = $container->testMap;
$this->assertInstanceOf('ContainerChild', $instance);
}
/**
* Mapping a non shared class.
*/
public function testMapClassNonShared()
{
$container = $this->container;
$container->testMapNonShared_class = '\ContainerChild';
$container->testMapNonShared_shared = false;
$first = $container->testMapNonShared;
$second = $container->testMapNonShared;
$this->assertNotSame($first, $second);
}
/**
* Mapping a class with "chain injection".
*/
public function testMapClassChainInject()
{
$container = $this->container;
$container->testMapInject_class = '\ContainerChild';
$container->testMapInjectSecond_class = '\ContainerChild';
$container->testMapInjectSecond_inject = array('testMapInject');
$first = $container->testMapInject;
$second = $container->testMapInjectSecond->testMapInject;
$this->assertSame($first, $second);
}
/**
* Adding a set filter.
*/
public function testAddSetFilter()
{
$container = $this->container;
$container->addSetFilter('*', function($c, &$v) {
$v = true;
});
$container->testVarSetFilter = false;
$retreived = $container->testVarSetFilter;
$this->assertTrue($retreived);
}
/**
* Adding a get filter.
*/
public function testAddGetFilter()
{
$container = $this->container;
$container->addGetFilter('*', function($c, &$v, &$o) {
$o = false;
});
$container->testVarGetFilter = true;
$retreived = $container->testVarGetFilter;
$this->assertFalse($retreived);
}
}Code : Tout sélectionner
$container->path_to_var = true;Code : Tout sélectionner
$retreived = $container->path_to_var;Code : Tout sélectionner
$container->db_pdo = array('class' => '\PDO',
'args' => array('127.0.0.1', 'root', ''),
'shared' => false);
$pdo = $container->db_pdo;Code : Tout sélectionner
$container->test1 = array('class' => '\ContainerChild');
$container->test2 = array('class' => '\ContainerChild',
'inject' => array('test1'));
$test1 = $container->test2->test1;