Site iconIndigo Tree Digital

Automatic Dependency Injection With PHP’s Reflection API

Reflection is the ability to introspect and reverse engineer functions, classes, methods and interfaces during runtime. This makes it possible to find specific information about your code, such as a classes internal properties, methods & even doc blocks for those methods.

One of my favourite uses for the Reflection API is to have all of my class dependencies automatically injected when possible. If you’re new to dependency injection, then don’t worry, it’s quite simple. This makes my code cleaner to look at, and far more maintainable as the codebase grows. We’ve recently updated our company starter theme/framework to do just this. I figure it’s something I can show you how to do to. The full code & example can be found at the bottom of this post (TL/DR).

Lets get to work. First you’ll want to setup a class to perform the automatic resolution. I’ve opted to call mine Resolver due to lack of a better name. This class should contain a method called resolve which takes one property:

class Resolver {

public function resolve($class)
{

}
}

We need to create an instance of our reflected class. For this we can use the ReflectionClass. It should look exactly like the following example:

$reflector = new \ReflectionClass($class);

The $reflector variable now contains various information about our class. Such as checking to see if our class is instantiable. For example, we need to ensure people don’t attempt to instantiate an interface or abstract class.

if( ! $reflector->isInstantiable())
{
throw new \Exception("[$class] is not instantiable");
}

If our class is instantiable, we can go ahead an check to see if it contains a __construct() method. If it doesn’t then we can just return an instance of the class as we have no dependencies to inject into it.

if(is_null($constructor))
{
return new $class;
}

If we do have a constructor, we will need to loop through all of the parameters and determine if each parameter is a resolvable class. If it is, we can run it back through our initial “resolve” method allowing us to indefinately nest our automatic class resoultion. If the parameter is not a class, and no default value was supplied we have nothing left to do but throw an Exception as we cannot resolve an unkown parameter.

$parameters = $constructor->getParameters();

$dependencies = array();

foreach($parameters as $parameter)
{
$dependency = $parameter->getClass();

if(is_null($dependency))
{
if($parameter->isDefaultValueAvailable())
{
$dependencies[] = $parameter->getDefaultValue();
}

throw new \Exception("Erm.. Cannot resolve the unkown!?");
}
else
{
$dependencies[] = $this->resolve($dependency->name);
}
}

If all goes well and no Exceptions were thrown, we just need to return a new instance of our resolved class. This should be the last line of our resolve method.

return $reflector->newInstanceArgs($dependencies);

And that’s everything. I’ve cleaned up the code a bit, split it into a few other methods and added doc blocks. You should be good to copy/paste this into your project.

class Resolver {

/**
 * Build an instance of the given class
* 
 * @param string $class
 * @return mixed
 *
 * @throws Exception
 */public function resolve($class)
{
$reflector = new \ReflectionClass($class);

if( ! $reflector->isInstantiable())
 {
 throw new \Exception("[$class] is not instantiable");
 }

 $constructor = $reflector->getConstructor();

 if(is_null($constructor))
 {
 return new $class;
 }

 $parameters = $constructor->getParameters();
 $dependencies = $this->getDependencies($parameters);

 return $reflector->newInstanceArgs($dependencies);
}

/**
 * Build up a list of dependencies for a given methods parameters
 *
 * @param array $parameters
 * @return array
 */public function getDependencies($parameters)
{
$dependencies = array();

foreach($parameters as $parameter)
{
$dependency = $parameter->getClass();

if(is_null($dependency))
{
$dependencies[] = $this->resolveNonClass($parameter);
}
else
{
$dependencies[] = $this->resolve($dependency->name);
}
}

return $dependencies;
}

/**
 * Determine what to do with a non-class value
 *
 * @param ReflectionParameter $parameter
 * @return mixed
 *
 * @throws Exception
 */public function resolveNonClass(ReflectionParameter $parameter)
{
if($parameter->isDefaultValueAvailable())
{
return $parameter->getDefaultValue();
}

throw new Exception("Erm.. Cannot resolve the unkown!?");
}
}

Using Our Resolve Class

Now that we’ve put together a class to automatically inject our dependencies, lets find out how to use it with a simple example. I’ve put together 5 classes which are in some way, reliant on one of the others. The Foo class expects to receive instances of both Bar and Baz, Baz expects an instance of Qux, and Qux expects an instance of Norf.

If you copy/paste the following dummy classes into your project, then attempt to resolve them using our new class you should find that everything automatically injects itself. How cool is that?

class Foo {

protected $bar;
protected $baz;

public function __construct(Bar $bar, Baz $baz)
{
$this->bar = $bar;
$this->baz = $baz;
}
}

class Bar {}

class Baz {

protected $qux;

public function __construct(Qux $qux)
{
$this->qux = $qux;
}
}

class Qux {

protected $norf;

public funciton __construct(Norf $norf)
{
$this->norf = $norf;
}
}

class Norf {}

If we weren’t using the Reflection API we would need to have some code similar to this example in order create a working instance of Foo.

$foo = new Foo(new Bar, new Baz(new Qux(new Norf)));

But, as we have the power of Reflection at our fingertips we can simply use:

$foo = (new Resolver)->resolve('Foo');

This will also work with name-spaced classes, you simply pass through the full class namespace & class name like so:

$foo = (new Resolver)->resolve('Acme\Repositories\Foo');

Besides the obvious readability improvement here, we are gaining a huge improvement in how maintainable our code has become. Lets assume you’ve used the class Foo throughout your project, what happens if Foo was modified to accept a third dependency? Well, without the Reflection API you would likely find yourself modifying your code in multiple locations (potentially hundreds, if it were a heavily relied upon class). You don’t have to do that anymore, Reflection API will do that for you!

If you wanted to take this a step further, we could introduce new functionality to our class, such as the ability to bind interfaces to concrete implementations of these classes, or bind a class name to an existing instance of the given class (singleton pattern). Perhaps another article is in order.

Exit mobile version