Skip to content

Commit 1fede1a

Browse files
committed
Add support of mapping for properties in XML and YAML
1 parent 42e6b08 commit 1fede1a

30 files changed

+1295
-12
lines changed

features/bootstrap/FeatureContext.php

+1
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,7 @@ public function thereIsAFileConfigDummyObject()
298298
{
299299
$fileConfigDummy = new FileConfigDummy();
300300
$fileConfigDummy->setName('ConfigDummy');
301+
$fileConfigDummy->setFoo('Foo');
301302

302303
$this->manager->persist($fileConfigDummy);
303304
$this->manager->flush();

features/configurable.feature

+2
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ Feature: Configurable resource CRUD
1919
{
2020
"@id": "/fileconfigdummies/1",
2121
"@type": "fileconfigdummy",
22+
"foo": "Foo",
2223
"id": 1,
2324
"name": "ConfigDummy"
2425
}
@@ -55,6 +56,7 @@ Feature: Configurable resource CRUD
5556
"@context": "\/contexts\/fileconfigdummy",
5657
"@id": "\/fileconfigdummies\/1",
5758
"@type": "fileconfigdummy",
59+
"foo": "Foo",
5860
"id": 1,
5961
"name": "ConfigDummy"
6062
}

src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php

+6
Original file line numberDiff line numberDiff line change
@@ -255,8 +255,14 @@ private function registerLoaders(ContainerBuilder $container, array $bundles)
255255
$container->getDefinition('api_platform.metadata.resource.name_collection_factory.yaml')->replaceArgument(0, $yamlResources);
256256
$container->getDefinition('api_platform.metadata.resource.metadata_factory.yaml')->replaceArgument(0, $yamlResources);
257257

258+
$container->getDefinition('api_platform.metadata.property.name_collection_factory.yaml')->replaceArgument(0, $yamlResources);
259+
$container->getDefinition('api_platform.metadata.property.metadata_factory.yaml')->replaceArgument(0, $yamlResources);
260+
258261
$container->getDefinition('api_platform.metadata.resource.name_collection_factory.xml')->replaceArgument(0, $xmlResources);
259262
$container->getDefinition('api_platform.metadata.resource.metadata_factory.xml')->replaceArgument(0, $xmlResources);
263+
264+
$container->getDefinition('api_platform.metadata.property.name_collection_factory.xml')->replaceArgument(0, $xmlResources);
265+
$container->getDefinition('api_platform.metadata.property.metadata_factory.xml')->replaceArgument(0, $xmlResources);
260266
}
261267

