A few weeks ago, I asked for another DIC review, but didn't get any response (but only for one about my error handling). So I tried to rework my DIC without any outside insight on it, and the first thing that came to my mind is that my DIC wasn't really ergonomic. So I tried making one with dynamic setters/getters. Then I had another idea to had filters that would allow for automated behaviors when getting/setting a variable, say, for example, if I get a string that is a class, then return an instance of that class. So, as my class is more of an "experiment", I would like to have some opinions on the quality of my DIC.
<?php
// WIP DIC Class 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 __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;
}
}
And here is the PHPUnit test:
<?php
class ContainerChild extends \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 \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 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);
}
}
It is rather easy to use. To set a variable, you do:
$container->path_to_var = true;
And to get a variable, you do:
$retreived = $container->path_to_var;
A default filter allows for classes that can be shared (stored) and have arguments passed to its constructor:
$container->db_pdo = array('class' => '\PDO',
'args' => array('127.0.0.1', 'root', ''),
'shared' => false);
$pdo = $container->db_pdo;
You can also do what I called "chained injection", which means that you can inject a dependency inside another dependency automatically:
$container->test1 = array('class' => '\ContainerChild');
$container->test2 = array('class' => '\ContainerChild',
'inject' => array('test1'));
$test1 = $container->test2->test1;
So, what do you think of it? Is it a good class? Is there anything I should change, add or remove?