Skip to content

Commit

Permalink
Add support of mapping for properties in XML and YAML
Browse files Browse the repository at this point in the history
  • Loading branch information
meyerbaptiste committed Oct 31, 2016
1 parent 42e6b08 commit ef5d037
Show file tree
Hide file tree
Showing 30 changed files with 1,318 additions and 12 deletions.
1 change: 1 addition & 0 deletions features/bootstrap/FeatureContext.php
Expand Up @@ -298,6 +298,7 @@ public function thereIsAFileConfigDummyObject()
{
$fileConfigDummy = new FileConfigDummy();
$fileConfigDummy->setName('ConfigDummy');
$fileConfigDummy->setFoo('Foo');

$this->manager->persist($fileConfigDummy);
$this->manager->flush();
Expand Down
2 changes: 2 additions & 0 deletions features/configurable.feature
Expand Up @@ -19,6 +19,7 @@ Feature: Configurable resource CRUD
{
"@id": "/fileconfigdummies/1",
"@type": "fileconfigdummy",
"foo": "Foo",
"id": 1,
"name": "ConfigDummy"
}
Expand Down Expand Up @@ -55,6 +56,7 @@ Feature: Configurable resource CRUD
"@context": "\/contexts\/fileconfigdummy",
"@id": "\/fileconfigdummies\/1",
"@type": "fileconfigdummy",
"foo": "Foo",
"id": 1,
"name": "ConfigDummy"
}
Expand Down
Expand Up @@ -255,8 +255,14 @@ private function registerLoaders(ContainerBuilder $container, array $bundles)
$container->getDefinition('api_platform.metadata.resource.name_collection_factory.yaml')->replaceArgument(0, $yamlResources);
$container->getDefinition('api_platform.metadata.resource.metadata_factory.yaml')->replaceArgument(0, $yamlResources);

$container->getDefinition('api_platform.metadata.property.name_collection_factory.yaml')->replaceArgument(0, $yamlResources);
$container->getDefinition('api_platform.metadata.property.metadata_factory.yaml')->replaceArgument(0, $yamlResources);

$container->getDefinition('api_platform.metadata.resource.name_collection_factory.xml')->replaceArgument(0, $xmlResources);
$container->getDefinition('api_platform.metadata.resource.metadata_factory.xml')->replaceArgument(0, $xmlResources);

$container->getDefinition('api_platform.metadata.property.name_collection_factory.xml')->replaceArgument(0, $xmlResources);
$container->getDefinition('api_platform.metadata.property.metadata_factory.xml')->replaceArgument(0, $xmlResources);
}

