Buy

After a container is built, you should compile it:

24 lines dino_container/roar.php
... lines 1 - 10
$container = new ContainerBuilder();
... lines 12 - 16
$container->compile();
runApp($container);
... lines 19 - 24

This starts one final layer to the build process, which anyone can hook into to make final adjustments. For now, it's not doing anything - but it's really important inside the framework.

In a big project - parsing Yaml files and collecting all this service Definition stuff can start to take a lot of time. Our container is nice, but it's coming at a performance cost.

Let's see how much by adding some really basic profiling code. Up top, add a $startTime variable. And down below, figure out how much time elapsed, multiply it by 1000 to get microseconds, and while we're here, round it. And hey, let's use our container to get out the logger and debug a message about this:

29 lines dino_container/roar.php
... lines 1 - 8
$start = microtime(true);
... lines 10 - 19
runApp($container);
$elapsed = round((microtime(true) - $start) * 1000);
$container->get('logger')->debug('Elapsed Time: '.$elapsed.'ms');
... lines 24 - 29

So let's see how long this takes:

php dino_container/roar.php

37ms at first, but then it settles to about 19ms after running a few times. Not bad, but this is a tiny project. Just keep that 19ms number in mind.

Caching the Container

Here's the question: can we take all of this metadata about the container and cache it somehow? Absolutely - and the way it caches is incredible.

After compiling, create a new variable called $dumper and set it to a new PhpDumper object. Pass the $container to the dumper:

33 lines dino_container/roar.php
... lines 1 - 19
$container->compile();
$dumper = new PhpDumper($container);
... lines 22 - 33

This guy is an expert at taking that metadata and caching it to a file. To do that, use the good ol' fashioned file_put_contents - pass it some new file path - how about cached_container.php and for the contents, call $dumper->dump():

33 lines dino_container/roar.php
... lines 1 - 19
$container->compile();
$dumper = new PhpDumper($container);
file_put_contents(__DIR__.'/cached_container.php', $dumper->dump());
... lines 23 - 33

Let's see what this does! Run the script again:

php dino_container/roar.php

Now the cached_container.php file pops into existence. And it's awesome.

The Cached Container

Oh, so many good things to see. First, notice that this dumps a PHP class that extends Container:

142 lines dino_container/cached_container.php
... lines 1 - 3
use Symfony\Component\DependencyInjection\Container;
... lines 5 - 16
class ProjectServiceContainer extends Container
{
... lines 19 - 140
}

That's actually the same base class as the ContainerBuilder we've been working with, and it houses the all-important get() function that fetches out services. In other words, this ProjectServiceContainer looks and acts just like the $container we're using now.

Next, this has our two parameter values sitting on top. And if you call getParameter() to fetch one, it just uses this array:

142 lines dino_container/cached_container.php
... lines 1 - 16
class ProjectServiceContainer extends Container
{
private static $parameters = array(
'root_dir' => '/Users/weaverryan/Sites/knp/knpu-repos/symfony-journey-to-center/dino_container',
'logger_startup_message' => 'Logger just got started!!!',
);
... lines 23 - 100
public function getParameter($name)
{
$name = strtolower($name);
if (!(isset(self::$parameters[$name]) || array_key_exists($name, self::$parameters))) {
throw new InvalidArgumentException(sprintf('The parameter "%s" must be defined.', $name));
}
return self::$parameters[$name];
}
... lines 111 - 140
}

And now, the most important thing to notice: for each of our three services, there's a concrete method that's called when we ask for that service:

142 lines dino_container/cached_container.php
... lines 1 - 53
/**
* Gets the 'logger' service.
... lines 56 - 60
*/
protected function getLoggerService()
{
$this->services['logger'] = $instance = new \Monolog\Logger('main', array(0 => $this->get('logger.stream_handler')));
$instance->pushHandler($this->get('logger.std_out_handler'));
$instance->debug('Logger just got started!!!');
return $instance;
}
... line 71
/**
* Gets the 'logger.std_out_handler' service.
... lines 74 - 78
*/
protected function getLogger_StdOutHandlerService()
{
return $this->services['logger.std_out_handler'] = new \Monolog\Handler\StreamHandler('php://stdout');
}
... line 84
/**
* Gets the 'logger.stream_handler' service.
... lines 87 - 91
*/
protected function getLogger_StreamHandlerService()
{
return $this->services['logger.stream_handler'] = new \Monolog\Handler\StreamHandler('/Users/weaverryan/Sites/knp/knpu-repos/symfony-journey-to-center/dino_container/dino.log');
}
... lines 97 - 142

