6
\$\begingroup\$

I've been working on a class to handle dependency injection across some code akin to a micro-framework. This is also my first real dive into any sort of wrapper for dependency injection. I decided to write a pseudo-intelligent dependency provider that recursively constructs dependencies. I also included the ability to share objects, which seems to put it somewhere between design patterns.

The most abstract feature is the recursion in make(), where it uses reflection classes to automatically inject dependencies into the dependency it's currently creating.

I've got a couple of questions:

  • My first question is whether or not this would be considered a "container" or a "registry" - Initially I named it a "provider" to stay close to literal meaning, but it seems most like a container to me. The class turned out to have components of a few design patterns (not exactly what i intended, but it's working), but I'm not looking to invent a mashup of design patterns...

  • Secondly (and more importantly), the shared object pool is dancing on a fine line with the global state, which I want to avoid. So far only a few classes get put into this pool, namely the input handler class, which handles superglobal interactions. Is there a more sophisticated way to ensure the same object is injected for certain classes, or is almost-global-state the only way to go?

Recursively injecting dependencies has been great so far, cutting out a lot of code by eliminating the need to even recognize those dependencies. However it has caused some roadblocks along the way, where constructor parameters are limited to namespaced objects. I am slightly worried that this sort of requirement could cause major problems down the road, so I'm all ears for words of advice.

Finally, the code:

<?php

namespace Framework\Injection;

use Framework\Exception\InjectionProviderException;


/**
 * Dependency provider class that will inject dependencies
 * by either pulling them from the shared object pool,
 * or by making a new object.
 *
 * @author      Kevin O'Rourke
 * @category    Framework
 * @package     Injection
 * @implements  Injector
 */
class Provider implements Injector {

    /**
     * The shared object pool
     *
     * @var array
     * @access protected
     */
    protected $_sharedObjects = array();

    /**
     * Container used to gather info on classes, such as
     * constructor parameters, and isInstantiable()
     *
     * @var ReflectionContainer
     * @access protected
     */
    protected $_reflectionContainer;


    /**
     * @access public
     * @param ReflectionContainer $reflectionContainer
     * @return void
     */
    public function __construct(ReflectionContainer $reflectionContainer)
    {
        $this->_reflectionContainer = $reflectionContainer;
    }

    /**
     * Called to inject a dependency - will either pull an object out of the shared
     * pool, or create a new object. Sub-dependencies are resolved and instantiated
     * automatically, provided they aren't a primitive type
     *
     * @access public
     * @param mixed $class
     * @return void
     * @throws Exception\InjectionProviderException
     */
    public function make($class)
    {
        //need to trim off the slashes, that's how they're stored in the shared array
        if(isset($this->_sharedObjects[trim($class, "\\")]))
        {
            return $this->_sharedObjects[trim($class, "\\")];
        }

        /*
         * This is an edge case where something is requesting a di provider
         * through a di provider. This should ONLY happen when make() is called
         * for a class that has a provide dependency in the constructor. The
         * provider will automatically create all the dependencies for that
         * constructor, including a provider.
         */
        if($class === "\\" . __CLASS__)
        {
            return $this;
        }

        try {
            $reflectionClass = $this->_reflectionContainer->get($class);
        }
        catch(\ReflectionException $e)
        {
            throw new InjectionProviderException("Provider failure: " . $e->getMessage());
        }

        if(false === $reflectionClass->isInstantiable())
        {
            throw new InjectionProviderException("Cannot instantiate " . ($reflectionClass->isInterface()? 'interface' : 'class') . " '$class'");
        }

        if( ! $reflectionClass->hasMethod('__construct'))
        {
            //$reflectionClass->newInstanceWithoutConstructor() requires php > 5.4, so just a plain instantiation
            return new $class();
        }

        //If we're here, then the class has a constructor, so lets resolve the dependencies
        $constructorParameters = $this->_getConstructorParameters($reflectionClass);

        $paramsFinal = array();
        foreach($constructorParameters as $paramClassName)
        {
            $paramsFinal[] = $this->make("\\" . $paramClassName);
        }

        return $reflectionClass->newInstanceArgs($paramsFinal);
    }

    /**
     * Uses the reflectionContainer to assess the constructor of the given [reflection] class
     *
     * @access protected
     * @param \ReflectionClass $reflectionClass
     * @return void
     */
    protected function _getConstructorParameters(\ReflectionClass $reflectionClass)
    {
        $params = $reflectionClass->getConstructor()->getParameters();

        $paramClasses = array();
        foreach($params as $param)
        {
            $class = $param->getClass();
            if(null === $class)
            {
                throw new InjectionProviderException("Invalid/unknown constructor parameter(s) in '{$reflectionClass->getName()}'");
            }
            $paramClasses[] = $class->name;
        }

        return $paramClasses;
    }

    /**
     * Shares an object with the shared object pool
     *
     * @access public
     * @param mixed $object
     * @return void
     * @throws \InvalidArgumentException
     */
    public function share($object)
    {
        if( ! is_object($object))
        {
            throw new \InvalidArgumentException("Invalid object passed to " . __CLASS__ . "/" . __METHOD__);
        }

        $this->_sharedObjects[get_class($object)] = $object;
    }

    /**
     * Adds an object to the shared pool
     *
     * @access public
     * @param mixed $class
     * @return void
     */
    public function isShared($class)
    {
        return array_key_exists($class, $this->_sharedObjects);
    }

    /**
     * Removes an object from the shared pool
     *
     * @access public
     * @param mixed $class
     * @return void
     */
    public function unShare($class)
    {
        if(array_key_exists($class, $this->_sharedObjects))
        {
            unset($this->_sharedObjects[$class]);
        }
    }

}

The reflection container class is very simple:

<?php

namespace Framework\Injection;

use ReflectionClass;

/**
 * A reflection container for gathering information about
 * arbitrary classes, namely isInstantiable() and their
 * constructor parameters, if any
 *
 * @author      Kevin O'Rourke
 * @category    Framework
 * @package     Injection
 */
class ReflectionContainer {

    /**
     * Holds the classes already processed, as to not re-make
     * their respective reflections
     *
     * @var array
     * @access protected
     */
    protected $_classes = array();


    /**
     * If a reflection class of that type hasn't yet been
     * processed, one is created and stored. Then that object
     * (or the pre-existing one) is returned
     *
     * @access public
     * @param string $class
     * @return \ReflectionClass
     */
    public function get($class)
    {
        if( ! isset($this->_classes[$class]))
        {
            $reflection = new ReflectionClass($class);
            $this->_classes[$class] = $reflection;
        }

        return $this->_classes[$class];
    }
}
\$\endgroup\$
  • \$\begingroup\$ TIL: DI Containers and Registry pattern. From my newly acquired understanding, this looks like both. Registry pattern by "making" classes, and a Container because there's a bunch (I'm still a bit iffy on the container bit). Here's a link I found for Containers, maybe it will help. I'm not sure about your implementation, mostly because I'm not sure what's going on here, but hopefully someone will answer, if not, I'll try again once I have a better understanding of the subject. Sorry I couldn't be of more help \$\endgroup\$ – mseancole Sep 12 '12 at 21:01
