Dependency Injection Extensions
Keep on Learning!
If you liked what you've learned so far, dive in! Subscribe to get access to this tutorial plus video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeThese 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:
// ... 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
:
// ... 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
:
// ... 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:
<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:
// ... 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.