Logan Bailey

Adventures In Web Development

Blog, About, GitHub, and LinkedIn

Auto dependency resolution for an IOC (Inversion of Control) may at first glance seem magical, but when you really get down to it, is pretty simple to implement. To prove this point, lets build our own. First, setup two classes that will provide test objects we plan on resolving.

<?php
// file: acme.php
// Nested namespaces for code brevity.
namespace Portico\Acme {
    class Controller
    {
        protected $request;

        public function __construct(Request $request)
        {
            $this->request = $request;
        }
    }

    class Request
    {
        public function __construct()
        {
            // I don't have any dependencies
        }
    }
} 

This is a pretty straight forward setup, other than the single namespace. We have Controller and it requires Request.

Where to start?

We'll begin by automatically determining the type of each dependency. To do this, we'll create a class called ParameterResolver, which will contain the necessary logic. We'll also create a script file, test.php, so that we can check out our progress.

<?php
// file: src/ParameterResolver.php
namespace Portico\Ioc;

class ParameterResolver
{
    public function resolve($className)
    {
        return "We're trying to find the parameters of $className\n";
    }
}
<?php
// file: test.php
use Portico\Ioc\ParameterResolver;

require_once 'acme.php';
require_once 'src/ParameterResolver.php';
$resolver = new ParameterResolver();
var_dump($resolver->resolve('Portico\Acme\Controller')); 

From here, you can run php test.php. It's nothing fancy right now, but it's a good starting point to work with.

Determining Parameter Type

We'll begin by using php's reflection library to determine the type of each of __constructor's parameters.

public function resolve($className)
{
    $reflectionClass = new \ReflectionClass($className);
    $constructor = $reflectionClass->getConstructor();
    if (!$constructor) {
        // There is either no constructor or arguments to resolve.
        return [];
    }
    $parameterTypes = [];
    foreach ($constructor->getParameters() as $parameter) {
        $parameterTypes[] = $parameter->getClass()->getName();
    }
    return $parameterTypes;
}

If you run php test.php the output should be:

array(1) { [0] => string(20) "Portico\Acme\Request" } 

Pretty amazing, we're already half way there and have written barely any code! If we look at the first two lines, we create a ReflectionMethod object. This is an object that reports information on a given method, in our case __construct. You can create a reflection method directly using new ReflectionMethod($className, '__construct'), but it will throw an exception if the class doesn't have a constructor. getConstructor(), on the other hand, will return NULL if there is no defined constructor. If the object we're resolving has a constructor, we iterate over the result of ReflectionMethod::getParameters(), which is an array of ReflectionParameters. Like ReflectionMethod, ReflectionParameters provides us with information about each individual parameter. Using these objects, we can determine the type hinted class of a parameter.

It's important to note, that parameters that are not type hinted will not return a value but for brevity we're not checking for that. In a production environment, you'd want to handle this case. You can find out this information by calling ReflectionParameter::isOptional().

Building The Object

Now that we can determine which classes are required, we have to know how to build them. Here we'll use a Factory class that is powered by our ParameterResolver.

<?php // file: src/Factory.php
namespace Portico\Ioc;

class Factory
{
    protected $resolver;

    public function __construct(ParameterResolver $resolver)
    {
        $this->resolver = $resolver;
    }

    public function make($className)
    {
        $argumentList = $this->resolveArguments($this->resolver->resolve($className));
        return $this->build($className, $argumentList);
    }

    protected function resolveArguments(array $argumentTypes)
    {
        $resolvedArguments = [];
        foreach ($argumentTypes as $type) {
            $resolvedArguments[] = $this->make($type);
        }
        return $resolvedArguments;
    }

    protected function build($className, array $arguments)
    {
        if (!$arguments) {
            return new $className;
        }
        $reflectionClass = new \ReflectionClass($className);
        return $reflectionClass->newInstanceArgs($arguments);
    }
} 

This may seem like a lot of code to throw up at once, but it's pretty straight forward. Our Factory is performing two tasks, creating new objects and using recursion to handle nested dependencies.

Creation of new objects

The creation of new objects occurs in the build method. Once again you'll notice that we're using reflection. You may be more familiar with the call_user_func_array([$className, '__construct'], $arguments) syntax, unfortunately it errors out in this case. It will attempt to call __construct as a static method on the object of type $className. Also we've added a check to makes sure there are arguments. If there are not any arguments, we won't need to use reflection and can just use the new operator.

Solving Nested Dependencies

Next we solve the issue of nested dependencies. Any parameter can have any number of its own dependencies and we'll need to resolve these before we can create the initially requested object. If you're a little confused, here's an example of what I'm talking about.

$instance = new UserController(
    new UserCacheRepository(new UserDatabaseRepository(new DatabaseConnection())),
    new Request()
); 

To build UserController we'll need to build the other 4 objects first. Generally the easiest way to handle nesting problems like this is to use recursion, so that's what we'll do. When we iterate over the array of $argumentTypes in resolveArguments we call $this->make($type);. This is a recursive call to the Factory object. Here's an example call log using the example from above.

* Initial Make Call: `Factory->make('UserController')`
	* Nested Make call: `Factory->make('UserCacheRepository')`
		* Nested Make call: `Factory->make('UserDatabaseRepository')` 
			* Nested Make call: `Factory->make('DatabaseConnection')` 
			* Return newly created `DatabaseConnection` 
		* Return newly created UserDatabaseRepository, which contains DatabaseConnection 
	* Return newly created UserCacheRepository, which contains UserDatabaseRepository
* Looped make call: Factory->Make('Request')
* Return newly created UserController 

Now that our automated resolver is working correctly, we need to update our test.php.

<?php //file: test.php
use Portico\Ioc\ParameterResolver;
use Portico\Ioc\Factory;

require_once 'acme.php';
require_once 'src/ParameterResolver.php';
require_once 'src/Factory.php';
$factory = new Factory(new ParameterResolver);
var_dump($factory->make('Portico\Acme\Controller')); 

If you run php test.php you should see an Controller object with a protected Request object. This is the bare minimum requirement for automated resolution. There are lots of optimizations this could have, but the main point is the theory. There are some other questions to take into account:

  • If a parameter is not type hinted, what should we do?
  • If a parameter type is a interface, how will you determine which implementation to build?
  • If a class is a singleton, how would that work?