Dependency Injection is one of the fundamental pillars of modern object-oriented software design - it is a prime example of the D in SOLID. The idea is that instead of creating objects inside other objects, you create your "inner objects" (dependencies) separately and then inject (by passing as an argument to the constructor or a setter method) them into the "outer object" (dependent). For example, if you have class Owl
:
class Owl
{
protected $nest;
public function __construct()
{
$this->nest = new Nest();
}
}
an Owl
would create its own Nest
in its constructor:
// Nest is automatically created in Owl's constructor
$owl = new Owl();
This might seem like a nice, convenient way of packaging things - after all, Nest
seems like an implementation detail that we shouldn't have to worry about. However, what happens if we come along later with an ImprovedNest
, and we want our Owl
to use that instead?
Unfortunately we can't. Our classes Owl
and Nest
are what we would call tightly coupled - Owls can use Nests and only Nests. Dependency injection solves this problem:
class Owl
{
protected $nest;
public function __construct($nest)
{
$this->nest = $nest;
}
}
now, we create our Nest
externally to our Owl
, and then pass it in:
$nest = new Nest();
$owl = new Owl($nest);
If later we define ImprovedNest
(either as an inherited class of Nest
, or they both implement a common NestInterface
), we can create Owls with different types of Nests:
$nest = new Nest();
$improvedNest = new ImprovedNest();
$owl1 = new Owl($nest);
$owl2 = new Owl($improvedNest);
This is of course a contrived example, but the general strategy of keeping your classes loosely coupled is a good way to make your code more reusable and easily tested.
The obvious issue with dependency injection, of course, is that it becomes harder to encapsulate functionality. Injecting a Nest
into an Owl
's constructor requires that we build the Nest ourselves, instead of delegating it to the Owl. For classes with many, many dependencies, we can end up with a lot of code just to create an instance. For example, let's look at the code required to create a new Monolog logging object:
$logger = new Logger('debug');
$logFile = $c->locator->findResource('log://userfrosting.log', true, true);
$handler = new StreamHandler($logFile);
$formatter = new MixedFormatter(null, null, true);
$handler->setFormatter($formatter);
$logger->pushHandler($handler);
$logger
object requires the $handler
object, which we inject using pushHandler()
;$handler
object requires a $formatter
object, which we inject using setFormatter()
;$handler
object also requires the path to the log file, which we need to look up using the locator
.This is a lot of code to write just to create one measly object! It would be great if we could somehow encapsulate the creation of the object, but without creating tight couplings by doing that within the object itself.
This is where the dependency injection container (DIC) comes into play. The DIC handles basic management of dependencies, encapsulating their creation into simple callbacks. We will call these callbacks services. The DIC implementation that we use, Pimple, has two powerful features that we rely on:
$logger
(and its dependencies) won't be created until the first time I actually try to access them through the container ($container->logger
).$logger = $container->logger; // Pimple creates the Logger object
... // Do some stuff
$logger = $container->logger; // Pimple returns the same Logger it created earlier
Taken together, this means we can define our services without needing to worry about when and where their dependencies are created in our application's lifecycle.
When we talk about services, this might bring to mind an anti-pattern called the Service Locator Pattern. It is true that the DIC can be used as a service locator, especially if you inject the entire container into your objects. With the exception of controllers and a few other types of classes that have a very large number of dependencies, we try to avoid implementing the Service Locator Pattern whenever possible.
UserFrosting sets up its services through service provider classes. Each Sprinkle can define a service provider class in src/ServicesProvider/ServicesProvider.php
. A service provider class typically contains a single method, register
, which takes a single argument, the Pimple DIC. For example, the ServicesProvider
class in the core
Sprinkle starts like this:
class ServicesProvider
{
/**
* Register UserFrosting's core services.
*
* @param ContainerInterface $container A DI container implementing ArrayAccess and container-interop.
*/
public function register(ContainerInterface $container)
{
/**
* Flash messaging service.
*
* Persists error/success messages between requests in the session.
*/
$container['alerts'] = function ($c) {
return new MessageStream($c->session, $c->config['session.keys.alerts'], $c->translator);
};
...
}
The alerts
service is defined by simply assigning a callback to $container['alerts']
which returns our service object (in this case, an instance of MessageStream
).
You'll notice that the callback itself takes a parameter $c
, which is also a reference to the DIC. This allows us to reference services inside other services. For example, you'll notice that MessageStream
depends on $c->session
, $c->config
, and $c->translator
. These are other services that are defined further down in ServicesProvider
. Thus, the first time we reference $c->alerts
, it will not only create our MessageStream
object, but any dependencies that have not yet been created as well.
The service provider class itself is registered during the application lifecycle, when each Sprinkle is set up.
Next, we list the default services that ship with UserFrosting. After that, we talk about how you can add your own services, extend existing services, or completely replace certain services in your own Sprinkle.