When to Inject the Container post
Posted on 2014-11-27 by jwage
Deciding when to inject the container in Symfony is a frequent topic of discussion. Many would have you believe that you should NEVER inject the container because it breaks the “rules” and is an anti-pattern. This is not always true and, just like most things, it should not be applied blindly to everything you do. This post aims to demonstrate cases where injecting the container makes sense.
The problem with unused dependencies
Imagine you have a listener that records some query string parameters and you’d like this to run on each request:
<?php
use Doctrine\DBAL\Connection;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
class RequestParameterLoggerListener
{
private $requestParameterLogger;
public function __construct(RequestParameterLogger $requestParameterLogger)
{
$this->requestParameterLogger = $requestParameterLogger;
}
public function onKernelRequest(GetResponseEvent $event)
{
$request = $event->getRequest();
if (!$request->query->has('utm_origin')) {
return;
}
$this->requestParameterLogger->logUtmOrigin($request);
}
}
class RequestParameterLogger
{
private $connection;
public function __construct(Connection $connection)
{
$this->connection = $connection;
}
public function logUtmOrigin(Request $request)
{
// record utm origin to the database using the connection
}
}
As you can see, logUtmOrigin
is only called when the Request’s query string contains the parameter named utm_origin
. This means that even on requests where utm_origin
does not exist, we are constructing the RequestParameterLogger
and injecting it to RequestParameterLoggerListener
.
This is a simple example; however, in a large application with many such listeners, you can imagine the application constructing potentially dozens of services like RequestParameterLogger, which would be injected but never used.
Injecting the Container
This problem can be easily fixed by injecting the container and lazily requesting the service from the container when it is needed. Here is the same listener above, but rewritten with container injection.
<?php
use Symfony\Component\DependencyInjection\ContainerAware;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
class RequestParameterLoggerListener extends ContainerAware
{
public function onKernelRequest(GetResponseEvent $event)
{
$request = $event->getRequest();
if (!$request->query->has('utm_origin')) {
return;
}
$this->container->get('request.parameter_logger')->logUtmOrigin($request);
}
}
Now, when the query parameter utm_origin
does not exist, the RequestParameterLogger
will not be constructed.
In the Wild
At OpenSky we have always injected services to listeners, security voters, security providers, etc. As a result, every request would eagerly construct many services that would never be used. By rewriting these services to use the strategy demonstrated above, I was able to shave 10-20 milliseconds off of every request and significantly reduce the number of services constructed to handle a request that serves a blank controller and template.
To make it easy to rewrite all of our prior art, I wrote a simple class named LazyService
.
<?php
use Symfony\Component\DependencyInjection\ContainerAware;
abstract class LazyService extends ContainerAware
{
protected $propertyMap = array();
protected $values = array();
public function __get($key)
{
if (!isset($this->propertyMap[$key])) {
throw new \InvalidArgumentException(sprintf('Could not find service for key %s', $key));
}
if (!isset($this->values[$key])) {
if ($this->propertyMap[$key][0] === '%') {
$this->values[$key] = $this->container->getParameter(trim($this->propertyMap[$key], '%'));
} else {
$this->values[$key] = $this->container->get($this->propertyMap[$key]);
}
}
return $this->values[$key];
}
}
Here is our original example, but rewritten to use this LazyService class:
class RequestParameterLoggerListener extends LazyService
{
protected $propertyMap = array(
'requestParameterLogger' => 'request.parameter_logger',
);
public function onKernelRequest(GetResponseEvent $event)
{
$request = $event->getRequest();
if (!$request->query->has('utm_origin')) {
return;
}
$this->requestParameterLogger->logUtmOrigin($request);
}
}
This made it easy for me to make dozens of services extra lazy without having to rewrite too much of the service themselves or the associated tests.
Why not use lazy services provided by Symfony?
I chose not to use lazy services provided by Symfony because I didn’t want to add yet more complexity and weight to our application. Even if you mark a service as lazy, a proxy still has to be instantiated and injected. I wanted to completely eliminate the construction of these classes.
That is it! I hope this post was helpful in realizing when to inject the container and not blindly follow design theory. Happy Thanksgiving!