How Symfony Builds the Container
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 SubscribeWe 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:
// ... 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
:
// ... 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
:
// ... 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:
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
:
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
:
// ... 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.