Buy Access to Course
07.

How Symfony Builds the Container

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

We rock at building containers. So now let's see how it's build inside of Symfony.

Setting up app_dev.php for Debugging

To figure things out, let's jump straight to the code, starting with the app_dev.php front controller. We're going to add some var_dump statements to core classes, and for that to actually work, we need to make a few changes here. First, instead of loading bootstrap.php.cache, require autoload.php. Second, make sure this $kernel->loadClassCache() line is commented out:

32 lines | web/app_dev.php
// ... lines 1 - 19
//$loader = require_once __DIR__.'/../app/bootstrap.php.cache';
$loader = require_once __DIR__.'/../app/autoload.php';
// ... lines 22 - 25
$kernel = new AppKernel('dev', true);
//$kernel->loadClassCache();
// ... lines 28 - 32

A copy of some really core classes in Symfony are stored in the cache directory for a little performance boost. These two changes turn that off so that if we var_dump somewhere, it'll definitely work.

Booting the Kernel

In the first journey episode, we followed this $kernel->handle() method to find out what happens between the request and response. But this method does something else too. Click to open it up: it lives in a core Kernel class. Inside handle(), it calls boot() on itself:

// ... lines 1 - 11
namespace Symfony\Component\HttpKernel;
// ... lines 13 - 44
abstract class Kernel implements KernelInterface, TerminableInterface
{
// ... lines 47 - 178
public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQUEST, $catch = true)
{
if (false === $this->booted) {
$this->boot();
}
return $this->getHttpKernel()->handle($request, $type, $catch);
}
// ... lines 187 - 808
}

But first, let me back up a second. Remember that the $kernel here is an instance of our AppKernel, and that extends this core Kernel.

The boot() method has one job: build the container. And most of the real work happens inside the initializeContainer() function:

// ... lines 1 - 551
protected function initializeContainer()
{
$class = $this->getContainerClass();
$cache = new ConfigCache($this->getCacheDir().'/'.$class.'.php', $this->debug);
// ... line 556
if (!$cache->isFresh()) {
$container = $this->buildContainer();
$container->compile();
$this->dumpContainer($cache, $container, $class, $this->getContainerBaseClass());
// ... lines 561 - 562
}
require_once $cache;
$this->container = new $class();
// ... lines 568 - 572
}
// ... lines 574 - 810

Hey, this looks really familiar. The container is built on line 558, and we'll look more at that function. Then its compiled and dumpContainer() writes the cached PHP container class. I'll show you - jump into the dumpContainer() function:

// ... lines 1 - 703
protected function dumpContainer(ConfigCache $cache, ContainerBuilder $container, $class, $baseClass)
{
// cache the container
$dumper = new PhpDumper($container);
// ... lines 708 - 712
$content = $dumper->dump(array('class' => $class, 'base_class' => $baseClass));
// ... lines 714 - 717
$cache->write($content, $container->getResources());
}
// ... lines 720 - 810

Hey, there's our PhpDumper class - it does the same thing we did by hand before.

Back in initializeContainer(), it finishes off by requiring the cached container file and creating a new instance:

// ... lines 1 - 551
protected function initializeContainer()
{
$class = $this->getContainerClass();
$cache = new ConfigCache($this->getCacheDir().'/'.$class.'.php', $this->debug);
// ... lines 556 - 564
require_once $cache;
$this->container = new $class();
$this->container->set('kernel', $this);
// ... lines 569 - 572
}
// ... lines 574 - 810

So Symfony creates and dumps the container just like we did.

kernel. and Environment Parameters

There are a lot of little steps that go into building the container, so I'll jump us to the important parts. Go into buildContainer() and look at the line that calls $this->getContainerBuilder():

