Why Another DI Solution?

There are plenty of existing dependency-injection solutions already available for PHP. So why create another one?

It all started with an overhaul of the way the Ganbaro Digital libraries create and throw exceptions.

Exceptional Needs

Catch Ladders

The Ganbaro Digital PHP libraries use exceptions for all error handling. Many of the libraries are built on top of other libraries. This led to situations like this:

try {
    $obj->doSomething();
}
catch (Library1\Exceptions\UnsupportedType $e) {
    // ...
}
catch (Library2\Exceptions\UnsupportedType $e) {
    // ...
}

In the example above, any of the libraries can throw an exception that means the same thing (UnsupportedType). But each library (rightly) has its own class to do so. There are several problems with this situation:

The encapsulation principle is also breached if both Library1 and Library2 agree to throw the same UnsupportedType exception from a common base library.

What does a completely-encapsulated exception strategy look like?

Ideal Encapsulation

What we want to be able to code is this:

try {
    $obj->doSomething();
}
catch (Library1\Exceptions\UnsupportedType $e) {
    // ...
}

and make it Library1s job to only ever return Library1\Exceptions\UnsupportedType regardless of what kind of exception Library1s dependencies choose to throw.

That can be done inside Library1 like this:

try {
    $library2obj->doSomething();
}
catch (Library2\Exceptions\UnsupportedType $e) {
    throw new Library1\Exceptions\UnsupportedType(...);
}

but this switches one set of problems for another:

What we really want is to tell Library2 et al which exceptions we want them to throw. That sounds like a classic dependency-injection approach.

Only, as we'll see, it really isn't.

Traditional DI - Pass In Ready-Built Objects

The traditional dependency-injection solution is to pass ready-built objects in as parameters. This might be into the constructor or into setter methods. (We're not going to argue about the merits of either approach here).

class MyClass
{
    public function __construct(..., UnsupportedType $e1)
    {
        // ...
        $this->unsupportedType = $e1;
    }

    public function doSomething()
    {
        throw $this->unsupportedType;
    }
}

That can never work for PHP exceptions.

On top of that, many PHP dependency-injection containers are actually service locators: they return the same instance of an object time and time again. That doesn't work for exceptions, because we want new instances each and every time.

Bending The Rules - Pass In The Exception Class

In earlier iterations of the Ganbaro Digital libraries, we tried a compromise. Instead of passing in ready-built exceptions (which clearly was a total non-starter), we decided to pass in class names instead.

class MyClass
{
    public function __construct(..., $e1)
    {
        // ...
        $this->unsupportedType = $e1;
    }

    public function doSomething()
    {
        $eType = $this->unsupportedType;
        throw new $eType(...);
    }
}

And that worked, to a point.

Breaking The Rules - Pass In The List Of Exception Factories

What we really wanted to do was to pass in a list of static factory methods - a list of PHP callables - and have our classes use these callables to create the exception right where they're going to be thrown from.

class MyClass
{
    public function __construct(..., $exceptionsList)
    {
        $this->exceptionsList = $exceptionsList;
    }

    public function doSomething()
    {
        throw $this->exceptionsList['UnsupportedType'](...);
    }
}

Never mind bending the rules, this breaks the rules of existing dependency-injection practice and dependency-injection containers.

Ignoring the lynch-mobs for now, passing in an array of exception factories turned out to be an incomplete solution.

One of our guiding principles is to make it as easy as possible to detect programming errors before the code is shipped to production. A PHP array is lightning-quick, but there's no way to attach these kinds of robustness checks to one. We have to create some kind of container to do so.

We looked around at the existing DI containers, but couldn't find one that met our needs.

We decided that it made sense to build our own.