Symfony2 & Metadata: Caching Class- and PropertyMetadata

Posted on by Matthias Noback

After creating a metadata factory and metadata drivers, we now need a way to cache the metadata, since collecting them at each request is way too inefficient. Luckily, the Metadata library provides us with a FileCache (though it may be any kind of a cache, as long as it implements the same CacheInterface). First, we make the FileCache available as a service, and add a call to setCache on the metadata factory:

<parameter key="matthias_annotation.metadata.cache_class">Metadata\Cache\FileCache</parameter>

<!-- ... -->

<service id="matthias_annotation.metadata.cache" class="%matthias_annotation.metadata.cache_class%" public="false">
    <argument /><!-- the cache directory (to be set later) -->
</service>

<service id="matthias_annotation.metadata_factory" class="%matthias_annotation.metadata_factory.class%" public="false">
    <argument type="service" id="matthias_annotation.driver_chain" />
    <!-- call setCache with the new cache service: -->
    <call method="setCache">
        <argument type="service" id="matthias_annotation.metadata.cache" />
    </call>
</service>

This provides the metadata factory with the file cache.

Prepare the cache directory

The FileCache needs to be told where to save the cached metadata to - a directory. We obviously want to place the metadata cache directory inside the cache directory used by the framework. The location of this directory is available as the kernel.cache_dir parameter. We can resolve this parameter using $container->getParameterBag()->resolveValue() to it's real value when we are inside the MatthiasAnnotationExtension::load() method. Then we make the resolved value (i.e. the real location of the cache directory) the first argument of the cache service, only after we have created it (if it does not already exist):

class MatthiasAnnotationExtension extends Extension
{
    public function load(array $configs, ContainerBuilder $container)
    {
        // ...

        // probably make this configurable...
        $cacheDirectory = '%kernel.cache_dir%/matthias_annotation';
        $cacheDirectory = $container->getParameterBag()->resolveValue($cacheDirectory);
        if (!is_dir($cacheDirectory)) {
            mkdir($cacheDirectory, 0777, true);
        }

        // the cache directory should be the first argument of the cache service
        $container
            ->getDefinition('matthias_annotation.metadata.cache')
            ->replaceArgument(0, $cacheDirectory)
        ;
    }
}

Prepare the PropertyMetadata for caching

When things need to be cached, they should be prepared for it. As you may remember, we stored the default value for properties as the public property $defaultValue of the custom PropertyMetadata class. Inside the serialize() and unserialize() methods, we need to take this $defaultValue into account, otherwise, it won't be cached and thus will not survive another request. Looking at the parent methods, we should also take into account the values for PropertyMetadata::$class and PropertyMetadata::$name:

namespace Matthias\AnnotationBundle\Metadata;

use Metadata\PropertyMetadata as BasePropertyMetadata;

class PropertyMetadata extends BasePropertyMetadata
{
    public $defaultValue;

    public function serialize()
    {
        return serialize(array(
            $this->class,
            $this->name,
            $this->defaultValue,
        ));
    }

    public function unserialize($str)
    {
        list($this->class, $this->name, $this->defaultValue) = unserialize($str);

        $this->reflection = new \ReflectionProperty($this->class, $this->name);
        $this->reflection->setAccessible(true);
    }
}

Take a look at the cache

After a first round using the infamous DefaultValueProcessor I created in my first post about this subject, we now have inside the cache directory a file called Matthias-AnnotationBundle-Data-SomeClass.cache.php. Indeed, it contains the serialized data:

<?php return unserialize('C:31:"Metadata\\MergeableClassMetadata":277:{a:5:{i:0;s:40:"Matthias\\AnnotationBundle\\Data\\SomeClass";i:1;a:0:{}i:2;a:1:{s:4:"name";C:51:"Matthias\\AnnotationBundle\\Metadata\\PropertyMetadata":97:{a:3:{i:0;s:40:"Matthias\\AnnotationBundle\\Data\\SomeClass";i:1;s:4:"name";i:2;s:12:"Menno Backer";}}}i:3;a:0:{}i:4;i:1333396090;}}');

This will of course save the metadata factory a lot of reflection work.

PHP Symfony2 annotations cache metadata reflection