Skip to content

Commit ef5d037

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

30 files changed

+1318
-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,202 @@
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 (
56+
!property_exists($resourceClass, $property) ||
57+
empty($propertyMetadata = $this->getMetadata($resourceClass, $property))
58+
) {
59+
return $this->handleNotFound($parentPropertyMetadata, $resourceClass, $property);
60+
}
61+
62+
if ($parentPropertyMetadata) {
63+
return $this->update($parentPropertyMetadata, $propertyMetadata);
64+
}
65+
66+
return new PropertyMetadata(
67+
null,
68+
$propertyMetadata['description'],
69+
$propertyMetadata['readable'],
70+
$propertyMetadata['writable'],
71+
$propertyMetadata['readableLink'],
72+
$propertyMetadata['writableLink'],
73+
$propertyMetadata['required'],
74+
$propertyMetadata['identifier'],
75+
$propertyMetadata['iri'],
76+
null,
77+
$propertyMetadata['attributes']
78+
);
79+
}
80+
81+
/**
82+
* Returns the metadata from the decorated factory if available or throws an exception.
83+
*
84+
* @param PropertyMetadata|null $parentPropertyMetadata
85+
* @param string $resourceClass
86+
* @param string $property
87+
*
88+
* @throws PropertyNotFoundException
89+
*
90+
* @return PropertyMetadata
91+
*/
92+
private function handleNotFound(PropertyMetadata $parentPropertyMetadata = null, string $resourceClass, string $property) : PropertyMetadata
93+
{
94+
if ($parentPropertyMetadata) {
95+
return $parentPropertyMetadata;
96+
}
97+
98+
throw new PropertyNotFoundException(sprintf('Property "%s" of the resource class "%s" not found.', $property, $resourceClass));
99+
}
100+
101+
/**
102+
* Extracts metadata from the XML tree.
103+
*
104+
* @param string $resourceClass
105+
* @param string $propertyName
106+
*
107+
* @throws InvalidArgumentException
108+
*
109+
* @return array
110+
*/
111+
private function getMetadata(string $resourceClass, string $propertyName) : array
112+
{
113+
foreach ($this->paths as $path) {
114+
try {
115+
$domDocument = XmlUtils::loadFile($path, self::RESOURCE_SCHEMA);
116+
} catch (\InvalidArgumentException $e) {
117+
throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e);
118+
}
119+
120+
$properties = (new \DOMXPath($domDocument))->query(sprintf('//resources/resource[@class="%s"]/property[@name="%s"]', $resourceClass, $propertyName));
121+
122+
if (
123+
false === $properties ||
124+
0 >= $properties->length ||
125+
null === $properties->item(0) ||
126+
false === $property = simplexml_import_dom($properties->item(0))
127+
) {
128+
continue;
129+
}
130+
131+
return [
132+
'description' => (string) $property['description'] ?: null,
133+
'readable' => $property['readable'] ? (bool) XmlUtils::phpize($property['readable']) : null,
134+
'writable' => $property['writable'] ? (bool) XmlUtils::phpize($property['writable']) : null,
135+
'readableLink' => $property['readableLink'] ? (bool) XmlUtils::phpize($property['readableLink']) : null,
136+
'writableLink' => $property['writableLink'] ? (bool) XmlUtils::phpize($property['writableLink']) : null,
137+
'required' => $property['required'] ? (bool) XmlUtils::phpize($property['required']) : null,
138+
'identifier' => $property['identifier'] ? (bool) XmlUtils::phpize($property['identifier']) : null,
139+
'iri' => (string) $property['iri'] ?: null,
140+
'attributes' => $this->getAttributes($property),
141+
];
142+
}
143+
144+
return [];
145+
}
146+
147+
/**
148+
* Recursively transforms an attribute structure into an associative array.
149+
*
150+
* @param \SimpleXMLElement $element
151+
*
152+
* @return array
153+
*/
154+
private function getAttributes(\SimpleXMLElement $element) : array
155+
{
156+
$attributes = [];
157+
foreach ($element->attribute as $attribute) {
158+
$value = isset($attribute->attribute[0]) ? $this->getAttributes($attribute) : (string) $attribute;
159+
160+
if (isset($attribute['name'])) {
161+
$attributes[(string) $attribute['name']] = $value;
162+
} else {
163+
$attributes[] = $value;
164+
}
165+
}
166+
167+
return $attributes;
168+
}
169+
170+
/**
171+
* Creates a new instance of metadata if the property is not already set.
172+
*
173+
* @param PropertyMetadata $propertyMetadata
174+
* @param array $metadata
175+
*
176+
* @return PropertyMetadata
177+
*/
178+
private function update(PropertyMetadata $propertyMetadata, array $metadata) : PropertyMetadata
179+
{
180+
$metadataAccessors = [
181+
'description' => 'get',
182+
'readable' => 'is',
183+
'writable' => 'is',
184+
'writableLink' => 'is',
185+
'readableLink' => 'is',
186+
'required' => 'is',
187+
'identifier' => 'is',
188+
'iri' => 'get',
189+
'attributes' => 'get',
190+
];
191+
192+
foreach ($metadataAccessors as $metadataKey => $accessorPrefix) {
193+
if (null === $metadata[$metadataKey] || null !== $propertyMetadata->{$accessorPrefix.ucfirst($metadataKey)}()) {
194+
continue;
195+
}
196+
197+
$propertyMetadata = $propertyMetadata->{'with'.ucfirst($metadataKey)}($metadata[$metadataKey]);
198+
}
199+
200+
return $propertyMetadata;
201+
}
202+
}
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)