3
\$\begingroup\$
  1. It depends on how you use it. Your "sharedObjects" part would probably fit Registry pattern if you start using it as global object. On the other hand usually Registry does not create objects, but rather store them. Your implementation is a proper container as long as you don't start passing it around or start using it globally (that would make it "Service locator" pattern).
  2. I wouldn't call that global state. I assume you need that to make sure your object gets instantiated only once. The way I used to do it in my C++ implementation of container is store a map of factories in it. That factory could be any kind of a "callable" function (lambda expression, pointer to function, functor, etc..) and it was wrapped in a "ICreationStrategy" interface. If I needed to create object only once - I used SingletonCreationStrategy which worked exactly the same way as Singleton design pattern does, but without using static variable. This way you can store object instance in CreationStrategy instead of container directly (although I don't really see a problem with storing it the way you did).

I'll write an example when I'm gonna have more time.

\$\endgroup\$
  • \$\begingroup\$ I've now been using this in a few cases over the last couple of months, with some successes and failures; A few times I've had to stop myself from passing this object around freely - just as you mentioned - but overall it's been a great little class. I'm still a little worried about it being present in more places that it should, which gives some credence to your note about the service locator pattern. On the subject of the shared objects, the only class I've been sharing consistently is an input class (a wrapper for superglobals, with filtering capabilities), which is "global" by nature \$\endgroup\$ – orourkek Oct 25 '12 at 17:43
  • \$\begingroup\$ @orourkek If you feel the need to pass container at more than one place you are probably not using it the way it should be used. It only should be used at bootstrap logic. If you are passing it around because at some point in your code you have to create a class and you dont know its type beforehand you need to use a factory. Ideally you should write a factory and pass it to the class constructor that needs to create new instances of that class. All this gets wired only once - at bootstrap. It is okay to pass container to that factory in case you really need to, but you should still avoid it. \$\endgroup\$ – Anton Oct 25 '12 at 19:01

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.