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.
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!
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.
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.
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.
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.
I didn't remember it was this hard! Thanks for digging into it.
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.
Thanks for letting me know!
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.
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!
Whoops...didn't think to follow that link apparently. Thanks again, finally got everything working.