If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
After a container is built, you should compile
it:
... 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:
... 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.
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:
... 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()
:
... 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.
Oh, so many good things to see. First, notice that this dumps a PHP class
that extends Container
:
... 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:
... 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:
... 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:
... 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!
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:
... 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:
... 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
:
... 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.
// composer.json
{
"require": {
"php": ">=5.3.3",
"symfony/symfony": "2.6.x-dev", // 2.6.x-dev
"doctrine/orm": "~2.2,>=2.2.3", // v2.4.6
"doctrine/doctrine-bundle": "~1.2", // v1.2.0
"twig/extensions": "~1.0", // v1.2.0
"symfony/assetic-bundle": "~2.3", // v2.5.0
"symfony/swiftmailer-bundle": "~2.3", // v2.3.7
"symfony/monolog-bundle": "~2.4", // v2.6.1
"sensio/distribution-bundle": "~3.0", // v3.0.9
"sensio/framework-extra-bundle": "~3.0", // v3.0.3
"incenteev/composer-parameter-handler": "~2.0", // v2.1.0
"hautelook/alice-bundle": "~0.2" // 0.2
},
"require-dev": {
"sensio/generator-bundle": "~2.3" // v2.4.0
}
}