In my previous post, I wrote about annotations and how to gather them using a metadata factory and an annotation driver. I also mentioned a future post in which I explain other ways of collecting metadata, namely by locating and parsing files in several different formats (distinguished by their extension). This is the post. In the case of our @DefaultValue
annotation, I'm thinking of a Yaml flavoured alternative. This allows us to store all default values as an array of property "name" - "value" pairs, like
# /src/Matthias/AnnotationBundle/Resources/metadata/Data.SomeClass.yml
name: "Noback, Matthias"
After reading and parsing this file, we should be able to run the previously created DefaultValueProcessor
with an instance of Matthias\AnnotationBundle\Data\SomeClass
and retrieve an object of which the "name" property has the default value "Noback, Matthias".
Adding a file driver makes things slightly more complicated. We have the following requirements:
- The annotation driver and the file driver should live side by side
- The file driver needs a file locator to find the right files containing the metadata in Yaml format
- The file locator needs a collection of locations (directories) where it can look for the Yaml files
- The list of directories should be configurable
The Yaml driver
Below you will find the code for the YamlDriver
, which parses a Yaml file and creates PropertyMetadata
objects in the same way the AnnotationDriver
does:
namespace Matthias\AnnotationBundle\Metadata\Driver;
use Metadata\Driver\AbstractFileDriver;
use Metadata\MergeableClassMetadata;
use Matthias\AnnotationBundle\Metadata\PropertyMetadata;
use Symfony\Component\Yaml\Yaml;
class YamlDriver extends AbstractFileDriver
{
protected function loadMetadataFromFile(\ReflectionClass $class, $file)
{
$classMetadata = new MergeableClassMetadata($class->getName());
$data = Yaml::parse($file);
foreach ($data as $propertyName => $value) {
$propertyMetadata = new PropertyMetadata($class->getName(), $propertyName);
$propertyMetadata->defaultValue = $value;
$classMetadata->addPropertyMetadata($propertyMetadata);
}
return $classMetadata;
}
protected function getExtension()
{
return 'yml';
}
}
As you can see, the YamlDriver
extends the AbstractFileDriver
, so we can skip some file finding business.
This also means, that we should supply a FileLocator
. The FileLocator
looks through a given collection of directories for files with the extension "yml" (as returned by YamlDriver::getExtension
).
We should add service definitions for the FileLocator
and the YamlDriver
. We will make the first constructor argument of the FileLocator
configurable, until then, we leave it empty.
<parameter key="matthias_annotation.metadata.file_locator_class">Metadata\Driver\FileLocator</parameter>
<parameter key="matthias_annotation.metadata.yaml_driver.class">Matthias\AnnotationBundle\Metadata\Driver\YamlDriver</parameter>
<!-- ... -->
<service id="matthias_annotation.metadata.file_locator" class="%matthias_annotation.metadata.file_locator_class%">
<argument /><!-- collection of directories -->
</service>
<service id="matthias_annotation.metadata.yaml_driver" class="%matthias_annotation.metadata.yaml_driver.class%" public="false">
<argument type="service" id="matthias_annotation.metadata.file_locator" />
</service>
Creating a driver chain
The YamlDriver
should be equally important as the AnnotationDriver
, and both should be available at the same time and for different classes. The DriverChain
comes to the rescue! This driver accepts a collection of drivers and when asked for ClassMetadata
, it loops over all available drivers (AnnotationDriver
and YamlDriver
) and whichever returns ClassMetadata
first, wins.
The only thing we should do to accomplish this, is add a service definition for the DriverChain
and provide it with the collection of existing drivers. Also, the DriverChain
will be the one and only driver that is given to the metadata factory.
<parameter key="matthias_annotation.metadata.driver_chain.class">Metadata\Driver\DriverChain</parameter>
<!-- ... -->
<service id="matthias_annotation.driver_chain" class="%matthias_annotation.metadata.driver_chain.class%">
<argument type="collection">
<argument type="service" id="matthias_annotation.metadata.yaml_driver" />
<argument type="service" id="matthias_annotation.metadata.annotation_driver" />
</argument>
</service>
<service id="matthias_annotation.metadata_factory" class="%matthias_annotation.metadata_factory.class%" public="false">
<argument type="service" id="matthias_annotation.driver_chain" />
</service>
Configuring the collection of directories
We need to find a way to set the first and only constructor argument of the FileLocator
. This argument should be an array of "namespace prefix" => "directory"
pairs, like
array('Matthias\\AnnotationBundle' => '/src/Matthias/AnnotationBundle/Resources/metadata');
We can accomplish this in a few steps:
- Define a configuration for the bundle, so it is possible to add a list of namespace prefix/directory combinations to
config.yml
- Process the configuration files, collect the namespace prefix/directory combinations and hand them over to the
FileLocator
by modifying it's service definition
First, the Configuration
class should look like this:
namespace Matthias\AnnotationBundle\DependencyInjection;
use Symfony\Component\Config\Definition\ConfigurationInterface;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
class Configuration implements ConfigurationInterface
{
function getConfigTreeBuilder()
{
$builder = new TreeBuilder;
$builder
->root('matthias_annotation')
->children()
->arrayNode('metadata')
->children()
->arrayNode('directories')
->prototype('array')
->children()
->scalarNode('path')->isRequired()->end()
->scalarNode('namespace_prefix')->defaultValue('')->end()
->end()
->end()
->end()
->end()
;
return $builder;
}
}
(This configuration structure was adapted from the JMSSerializerBundle
library.)
So now we can have something like this is /app/config.yml
:
matthias_annotation:
metadata:
directories:
annotationbundle:
namespace_prefix: 'Matthias\AnnotationBundle'
path: '%kernel.root_dir%/../src/Matthias/AnnotationBundle/Resources/metadata'
This means that metadata for classes within the namespace Matthias\AnnotationBundle
can be found in the given directory.
Finally, the MatthiasAnnotationExtension
class should look like this:
namespace Matthias\AnnotationBundle\DependencyInjection;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\Config\FileLocator;
class MatthiasAnnotationExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container)
{
$loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
$loader->load('services.xml');
$configuration = new Configuration;
$config = $this->processConfiguration($configuration, $configs);
$directories = array();
foreach ($config['metadata']['directories'] as $directory) {
$directories[$directory['namespace_prefix']] = $directory['path'];
}
$container
->getDefinition('matthias_annotation.metadata.file_locator')
->replaceArgument(0, $directories)
;
}
}
As you can see, the directories as defined in config.yml
will be parsed to comply to the "namespace prefix" => "directory"
format and will be set as the first constructor argument of the FileLocator
service definition.
Now you can use files like /src/Matthias/AnnotationBundle/Resources/metadata/Data.SomeClass.yml
to contain metadata for the class Matthias\AnnotationBundle\Data\SomeClass
(when your configuration looks the same as mine does - see above).
Thank you!
Also, great series. Exactly what I was looking for.
You can also automate the mapping (if you always use the same config-directory structure) as done in. https://github.com/symfony/...
And its also wise to register any file or directory the container as a resource so that any changes will be automatically picked-up in the development env.
What do you mean by
?Are you speaking of what is described at the config/caching and di/compilation docs?
That's right: adding resources to the container will make sure that the container gets compiled again when one of these resources is not "fresh" anymore, i.e. has been modified.
Thanks, I was unsure as to whether Sebastiaan was referring to another concept. As a side note, quotations (q) in comments do not seem to get any special formatting.
Thanks Sebastiaan - excellent suggestion!