I am trying to understand the role of a Dependency Injection Container because it strikes me as fundamental in maintainable code.
As I understand it, a DIC is just as the title suggests: a container where all your dependencies are collected together. Instead of seeing new Foo\Bar
all over the application, all the new instances are generated inside of the container and then passed through to each other where they are needed (e.g., Model
is instantiated with an instance of Database
, which is instantiated with an Instance of Config
).
I have attempted to make a very simple DIC. This is the result.
In my front controller I am instantiating a new App\Core\Container
.
My Container
looks like this:
<?php
namespace App\Core;
use App\Config;
class Container
{
public $config;
public $router;
public $database;
public $model;
public $view;
public $controller;
public function __construct()
{
$this->config = new Config;
$this->router = new Router;
$this->database = new Database($this->config);
}
public function add()
{
// add dependencies from the outside?
}
public function getInstance(/* string */)
{
// return an instance for use somewhere?
}
public function newModel($model)
{
$model = $this->getModelNamespace() . $model;
$this->model = new $model($this->database);
return $this->model;
}
private function getModelNamespace()
{
$namespace = 'App\Models\\';
if (array_key_exists('namespace', $this->params = [])) {
$namespace .= $this->params['namespace'] . '\\';
}
return $namespace;
}
public function newView($params)
{
$this->view = new View($this->model, $params);
return $this->view;
}
public function newController($controller)
{
$controller = $this->getControllerNamespace() . $controller;
$this->controller = new $controller;
return $this->controller;
}
private function getControllerNamespace()
{
$namespace = 'App\Controllers\\';
if (array_key_exists('namespace', $this->params = [])) {
$namespace .= $this->params['namespace'] . '\\';
}
return $namespace;
}
}
Questions
- Could my implementation above, although very simple, be classed as a basic dependency injector?
- Is a Dependency Injection Container generally comprised of one class?
Note: the first three headings answer your questions, while the following ones answer anticipated questions and provide coverage of anything in the first two sections.
Could this be classified as a dependency injection container?
No, this does not look like a dependency injection container. A dependency injection container is meant to reduce the work that instantiation requires by determining, creating, and injecting all dependencies. Rather what you have there appears to be a combination of a factory and a service locator.
Factories abstract the creation of objects. This is essentially what your Container
class is doing. By calling designated methods (i.e., newModel
), your container takes on the responsibility of locating the exact object to be instantiated and constructing an instance of that object.
The reason I would call this a "poor" factory is that it's beginning to look like it might be used to locate services. Service locators work by hiding an object's dependencies: instead of being dependent on GenericService
, an object might depend on a service locator. Given the service locator, it can request an instance of GenericService
. I see similar behavior beginning to take hold in your add()
and getInstance()
methods. Service locators are generally considered anti-patterns because they abstract dependencies therefore making code impossible to test!
Is a dependency injection container comprised of one class?
It depends. You could very easily make a simple dependency injection container with one class. The issue is that the nature of a simple container tends to get more advanced into a not-so-simple container. When you start improving your pattern, you need to consider how the different components play together. Ask yourself: do they follow SOLID principles? If not, refactoring is necessary.
What is a dependency injection container?
I said it above, but again: a dependency injection container is meant to reduce the work that instantiation requires by determining, creating, and injecting all dependencies. A DIC will look at all dependencies of a class, and all dependencies those dependencies may have, and so on... In this sense, the container is responsible for hierarchically instantiating all dependencies.
The Container
class you provide relies on very strict definitions of pre-defined classes. For example, classes in your model layer appear to only be dependent on a database connection. (Similar statements can be said about classes in your controller & view layer).
How does a dependency injection container find dependencies?
A dependency injection container will detect dependencies. Typically this happens through 1 of 3 mechanisms: autowiring, annotations, and definitions. PHP-DI docs provide a good idea of what all three of these entail here. In short, though: autowiring detects dependencies by reflecting on a class, annotations are used to write in dependencies using comments above a class, and definitions are used to hard-code dependencies. Personally, I prefer autowiring because it's clean & simple.
Can I create a simple dependency injection container or no?
Yes, you can. Start with the idea that an injector should be able to instantiate any object (service, view, controller, etc...). It needs to look at the relevant object and hierarchically instantiate all dependencies (hint: possibly through some method of recursion).
A quick example of a simple injector using autowiring looks like this:
<?php
class Injector
{
public function make($className)
{
$dependencies = [];
//Create reflection of the class-to-make's constructor to get dependencies
$classReflection = new ReflectionMethod($className, "__construct");
foreach($classReflection->getParameters() as $parameter) {
$dependencyName = $parameter->getClass()->getName();
//Use the injector to make an instance of the dependency
$dependencies[] = $this->make($dependencyName);
}
$class = new ReflectionClass($className);
//Instantiate the class with all dependencies
return $class->newInstanceArgs($dependencies);
}
}
Tested with something like the following, you can see how the injector recursively checks and instantiates all dependencies
class A {
protected $b;
public function __construct(B $b) { $this->b = $b; }
public function output(){ $this->b->foo(); }
}
class B {
protected $c;
public function __construct(C $c) { $this->c = $c; }
public function foo() { $this->c->bar(); }
}
class C {
public function __construct() { }
public function bar() { echo "World!"; }
}
$injector = new Injector;
$a = $injector->make("A");
//No need to manually instantiate A's dependency, B, or B's dependency, C
$a->output();
This basic injector has obvious faults. For example, there is an opportunity to create a recursion disaster if two classes are dependent on each other (there should be a check for that). However, as is, this works as a basic example of what an injector looks like.
Injector vs. Dependency Injection Container
To make this more powerful and fall under the definition of "dependency injection container", you'd want a way to share instantiated instances across multiple make()
calls. For example, you may have another method called share()
. This method would store the instance passed to it. Whenever a class is built through the make()
method and depends on a class previously shared, instead of instantiating a new instance, it would use the already-instantiated one.
For a simple & powerful dependency injection container I suggest Auryn, but by all means, try to understand & create your own before using the ones already available.