// ... lines 1 - 628
protected function buildContainer()
{
// ... lines 631 - 640
$container = $this->getContainerBuilder();
// ... line 642
$this->prepareContainer($container);
if (null !== $cont = $this->registerContainerConfiguration($this->getContainerLoader($container))) {
$container->merge($cont);
}
// ... lines 648 - 650
return $container;
// ... lines 652 - 810

If we jump to that function, we can see the line that actually creates the new ContainerBuilder object - just like we did before:

// ... lines 1 - 684
protected function getContainerBuilder()
{
$container = new ContainerBuilder(new ParameterBag($this->getKernelParameters()));
// ... lines 688 - 692
return $container;
}
// ... lines 695 - 810

The only addition is that it passes it some parameters to start out. These are in getKernelParameters():

// ... lines 1 - 579
protected function getKernelParameters()
{
$bundles = array();
foreach ($this->bundles as $name => $bundle) {
$bundles[$name] = get_class($bundle);
}
return array_merge(
array(
'kernel.root_dir' => $this->rootDir,
'kernel.environment' => $this->environment,
'kernel.debug' => $this->debug,
'kernel.name' => $this->name,
'kernel.cache_dir' => $this->getCacheDir(),
'kernel.logs_dir' => $this->getLogDir(),
'kernel.bundles' => $bundles,
'kernel.charset' => $this->getCharset(),
'kernel.container_class' => $this->getContainerClass(),
),
$this->getEnvParameters()
);
}
// ... lines 602 - 810

You probably recognize some of these - like kernel.root_dir, and now you know where they come from. It also calls getEnvParameters():

// ... lines 1 - 609
protected function getEnvParameters()
{
$parameters = array();
foreach ($_SERVER as $key => $value) {
if (0 === strpos($key, 'SYMFONY__')) {
$parameters[strtolower(str_replace('__', '.', substr($key, 9)))] = $value;
}
}
return $parameters;
}
// ... lines 621 - 810

You may not know about this feature: if you set an environment variable that starts with SYMFONY__, that prefix is stripped and its added as a parameter automatically. That magic comes from right here

The Cached Container

Back in buildContainer(), let's var_dump() the $container so far to see what we've got:

// ... lines 1 - 628
protected function buildContainer()
{
// ... lines 631 - 640
$container = $this->getContainerBuilder();
var_dump($container);die;
// ... lines 643 - 652
}
// ... lines 654 - 811

Ok, refresh! Hmm, it didn't hit my code. Why? Well, the container might already be cached, so it's not going through the building process. To force a build, you can delete the cached container file. But before you do that, I'll look inside - it's located at app/cache/dev/appDevDebugProjectContainer.php:

4243 lines | app/cache/dev/appDevDebugProjectContainer.php
// ... lines 1 - 16
class appDevDebugProjectContainer extends Container
{
private static $parameters = array(
'kernel.root_dir' => '/Users/weaverryan/Sites/knp/knpu-repos/symfony-journey-to-center/app',
'kernel.environment' => 'dev',
// ... lines 22 - 640
);
// ... lines 642 - 3820
/**
* Gets the 'user_agent_subscriber' service.
// ... lines 3823 - 3828
protected function getUserAgentSubscriberService()
{
return $this->services['user_agent_subscriber'] = new \AppBundle\EventListener\UserAgentSubscriber($this->get('logger'));
}
// ... lines 3833 - 4241
}

It's a lot bigger and has a different class name, but this is just like our cached container: it has all the parameters on top, then a bunch of methods to create the services. Now go delete that file and refresh.

rm app/cache/dev/appDevDebugProjectContainer.php

Great: now we see the dumped container. I want you to notice a few things. First, there are no service definitions at all. But we do have the 9 parameters. And that's it - the container is basically empty so far.

Loading the Yaml Files

To fill it with services, we'll load a Yaml file that'll supply some service definitions. Back in buildContainer(), this happens when the registerContainerConfiguration() method is called:

// ... lines 1 - 628
protected function buildContainer()
{
// ... lines 631 - 640
$container = $this->getContainerBuilder();
// ... line 642
$this->prepareContainer($container);
if (null !== $cont = $this->registerContainerConfiguration($this->getContainerLoader($container))) {
$container->merge($cont);
}
// ... lines 648 - 650
return $container;
}
// ... lines 653 - 810

I did skip a few things - but no worries, we'll cover them in a minute. This function actually lives in our AppKernel:

40 lines | app/AppKernel.php
// ... lines 1 - 5
class AppKernel extends Kernel
{
// ... lines 8 - 34
public function registerContainerConfiguration(LoaderInterface $loader)
{
$loader->load(__DIR__.'/config/config_'.$this->getEnvironment().'.yml');
}
}

The LoaderInterface argument is an object that's a lot like the YamlFileLoader that we created manually in roar.php. This loader can also read other formats, like XML. But beyond that, it's the same: you create a loader and then pass it a file full of services.

When Symfony boots, it only loads one configuration file - config_dev.yml if you're in the dev environment:

49 lines | app/config/config_dev.yml
imports:
- { resource: config.yml }
framework:
router:
resource: "%kernel.root_dir%/config/routing_dev.yml"
strict_requirements: true
profiler: { only_exceptions: false }
web_profiler:
toolbar: true
intercept_redirects: false
// ... lines 13 - 49

I know you've looked at that file before, but two really important things are hiding here. I mentioned earlier that these configuration files have only three valid root keys: services (of course), parameters (of course) and imports - to load other files. But in this file - and almost every file in this directory - you see mostly other stuff, like framework, webprofiler and monolog. Having these root keys should be illegal. But in fact, they're the secret to how almost every service is added to the container. We'll explore those next - so ignore them for now.

The other important thing is that config_dev.yml imports config.yml:

74 lines | app/config/config.yml
imports:
- { resource: parameters.yml }
- { resource: security.yml }
- { resource: services.yml }
// ... lines 5 - 74

And config.yml loads parameters.yml, security.yml and services.yml. Every file in the app/config directory - except the routing files - are being loaded by the container in order to provide services. In other words, all of these files have the exact same purpose as the services.yml file we played with before inside of dino_container.

The weird part is that none of these files have any services in them, except for one: services.yml:

12 lines | app/config/services.yml
// ... lines 1 - 5
services:
user_agent_subscriber:
class: AppBundle\EventListener\UserAgentSubscriber
arguments: ["@logger"]
tags:
- { name: kernel.event_subscriber }

It holds our user_agent_subscriber service from episode 1. This gives us one service definition and parameters.yml adds a few parameters.

So after the registerContainerConfiguration() line is done, we've gone from zero services to only 1. Let's dump to prove it - $container->getDefinitions().

// ... lines 1 - 628
protected function buildContainer()
{
// ... lines 631 - 644
if (null !== $cont = $this->registerContainerConfiguration($this->getContainerLoader($container))) {
$container->merge($cont);
}
var_dump($container->getDefinitions());die;
// ... lines 649 - 652
}
// ... lines 654 - 811

Refresh! Yep, there's just our one user_agent_subscriber service. We can dump the parameters too - $container->getParameterBag()->all():

// ... lines 1 - 628
protected function buildContainer()
{
// ... lines 631 - 644
if (null !== $cont = $this->registerContainerConfiguration($this->getContainerLoader($container))) {
$container->merge($cont);
}
var_dump($container->getParameterBag()->all());die;
// ... lines 649 - 652
}
// ... lines 654 - 811

This dumps out the kernel parameters from earlier plus the stuff from parameters.yml.

So even though the container is still almost empty, we've nearly reached the end. This empty-ish container is returned to initializeContainer() where it's compiled and then dumped:

// ... lines 1 - 551
protected function initializeContainer()
{
// ... lines 554 - 557
$container = $this->buildContainer();
$container->compile();
$this->dumpContainer($cache, $container, $class, $this->getContainerBaseClass());
// ... lines 561 - 572
}
// ... lines 574 - 810

Before compiling, we only have 1 service. But we know from running container:debug that there are a lot of services when things finish. The secret is in the compile() function, which does two special things: process dependency injection extensions and run compiler passes. Those are up next.