In this post I will go through removing directly created dependencies from a Symfony2 controller and instead injecting them using The Dependency Injection Container (DIC).
At the end of my last post I had an Asset Controller which looked like this:
<?php
namespace LimeThinking\SpringBundle\Controller;
use Assetic\AssetManager;
use Assetic\Cache\FilesystemCache;
use Assetic\Factory\AssetFactory;
use Assetic\Factory\LazyAssetManager;
use Assetic\Filter\Yui\CssCompressorFilter;
use Assetic\FilterManager;
use LimeThinking\SpringBundle\Asset\Loader\XMLLoader;
use Symfony\Bundle\AsseticBundle\Controller\AsseticController;
use Symfony\Component\HttpFoundation\Response;
class AssetController
{
public function resourceAction($name, $format)
{
$yui = new CssCompressorFilter('/path/to/yuicompressor.jar');
$fm = new FilterManager();
$fm->set('yui_css', $yui);
$factory = new AssetFactory('/path/to/css');
$factory->setAssetManager(new AssetManager());
$factory->setFilterManager($fm);
$file = $name.$format;
$formulaLoader = new XMLLoader;
$formula = $formulaLoader->GetFormula($file);
$lazyAm = new LazyAssetManager($factory);
$lazyAm->setFormula($file, $formula['formula']);
$cache = new FilesystemCache('/path/to/cache');
$request = new Request();
$controller = new AsseticController($request, $lazyAm, $cache);
$response = $controller->render($file);
$response->headers->set('Content-Type', $formula['contentType']);
return $response;
}
}
In the post I am going to go through the steps I went through to change it to this:
<?php
namespace LimeThinking\SpringBundle\Controller;
use Assetic\Factory\LazyAssetManager;
use Symfony\Bundle\AsseticBundle\Controller\AsseticController;
class AssetController
{
protected $asseticController;
protected $lazyAm;
protected $formulaLoader;
public function __construct(LazyAssetManager $lazyAm,
AsseticController $asseticController,
$formulaLoader)
{
$this->asseticController = $asseticController;
$this->lazyAm = $lazyAm;
$this->formulaLoader = $formulaLoader;
}
public function resourceAction($name, $format)
{
$file = $name.$format;
$formula = $this->formulaLoader->GetFormula($file);
$this->lazyAm->setFormula($file, $formula['formula']);
$response = $this->asseticController->render($file);
$response->headers->set('Content-Type', $formula['contentType']);
return $response;
}
}
The second is much cleaner, much of the leg work of setting up the Asset and Filter managers has been removed. Also all the direct instantiation of objects has been removed, this means that testing will be much easier. So where has the missing code gone? It has been replaced by using the Symfony2 Dependency Injection Container to do the work for us. I am going to go through the steps I took to start removing the dependencies.
Note: this asset controller is not actually necessary as Assetic can be configured directly from Twig, see Kris Wallsmith's comment on my previous post. My follow up post Symfony2: Using Assetic from Twig has more on this. However what it actually does is not relevant to the content of this post, it is more an exercise in how to inject dependencies.
To start with the controller needs to be called as a service and an XML config file created for the bundle. Please read my post Symfony2: Controller as Service for more details. We can start with the low hanging fruit, the controller's dependencies that have no dependencies themselves. Instead of creating these in the controller they can be passed in via constructor injection. The controller now looks like this:
<?php
namespace LimeThinking\SpringBundle\Controller;
use Assetic\AssetManager;
use Assetic\Cache\FilesystemCache;
use Assetic\Factory\AssetFactory;
use Assetic\Factory\LazyAssetManager;
use Assetic\Filter\Yui\CssCompressorFilter;
use Assetic\FilterManager;
use LimeThinking\SpringBundle\Asset\Loader\XMLLoader;
use Symfony\Bundle\AsseticBundle\Controller\AsseticController;
use Symfony\Component\HttpFoundation\Response;
class AssetController
{
protected $fm;
protected $am;
protected $formulaLoader;
protected $request;
public function __construct(FilterManager $fm,
AssetManager $am,
$formulaLoader,
Request $request)
{
$this->fm = $fm;
$this->am = $am;
$this->formulaLoader = $formulaLoader;
$this->request = $request;
}
public function resourceAction($name, $format)
{
$yui = new CssCompressorFilter('/path/to/yuicompressor.jar');
$this->fm->set('yui_css', $yui);
$factory = new AssetFactory('/path/to/css');
$factory->setAssetManager($this->am);
$factory->setFilterManager($fm);
$file = $name.$format;
$formula = $this->formulaLoader->GetFormula($file);
$lazyAm = new LazyAssetManager($factory);
$lazyAm->setFormula($file, $formula['formula']);
$cache = new FilesystemCache('/path/to/cache');
$controller = new AsseticController($this->request, $lazyAm, $cache);
$response = $controller->render($file);
$response->headers->set('Content-Type', $formula['contentType']);
return $response;
}
}
and the XML config file like this:
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<parameters>
<parameter
key="lime_thinking_spring.formulaloaderclass"
>LimeThinking\SpringBundle\Asset\Loader\XMLLoader</parameter>
</parameters>
<services>
<service id="lime_thinking_spring.asset"
class="LimeThinking\SpringBundle\Controller\AssetController">
<argument type="service" id="lime_thinking_spring.filtermanager" />
<argument type="service" id="lime_thinking_spring.assetmanager" />
<argument type="service" id="lime_thinking_spring.formulaloader" />
<argument type="service" id="lime_thinking_spring.request" />
</service>
<service id="lime_thinking_spring.formulaloader"
class="%lime_thinking_spring.formulaloaderclass%"/>
<service id="lime_thinking_spring.filtermanager"
class="Assetic\FilterManager"/>
<service id="lime_thinking_spring.assetmanager"
class="Assetic\AssetManager"/>
<service id="lime_thinking_spring.request"
class="Symfony\Component\HttpFoundation\Request"/>
</services>
</container>
Each of the dependencies we are passing in is defined as a service and the id associated with used to pass them as constructor arguments to the controller service. Also of note is that you can parametrise the settings themselves, I have pulled out the name of the class being used for formula loading in order to make this easier to change.
For the next step we can remove the dependencies which have their own simple constructor parameters.
<?php
namespace LimeThinking\SpringBundle\Controller;
use Assetic\AssetManager;
use Assetic\Cache\FilesystemCache;
use Assetic\Factory\AssetFactory;
use Assetic\Factory\LazyAssetManager;
use Assetic\Filter\Yui\CssCompressorFilter;
use Assetic\FilterManager;
use LimeThinking\SpringBundle\Asset\Loader\XMLLoader;
use Symfony\Bundle\AsseticBundle\Controller\AsseticController;
use Symfony\Component\HttpFoundation\Response;
class AssetController
{
protected $fm;
protected $am;
protected $formulaLoader;
protected $request;
protected $yui;
protected $factory;
protected $cache;
public function __construct(FilterManager $fm,
AssetManager $am,
$formulaLoader,
Request $request,
CssCompressorFilter $yui,
AssetFactory $factory,
FilesystemCache $cache)
{
$this->fm = $fm;
$this->am = $am;
$this->formulaLoader = $formulaLoader;
$this->request = $request;
$this->yui = $yui;
$this->factory = $factory;
$this->cache = $cache;
}
public function resourceAction($name, $format)
{
$this->fm->set('yui_css', $this->yui);
$this->factory->setAssetManager($this->am);
$this->factory->setFilterManager($fm);
$file = $name.$format;
$formula = $this->formulaLoader->GetFormula($file);
$lazyAm = new LazyAssetManager($this->factory);
$lazyAm->setFormula($file, $formula['formula']);
$controller = new AsseticController($this->request, $lazyAm, $this->cache);
$response = $controller->render($file);
$response->headers->set('Content-Type', $formula['contentType']);
return $response;
}
}
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<parameters>
<parameter
key="lime_thinking_spring.formulaloaderclass"
>LimeThinking\SpringBundle\Asset\Loader\XMLLoader</parameter>
<parameter
key="lime_thinking_spring.cachedir"
>%kernel.cache_dir%/assets</parameter>
<parameter
key="lime_thinking_spring.yuicomppath"
>/path/to/yuicompressor.jar</parameter>
<parameter
key="lime_thinking_spring.assetdir"
>%kernel.root_dir%/../src/LimeThinking/SpringBundle/Resources</parameter>
</parameters>
<services>
<service id="lime_thinking_spring.asset"
class="LimeThinking\SpringBundle\Controller\AssetController">
<argument type="service" id="lime_thinking_spring.filtermanager" />
<argument type="service" id="lime_thinking_spring.assetmanager" />
<argument type="service" id="lime_thinking_spring.formulaloader" />
<argument type="service" id="lime_thinking_spring.request" />
<argument type="service" id="lime_thinking_spring.yuicss" />
<argument type="service" id="lime_thinking_spring.assetFactory" />
<argument type="service" id="lime_thinking_spring.cache" />
</service>
<service id="lime_thinking_spring.formulaloader"
class="%lime_thinking_spring.formulaloaderclass%"/>
<service id="lime_thinking_spring.filtermanager"
class="Assetic\FilterManager"/>
<service id="lime_thinking_spring.assetmanager"
class="Assetic\AssetManager"/>
<service id="lime_thinking_spring.request"
class="Symfony\Component\HttpFoundation\Request"/>
<service id="lime_thinking_spring.cache"
class="Assetic\Cache\FilesystemCache">
<argument>%lime_thinking_spring.cachedir%</argument>
</service>
<service id="lime_thinking_spring.yuicss"
class="Assetic\Filter\Yui\CssCompressorFilter">
<argument>%lime_thinking_spring.yuicomppath%</argument>
</service>
<service id="lime_thinking_spring.assetfactory"
class="Assetic\Factory\AssetFactory">
<argument>%lime_thinking_spring.assetdir%</argument>
</service>
</services>
</container>
One new introduction here is the use of some of the global parameters: kernel.root_dir and kernel.cache_dir within the parameters. These are fairly self explanatory and are an excellent way to avoid having absolute paths in the service configuration. A list of the available global parameters appears in this page of the Symfony2 documentation.
We're starting to get there now, unfortunately our constructor argument list is growing, fortunately the next step will help with this. Quite a few of the remaining lines of code are calling setter methods on the dependencies, usually with another of our dependencies being passed in. We can use the Symfony2 Dependency Injection Container's support for Setter Injection to deal with these. The controller can now be changed to this:
<?php
namespace LimeThinking\SpringBundle\Controller;
use Assetic\AssetManager;
use Assetic\Cache\FilesystemCache;
use Assetic\Factory\AssetFactory;
use Assetic\Factory\LazyAssetManager;
use Assetic\Filter\Yui\CssCompressorFilter;
use Assetic\FilterManager;
use LimeThinking\SpringBundle\Asset\Loader\XMLLoader;
use Symfony\Bundle\AsseticBundle\Controller\AsseticController;
use Symfony\Component\HttpFoundation\Response;
class AssetController
{
protected $formulaLoader;
protected $request;
protected $factory;
protected $cache;
public function __construct($formulaLoader,
Request $request,
AssetFactory $factory,
FilesystemCache $cache)
{
$this->formulaLoader = $formulaLoader;
$this->request = $request;
$this->factory = $factory;
$this->cache = $cache;
}
public function resourceAction($name, $format)
{
$file = $name.$format;
$formula = $this->formulaLoader->GetFormula($file);
$lazyAm = new LazyAssetManager($this->factory);
$lazyAm->setFormula($file, $formula['formula']);
$controller = new AsseticController($this->request, $lazyAm, $this->cache);
$response = $controller->render($file);
$response->headers->set('Content-Type', $formula['contentType']);
return $response;
}
}
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<parameters>
<parameter
key="lime_thinking_spring.formulaloaderclass"
>LimeThinking\SpringBundle\Asset\Loader\XMLLoader</parameter>
<parameter
key="lime_thinking_spring.cachedir"
>%kernel.cache_dir%/assets</parameter>
<parameter
key="lime_thinking_spring.yuicomppath"
>/path/to/yuicompressor.jar</parameter>
<parameter
key="lime_thinking_spring.assetdir"
>%kernel.root_dir%/../src/LimeThinking/SpringBundle/Resources</parameter>
</parameters>
<services>
<service id="lime_thinking_spring.asset"
class="LimeThinking\SpringBundle\Controller\AssetController">
<argument type="service" id="lime_thinking_spring.formulaloader" />
<argument type="service" id="lime_thinking_spring.request" />
<argument type="service" id="lime_thinking_spring.assetFactory" />
<argument type="service" id="lime_thinking_spring.cache" />
</service>
<service id="lime_thinking_spring.formulaloader"
class="%lime_thinking_spring.formulaloaderclass%"/>
<service id="lime_thinking_spring.filtermanager"
class="Assetic\FilterManager">
<call method="set">
<argument>yui_css</argument>
<argument type="service" id="lime_thinking_spring.yuicss" />
</call>
</service>
<service id="lime_thinking_spring.assetmanager"
class="Assetic\AssetManager"/>
<service id="lime_thinking_spring.request"
class="Symfony\Component\HttpFoundation\Request"/>
<service id="lime_thinking_spring.cache"
class="Assetic\Cache\FilesystemCache">
<argument>%lime_thinking_spring.cachedir%</argument>
</service>
<service id="lime_thinking_spring.yuicss"
class="Assetic\Filter\Yui\CssCompressorFilter">
<argument>%lime_thinking_spring.yuicomppath%</argument>
</service>
<service id="lime_thinking_spring.assetfactory"
class="Assetic\Factory\AssetFactory">
<argument>%lime_thinking_spring.assetdir%</argument>
<call method="setAssetManager">
<argument type="service" id="lime_thinking_spring.assetmanager" />
</call>
<call method="setFilterManager">
<argument type="service" id="lime_thinking_spring.filtermanager" />
</call>
</service>
</services>
</container>
The XML file now instructs the DIC to call the methods and which service to pass to them. The YUI Compressor is passed to the filter manager, which is in turn, along with the asset manager, passed to the asset factory. This means that the compressor, filter manager and asset manager can all be removed from the PHP code altogether as the asset factory is injected already set up with them. In a similar way we can now go further and just pass in the AsseticController set up with its dependencies already and eliminate these from the code in the final step.
<?php
namespace LimeThinking\SpringBundle\Controller;
use Assetic\Factory\LazyAssetManager;
use Symfony\Bundle\AsseticBundle\Controller\AsseticController;
class AssetController
{
protected $asseticController;
protected $lazyAm;
protected $formulaLoader;
public function __construct(LazyAssetManager $lazyAm,
AsseticController $asseticController,
$formulaLoader)
{
$this->asseticController = $asseticController;
$this->lazyAm = $lazyAm;
$this->formulaLoader = $formulaLoader;
}
public function resourceAction($name, $format)
{
$file = $name.$format;
$formula = $this->formulaLoader->GetFormula($file);
$this->lazyAm->setFormula($file, $formula['formula']);
$response = $this->asseticController->render($file);
$response->headers->set('Content-Type', $formula['contentType']);
return $response;
}
}
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<parameters>
<parameter
key="lime_thinking_spring.formulaloaderclass"
>LimeThinking\SpringBundle\Asset\Loader\XMLLoader</parameter>
<parameter
key="lime_thinking_spring.cachedir"
>%kernel.cache_dir%/assets</parameter>
<parameter
key="lime_thinking_spring.yuicomppath"
>/path/to/yuicompressor.jar</parameter>
<parameter
key="lime_thinking_spring.assetdir"
>%kernel.root_dir%/../src/LimeThinking/SpringBundle/Resources</parameter>
</parameters>
<services>
<service id="lime_thinking_spring.asset" class="LimeThinking\SpringBundle\Controller\AssetController">
<argument type="service" id="lime_thinking_spring.lazyassetmanager" />
<argument type="service" id="lime_thinking_spring.asseticcontroller" />
<argument type="service" id="lime_thinking_spring.formulaloader" />
</service>
<service id="lime_thinking_spring.formulaloader"
class="%lime_thinking_spring.formulaloaderclass%"/>
<service id="lime_thinking_spring.filtermanager"
class="Assetic\FilterManager">
<call method="set">
<argument>yui_css</argument>
<argument type="service" id="lime_thinking_spring.yuicss" />
</call>
</service>
<service id="lime_thinking_spring.assetmanager"
class="Assetic\AssetManager"/>
<service id="lime_thinking_spring.request"
class="Symfony\Component\HttpFoundation\Request"/>
<service id="lime_thinking_spring.cache"
class="Assetic\Cache\FilesystemCache">
<argument>%lime_thinking_spring.cachedir%</argument>
</service>
<service id="lime_thinking_spring.yuicss"
class="Assetic\Filter\Yui\CssCompressorFilter">
<argument>%lime_thinking_spring.yuicomppath%</argument>
</service>
<service id="lime_thinking_spring.assetfactory"
class="Assetic\Factory\AssetFactory">
<argument>%lime_thinking_spring.assetdir%</argument>
<call method="setAssetManager">
<argument type="service" id="lime_thinking_spring.assetmanager" />
</call>
<call method="setFilterManager">
<argument type="service" id="lime_thinking_spring.filtermanager" />
</call>
</service>
<service id="lime_thinking_spring.lazyassetmanager"
class="Assetic\Factory\LazyAssetManager">
<argument type="service" id="lime_thinking_spring.assetfactory" />
</service>
<service id="lime_thinking_spring.asseticcontroller"
class="Symfony\Bundle\AsseticBundle\Controller\AsseticController">
<argument type="service" id="request" />
<argument type="service" id="lime_thinking_spring.lazyassetmanager" />
<argument type="service" id="lime_thinking_spring.cache" />
</service>
</services>
</container>
The LazyAssetManager is passed to the AsseticController by the DIC but as we need to set the formula it still needs to be injected in to the controller as well. It is the same service that is injected into each so this presents to problems, by setting the formula in the LazyAssetManager passed in it is available in the AsseticController.
The code is now cleaner and whilst we have a fairly hefty config file it is now easy to change the configuration by just editing this file. For example to make additional filters available in the filter manager I do not need to touch the PHP code at all, to also make a JavaScript YUICompressor available the config needs changing to this:
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<parameters>
<parameter
key="lime_thinking_spring.formulaloaderclass"
>LimeThinking\SpringBundle\Asset\Loader\XMLLoader</parameter>
<parameter
key="lime_thinking_spring.cachedir"
>%kernel.cache_dir%/assets</parameter>
<parameter
key="lime_thinking_spring.yuicomppath"
>/path/to/yuicompressor.jar</parameter>
<parameter
key="lime_thinking_spring.assetdir"
>%kernel.root_dir%/../src/LimeThinking/SpringBundle/Resources</parameter>
</parameters>
<services>
<service id="lime_thinking_spring.asset"
class="LimeThinking\SpringBundle\Controller\AssetController">
<argument type="service" id="lime_thinking_spring.lazyassetmanager" />
<argument type="service" id="lime_thinking_spring.asseticcontroller" />
<argument type="service" id="lime_thinking_spring.formulaloader" />
</service>
<service id="lime_thinking_spring.formulaloader"
class="%lime_thinking_spring.formulaloaderclass%"/>
<service id="lime_thinking_spring.filtermanager"
class="Assetic\FilterManager">
<call method="set">
<argument>yui_css</argument>
<argument type="service" id="lime_thinking_spring.yuicss" />
</call>
<call method="set">
<argument>yui_js</argument>
<argument type="service" id="lime_thinking_spring.yuijs" />
</call>
</service>
<service id="lime_thinking_spring.assetmanager"
class="Assetic\AssetManager"/>
<service id="lime_thinking_spring.request"
class="Symfony\Component\HttpFoundation\Request"/>
<service id="lime_thinking_spring.cache"
class="Assetic\Cache\FilesystemCache">
<argument>%lime_thinking_spring.cachedir%</argument>
</service>
<service id="lime_thinking_spring.yuicss"
class="Assetic\Filter\Yui\CssCompressorFilter">
<argument>%lime_thinking_spring.yuicomppath%</argument>
</service>
<service id="lime_thinking_spring.yuijs"
class="Assetic\Filter\Yui\JsCompressorFilter">
<argument>%lime_thinking_spring.yuicomppath%</argument>
</service>
<service id="lime_thinking_spring.assetfactory"
class="Assetic\Factory\AssetFactory">
<argument>%lime_thinking_spring.assetdir%</argument>
<call method="setAssetManager">
<argument type="service" id="lime_thinking_spring.assetmanager" />
</call>
<call method="setFilterManager">
<argument type="service" id="lime_thinking_spring.filtermanager" />
</call>
</service>
<service id="lime_thinking_spring.lazyassetmanager"
class="Assetic\Factory\LazyAssetManager">
<argument type="service" id="lime_thinking_spring.assetfactory" />
</service>
<service id="lime_thinking_spring.asseticcontroller"
class="Symfony\Bundle\AsseticBundle\Controller\AsseticController">
<argument type="service" id="request" />
<argument type="service" id="lime_thinking_spring.lazyassetmanager" />
<argument type="service" id="lime_thinking_spring.cache" />
</service>
</services>
</container>
Hopefully this post helps with showing you Dependency Injection can be used to clean up code by separating the configuration or "wiring" of the object tree from the execution, as well as how to do this with the Symfony2 Dependency Injection Container.