A search for “dependency injection container” on packagist currently provides over 95 pages of results. It is safe to say that this particular “wheel” has been invented.
However, no chef ever learned to cook using only ready meals. Likewise, no developer ever learned programming using only “ready code”.
In this article, we are going to learn how to make a simple dependency injection container package. All of the code written in this article, plus PHPDoc annotations and unit tests with 100% coverage is available at this GitHub repository. It is also listed on Packagist.
Planning Our Dependency Injection Container
Let us start by planning what it is that we want our container to do. A good start is to split “Dependency Injection Container” into two roles, “Dependency Injection” and “Container”.
The two most common methods for accomplishing dependency injection is through constructor injection or setter injection. That is, passing class dependencies through constructor arguments or method calls. If our container is going to be able to instantiate and contain services, it needs to be able to do both of these.
To be a container, it has to be able to store and retrieve instances of services. This is quite a trivial task compared to creating the services, but it is still worth some consideration. The container-interop package provides a set of interfaces that containers can implement. The primary interface is the ContainerInterface
that defines two methods, one for retrieving a service and one for testing if a service has been defined.
interface ContainerInterface
{
public function get($id);
public function has($id);
}
Learning From Other Dependency Injection Containers
The Symfony Dependency Injection Container allows us to define services in a variety of different ways. In YAML, the configuration for a container might look like this:
parameters:
# ...
mailer.transport: sendmail
services:
mailer:
class: Mailer
arguments: ["%mailer.transport%"]
newsletter_manager:
class: NewsletterManager
calls:
- [setMailer, ["@mailer"]]
The way Symfony splits the container configuration into configuration of parameters and services is very useful. This allows for application secrets such as API keys, encryption keys and auth tokens to be stored in parameters files that are excluded from source code repositories.
In PHP, the same configuration for the Symfony Dependency Injection component would look like this:
use Symfony\Component\DependencyInjection\Reference;
// ...
$container->setParameter('mailer.transport', 'sendmail');
$container
->register('mailer', 'Mailer')
->addArgument('%mailer.transport%');
$container
->register('newsletter_manager', 'NewsletterManager')
->addMethodCall('setMailer', array(new Reference('mailer')));
By using a Reference
object in the method call to setMailer
, the dependency injection logic can detect that this value should not be passed directly, but replaced with the service that it references in the container. This allows for both PHP values and other services to be easily injected into a service without confusion.
Getting Started
The first thing to do is create a new project directory and make a composer.json
file that can be used by Composer to autoload our classes. All this file does at the moment is map the SitePoint\Container
namespace to the src
directory.
{
"autoload": {
"psr-4": {
"SitePoint\\Container\\": "src/"
}
},
}
Next, as we are going to make our container implement the container-interop
interfaces, we need to make composer download them and add them to our composer.json
file:
composer require container-interop/container-interop
Along with the primary ContainerInterface
, the container-interop
package also defines two exception interfaces. The first for general exceptions encountered creating a service and another for when a service that has been requested could not be found. We will also add another exception to this list, for when a parameter that has been requested cannot be found.
As we do not need to add any functionality beyond what is offered by the core PHP Exception
class, these classes are pretty simple. Whilst they might seem pointless, splitting them up like this allows us to easily catch and handle them independently.
Make the src
directory and create these three files at src/Exception/ContainerException.php
, src/Exception/ServiceNotFoundException.php
and src/Exception/ParameterNotFoundException.php
respectively:
<?php
namespace SitePoint\Container\Exception;
use Interop\Container\Exception\ContainerException as InteropContainerException;
class ContainerException extends \Exception implements InteropContainerException {}
<?php
namespace SitePoint\Container\Exception;
use Interop\Container\Exception\NotFoundException as InteropNotFoundException;
class ServiceNotFoundException extends \Exception implements InteropNotFoundException {}
<?php
namespace SitePoint\Container\Exception;
class ParameterNotFoundException extends \Exception {}
Container References
The Symfony Reference
class discussed earlier allowed the library to distinguish between PHP values to be used directly and arguments that needed to be replaced by other services in the container.
Let us steal that idea, and create two classes for references to parameters and services. As both of these classes are going to be value objects storing just the name of the resource that they refer to, it makes sense to use an abstract class as a base. That way we do not have to write the same code twice.
Create the following files at src/Reference/AbstractReference.php
, src/Reference/ServiceReference.php
and src/Reference/ParameterReference.php
respectively:
<?php
namespace SitePoint\Container\Reference;
abstract class AbstractReference
{
private $name;
public function __construct($name)
{
$this->name = $name;
}
public function getName()
{
return $this->name;
}
}
<?php
namespace SitePoint\Container\Reference;
class ServiceReference extends AbstractReference {}
<?php
namespace SitePoint\Container\Reference;
class ParameterReference extends AbstractReference {}
The Container Class
It is now time to create our container. We are going to start with a basic sketch map of our container class, and we will add methods to this as we go along.
The general idea will be to accept two arrays in the constructor of our container. The first array will contain the service definitions and the second will contain the parameter definitions.
At src/Container.php
, place the following code:
<?php
namespace SitePoint\Container;
use Interop\Container\ContainerInterface as InteropContainerInterface;
class Container implements InteropContainerInterface
{
private $services;
private $parameters;
private $serviceStore;
public function __construct(array $services = [], array $parameters = [])
{
$this->services = $services;
$this->parameters = $parameters;
$this->serviceStore = [];
}
}
All we are doing here is implementing the ContainerInterface
from container-interop
and loading the definitions into properties that can be accessed later. We have also created a serviceStore
property, and initialized it to be an empty array. When the container is asked to create services, we will save these in this array so that they can be retrieved later without having to recreate them.
Now let us begin writing the methods defined by container-interop
. Starting with get($name)
, add the following method to the class:
use SitePoint\Container\Exception\ServiceNotFoundException;
// ...
public function get($name)
{
if (!$this->has($name)) {
throw new ServiceNotFoundException('Service not found: '.$name);
}
if (!isset($this->serviceStore[$name])) {
$this->serviceStore[$name] = $this->createService($name);
}
return $this->serviceStore[$name];
}
// ...
Be sure to add the use
statement to the top of the file. Our get($name)
method simply checks to see if the container has the definition for a service. If it does not, the ServiceNotFoundException
that we created earlier is thrown. If it does, it returns the service, creating it and saving it to the store if it has not already done so.
While we are at it, we should make a method for retrieving a parameter from the container. Assuming the parameters passed to the constructor form an N-dimensional associative array, we need some way of cleanly accessing any element within that array using a single string. An easy way of doing this is to use .
as a delimiter, so that the string foo.bar
refers to the bar
key in the foo
key of the root parameters array.
use SitePoint\Container\Exception\ParameterNotFoundException;
// ...
public function getParameter($name)
{
$tokens = explode('.', $name);
$context = $this->parameters;
while (null !== ($token = array_shift($tokens))) {
if (!isset($context[$token])) {
throw new ParameterNotFoundException('Parameter not found: '.$name);
}
$context = $context[$token];
}
return $context;
}
// ...
Now, we have used a couple of methods that we have not yet written. The first of those is the has($name)
method, that is also defined by container-interop
. This is a pretty simple method, and it just needs to check if the definitions array provided to the constructor contains an entry for the $name
service.
// ...
public function has($name)
{
return isset($this->services[$name]);
}
// ...
The other method we called that we are yet to write is the createService($name)
method. This method will use the definitions provided to create the service. As we do not want this method to be called from outside the container, we shall make it private.
The first thing to do in this method is some sanity checks. For each service definition we require an array containing a class
key and optional arguments
and calls
keys. These will be used for constructor injection and setter injection respectively. We can also add protection against circular references by checking to see if we have already attempted to create the service.
If the arguments
key exists, we want to convert that array of argument definitions into an array of PHP values that can be passed to the constructor. To do this, we will need to convert the reference objects that we defined earlier to the values that they reference in from the container. For now, we will take this logic into the resolveArguments($name, array $argumentDefinitons)
method. We use the ReflectionClass::newInstanceArgs()
method to create the service using the arguments
array. This is the constructor injection.
If the calls
key exists, we want to use the array of call definitions
and apply them to the service that we have just created. Again, we will take this logic into a separate method defined as initializeService($service, $name, array $callDefinitions)
. This is the setter injection.
use SitePoint\Container\Exception\ContainerException;
// ...
private function createService($name)
{
$entry = &$this->services[$name];
if (!is_array($entry) || !isset($entry['class'])) {
throw new ContainerException($name.' service entry must be an array containing a \'class\' key');
} elseif (!class_exists($entry['class'])) {
throw new ContainerException($name.' service class does not exist: '.$entry['class']);
} elseif (isset($entry['lock'])) {
throw new ContainerException($name.' service contains a circular reference');
}
$entry['lock'] = true;
$arguments = isset($entry['arguments']) ? $this->resolveArguments($name, $entry['arguments']) : [];
$reflector = new \ReflectionClass($entry['class']);
$service = $reflector->newInstanceArgs($arguments);
if (isset($entry['calls'])) {
$this->initializeService($service, $name, $entry['calls']);
}
return $service;
}
// ...
That leaves us with two final methods to create. The first should convert an array of argument definitions into an array of PHP values. To do this it will need to replace ParameterReference
and ServiceReference
objects with the appropriate parameters and services from the container.
use SitePoint\Container\Reference\ParameterReference;
use SitePoint\Container\Reference\ServiceReference;
// ...
private function resolveArguments($name, array $argumentDefinitions)
{
$arguments = [];
foreach ($argumentDefinitions as $argumentDefinition) {
if ($argumentDefinition instanceof ServiceReference) {
$argumentServiceName = $argumentDefinition->getName();
$arguments[] = $this->get($argumentServiceName);
} elseif ($argumentDefinition instanceof ParameterReference) {
$argumentParameterName = $argumentDefinition->getName();
$arguments[] = $this->getParameter($argumentParameterName);
} else {
$arguments[] = $argumentDefinition;
}
}
return $arguments;
}
The last method performs the setter injection on the instantiated service object. To do this it needs to loop through an array of method call definitions. The method
key is used to specify the method, and an optional arguments
key can be used to provide arguments to that method call. We can reuse the method we just wrote to translate those arguments into PHP values.
private function initializeService($service, $name, array $callDefinitions)
{
foreach ($callDefinitions as $callDefinition) {
if (!is_array($callDefinition) || !isset($callDefinition['method'])) {
throw new ContainerException($name.' service calls must be arrays containing a \'method\' key');
} elseif (!is_callable([$service, $callDefinition['method']])) {
throw new ContainerException($name.' service asks for call to uncallable method: '.$callDefinition['method']);
}
$arguments = isset($callDefinition['arguments']) ? $this->resolveArguments($name, $callDefinition['arguments']) : [];
call_user_func_array([$service, $callDefinition['method']], $arguments);
}
}
}
And we now have a usable dependency injection container! To see usage examples, check out the repository on GitHub.
Finishing Thoughts
We have learned how to make a simple dependency injection container, but there are loads of containers out there with cool features that ours does not have yet!
Some dependency injection containers, such as PHP-DI and Aura.Di provide a feature called auto-wiring. This is where the container guesses which services from the container should be injected into others. To do this, they use the reflection API to find out information about the constructor parameters.
Feel free to fork the repository and add features such as auto-wiring or whatever else you can think of, it’s great practice! Furthermore, we keep a public list all known forks of this container so that others can see the work you have done. Just use the comments below to share your work with us, and we will make sure it gets added.
You can also use the comments below to get in touch. Let us know about anything that you would like clarified or explained, or any bugs that you have spotted.
Keep your eyes open for more articles like this on SitePoint PHP. We will soon be explaining how to reinvent the wheel with a range of common PHP components!
Frequently Asked Questions (FAQs) about Building Your Own Dependency Injection Container
What is a Dependency Injection Container and why is it important?
A Dependency Injection Container, also known as a DI Container, is a tool used in software development to manage dependencies. It is a form of Inversion of Control (IoC) that allows for loose coupling, making code more maintainable, reusable, and testable. It is important because it simplifies the process of providing dependencies to objects, reducing the need for manual wiring and making code easier to understand and manage.
How does a Dependency Injection Container work in PHP?
In PHP, a Dependency Injection Container works by creating and managing object instances. It uses a configuration file to determine which dependencies to inject into each object. When an object is requested, the container will instantiate it and inject the necessary dependencies, handling the creation and wiring of objects automatically.
Can I build my own Dependency Injection Container in Java?
Yes, you can build your own Dependency Injection Container in Java. The process is similar to PHP, but with some differences due to the language’s unique features. You would need to create a container class that manages object creation and dependency injection, using reflection to instantiate objects and inject dependencies.
What are the benefits of using a Dependency Injection Container in ASP.NET Core?
Using a Dependency Injection Container in ASP.NET Core provides several benefits. It simplifies the process of managing dependencies, making code easier to understand and maintain. It also promotes loose coupling, which improves code reusability and testability. Additionally, it provides a consistent way to handle object lifetimes, ensuring that objects are properly disposed of when they are no longer needed.
How can I create a Dependency Injection Container in PHP?
To create a Dependency Injection Container in PHP, you would need to create a container class that manages object creation and dependency injection. This class would use a configuration file to determine which dependencies to inject into each object. When an object is requested, the container would instantiate it and inject the necessary dependencies.
What are some common challenges when building a Dependency Injection Container?
Some common challenges when building a Dependency Injection Container include managing object lifetimes, handling circular dependencies, and dealing with complex dependency graphs. These challenges can be mitigated by using a well-designed container and following best practices for dependency injection.
Can I use a Dependency Injection Container with other programming languages?
Yes, Dependency Injection Containers can be used with many programming languages, including Java, C#, PHP, and Python. The specific implementation details may vary depending on the language, but the basic principles of dependency injection and inversion of control remain the same.
How does a Dependency Injection Container improve code testability?
A Dependency Injection Container improves code testability by making it easier to replace dependencies with mock objects during testing. This allows for isolated testing of individual components, making it easier to identify and fix bugs.
What is the difference between a Dependency Injection Container and a Service Locator?
A Dependency Injection Container and a Service Locator both manage object creation and dependency injection, but they do so in different ways. A Dependency Injection Container injects dependencies into objects automatically, while a Service Locator requires objects to request their dependencies explicitly. This makes a Dependency Injection Container more transparent and easier to use, but a Service Locator can provide more control over object creation.
How can I handle circular dependencies with a Dependency Injection Container?
Handling circular dependencies with a Dependency Injection Container can be challenging, but it is possible with careful design. One approach is to use lazy loading, where dependencies are not instantiated until they are actually needed. This can break the circular dependency chain and prevent infinite loops. Another approach is to redesign the code to eliminate the circular dependency, which is often a sign of poor design.
Andrew is a software developer from the United Kingdom with a Master's Degree in Physics. He's the Head of Technical Development at MoneyMaxim, a developer of open source software and an up-and-coming speaker. In his spare time he dabbles in photography and practices Muay Thai.