Automated Dependency Injection using Containers
I recently posted about dependency injection, a guide that I hope was easy to read and understand. It's a topic that's confusing, due in no small part to its name. As I mention there
the same term defines the simple process of externally providing instance variables to a class as it does to the Container object's magical auto-instantiation of object (which also "injects dependencies.")
I'd like to talk a bit more about that. Now that we understand the term "Dependency Injection" (which is simply providing classes to classes through a constructor or a setter), let's talk about how we might make this a little less onerous. Consider the Concrete\Core\Express\ObjectBuilder
class in concrete5:
public function __construct(
AttributeKeyHandleGenerator $generator,
EntityManagerInterface $entityManager,
TypeFactory $attributeTypeFactory)
{
$this->attributeTypeFactory = $attributeTypeFactory;
$this->generator = $generator;
$this->entityManager = $entityManager;
}
Every time you instantiate this class, you need to provide it three classes. If you call this code from multiple places, that's going to get frustrating fast (don't worry about the specifics of what these classes are and do – that's not really important for this tutorial.)
Fortunately, concrete5 and other modern PHP frameworks provide what are commonly called "Dependency Injection Container" objects, which make it easy to automatically create classes with the dependencies already provided! So instead of having to write this:
$builder = new \Concrete\Core\Express\ObjectBuilder($generator, $entityManager, $typeFactory);
You can simply type this:
$builder = \Core::make('Concrete\Core\Express\ObjectBuilder');
and the dependencies will automatically be resolved and injected into the instance! Let's talk about some of the nuances and features available in this setup.
Note – Implementation
Just a quick note before we dive in: the examples here will be concrete5-specific, but as I said before, any modern PHP framework will have a dependency injection container, and this features will likely apply to all of them. Additionally, concrete5 uses Laravel's Container object, so these examples will likely apply to Laravel as well.
Cascading Dependencies
One of the best part of injecting dependencies via a Container is the ability to inject the dependencies of your dependencies (and their dependencies – and so on!). For example, my ObjectBuilder
class depends on three classes. But those classes depend on classes! For example, my AttributeKeyHandleGenerator
class above depends on Concrete\Core\Attribute\Category\ExpressCategory
:
public function __construct(ExpressCategory $category)
{
$this->category = $category;
}
and my TypeFactory
class depends on Environment
and EntityManager
as well:
public function __construct(Environment $environment, EntityManager $entityManager)
{
$this->environment = $environment;
$this->entityManager = $entityManager;
}
Without a Container, I'd have to instantiate all these dependencies in order to create these three objects, in order to pass them to ObjectBuilder.
But my Container takes care of all that. My Core::make()
call above will automatically build all the dependencies properly, without having to do a thing.
Special Logic for Class Creation
By default, the Container object will simply try and instantiate the dependency object with any other classes listed (using PHP's Reflection functionality.) But you can bind objects to your container in fancy ways that are more advanced than this.
Singletons
If you want an object to be a singleton (i.e. – a shared instance only used once in your application, vs. one that is newly instantiated every time its used – something like a database connection), you can specify that in the Container object at runtime. Here's our database service provider, which concrete5 runs on startup:
$this->app->singleton('Concrete\Core\Database\DatabaseManager');
Any time DatabaseManager
is used in a __construct()
method and called through Core::make()
, the same DatabaseManager
instance will always be used.
Interfaces and Extensibility
The Laravel Container object allows you to bind a class to an interface, and type hint against those interfaces at runtime.
$this->app->bind('Concrete\Core\Database\EntityManagerConfigFactoryInterface',
'Concrete\Core\Database\EntityManagerConfigFactory');
Any time EntityManagerConfigFactoryInterface
is used in a __construct()
method and called through Core::make()
, the actualy EntityManagerConfigFactory
implementation of said interface will be used. This means you can bind concrete implementations to interfaces at runtime, and override them.
Get Fancy With It
A Container also allows you to specify how your objects get created, when they are created. Here's an example of how we create Doctrine\ORM\Configuration
any time we instantiate it through the container:
$this->app->bind('Doctrine\ORM\Configuration',
function($app) {
$isDevMode = $app->make('config')->get('concrete.cache.doctrine_dev_mode');
$proxyDir = $app->make('config')->get('database.proxy_classes');
$cache = $app->make('orm/cache');
$config = \Doctrine\ORM\Tools\Setup::createConfiguration(
$isDevMode, $proxyDir, $cache);
foreach($app->make('config')->get('app.entity_namespaces') as $namespace => $class) {
$config->addEntityNamespace($namespace, $class);
}
return $config;
});
We're reading from configuration, and even running special logic when we create the configuration.
Conclusion
The bottom line? If you're running a modern PHP framework, you're going to want to its service Container. It will let you specify classes as singletons, and remove a lot of duplicated code – especially if you're passing your dependencies via the constructor as we've been talking about. Your code will be cleaner, more maintanable, and more testable.