0
\$\begingroup\$

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?

\$\endgroup\$

2 Answers 2

1
\$\begingroup\$

For me the code seems too complicated, I don't have specific advice, but I advise you to take a look to these three differents DI implementations which have really good API:

\$\endgroup\$
3
  • \$\begingroup\$ Thank you for your answer. Is the code really that complicated? I think that the only thing making it look complex is the usage of references. As for the DIC you linked, I already knew Pimple which is great for its simplicity of use, which inspired me. As for for Laravel's, I didn't really look into it, but I sure will. And for Orno\Di, I didn't even know it, but it is rather interesing, I noticed the usage of a "Definition" class, should I do the same for my DIC or should I leave it the way I did? Thank you again for you answer. \$\endgroup\$ Commented Jan 30, 2016 at 22:34
  • \$\begingroup\$ I think you should use method like get and set (or an array access) instead of trying to work with "magic" attributes \$\endgroup\$ Commented Jan 30, 2016 at 23:22
  • \$\begingroup\$ I will think about doing so. Is there anything more you think that I should do? Thank you. \$\endgroup\$ Commented Jan 31, 2016 at 20:57
-1
\$\begingroup\$

By extending other classes from your container you seriously limit yourself when inheritance is in question. Because PHP has single inheritance, this cannot be done later without having the parent class extend the container.

Your DI container implementation should look at the argument-list of methods/functions and determine dependencies through that. You can use the reflection and with PHP 7 where scalar type-hints are a thing, you can do this easier.

When you determine dependencies through arguments you do not impact the class and can decide not to use DI later without problems. You can also easier change DI implementation as you do not have to rewrite classes. A good example of a standalone DI container is this one.

Hope this can help you in the right direction.

\$\endgroup\$
3
  • \$\begingroup\$ Thank you for your answer, but I actually don't like DIC that parses the classes as they force you to use special method instead of the "new" keyword, maybe I'm in the wrong, But I feel like the user code should have to use dedicated method to instantiate classes. Moreover, WampServer 3 still is under PHP 5.6 (my attemps to upgrade failed). As for the fact my classes extends my container, you are right to point out this problem, but my class can also be used without extending just by storing the container as a property. Also, do you truly think that DI trought methods is bad? Thank you. \$\endgroup\$ Commented Feb 1, 2016 at 0:43
  • \$\begingroup\$ By having to store the DI container instance inside a property still forces you to modify your class with the direct purpose of how dependencies are loaded. If the dependencies are defined in the argument list and "magically" appear when a new instance is created, the class becomes more resilient to changes and more concise. It is ultimately your preferences concerning the use of methods for object creation, but you should not overuse the DI container, so the method calls should remain low. Hope this clarifies things a bit. \$\endgroup\$ Commented Feb 1, 2016 at 20:38
  • \$\begingroup\$ Thank you for the answer. So, if I understood correctly, having the container in a property isn't a valid option as it creates a "tight" dependency between the class and the container. Then if I still use setter injection, without storing the container, then what options are there left if inheritance is also a bad choice? Also, is it bad if I use setter injection in the constructor to define "default dependecies" and then let the user code change them throught setters? Thank you again. \$\endgroup\$ Commented Feb 1, 2016 at 21:00

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.