/**
Expand Down
20 changes: 20 additions & 0 deletions src/Bridge/Symfony/Bundle/Resources/config/metadata.xml
Expand Up @@ -89,6 +89,16 @@
<argument type="service" id="api_platform.metadata.property.name_collection_factory.cached.inner" />
</service>

<service id="api_platform.metadata.property.name_collection_factory.yaml" class="ApiPlatform\Core\Metadata\Property\Factory\YamlPropertyNameCollectionFactory" decorates="api_platform.metadata.property.name_collection_factory" public="false">
<argument type="collection" />
<argument type="service" id="api_platform.metadata.property.name_collection_factory.yaml.inner" />
</service>

<service id="api_platform.metadata.property.name_collection_factory.xml" class="ApiPlatform\Core\Metadata\Property\Factory\XmlPropertyNameCollectionFactory" decorates="api_platform.metadata.property.name_collection_factory" public="false">
<argument type="collection" />
<argument type="service" id="api_platform.metadata.property.name_collection_factory.xml.inner" />
</service>

<!-- Property metadata -->

<service id="api_platform.metadata.property.metadata_factory" alias="api_platform.metadata.property.metadata_factory.annotation" />
Expand All @@ -108,6 +118,16 @@
<argument type="service" id="api_platform.metadata.property.metadata_factory.inherited.inner" />
</service>

<service id="api_platform.metadata.property.metadata_factory.yaml" class="ApiPlatform\Core\Metadata\Property\Factory\YamlPropertyMetadataFactory" decorates="api_platform.metadata.property.metadata_factory" decoration-priority="40" public="false">
<argument type="collection" />
<argument type="service" id="api_platform.metadata.property.metadata_factory.yaml.inner" />
</service>

<service id="api_platform.metadata.property.metadata_factory.xml" class="ApiPlatform\Core\Metadata\Property\Factory\XmlPropertyMetadataFactory" decorates="api_platform.metadata.property.metadata_factory" decoration-priority="40" public="false">
<argument type="collection" />
<argument type="service" id="api_platform.metadata.property.metadata_factory.xml.inner" />
</service>

<service id="api_platform.metadata.property.metadata_factory.serializer" class="ApiPlatform\Core\Metadata\Property\Factory\SerializerPropertyMetadataFactory" decorates="api_platform.metadata.property.metadata_factory" decoration-priority="30" public="false">
<argument type="service" id="api_platform.metadata.resource.metadata_factory" />
<argument type="service" id="serializer.mapping.class_metadata_factory" />
Expand Down
202 changes: 202 additions & 0 deletions src/Metadata/Property/Factory/XmlPropertyMetadataFactory.php
@@ -0,0 +1,202 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace ApiPlatform\Core\Metadata\Property\Factory;

use ApiPlatform\Core\Exception\InvalidArgumentException;
use ApiPlatform\Core\Exception\PropertyNotFoundException;
use ApiPlatform\Core\Metadata\Property\PropertyMetadata;
use Symfony\Component\Config\Util\XmlUtils;

/**
* Creates a property metadata from XML {@see Property} configuration.
*
* @author Baptiste Meyer <baptiste.meyer@gmail.com>
*/
class XmlPropertyMetadataFactory implements PropertyMetadataFactoryInterface
{
const RESOURCE_SCHEMA = __DIR__.'/../../schema/metadata.xsd';

private $paths;
private $decorated;

/**
* @param string[] $paths
* @param PropertyMetadataFactoryInterface|null $decorated
*/
public function __construct(array $paths, PropertyMetadataFactoryInterface $decorated = null)
{
$this->paths = $paths;
$this->decorated = $decorated;
}

/**
* {@inheritdoc}
*/
public function create(string $resourceClass, string $property, array $options = []) : PropertyMetadata
{
$parentPropertyMetadata = null;
if ($this->decorated) {
try {
$parentPropertyMetadata = $this->decorated->create($resourceClass, $property, $options);
} catch (PropertyNotFoundException $propertyNotFoundException) {
// Ignore not found exception from decorated factories
}
}

if (
!property_exists($resourceClass, $property) ||
empty($propertyMetadata = $this->getMetadata($resourceClass, $property))
) {
return $this->handleNotFound($parentPropertyMetadata, $resourceClass, $property);
}

if ($parentPropertyMetadata) {
return $this->update($parentPropertyMetadata, $propertyMetadata);
}

return new PropertyMetadata(
null,
$propertyMetadata['description'],
$propertyMetadata['readable'],
$propertyMetadata['writable'],
$propertyMetadata['readableLink'],
$propertyMetadata['writableLink'],
$propertyMetadata['required'],
$propertyMetadata['identifier'],
$propertyMetadata['iri'],
null,
$propertyMetadata['attributes']
);
}

/**
* Returns the metadata from the decorated factory if available or throws an exception.
*
* @param PropertyMetadata|null $parentPropertyMetadata
* @param string $resourceClass
* @param string $property
*
* @throws PropertyNotFoundException
*
* @return PropertyMetadata
*/
private function handleNotFound(PropertyMetadata $parentPropertyMetadata = null, string $resourceClass, string $property) : PropertyMetadata
{
if ($parentPropertyMetadata) {
return $parentPropertyMetadata;
}

throw new PropertyNotFoundException(sprintf('Property "%s" of the resource class "%s" not found.', $property, $resourceClass));
}

/**
* Extracts metadata from the XML tree.
*
* @param string $resourceClass
* @param string $propertyName
*
* @throws InvalidArgumentException
*
* @return array
*/
private function getMetadata(string $resourceClass, string $propertyName) : array
{
foreach ($this->paths as $path) {
try {
$domDocument = XmlUtils::loadFile($path, self::RESOURCE_SCHEMA);
} catch (\InvalidArgumentException $e) {
throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e);
}

$properties = (new \DOMXPath($domDocument))->query(sprintf('//resources/resource[@class="%s"]/property[@name="%s"]', $resourceClass, $propertyName));

if (
false === $properties ||
0 >= $properties->length ||
null === $properties->item(0) ||
false === $property = simplexml_import_dom($properties->item(0))
) {
continue;
}

return [
'description' => (string) $property['description'] ?: null,
'readable' => $property['readable'] ? (bool) XmlUtils::phpize($property['readable']) : null,
'writable' => $property['writable'] ? (bool) XmlUtils::phpize($property['writable']) : null,
'readableLink' => $property['readableLink'] ? (bool) XmlUtils::phpize($property['readableLink']) : null,
'writableLink' => $property['writableLink'] ? (bool) XmlUtils::phpize($property['writableLink']) : null,
'required' => $property['required'] ? (bool) XmlUtils::phpize($property['required']) : null,
'identifier' => $property['identifier'] ? (bool) XmlUtils::phpize($property['identifier']) : null,
'iri' => (string) $property['iri'] ?: null,
'attributes' => $this->getAttributes($property),
];
}

return [];
}

