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