Buy Access to Course
08.

Dependency Injection Extensions

Share this awesome video!

|

Keep on Learning!

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

These Yaml files should only have keys for services, parameters and imports. What if I just make something up, like journey and put a dino_count of 10 under it:

77 lines | app/config/config.yml
// ... lines 1 - 5
journey:
dino_count: 10
// ... lines 8 - 77

When we refresh, we get a huge error!

There is no extension able to load the configuration for "journey".

And it says it found valid namespaces for framework, security, twig, monolog, blah blah blah. Hey, those are the root keys that we have in our config files. So what makes journey invalid but framework valid? And what does framework do anyways?

Take out that journey code.

Registering of Extension Classes

The answer lives in the bundle classes. Open up AppBundle:

10 lines | src/AppBundle/AppBundle.php
// ... lines 1 - 4
use Symfony\Component\HttpKernel\Bundle\Bundle;
class AppBundle extends Bundle
{
}

This is empty, but it extends Symfony's base Bundle class. The key method is getContainerExtension():

// ... lines 1 - 28
abstract class Bundle extends ContainerAware implements BundleInterface
{
// ... lines 31 - 71
public function getContainerExtension()
{
if (null === $this->extension) {
$class = $this->getContainerExtensionClass();
if (class_exists($class)) {
$extension = new $class();
// ... lines 78 - 88
$this->extension = $extension;
} else {
$this->extension = false;
}
}
if ($this->extension) {
return $this->extension;
}
}
// ... lines 99 - 212

When Symfony boots, it calls this method on each bundle looking for something called an Extension. This calls getContainerExtensionClass() and checks to see if that class exists. Move down to that method:

// ... lines 1 - 204
protected function getContainerExtensionClass()
{
$basename = preg_replace('/Bundle$/', '', $this->getName());
return $this->getNamespace().'\\DependencyInjection\\'.$basename.'Extension';
}
// ... lines 211 - 212

Ah, and here's the magic. To find this "extension" class, it looks for a DependencyInjection directory and a class with the same name as the bundle, except replacing Bundle with Extension. For example, for AppBundle, it's looking for a DependencyInjection\AppExtension class. We don't have that.

Open up the TwigBundle class and double-click the directory tree at the top to move PhpStorm here. TwigBundle does have a DependencyInjection directory and a TwigExtension inside:

// ... lines 1 - 25
class TwigExtension extends Extension
{
// ... lines 28 - 33
public function load(array $configs, ContainerBuilder $container)
{
// ... lines 36 - 130
}
// ... lines 132 - 155
}

So because this is here, it's automatically registered with the container. We may not know what an extension does yet, but we know how it's all setup.

Registering Twig Globals

Forget about extensions for a second and let me tell you about a totally unrelated feature. If you want to add a global variable to Twig, one way to do that is under the twig config. Just add globals, then set something up. I'll say twitter_username: weaverryan:

76 lines | app/config/config.yml
// ... lines 1 - 28
twig:
debug: "%kernel.debug%"
strict_variables: "%kernel.debug%"
globals:
twitter_username: weaverryan
// ... lines 34 - 76

And just by doing that, we could open up any Twig template and have access to a twitter_username variable. My question is: how does that work?

The Extension load() Method

To answer that, look back at TwigExtension. The first secret is that when we call compile() on the container, this load() method is called. In fact the load() method is called on every extension that's registered with Symfony: so every class that follows the DependencyInjection\Extension naming-convention.

Let's dump the $configs variable, because I don't know what that is yet:

// ... lines 1 - 33
public function load(array $configs, ContainerBuilder $container)
{
var_dump($configs);die;
// ... lines 37 - 131
}
// ... lines 133 - 158

Go back and refresh! Ok: it dumps an array with the twig configuration. Whatever we have in config.yml under twig is getting passed to TwigExtension:

In fact, that's the rule. The fact that we have a key called framework means that this config will be passed to a class called FrameworkExtension. If you want to see how this config is used, look there. With the assetic key, that's passed to AsseticExtension. These extension classes have a getAlias() method in them, and that returns a lower-cased version of the class name without the word Extension.

Extensions Load Services

These extensions have two jobs. First, they add service definitions to the container. Because after all, the main reason for adding a bundle is to add services to your container.

The way it does this is just like our roar.php file, except it loads an XML file instead of Yaml:

// ... lines 1 - 33
public function load(array $configs, ContainerBuilder $container)
{
// ... line 36
$loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
$loader->load('twig.xml');
// ... lines 39 - 131
}
// ... lines 133 - 158

Let's open up that Resources/config/twig.xml file:

<?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="twig.class">Twig_Environment</parameter>
// ... lines 9 - 28
</parameters>
<services>
<service id="twig" class="%twig.class%">
<argument type="service" id="twig.loader" />
<argument>%twig.options%</argument>
<call method="addGlobal">
<argument>app</argument>
<argument type="service" id="templating.globals" />
</call>
</service>
<service id="twig.cache_warmer" class="%twig.cache_warmer.class%" public="false">
<tag name="kernel.cache_warmer" />
<argument type="service" id="service_container" />
<argument type="service" id="templating.finder" />
</service>
// ... lines 46 - 141
</services>
</container>

If you ever wondered where the twig service comes from, it's right here! You can see it in container:debug:

php app/console container:debug twig

So the first job of an extension class is to add services, which it always does by loading one or more XML files.

Extensions Configuration

The second job is to read our configuration array and use that information to mutate the service definitions. We'll see code that does this shortly.

Most extensions will have two lines near the top that call getConfiguration() and processConfiguration:

// ... lines 1 - 33
public function load(array $configs, ContainerBuilder $container)
{
// ... lines 36 - 52
$configuration = $this->getConfiguration($configs, $container);
$config = $this->processConfiguration($configuration, $configs);
// ... lines 56 - 131
}
// ... lines 133 - 158

Next to every extension class, you'll find a class called Configuration:

// ... lines 1 - 22
class Configuration implements ConfigurationInterface
{
// ... lines 25 - 199
}

Watch out, a meteor! Oh, never mind, it's just the awesome fact that if I mess up some configuration - like globals as globalsss in Yaml, we'll get a really nice error. That doesn't happen by accident, that system evolved these Configuration classes to make that happen.

This is probably one of the more bizarre classes you'll see: it builds a tree of valid configuration that can be used under this key. It adds a globals section, which says that the children are an array. It even has some stuff to validate and normalize what we put here:

// ... lines 1 - 104
private function addGlobalsSection(ArrayNodeDefinition $rootNode)
{
$rootNode
->fixXmlConfig('global')
->children()
->arrayNode('globals')
->normalizeKeys(false)
->useAttributeAsKey('key')
->example(array('foo' => '"@bar"', 'pi' => 3.14))
->prototype('array')
// ... lines 115 - 124
->beforeNormalization()
->ifTrue(function ($v) {
if (is_array($v)) {
$keys = array_keys($v);
sort($keys);
return $keys !== array('id', 'type') && $keys !== array('value');
}
return true;
})
->then(function ($v) { return array('value' => $v); })
->end()
// ... lines 138 - 147
->end()
->end()
->end()
;
}
// ... lines 153 - 201

These Configuration classes are tough to write, but pretty easy to read. And if you can't get something to configure correctly, opening up the right Configuration class might give you a hint.

Back in TwigExtension, let's dump $config after calling processConfiguration():

// ... lines 1 - 33
public function load(array $configs, ContainerBuilder $container)
{
// ... lines 36 - 51
$configuration = $this->getConfiguration($configs, $container);
$config = $this->processConfiguration($configuration, $configs);
var_dump($config);die;
// ... lines 56 - 131
}
// ... lines 133 - 158

This dumps out a nice, normalized and validated version of our config, including keys we didn't have, with their default values.

Extensions Mutate Definitions

So finally, how is the globals key used? Scroll down to around line 90:

// ... lines 1 - 33
public function load(array $configs, ContainerBuilder $container)
{
// ... lines 36 - 86
if (!empty($config['globals'])) {
$def = $container->getDefinition('twig');
foreach ($config['globals'] as $key => $global) {
// ... lines 90 - 92
$def->addMethodCall('addGlobal', array($key, $global['value']));
// ... line 94
}
}
// ... lines 97 - 130
}
// ... lines 132 - 157

For most people, this code will look weird. But not us! If there are globals, it gets the twig Definition back out of the ContainerBuilder. This definition was added when it loaded twig.xml, and now we're going to tweak it. Just focus on the second part of the if: it calls $def->addMethodCall() and passes it addGlobal and two arguments: our key from the config, and the value - weaverryan in this case.

If you read the Twig documentation, it tells you that if you want to add a global variable, you can call addGlobal on the Twig_Environment object. And that's exactly what this does. This type of stuff is super typical for extensions.

If you refresh without any debug code, we'll get a working page again. Now open up the cached container - app/cache/dev/appDevDebugProjectContainer.php and find the method that creates the twig service - getTwigService(). Make sure you spell that correctly:

4244 lines | app/cache/dev/appDevDebugProjectContainer.php
// ... lines 1 - 3703
protected function getTwigService()
{
$this->services['twig'] = $instance = new \Twig_Environment($this->get('twig.loader'), array('debug' => true, 'strict_variables' => true, 'exception_controller' => 'twig.controller.exception:showAction', 'form_themes' => array(0 => 'form_div_layout.html.twig'), 'autoescape_service' => NULL, 'autoescape_service_method' => NULL, 'cache' => '/Users/weaverryan/Sites/knp/knpu-repos/symfony-journey-to-center/app/cache/dev/twig', 'charset' => 'UTF-8', 'paths' => array()));
// ... lines 3707 - 3725
$instance->addGlobal('twitter_username', 'weaverryan');
return $instance;
}
// ... lines 3730 - 4244

Near the bottom, we see it: $instance->addGlobal('twitter_username', 'weaverryan'). We passed in simple configuration, TwigExtension used that to mutate the twig Definition, and ultimately the dumped container is updated.

That's the power of the dependency injection extensions, and if it makes even a bit of sense, you're awesome.

Our Configuration Wins

Oh, and one more cool note. If I added a twig service to config.yml, would it override the one from TwigBundle? Actually yes: even though the extensions are called after loading these files, any parameters or services we add here win.