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
Comments
This website uses MailComments: you can send your comments to this post by email. Read more about MailComments, including suggestions for writing your comments (in HTML or Markdown).
Lewis Wright

Brilliant series, very useful indeed. Do you have any recommendations for loading the metadata for a whole directory or namespace (akin to how Doctrine does it for entities)? I could use the Symfony finder component, but I need a way of caching the results to speed things up a bit. I having a feeling that the ConfigCache component you've also written about might be useful for this, but I'm struggling to find a good example of combining this with the metadata library. Any tips would be very welcome!

Matthias Noback

Thank you very much! The Metadata library already offers a caching layer as described in this article, so you only need to load the metadata for all relevant classes. This should be done inside a cache warmer (as described for example here: http://blog.servergrove.com.... That way, at runtime, there will be the smallest possible performance penalty. You don't need to scan the directories and rebuild the cache, because the Metadata library itself will automatically reload metadata for modified classes if it is in debug mode.

David lin

Hi, just wonder how can we invalidate the cache automatically when we update the metadata, say, 'defaultValue'. I am not seeing the cache getting update automatically.

Matthias Noback

Good question. The third constructor argument of the metadata factory is "debug". When set to "true", this will be taken care off. You can use "%kernel.debug%" as the argument in the service definition.

David lin

Hi, more needs to be done. The checking of cache relies on checking the filemtime of cache files. the cache file path should be added to $classMetadata->fileResources[] array. However, to get the classMetadata cache file path, one has to extend the FileCache class and MetadataFactory class because the cache dir in the FileCache is private and the cache object in MetadataFactory is also private.

Matthias Noback

I didn't remember it was this hard! Thanks for digging into it.

David

This was a fantastic set of blogs. Thank you for taking the time to lay it all out so well, it was a huge help.

Matthias Noback

Thanks for letting me know!

Chad

Thanks for the write-up, it has been quite helpful! Do you have any details on the "matthias_annotation.driver_chain" you're using in the first step of the example? I've been using the JMSSerializerBundle as a reference but can't quite connect all the dots.

Matthias Noback

Hi Chad, thanks for commenting - this post is the third post in a row about metadata. You will find the driver_chain service defined in part 2: http://php-and-symfony.matt...
Good luck getting it all together!

Chad

Whoops...didn't think to follow that link apparently. Thanks again, finally got everything working.