/**
* Recursively transforms an attribute structure into an associative array.
*
* @param \SimpleXMLElement $element
*
* @return array
*/
private function getAttributes(\SimpleXMLElement $element) : array
{
$attributes = [];
foreach ($element->attribute as $attribute) {
$value = isset($attribute->attribute[0]) ? $this->getAttributes($attribute) : (string) $attribute;

if (isset($attribute['name'])) {
$attributes[(string) $attribute['name']] = $value;
} else {
$attributes[] = $value;
}
}

return $attributes;
}

/**
* Creates a new instance of metadata if the property is not already set.
*
* @param PropertyMetadata $propertyMetadata
* @param array $metadata
*
* @return PropertyMetadata
*/
private function update(PropertyMetadata $propertyMetadata, array $metadata) : PropertyMetadata
{
$metadataAccessors = [
'description' => 'get',
'readable' => 'is',
'writable' => 'is',
'writableLink' => 'is',
'readableLink' => 'is',
'required' => 'is',
'identifier' => 'is',
'iri' => 'get',
'attributes' => 'get',
];

foreach ($metadataAccessors as $metadataKey => $accessorPrefix) {
if (null === $metadata[$metadataKey] || null !== $propertyMetadata->{$accessorPrefix.ucfirst($metadataKey)}()) {
continue;
}

$propertyMetadata = $propertyMetadata->{'with'.ucfirst($metadataKey)}($metadata[$metadataKey]);
}

return $propertyMetadata;
}
}
96 changes: 96 additions & 0 deletions src/Metadata/Property/Factory/XmlPropertyNameCollectionFactory.php
@@ -0,0 +1,96 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace ApiPlatform\Core\Metadata\Property\Factory;

use ApiPlatform\Core\Exception\InvalidArgumentException;
use ApiPlatform\Core\Exception\ResourceClassNotFoundException;
use ApiPlatform\Core\Metadata\Property\PropertyNameCollection;
use Symfony\Component\Config\Util\XmlUtils;

/**
* Creates a property name collection from XML {@see Property} configuration files.
*
* @author Baptiste Meyer <baptiste.meyer@gmail.com>
*/
class XmlPropertyNameCollectionFactory implements PropertyNameCollectionFactoryInterface
{
const RESOURCE_SCHEMA = __DIR__.'/../../schema/metadata.xsd';

private $paths;
private $decorated;

/**
* @param array $paths
* @param PropertyNameCollectionFactoryInterface|null $decorated
*/
public function __construct(array $paths, PropertyNameCollectionFactoryInterface $decorated = null)
{
$this->paths = $paths;
$this->decorated = $decorated;
}

/**
* {@inheritdoc}
*
* @throws InvalidArgumentException
*/
public function create(string $resourceClass, array $options = []) : PropertyNameCollection
{
if ($this->decorated) {
try {
$propertyNameCollection = $this->decorated->create($resourceClass, $options);
} catch (ResourceClassNotFoundException $resourceClassNotFoundException) {
// Ignore not found exceptions from parent
}
}

if (!class_exists($resourceClass)) {
if (isset($propertyNameCollection)) {
return $propertyNameCollection;
}

throw new ResourceClassNotFoundException(sprintf('The resource class "%s" does not exist.', $resourceClass));
}

$propertyNames = [];

foreach ($this->paths as $path) {
try {
$domDocument = XmlUtils::loadFile($path, self::RESOURCE_SCHEMA);
} catch (\InvalidArgumentException $e) {
throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e);
}

$properties = (new \DOMXPath($domDocument))->query(sprintf('//resources/resource[@class="%s"]/property', $resourceClass));

if (false === $properties || 0 >= $properties->length) {
continue;
}

foreach ($properties as $property) {
if ('' === $propertyName = $property->getAttribute('name')) {
continue;
}

$propertyNames[$propertyName] = true;
}
}

if (isset($propertyNameCollection)) {
foreach ($propertyNameCollection as $propertyName) {
$propertyNames[$propertyName] = true;
}
}

return new PropertyNameCollection(array_keys($propertyNames));
}
}

0 comments on commit ef5d037

Please sign in to comment.