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];
}
}