Dependency Injection

Dependency injection has become a relatively common concept to ask during technical interviews. While it is the generally accepted best practice in modern PHP development, it seems terribly little understood beyond the provisions exposed by 3rd party libraries and frameworks.

Perhaps the most common explanation of DI (dependency injection) is a literal description such as passing objects into a constructor. This is, however, neither sufficient nor a satisfying explanation to an interview question or an introduction to the uninitiated.

While many things are said about DI, it's foremost purpose is the decoupling an object's creation from its usage. In practice, this is usually achieved by having a class define and require its dependencies rather than instantiating the dependencies by itself. This forces the "passing objects into the constructor" during object construction which is often cited during discussions of DI.

Not to be pedantic but I think it is important to know the advantages of DI despite already knowing the "motions". Primarily because, the knowledge can help maximize the returns of DI and justify the "overhead" of routing individual dependencies through the constructor rather than just using them outright wherever (as with Laravel facades, static methods, or instantiating dependencies on the spot).

Perhaps the most underrated advantage of DI is a strong decoupling of implementation – that is, dependencies can be of any implementation so long as the interface required is met. This is only possibly since we remove the responsibility of object creation which needs an exact concretion. It is then a matter of expressing the object's dependencies as abstractions such an interface or class to make them "swappable" with other objects implementing the same interface or inheriting the class.

class FrameBuilder
{
    public function __construct(Geometry $geometry) {}
}

interface Geometry
{
    public function getStack(): int;
    public function getReach(): int;
}

final class TrailGeometry implements Geometry
{
    public function getStack(): int
    {
        return 587;
    }
    
    public function getReach(): int
    {
        return 398;
    }
}

final class EnduroGeometry implements Geometry
{
    public function getStack(): int
    {
        return 618;
    }
    
    public function getReach(): int
    {
        return 435;
    }
}
Since we inject the dependency, coupling with a specific Geometry implementation is avoided

Another major advantage of DI is it naturally leads to the use of a DI container which takes over the responsibility of object creation, dependency resolution, and allows instance re-use. Best of all, there are already several excellent open source DI container implementations which means a huge effort saved and massive gains in code quality.

I can personally recommend Symfony's DI component and PHP-DI which I've both enjoyed using.

Another great benefit to dependency injected services is the possibility of employing an open-closed approach (think Open-Closed principle) when introducing new behavior or updating an existing one. Rather than modifying a class, we can simply create another one or extend the original and implement the new behavior. The new class can then be swapped with the original class wherever the new behavior is needed.

interface Logger
{
    public function log(string $message);
}

final class NoisyLogger implements Logger
{
    public function log(string $message)
    {
    	echo "$message\n";
    }
}

class Router
{
    private $pathToClassMap;
    private $logger;
    
    public function __construct(PathToClassMap $pathToClassMap, Logger $logger)
    {
        $this->pathToClassMap = $pathToClassMap;
        $this->logger = $logger;
    }
    
    public function route(string $path): string
    {
    	$this->logger->log("Routing $path...");
    
        if (!$this->pathToClassMap->hasPath($path)) {
            $this->logger->log("Path $path does not exist!");
            throw new RouteNotFoundException($path);
        }
        
        $controller = $this->pathToClassMap->getClass($path);
        
        $this->logger->log("Path $path will be routed to $controller");
        
        return $controller;
    }
}
How can we change this Router to stop logging without modifying the class itself?

To illustrate, say we have a Router class which logs information on what it is doing. Some need arises such that we need to be able to conditionally disable the Router from logging anything. Without modifying the class, we can achieve this simply by instantiating a Router with a different Logger.

final class MutedLogger implements Logger
{
    public function log(string $message)
    {
        // no-op
    }
}

// In some application level area we can conditionally disable
// a verbose router.
if ($isSilent)
    return new Router($pathToClassMap, new MutedLogger());

return new Router($pathToClassMap, new NoisyLogger());
Conditionally using a logger that does nothing

Despite being a best practice, there are some situations where DI is unsuitable and it usually means there are bigger design issues to address.

God classes — classes that are responsible for and do several things — are terrible candidates for dependency injection. Some objects are expensive to create such as wrappers for database connections. With God classes, DI would result in all dependencies of the God class to be instantiated regardless of whether it will be used. And as God classes are typically used in a lot of places, this could result in a systemic performance hit.

Another unsuitable scenario are classes with circular dependencies. Formally defining the dependencies of such classes in order to enable DI will actually make it impossible to instantiate the classes.

// non-DI
class Chicken
{
    public function lay(): Egg
    {
        return new Egg();
    }
}

class Egg
{
    public function hatch(): Chicken
    {
        return new Chicken();
    }
}


// DI
class Chicken
{
    public function __construct(Egg $egg) {}
}

class Egg
{
    public function __construct(Chicken $chicken) {}
}
DI won't work for classes with circular dependency

Knowing the why of DI as well as its strengths and weaknesses is helpful in reasoning about and communicating software design choices. It also enables fully taking advantage of DI when available and avoiding some potentially nasty pitfalls from blindly following best practices — especially when dealing with legacy code.