Dependency Injection Container Part 3

Time for the final modifications to my dependency injection container implementation which I’ve shown in my last two blog posts. This time I am making some parts configurable, adding the possibility to configure injected objects and… well, see yourself.

I’ve just tested what would happen if I’d inject Twig (my favorite template engine) using my DI Container. Twig_Environment uses the following constructor public function __construct(Twig_LoaderInterface $loader = null, $options = array()) and I would like to inject Twig_Loader_Filesystem into it. For that I do need to tell my container that it should inject Twig_Loader_Filesystem if Twig_LoaderInterface would need to be injected. Due to that I also found a variant in which my dependency injection would fail due to the regular expression:

Parameter #0 [ <optional> Twig_LoaderInterface or NULL $loader = NULL ]

The part or NULL and = NULL is currently not possible using my regular expression. Hence modified the regular expression to:

// @todo optimize me
$pattern = '/^Parameter #\d+ \[ <\w+>'
            . ' (?P<class>[a-zA-Z_\x7f-\xff][a-zA-Z0-9\\\\_\x7f-\xff]*)'
            . ' (OR .*)?'
            . ' \$(?P<var>[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)'
            . ' (.*)? \]$/is';

additional to the following additions:

    private $mappings = array();
 
    public function addMap($key, $value)
    {
        $this->mappings[$key] = $value;
    }

and in parseReflectionParameters

// @todo deduplicate
if (isset($this->mappings[$matches['class']])) {
    if (isset($this->instances[$matches['class']])) {
        $parameters[] = $this->instances[$this->mappings[$matches['class']]];
    } else {
        $parameters[] = $this->register($this->mappings[$matches['class']]);
    }
} else {
    if (isset($this->instances[$matches['class']])) {
        $parameters[] = $this->instances[$matches['class']];
    } else {
        $parameters[] = $this->register($matches['class']);
    }
}

example

$inj = new \Lyra\helpers\LyraInjector();
// whenever a constructor refers to Twig_LoaderInterface it would get Twig_Loader_Filesystem injected
$inj->addMap('Twig_LoaderInterface', 'Twig_Loader_Filesystem');
$twig = $inj->register('Twig_Environment');

But I guess this could really break things in reality. It would be better if such maps are defined against a given class. Like, whenever Twig_Environment refers to Twig_LoaderInterface in a constructor it gets Twig_Loader_Filesystem (or whatever you choose) injected, instead of making this globally as above. Like so:

$inj->addMap('Twig_Environment', array('Twig_LoaderInterface' => 'Twig_Loader_Filesystem'));

However, then it is questionable if I should use my DI-Container for external projects like Twig instead of just my own application/classes/objects. Probably it does make more sense to just register the ready twig object into the di container without it checking dependencies of twig, since twig should handle its dependencies quite fine on its own. For this specific case something like

$inj->addInstance('Twig_Environment', new Twig_Environment(new Twig_Loader_Filesystem('templates/')));

would do the job. I’ve implemented both by adding a method called addInstance:

    /**
     * used to register already set up objects into the di-container
     * which is useful if those objects take care of their dependencies
     * on their own
     *
     * @param $identifier
     * @param $object
     */
    public function addInstance($identifier, $object)
    {
        $this->instances[$identifier] = $object;
    }

and modifying my mapping-code to

    public function addMap($key, array $value)
    {
        $this->mappings[$key] = $value;
    }

and the part in parseReflectionParameters to

$className = $reflectionParameter->getDeclaringClass()->name;
// @todo deduplicate
if ((isset($this->mappings[$className])) && isset($this->mappings[$className][$matches['class']])) {
    if (isset($this->instances[$matches['class']])) {
        $parameters[] = $this->instances[$this->mappings[$className][$matches['class']]];
    } else {
        $parameters[] = $this->register($this->mappings[$className][$matches['class']]);
    }
} else {
    if (isset($this->instances[$matches['class']])) {
        $parameters[] = $this->instances[$matches['class']];
    } else {
        $parameters[] = $this->register($matches['class']);
    }
}

Now in my previous blog post I wrote it might be useful to disable some specific injection type in case you’re only using the setter injection using an inject method there’s no need to scan the interfaces. Hence added:

    private $disableConstructorInjection = false;
    private $disableSetterInjection = false;
    private $disableInterfaceInjection = false;
 
    public function disableConstructorInjection($disable = true)
    {
        $this->disableConstructorInjection = $disable;
    }
    public function disableSetterInjection($disable = true)
    {
        $this->disableSetterInjection = $disable;
    }
    public function disableInterfaceInjection($disable = true)
    {
        $this->disableInterfaceInjection = $disable;
    }

