Liip Metadata Parser
This project is Open Sourced based on work that we did initially as closed source at Liip, it may be lacking some documentation. If there is anything that you need or have questions about we would love to see you open an issue! :)
This is a parser for building model metadata from PHP classes. The metadata model can then be used to generate code or configuration. For example a serializer or ElasticSearch schema for types.
The metadata is geared toward this use case. PHP level constructs that represent the same information are grouped together: Methods for virtual properties and fields that have the same serialized name, but are valid in different versions.
This extensible parser can process PHP code and annotations or other metadata. You could write your own parsers, but this library comes with support for:
- Reflection
- PhpDoc
- JMSSerializer annotations
Contributing
If you want to contribute to the project (awesome!!), please read the Contributing Guidelines and adhere to our Code Of Conduct
Where do I go for help?
If you need, open an issue.
Setup
use Doctrine\Common\Annotations\AnnotationReader;
use Liip\MetadataParser\Builder;
use Liip\MetadataParser\Parser;
use Liip\MetadataParser\RecursionChecker;
use Liip\MetadataParser\ModelParser\JMSParser;
use Liip\MetadataParser\ModelParser\LiipMetadataAnnotationParser;
use Liip\MetadataParser\ModelParser\PhpDocParser;
use Liip\MetadataParser\ModelParser\ReflectionParser;
use Liip\MetadataParser\ModelParser\VisibilityAwarePropertyAccessGuesser;
$parser = new Parser(
new ReflectionParser(),
new PhpDocParser(),
new JMSParser(new AnnotationReader()),
new VisibilityAwarePropertyAccessGuesser(),
new LiipMetadataAnnotationParser(new AnnotationReader()),
);
$recursionChecker = new RecursionChecker(new NullLogger());
$builder = new Builder($parser, $recursionChecker);
Usage
The Builder::build
method is the main entry point to get a ClassMetadata
object. The builder accepts an array of Reducer
to select which property to
use when there are several options. A reducer can lead to drop a property if
none of the variants is acceptable. Multiple options for a single field mainly
come from the @SerializedName
and @VirtualProperty
annotations of
JMSSerializer:
- GroupReducer: Select the property based on whether it is in any of the specified groups;
- VersionReducer: Select the property based on whether it is included in the specified version;
- TakeBestReducer: Make sure that we end up with the property that has the same name as the serialized name, if we still have multiple options after the other reducers.
use Liip\MetadataParser\Reducer\GroupReducer;
use Liip\MetadataParser\Reducer\PreferredReducer;
use Liip\MetadataParser\Reducer\TakeBestReducer;
use Liip\MetadataParser\Reducer\VersionReducer;
$reducers = [
new VersionReducer('2'),
new GroupReducer(['api', 'detail']),
new PreferredReducer(),
new TakeBestReducer(),
];
$metadata = $builder->build(MyClass::class, $reducers);
The ClassMetadata
provides all information on a class. Properties have a
PropertyType
to tell what kind of property they are. Properties that hold
another class, are of the type PropertyTypeClass
that has the method
getClassMetadata()
to get the metadata of the nested class. This structure
is validated to not contain any infinite recursion.
Property naming strategy
By default, property names will be translated from a camelCased to a lower and
snake_cased name (e.g. myProperty
becomes my_property
). If you want to keep
the property name as is, you can change the strategy to identical
via the
following code:
\Liip\MetadataParser\ModelParser\RawMetadata\PropertyCollection::useIdenticalNamingStrategy();
Handling Edge Cases with @Preferred
This library provides its own annotation in Liip\MetadataParser\Annotation\Preferred
to specify which property to use in case there are several options. This can be
useful for example when serializing models without specifying a version, when
they use different virtual properties in different versions.
use JMS\Serializer\Annotation as JMS;
use Liip\MetadataParser\Annotation as Liip;
class Product
{
/**
* @JMS\Since("2")
* @JMS\Type("string")
*/
public $name;
/**
* @JMS\Until("1")
* @JMS\SerializedName("name")
* @JMS\Type("string")
* @Liip\Preferred
*/
public $legacyName;
}
Expected Recursion: Working with Flawed Models
JMS annotation
If you are using JMS annotations, you can add the MaxDepth
annotation to
properties that might be recursive.
The following example will tell the metadata parser that the recursion is
expected up to a maximum depth of 3
.
use JMS\Serializer\Annotation as JMS;
class RecursionModel
{
/**
* @JMS\MaxDepth(3)
* @JMS\Type("RecursionModel")
*/
public $recursion;
}
Pure PHP
The RecursionChecker accepts a second parameter to specify places where to break recursion. This is useful if your model tree looks like it has recursions but actually does not have them. JMSSerializer always acts on the actual data and therefore does not notice a recursion as long as it is not infinite.
For example, lets say you have a Product
that has a field variants
which is
again list of Product
. Those variant products use the same class and
therefore have the variants
field. However, in real data a variant never
contains further variants. To avoid a recursion exception for this example, you
would specify:
$expectedRecursions = [
['variants', 'variants'],
];
$recursionChecker = new RecursionChecker(new NullLogger(), $expectedRecursions);
With this configuration, the ClassMetadata
found in the property type for the
variants property of the final model will have no field variants
, so that
code working on the metadata does not need to worry about infinite recursion.
Extending the metadata parser
This library comes with a couple of parsers, but you can write your own to
handle custom information specific to your project. Use the
PropertyVariationMetadata::setCustomInformation
method to add custom data,
and use PropertyMetadata::getCustomInformation
to read it in your metadata
consumers.