262268
/**

src/Bridge/Symfony/Bundle/Resources/config/metadata.xml

+20
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,16 @@
8989
<argument type="service" id="api_platform.metadata.property.name_collection_factory.cached.inner" />
9090
</service>
9191

92+
<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">
93+
<argument type="collection" />
94+
<argument type="service" id="api_platform.metadata.property.name_collection_factory.yaml.inner" />
95+
</service>
96+
97+
<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">
98+
<argument type="collection" />
99+
<argument type="service" id="api_platform.metadata.property.name_collection_factory.xml.inner" />
100+
</service>
101+
92102
<!-- Property metadata -->
93103

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

121+
<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">
122+
<argument type="collection" />
123+
<argument type="service" id="api_platform.metadata.property.metadata_factory.yaml.inner" />
124+
</service>
125+
126+
<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">
127+
<argument type="collection" />
128+
<argument type="service" id="api_platform.metadata.property.metadata_factory.xml.inner" />
129+
</service>
130+
111131
<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">
112132
<argument type="service" id="api_platform.metadata.resource.metadata_factory" />
113133
<argument type="service" id="serializer.mapping.class_metadata_factory" />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <dunglas@gmail.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace ApiPlatform\Core\Metadata\Property\Factory;
13+
14+
use ApiPlatform\Core\Exception\InvalidArgumentException;
15+
use ApiPlatform\Core\Exception\PropertyNotFoundException;
16+
use ApiPlatform\Core\Metadata\Property\PropertyMetadata;
17+
use Symfony\Component\Config\Util\XmlUtils;
18+
19+
/**
20+
* Creates a property metadata from XML {@see Property} configuration.
21+
*
22+
* @author Baptiste Meyer <baptiste.meyer@gmail.com>
23+
*/
24+
class XmlPropertyMetadataFactory implements PropertyMetadataFactoryInterface
25+
{
26+
const RESOURCE_SCHEMA = __DIR__.'/../../schema/metadata.xsd';
27+
28+
private $paths;
29+
private $decorated;
30+
31+
/**
32+
* @param string[] $paths
33+
* @param PropertyMetadataFactoryInterface|null $decorated
34+
*/
35+
public function __construct(array $paths, PropertyMetadataFactoryInterface $decorated = null)
36+
{
37+
$this->paths = $paths;
38+
$this->decorated = $decorated;
39+
}
40+
41+
/**
42+
* {@inheritdoc}
43+
*/
44+
public function create(string $resourceClass, string $property, array $options = []) : PropertyMetadata
45+
{
46+
$parentPropertyMetadata = null;
47+
if ($this->decorated) {
48+
try {
49+
$parentPropertyMetadata = $this->decorated->create($resourceClass, $property, $options);
50+
} catch (PropertyNotFoundException $propertyNotFoundException) {
51+
// Ignore not found exception from decorated factories
52+
}
53+
}
54+
55+
if (!class_exists($resourceClass) || !property_exists($resourceClass, $property) || empty($propertyMetadata = $this->getMetadata($resourceClass, $property))) {
56+
return $this->handleNotFound($parentPropertyMetadata, $resourceClass, $property);
57+
}
58+
59+
if ($parentPropertyMetadata) {
60+
return $this->update($parentPropertyMetadata, $propertyMetadata);
61+
}
62+
63+
return new PropertyMetadata(
64+
null,
65+
$propertyMetadata['description'],
66+
$propertyMetadata['readable'],
67+
$propertyMetadata['writable'],
68+
$propertyMetadata['readableLink'],
69+
$propertyMetadata['writableLink'],
70+
$propertyMetadata['required'],
71+
$propertyMetadata['identifier'],
72+
$propertyMetadata['iri'],
73+
null,
74+
$propertyMetadata['attributes']
75+
);
76+
}
77+
78+
/**
79+
* Returns the metadata from the decorated factory if available or throws an exception.
80+
*
81+
* @param PropertyMetadata|null $parentPropertyMetadata
82+
* @param string $resourceClass
83+
* @param string $property
84+
*
85+
* @throws PropertyNotFoundException
86+
*
87+
* @return PropertyMetadata
88+
*/
89+
private function handleNotFound(PropertyMetadata $parentPropertyMetadata = null, string $resourceClass, string $property) : PropertyMetadata
90+
{
91+
if ($parentPropertyMetadata) {
92+
return $parentPropertyMetadata;
93+
}
94+
95+
throw new PropertyNotFoundException(sprintf('Property "%s" of the resource class "%s" not found.', $property, $resourceClass));
96+
}
97+
98+
/**
99+
* Extracts metadata from the XML tree.
100+
*
101+
* @param string $resourceClass
102+
* @param string $propertyName
103+
*
104+
* @throws InvalidArgumentException
105+
*
106+
* @return array
107+
*/
108+
private function getMetadata(string $resourceClass, string $propertyName) : array
109+
{
110+
foreach ($this->paths as $path) {
111+
try {
112+
$domDocument = XmlUtils::loadFile($path, self::RESOURCE_SCHEMA);
113+
} catch (\InvalidArgumentException $e) {
114+
throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e);
115+
}
116+
117+
$properties = (new \DOMXPath($domDocument))->query(sprintf('//resources/resource[@class="%s"]/property[@name="%s"]', $resourceClass, $propertyName));
118+
119+
if (false === $properties || 0 >= $properties->length || null === $properties->item(0) || false === $property = simplexml_import_dom($properties->item(0))) {
120+
continue;
121+
}
122+
123+
return [
124+
'description' => (string) $property['description'] ?: null,
125+
'readable' => $property['readable'] ? (bool) XmlUtils::phpize($property['readable']) : null,
126+
'writable' => $property['writable'] ? (bool) XmlUtils::phpize($property['writable']) : null,
127+
'readableLink' => $property['readableLink'] ? (bool) XmlUtils::phpize($property['readableLink']) : null,
128+
'writableLink' => $property['writableLink'] ? (bool) XmlUtils::phpize($property['writableLink']) : null,
129+
'required' => $property['required'] ? (bool) XmlUtils::phpize($property['required']) : null,
130+
'identifier' => $property['identifier'] ? (bool) XmlUtils::phpize($property['identifier']) : null,
131+
'iri' => (string) $property['iri'] ?: null,
132+
'attributes' => $this->getAttributes($property),
133+
];
134+
}
135+
136+
return [];
137+
}
138+
139+
/**
140+
* Recursively transforms an attribute structure into an associative array.
141+
*
142+
* @param \SimpleXMLElement $element
143+
*
144+
* @return array
145+
*/
146+
private function getAttributes(\SimpleXMLElement $element) : array
147+
{
148+
$attributes = [];
149+
foreach ($element->attribute as $attribute) {
150+
$value = isset($attribute->attribute[0]) ? $this->getAttributes($attribute) : (string) $attribute;
151+
152+
if (isset($attribute['name'])) {
153+
$attributes[(string) $attribute['name']] = $value;
154+
} else {
155+
$attributes[] = $value;
156+
}
157+
}
158+
159+
return $attributes;
160+
}
161+
162+
/**
163+
* Creates a new instance of metadata if the property is not already set.
164+
*
165+
* @param PropertyMetadata $propertyMetadata
166+
* @param array $metadata
167+
*
168+
* @return PropertyMetadata
169+
*/
170+
private function update(PropertyMetadata $propertyMetadata, array $metadata) : PropertyMetadata
171+
{
172+
$metadataAccessors = [
173+
'description' => 'get',
174+
'readable' => 'is',
175+
'writable' => 'is',
176+
'writableLink' => 'is',
177+
'readableLink' => 'is',
178+
'required' => 'is',
179+
'identifier' => 'is',
180+
'iri' => 'get',
181+
'attributes' => 'get',
182+
];
183+
184+
foreach ($metadataAccessors as $metadataKey => $accessorPrefix) {
185+
if (null === $metadata[$metadataKey] || null !== $propertyMetadata->{$accessorPrefix.ucfirst($metadataKey)}()) {
186+
continue;
187+
}
188+
189+
$propertyMetadata = $propertyMetadata->{'with'.ucfirst($metadataKey)}($metadata[$metadataKey]);
190+
}
191+
192+
return $propertyMetadata;
193+
}
194+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <dunglas@gmail.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace ApiPlatform\Core\Metadata\Property\Factory;
13+
14+
use ApiPlatform\Core\Exception\InvalidArgumentException;
15+
use ApiPlatform\Core\Exception\ResourceClassNotFoundException;
16+
use ApiPlatform\Core\Metadata\Property\PropertyNameCollection;
17+
use Symfony\Component\Config\Util\XmlUtils;
18+
19+
/**
20+
* Creates a property name collection from XML {@see Property} configuration files.
21+
*
22+
* @author Baptiste Meyer <baptiste.meyer@gmail.com>
23+
*/
24+
class XmlPropertyNameCollectionFactory implements PropertyNameCollectionFactoryInterface
25+
{
26+
const RESOURCE_SCHEMA = __DIR__.'/../../schema/metadata.xsd';
27+
28+
private $paths;
29+
private $decorated;
30+
31+
/**
32+
* @param array $paths
33+
* @param PropertyNameCollectionFactoryInterface|null $decorated
34+
*/
35+
public function __construct(array $paths, PropertyNameCollectionFactoryInterface $decorated = null)
36+
{
37+
$this->paths = $paths;
38+
$this->decorated = $decorated;
39+
}
40+
41+
/**
42+
* {@inheritdoc}
43+
*
44+
* @throws InvalidArgumentException
45+
*/
46+
public function create(string $resourceClass, array $options = []) : PropertyNameCollection
47+
{
48+
if ($this->decorated) {
49+
try {
50+
$propertyNameCollection = $this->decorated->create($resourceClass, $options);
51+
} catch (ResourceClassNotFoundException $resourceClassNotFoundException) {
52+
// Ignore not found exceptions from parent
53+
}
54+
}
55+
56+
if (!class_exists($resourceClass)) {
57+
if (isset($propertyNameCollection)) {
58+
return $propertyNameCollection;
59+
}
60+
61+
throw new ResourceClassNotFoundException(sprintf('The resource class "%s" does not exist.', $resourceClass));
62+
}
63+
64+
$propertyNames = [];
65+
66+
foreach ($this->paths as $path) {
67+
try {
68+
$domDocument = XmlUtils::loadFile($path, self::RESOURCE_SCHEMA);
69+
} catch (\InvalidArgumentException $e) {
70+
throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e);
71+
}
72+
73+
$properties = (new \DOMXPath($domDocument))->query(sprintf('//resources/resource[@class="%s"]/property', $resourceClass));
74+
75+
if (false === $properties || 0 >= $properties->length) {
76+
continue;
77+
}
78+
79+
foreach ($properties as $property) {
80+
if ('' === $propertyName = $property->getAttribute('name')) {
81+
continue;
82+
}
83+
84+
$propertyNames[$propertyName] = true;
85+
}
86+
}
87+
88+
if (isset($propertyNameCollection)) {
89+
foreach ($propertyNameCollection as $propertyName) {
90+
$propertyNames[$propertyName] = true;
91+
}
92+
}
93+
94+
return new PropertyNameCollection(array_keys($propertyNames));
95+
}
96+
}

0 commit comments

Comments
 (0)