and added some if’s to register()

    public function register($class)
    {
        if (!isset($this->reflected[$class])) {
            $this->reflected[$class] = new \ReflectionClass($class);
        }
 
        $reflectionClass = $this->reflected[$class];
        if ($reflectionClass instanceof \ReflectionClass && $reflectionClass->isInstantiable()) {
            if (!isset($this->instances[$reflectionClass->name])) {
                if (!$this->disableConstructorInjection)
                    $this->constructorInjection($reflectionClass);
                if (!$this->disableSetterInjection)
                    $this->setterInjection($reflectionClass);
                if (!$this->disableInterfaceInjection)
                    $this->interfaceInjection($reflectionClass);
            }
        }
 
        return $this->instances[$reflectionClass->name];
    }

So, in case you want to speed up processing and you do not need interface injection or setter injection just call ->disableSetterInjection();

Now in the current implementation there is no way to set / configure parameters. Let us take the following for example:

$twig = new Twig_Environment(new Twig_Loader_Filesystem('templates/'));

Currently there is no way to tell my dependency injection container that it has to inject the ‚templates/‘ string to Twig_Loader_Filesystem or set other parameters. For that purpose I’d just add a method similar to the mappings $inj->addParameters('ClassName', array('key' => 'value'));. Currently after some trying and bla.. the parseParameters Method looks like this (don’t laugh – it’s work in progress)

    /**
     * @param $reflectionParameters
     * @return array
     */
    private function parseReflectionParameters($reflectionParameters)
    {
        // @todo optimize me
        $pattern = '/^Parameter #\d+ \[ <\w+>'
            . ' (?P<class>[a-zA-Z_\x7f-\xff][a-zA-Z0-9\\\\_\x7f-\xff]*)'
            . ' (OR .*)?'
            . ' \$(?P<var>[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)'
            . ' (.*) \]$/is';
        $pattern2 = '/^Parameter #\d+ \[ <\w+>'
            . ' \$(?P<var>[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)'
            . ' (.*) \]$/is';
        $parameters = array();
 
        foreach ($reflectionParameters as $reflectionParameter) {
            if ($reflectionParameter instanceof \ReflectionParameter) {
                $className = $reflectionParameter->getDeclaringClass()->name;
                preg_match($pattern, $reflectionParameter, $matches);
                if (isset($matches['class'])) {
                    // @todo deduplicate
                    if ((isset($this->mappings[$className])) && isset($this->mappings[$className][$matches['class']])) {
                        if (isset($this->instances[$matches['class']])) {
                            $parameters[] = $this->instances[$this->mappings[$className][$matches['class']]];
                        } else {
                            $parameters[] = $this->register($this->mappings[$className][$matches['class']]);
                        }
                    } else {
                        if (isset($this->instances[$matches['class']])) {
                            $parameters[] = $this->instances[$matches['class']];
                        } else {
                            $parameters[] = $this->register($matches['class']);
                        }
                    }
                } else {
                    preg_match($pattern2, $reflectionParameter, $matches);
                    if (isset($this->parameters[$className][$matches['var']])) {
                        $parameters[] = $this->parameters[$className][$matches['var']];
                    }
                }
            }
        }
 
        return $parameters;
    }

The current call is:

$inj = new \Lyra\helpers\LyraInjector();
$inj->addMap('Twig_Environment', array('Twig_LoaderInterface' => 'Twig_Loader_Filesystem'));
$inj->addParameters('Twig_Loader_Filesystem', array('paths' => 'templates/'));
$twig = $inj->register('Twig_Environment');

Which first says that it should inject Twig_Loader_Filesystem if Twig_LoaderInterface appears when loading Twig_Environment and then tells it to hand ‚templates/‘ as parameter for $paths if Twig_Loader_Filesystem is initialized. And it seems to work:

  ["Twig_Environment"]=>
    object(Twig_Environment)#7 (30) {
      ["charset":protected]=>
      string(5) "UTF-8"
      ["loader":protected]=>
      object(Twig_Loader_Filesystem)#10 (3) {
        ["paths":protected]=>
        array(1) {
          ["__main__"]=>
          array(1) {
            [0]=>
            string(9) "templates"

Modified the regular expression to and the code to:

        $pattern = '/Parameter #\d+ \[ <(?:[A-Za-z]+)>(?: (?P<class>[a-zA-Z_\x7f-\xff][a-zA-Z0-9\\\\_\x7f-\xff]*)'
            . ' (?:or [A-Za-z]+))? \$(?P<variable>[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)(?: \= [A-Za-z]+)? \]/';

grab the gist here in case you’re interested in the final implementation. Still think there’s room for optimization but I did reach my initial goal as in writing a container which is not a hybrid service-locator and dependency-injection-container. Furthermore I got a possible implementation for interface injection.

1 Comment

  • Alex Takev

    13. Oktober 2016 at 8:01 Antworten

    Hello man,
    Thank you for this explanation. Was epic. I was browsing the Internet for explanation what is DI about, and your blog post was the thing I will try to experiment with it. Of course, if I solve something that you haven’t already, I’ll be happy to contact you and we’ll discuss it.
    Thank you for clearing the view between the Hybrid implementations and the real DI Container.
    I fell a lot smarter after this.
    Keep it up.

Post a Comment