Symfony's Third Generated PHP File

This is a correction to a previous post where I asserted the Symfony framework (which OroCRM is built in), has two generated PHP files to contend with. There are actually (at least) three.

In addition to the app container file and the bootstrap cache file, there's also an application class cache file. Like the container class, this file is generated and stored in the cache

app/cache/dev/classes.php
app/cache/prod/classes.php    

This file is similar to that bootstrap cache, in that it combines a number of Symfony class files into a single PHP include. The difference is, unlike app/bootstrap.php.cache, classes in classes.php come from bundles used in the specific application, and are not limited to files from Symfony itself.

Generating classes.php

The generation of classes.php is a bit complicated. First, this file is not generated as part of Symfony's cache warmup. You can call

php app/console cache:clear

all you like, but that won't regenerate classes.php. Instead, you need to load an actual application page, or otherwise boot your application kernel.

#File: vendor/symfony/symfony/src/Symfony/Component/HttpKernel/Kernel.php
public function boot()
{
    //...
    if ($this->loadClassCache) {
        $this->doLoadClassCache($this->loadClassCache[0], $this->loadClassCache[1]);
    }
    //...
}    

It's the call to doLoadClassCache which triggers a call to ClassCollectionLoader::load

#File: vendor/symfony/symfony/src/Symfony/Component/HttpKernel/Kernel.php
protected function doLoadClassCache($name, $extension)
{
    if (!$this->booted && is_file($this->getCacheDir().'/classes.map')) {
        ClassCollectionLoader::load(include($this->getCacheDir().'/classes.map'), $this->getCacheDir(), $name, $this->debug, false, $extension);
    }
}

The work of generating classes.php is done in ClassCollectionLoader::load. Simple enough — except you need to pass in a list of classes you want to combine. These classes come from the classes.map file.

app/cache/dev/classes.map
app/cache/prod/classes.map

Of course, now our question is "where does classes.map come from?". The classes.map file is generated at the same time as the app container file, right after the call to $container->compile() in initializeContainer.

#File: vendor/symfony/symfony/src/Symfony/Component/HttpKernel/Kernel.php
protected function initializeContainer()
{
    //...
    if (!$cache->isFresh()) {
        //...
        $container->compile();
        //...
    }
    //...
}

Container compilation is a bit much to get into for a quickies post, but from a high level view the compiler looks through every installed bundle in the system, and checks if it has a dependency injection extension. A dependency injection extension class is a file located in a bundle's DependencyInjection folder, ending in the word Extension, and extending the base class Sensio\Bundle\FrameworkExtraBundle\DependencyInjectionExtension.

One example is the following file

vendor/sensio/framework-extra-bundle/Sensio/Bundle/FrameworkExtraBundle/DependencyInjection/SensioFrameworkExtraExtension.php

The dependency injection extension is a standard part of bundles generated with the app/console generate:bundle command.

If the bundle has this sort of extension, the container compiler asks it if it wants to put any files in the classes.php cache. A bundle developer specifies if they want any class files dropped into this class cache in the extension's load method.

#File: vendor/sensio/framework-extra-bundle/Sensio/Bundle/FrameworkExtraBundle/DependencyInjection/SensioFrameworkExtraExtension.php
public function load(array $configs, ContainerBuilder $container)
{
    //...
    $this->addClassesToCompile(array(
        // cannot be added because it has some annotations
        //'Sensio\\Bundle\\FrameworkExtraBundle\\Configuration\\ParamConverter',
        'Sensio\\Bundle\\FrameworkExtraBundle\\EventListener\\ParamConverterListener',
        'Sensio\\Bundle\\FrameworkExtraBundle\\Request\\ParamConverter\\DateTimeParamConverter',
        'Sensio\\Bundle\\FrameworkExtraBundle\\Request\\ParamConverter\\DoctrineParamConverter',
        'Sensio\\Bundle\\FrameworkExtraBundle\\Request\\ParamConverter\\ParamConverterInterface',
        'Sensio\\Bundle\\FrameworkExtraBundle\\Request\\ParamConverter\\ParamConverterManager',
    ));        
    //...
}

This tells the extension about the classes, the extension tells the container compiler, the container compiler generates classes.map, and the app kernel uses classes.map to generate classes.php.

Not the most straight forward process for a newcomer — but not something a standard system user is going to encounter on a regular basis. The most important takeaway is if you're the sort of developer who likes adding debug code to core framework files, you may need to do it in classes.php instead of the files in vendor/.