IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
Navigation

Inscrivez-vous gratuitement
pour pouvoir participer, suivre les réponses en temps réel, voter pour les messages, poser vos propres questions et recevoir la newsletter

Contribuez / Téléchargez Sources et Outils PHP Discussion :

Demande d'avis: Conteneur d'injection de dépendances avec surcharge dynamique


Sujet :

Contribuez / Téléchargez Sources et Outils PHP

  1. #1
    Candidat au Club
    Homme Profil pro
    Développeur Indépendant
    Inscrit en
    Mars 2016
    Messages
    3
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Puy de Dôme (Auvergne)

    Informations professionnelles :
    Activité : Développeur Indépendant

    Informations forums :
    Inscription : Mars 2016
    Messages : 3
    Points : 4
    Points
    4
    Par défaut Demande d'avis: Conteneur d'injection de dépendances avec surcharge dynamique
    Bonjour tout le monde, je travaille actuellement seul sur 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, je poste donc le code si-dessous avec son test unitaire, suivi de quelques exemples d'utilisations et explications.

    Code du conteneur
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    <?php
     
    // WIP DIC 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;
    	}
    }

    Test unitaire
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    <?php
     
    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;
    	}
    }
     
    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);
    	}
    }

    Utilisation est exemples

    Utiliser le code est assez simple, pour mapper un variable, il suffit de faire ça:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
     
    $container->path_to_var = true;

    Pour retourner une variable mappée, il suffit de faire cela:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
     
    $retreived = $container->path_to_var;

    Un filtre par défaut permet aussi de mapper des classes comme montré si-dessous:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
     
    $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 : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
     
    $container->test1 = array('class' => '\ContainerChild');
    $container->test2 = array('class' => '\ContainerChild',
        'inject' => array('test1'));
     
    $test1 = $container->test2->test1;


    Que pensez vous de ce Conteneur d'Injection de Dépendances, y a t'il des choses à modifier? Est il ergonomique et compréhensible? Donnez moi vos avis s'il vous plait, j'aimerais avoir des retours quant à la qualitée de cette classe. Merci d'avance!

    P.S: Veuillez m'excuser si je me suis trompé de section pour poster ce sujet, mais les autres ne semblaient pas êtres adéquates pour ce sujet.

  2. #2
    Membre émérite

    Profil pro
    Inscrit en
    Mai 2008
    Messages
    1 576
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Mai 2008
    Messages : 1 576
    Points : 2 440
    Points
    2 440
    Par défaut
    Bonsoir!

    Félicitations pour ce super boulot, et surtout bravo pour avoir écrit un conteneur configurable en pur PHP, sans XML, yml ou annotations.

    Deux remarques/notes:
    - Peux-tu dire ce que ce conteneur a de plus ou de différent par rapport aux conteneurs comparables comme Pimple, PHP-DI ou Aura.Di?

    - Est-ce que tu peux envisager de le rendre compatible avec container-interop? Le but est d'adoper un ensemble d'interface communes aux conteneurs afin de pouvoir remplacer facilement le conteneur (de la même manière qu'on peut aujourd'hui remplacer les caches avec PSR-6 et une représentation unifiée des messages HTTP grâce à PSR-7, container-interop serait potentiellement PSR-11.

  3. #3
    Candidat au Club
    Homme Profil pro
    Développeur Indépendant
    Inscrit en
    Mars 2016
    Messages
    3
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Puy de Dôme (Auvergne)

    Informations professionnelles :
    Activité : Développeur Indépendant

    Informations forums :
    Inscription : Mars 2016
    Messages : 3
    Points : 4
    Points
    4
    Par défaut
    Merci beaucoup pour l'avis positif! Pour les différences avec les autres conteneur bien connus, je pourrais dire que:
    - Il est en pur PHP, pas de parsage de la documentation ou de XML, seul l’implémentation du conteneur est requise;
    - Le système de filtres offre des possibilités intéressantes et est facile à prendre en main;
    - Il permet de faire de "l'injection en chaine" automatisée (chose que je n'ai pas vue chez les autres conteneurs);
    - Son utilisation est simple avec des getters et setters (très proche de Pimple pour le coup) et se veut intuitive.

    Pour ce qui est de la compatibilité avec container-interop, c'est bien sûr envisagé. Mais je n'ai actuellement jamais créé de conteneur avec ce système et je vais devoir lire toute la documentation, ce qui risque de prendre du temps.

    A noté qu'il s’agit toujours d'un work in progress et que certaines fonctionnalités et méthodes se verront surement modifiées (par exemple, l'utilisation de regex pour les filtres sera surement remplacé par un autre système).


    Et n'hésitez pas à faire d'autres remarques ou à poser d'autres questions, je suis content d'avoirs des retours. Merci beaucoup!

  4. #4
    Membre émérite

    Profil pro
    Inscrit en
    Mai 2008
    Messages
    1 576
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Mai 2008
    Messages : 1 576
    Points : 2 440
    Points
    2 440
    Par défaut
    J'ai surtout regardé les tests et juste parcouru la classe elle-même, donc pardonne-moi si je me trompe, mais il me semble que tu lorsque tu fais par exemple:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    $container->testMapNonShared_class = '\ContainerChild';
    $container->testMapNonShared_shared = false;
    Tu définis des paramètres (testMapNonShared appartient à la classe ContainerChild et n'est pas shared) en utilisant le nom de la variable?

    Autre point, dans tes tests tu donnes le nom de la classe en tant que string. Le problème de string est que ce n'est pas identifiable/validable par une analyse statique de l'IDE. De plus, en utilisant un string, tu dois indiquer un FQN du genre \Mon\Namespace\Et\Ma\Classe, ce qui est long (les développeurs sont fainéants). Tu peux utiliser à la place la classe name resolution (PHP 5.5 et +) et faire ça par exemple:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
     
    use \Mon\Namespace\Et\Ma\Classe as MaClasse;
     
    //add something to the container
    $container->maClasse_class = MaClasse::class;
    Résultat: c'est plus court et validé par l'IDE.

  5. #5
    Candidat au Club
    Homme Profil pro
    Développeur Indépendant
    Inscrit en
    Mars 2016
    Messages
    3
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Puy de Dôme (Auvergne)

    Informations professionnelles :
    Activité : Développeur Indépendant

    Informations forums :
    Inscription : Mars 2016
    Messages : 3
    Points : 4
    Points
    4
    Par défaut
    Pour ta première question sur ce code:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    $container->testMapNonShared_class = '\ContainerChild';
    $container->testMapNonShared_shared = false;
    Quand tu fait cela, tu map deux variables dans le conteneur, testMapNonShared_class et testMapNonShared_shared. Vus que le conteneur stock le tout dans un "arbre", le underscore "_" est un séparateur (comme un slash "\" dans un chemin de fichier), ce qui veut dire que la variable testMapNonShared_class est mappé dans un array à l'index testMapNonShared et que la valeur est à l'index class de cet array.

    L’intérêt de faire cela viens du système de filtres que j'explique dans mon premier post. Grâce à un filtre par défaut sur les getters, si je retourne juste testMapNonShared, le filtre va vérifié si l'index class de l'array retourné est bien un nom de classe valide, si oui, le filtre va retourner une instance de la classe au lieu de l'array brut. Si l'index shared est sur true, alors ce sera toujours la même instance qui sera retourné, sinon, une nouvelle instance sera retournée à chaque fois.


    Pour ce qui est des nom de classes en string, je note ta remarque, ça ne change rien du côté des fonctionnalités mais c'est effectivement plus ergonomique. Mon conteneur peut tolérer aussi bien le nom de la classe dans un string que le nom de la classe retourné par la résolution de nom. En tout cas, merci beaucoup!

Discussions similaires

  1. Réponses: 14
    Dernier message: 09/09/2011, 19h15
  2. [Framework] Injection d'une dépendance initialisée avec passage dynamique d'arguments
    Par Samouraï virtuel dans le forum Spring
    Réponses: 1
    Dernier message: 24/12/2009, 12h48
  3. Injection de dépendance avec MVVM => Boucle infinie
    Par Stunt_las dans le forum Silverlight
    Réponses: 21
    Dernier message: 02/11/2009, 14h48

Partager

Partager
  • Envoyer la discussion sur Viadeo
  • Envoyer la discussion sur Twitter
  • Envoyer la discussion sur Google
  • Envoyer la discussion sur Facebook
  • Envoyer la discussion sur Digg
  • Envoyer la discussion sur Delicious
  • Envoyer la discussion sur MySpace
  • Envoyer la discussion sur Yahoo