Seriously, if you look at the get() function in the parent class, you'll find that calling $container->get('logger.std_out_logger') will ultimately execute this getLogger_StdOutLoggerService() method.

And these methods use the exact PHP code we would write to instantiate these objects directly. We pass the container Definition objects, and it dumps the raw PHP code that those represent.

This is even more incredible when you look at the getLoggerService() method:

142 lines dino_container/cached_container.php
... lines 1 - 61
protected function getLoggerService()
{
$this->services['logger'] = $instance = new \Monolog\Logger('main', array(0 => $this->get('logger.stream_handler')));
$instance->pushHandler($this->get('logger.std_out_handler'));
$instance->debug('Logger just got started!!!');
return $instance;
}
... lines 71 - 142

Look closely: it creates the new Logger object, passes main and then passes an array, with a call to $this->get('logger.stream_handler') to fetch that service from itself - the container. The second arguments key in the Yaml file causes this.

Next, it has our two method calls: pushHandler() with $this->get('logger.std_out_logger') and then a call to debug(). Everything we put into those Definitions are dumped into a real PHP file that contains the raw code we would've written anyways.

So, if we use this container class directly, then fetching objects out of it could not be faster. Let's do it!

Using the Cached Container

Copy the path to the file and create a new $cachedContainer variable way up top before we even start with the ContainerBuilder. Our app now has two options: we can create the ContainerBuilder, load it up with the Definition config and then use it, OR, if that cached container is available, we can skip everything and just use it. After all. if we call get('logger') on it, it'll give us the exact same Logger.

So, if (!file_exists($cachedContainer)), then we do need to do all the building work to dump the container:

39 lines dino_container/roar.php
... lines 1 - 12
require __DIR__.'/../vendor/autoload.php';
$cachedContainer = __DIR__.'/cached_container.php';
if (!file_exists($cachedContainer)) {
$container = new ContainerBuilder();
$container->setParameter('root_dir', __DIR__);
$loader = new YamlFileLoader($container, new FileLocator(__DIR__ . '/config'));
$loader->load('services.yml');
$container->compile();
$dumper = new PhpDumper($container);
file_put_contents(__DIR__ . '/cached_container.php', $dumper->dump());
}
... lines 27 - 39

But one way or another, that file eventually exists. So if we require it, we can say $container = new \ProjectServiceContainer(), which is the class name used in the cache file:

39 lines dino_container/roar.php
... lines 1 - 14
$cachedContainer = __DIR__.'/cached_container.php';
if (!file_exists($cachedContainer)) {
... lines 17 - 25
}
require $cachedContainer;
$container = new \ProjectServiceContainer();
runApp($container);
... lines 31 - 39

We're still passing this $container into runApp(), and even though it's technically a different object, it's not going to make any difference. The only thing we need to change is that runApp() is type-hinted with ContainerBuilder. Well, it turns out that what we really need is Container, which is the base class for the builder and our cached class.

So I'll change the type-hint to Container. And we can go a step further: the Container class implements an interface called ContainerInterface:

39 lines dino_container/roar.php
... lines 1 - 6
use Symfony\Component\DependencyInjection\ContainerInterface;
... lines 8 - 34
function runApp(ContainerInterface $container)
{
$container->get('logger')->info('ROOOOAR');
}

Ok, try out the brand new cached container!

php dino_container/roar.php

It works! And woh - check out that elapsed time: 4ms, down from 19. If you delete the cached_container.php file, the next run takes 22ms because it needs to rebuild it. Then we're right back down to 4ms. This is one reason why Symfony is able to be so fast, even in big systems.

Now that you've got the real story of how container building works, let's see how things look inside Symfony.

Leave a comment!