Page 1 sur 1

Conteneur d'Injection de Dépendances (DIC) avec surcharge magique

Posté : 15 févr. 2016, 20:28
par TheKitsuneWithATie
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é):

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; } }
Et voici le code du test unitaire:

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); } }
Utiliser le code est assez simple, pour mapper un variable, il suffit de faire ça:

Code : Tout sélectionner

$container->path_to_var = true;
Pour retourner une variable, mapper, il suffit de faire cela:

Code : Tout sélectionner

$retreived = $container->path_to_var;
Un filtre par défaut permet aussi de mapper des classes comme montré si-dessous:

Code : Tout sélectionner

$container->db_pdo = array('class' => '\PDO', 'args' => array('127.0.0.1', 'root', ''), 'shared' => false); $pdo = $container->db_pdo;
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:

Code : Tout sélectionner

$container->test1 = array('class' => '\ContainerChild'); $container->test2 = array('class' => '\ContainerChild', 'inject' => array('test1')); $test1 = $container->test2->test1;
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!

Re: Conteneur d'Injection de Dépendances (DIC) avec surcharge magique

Posté : 01 mars 2016, 20:09
par TheKitsuneWithATie
Voici la classe ContainerChild utilisé dans le teste unitaire que j'avais oublié de donner:

Code : Tout sélectionner

class ContainerChild extends \Bonzai\core\di\Container { private $_property; public function __construct($value = null) { parent::__construct(); $this->_property = $value; } public function getProperty() { return $this->_property; } public function setProperty($value) { $this->_property = $value; return $this; } }
Merci et bonne journée.

Re: Conteneur d'Injection de Dépendances (DIC) avec surcharge magique

Posté : 02 mars 2016, 18:37
par TheKitsuneWithATie
Je me permet de remonter ce sujet car j'aimerais vraiment avoir des avis sur mon code. Merci!