Skip to content

Commit ef5d037

Browse files
committedOct 31, 2016
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+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
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\Yaml\Exception\ParseException;
18+
use Symfony\Component\Yaml\Yaml;
19+
20+
/**
21+
* Creates a property metadata from YAML {@see Property} configuration files.
22+
*
23+
* @author Baptiste Meyer <baptiste.meyer@gmail.com>
24+
*/
25+
class YamlPropertyMetadataFactory implements PropertyMetadataFactoryInterface
26+
{
27+
private $paths;
28+
private $decorated;
29+
30+
/**
31+
* @param array $paths
32+
* @param PropertyMetadataFactoryInterface|null $decorated
33+
*/
34+
public function __construct(array $paths, PropertyMetadataFactoryInterface $decorated = null)
35+
{
36+
$this->paths = $paths;
37+
$this->decorated = $decorated;
38+
}
39+
40+
/**
41+
* {@inheritdoc}
42+
*/
43+
public function create(string $resourceClass, string $property, array $options = []) : PropertyMetadata
44+
{
45+
$parentPropertyMetadata = null;
46+
if ($this->decorated) {
47+
try {
48+
$parentPropertyMetadata = $this->decorated->create($resourceClass, $property, $options);
49+
} catch (PropertyNotFoundException $propertyNotFoundException) {
50+
// Ignore not found exception from decorated factories
51+
}
52+
}
53+
54+
if (
55+
!property_exists($resourceClass, $property) ||
56+
empty($propertyMetadata = $this->getMetadata($resourceClass, $property))
57+
) {
58+
return $this->handleNotFound($parentPropertyMetadata, $resourceClass, $property);
59+
}
60+
61+
if ($parentPropertyMetadata) {
62+
return $this->update($parentPropertyMetadata, $propertyMetadata);
63+
}
64+
65+
return new PropertyMetadata(
66+
null,
67+
$propertyMetadata['description'],
68+
$propertyMetadata['readable'],
69+
$propertyMetadata['writable'],
70+
$propertyMetadata['readableLink'],
71+
$propertyMetadata['writableLink'],
72+
$propertyMetadata['required'],
73+
$propertyMetadata['identifier'],
74+
$propertyMetadata['iri'],
75+
null,
76+
$propertyMetadata['attributes']
77+
);
78+
}
79+
80+
/**
81+
* Returns the metadata from the decorated factory if available or throws an exception.
82+
*
83+
* @param PropertyMetadata|null $parentPropertyMetadata
84+
* @param string $resourceClass
85+
* @param string $property
86+
*
87+
* @throws PropertyNotFoundException
88+
*
89+
* @return PropertyMetadata
90+
*/
91+
private function handleNotFound(PropertyMetadata $parentPropertyMetadata = null, string $resourceClass, string $property) : PropertyMetadata
92+
{
93+
if ($parentPropertyMetadata) {
94+
return $parentPropertyMetadata;
95+
}
96+
97+
throw new PropertyNotFoundException(sprintf('Property "%s" of the resource class "%s" not found.', $property, $resourceClass));
98+
}
99+
100+
/**
101+
* Extracts metadata from the YAML tree.
102+
*
103+
* @param string $resourceClass
104+
* @param string $property
105+
*
106+
* @throws ParseException
107+
* @throws InvalidArgumentException
108+
*
109+
* @return array
110+
*/
111+
private function getMetadata(string $resourceClass, string $property) : array
112+
{
113+
foreach ($this->paths as $path) {
114+
try {
115+
$resources = Yaml::parse(file_get_contents($path));
116+
} catch (ParseException $parseException) {
117+
$parseException->setParsedFile($path);
118+
119+
throw $parseException;
120+
}
121+
122+
if (null === $resources = $resources['resources'] ?? $resources) {
123+
continue;
124+
}
125+
126+
if (!is_array($resources)) {
127+
throw new InvalidArgumentException(sprintf('"resources" setting is expected to be null or an array, %s given in "%s".', gettype($resources), $path));
128+
}
129+
130+
foreach ($resources as $resourceName => $resource) {
131+
if (null === $resource) {
132+
continue;
133+
}
134+
135+
if (!is_array($resource)) {
136+
throw new InvalidArgumentException(sprintf('"%s" setting is expected to be null or an array, %s given in "%s".', $resourceName, gettype($resource), $path));
137+
}
138+
139+
if (!isset($resource['class'])) {
140+
throw new InvalidArgumentException(sprintf('"class" setting is expected to be a string, none given in "%s".', $path));
141+
}
142+
143+
if ($resourceClass !== $resource['class'] || !isset($resource['properties'])) {
144+
continue;
145+
}
146+
147+
if (!is_array($resource['properties'])) {
148+
throw new InvalidArgumentException(sprintf('"properties" setting is expected to be null or an array, %s given in "%s".', gettype($resource['properties']), $path));
149+
}
150+
151+
foreach ($resource['properties'] as $propertyName => $propertyValues) {
152+
if (null === $propertyValues) {
153+
continue;
154+
}
155+
156+
if (!is_array($propertyValues)) {
157+
throw new InvalidArgumentException(sprintf('"%s" setting is expected to be null or an array, %s given in "%s".', $propertyName, gettype($propertyValues), $path));
158+
}
159+
160+
if ($property !== $propertyName) {
161+
continue;
162+
}
163+
164+
return [
165+
'description' => isset($propertyValues['description']) && is_scalar($propertyValues['description']) ? $propertyValues['description'] : null,
166+
'readable' => isset($propertyValues['readable']) && is_bool($propertyValues['readable']) ? $propertyValues['readable'] : null,
167+
'writable' => isset($propertyValues['writable']) && is_bool($propertyValues['writable']) ? $propertyValues['writable'] : null,
168+
'readableLink' => isset($propertyValues['readableLink']) && is_bool($propertyValues['readableLink']) ? $propertyValues['readableLink'] : null,
169+
'writableLink' => isset($propertyValues['writableLink']) && is_bool($propertyValues['writableLink']) ? $propertyValues['writableLink'] : null,
170+
'required' => isset($propertyValues['required']) && is_bool($propertyValues['required']) ? $propertyValues['required'] : null,
171+
'identifier' => isset($propertyValues['identifier']) && is_bool($propertyValues['identifier']) ? $propertyValues['identifier'] : null,
172+
'iri' => isset($propertyValues['iri']) && is_scalar($propertyValues['iri']) ? $propertyValues['iri'] : null,
173+
'attributes' => $propertyValues['attributes'] ?? null,
174+
];
175+
}
176+
}
177+
}
178+
179+
return [];
180+
}
181+
182+
/**
183+
* Creates a new instance of metadata if the property is not already set.
184+
*
185+
* @param PropertyMetadata $propertyMetadata
186+
* @param array $metadata
187+
*
188+
* @return PropertyMetadata
189+
*/
190+
private function update(PropertyMetadata $propertyMetadata, array $metadata) : PropertyMetadata
191+
{
192+
$metadataAccessors = [
193+
'description' => 'get',
194+
'readable' => 'is',
195+
'writable' => 'is',
196+
'writableLink' => 'is',
197+
'readableLink' => 'is',
198+
'required' => 'is',
199+
'identifier' => 'is',
200+
'iri' => 'get',
201+
'attributes' => 'get',
202+
];
203+
204+
foreach ($metadataAccessors as $metadataKey => $accessorPrefix) {
205+
if (null === $metadata[$metadataKey] || null !== $propertyMetadata->{$accessorPrefix.ucfirst($metadataKey)}()) {
206+
continue;
207+
}
208+
209+
$propertyMetadata = $propertyMetadata->{'with'.ucfirst($metadataKey)}($metadata[$metadataKey]);
210+
}
211+
212+
return $propertyMetadata;
213+
}
214+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
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\Yaml\Exception\ParseException;
18+
use Symfony\Component\Yaml\Yaml;
19+
20+
/**
21+
* Creates a property name collection from YAML {@see Property} configuration files.
22+
*
23+
* @author Baptiste Meyer <baptiste.meyer@gmail.com>
24+
*/
25+
class YamlPropertyNameCollectionFactory implements PropertyNameCollectionFactoryInterface
26+
{
27+
private $paths;
28+
private $decorated;
29+
30+
/**
31+
* @param array $paths
32+
* @param PropertyNameCollectionFactoryInterface|null $decorated
33+
*/
34+
public function __construct(array $paths, PropertyNameCollectionFactoryInterface $decorated = null)
35+
{
36+
$this->paths = $paths;
37+
$this->decorated = $decorated;
38+
}
39+
40+
/**
41+
* {@inheritdoc}
42+
*
43+
* @throws ParseException
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+
$resources = Yaml::parse(file_get_contents($path));
69+
} catch (ParseException $parseException) {
70+
$parseException->setParsedFile($path);
71+
72+
throw $parseException;
73+
}
74+
75+
if (null === $resources = $resources['resources'] ?? $resources) {
76+
continue;
77+
}
78+
79+
if (!is_array($resources)) {
80+
throw new InvalidArgumentException(sprintf('"resources" setting is expected to be null or an array, %s given in "%s".', gettype($resources), $path));
81+
}
82+
83+
foreach ($resources as $resourceName => $resource) {
84+
if (null === $resource) {
85+
continue;
86+
}
87+
88+
if (!is_array($resource)) {
89+
throw new InvalidArgumentException(sprintf('"%s" setting is expected to be null or an array, %s given in "%s".', $resourceName, gettype($resource), $path));
90+
}
91+
92+
if (!isset($resource['class'])) {
93+
throw new InvalidArgumentException(sprintf('"class" setting is expected to be a string, none given in "%s".', $path));
94+
}
95+
96+
if ($resourceClass !== $resource['class'] || !isset($resource['properties'])) {
97+
continue;
98+
}
99+
100+
if (!is_array($resource['properties'])) {
101+
throw new InvalidArgumentException(sprintf('"properties" setting is expected to be null or an array, %s given in "%s".', gettype($resource['properties']), $path));
102+
}
103+
104+
foreach ($resource['properties'] as $propertyName => $propertyValues) {
105+
if (null === $propertyValues) {
106+
continue;
107+
}
108+
109+
if (!is_array($propertyValues)) {
110+
throw new InvalidArgumentException(sprintf('"%s" setting is expected to be null or an array, %s given in "%s".', $propertyName, gettype($propertyValues), $path));
111+
}
112+
113+
$propertyNames[$propertyName] = true;
114+
}
115+
}
116+
}
117+
118+
if (isset($propertyNameCollection)) {
119+
foreach ($propertyNameCollection as $propertyName) {
120+
$propertyNames[$propertyName] = true;
121+
}
122+
}
123+
124+
return new PropertyNameCollection(array_keys($propertyNames));
125+
}
126+
}

‎src/Metadata/Resource/Factory/XmlResourceMetadataFactory.php

+3-3
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,9 @@ private function getMetadata(string $resourceClass) : array
8383
}
8484

8585
return [
86-
(string) $resource['shortName'] ?? null,
87-
(string) $resource['description'] ?? null,
88-
(string) $resource['iri'] ?? null,
86+
(string) $resource['shortName'] ?: null,
87+
(string) $resource['description'] ?: null,
88+
(string) $resource['iri'] ?: null,
8989
$this->getAttributes($resource, 'itemOperation') ?: null,
9090
$this->getAttributes($resource, 'collectionOperation') ?: null,
9191
$this->getAttributes($resource, 'attribute') ?: null,

‎src/Metadata/Resource/Factory/YamlResourceMetadataFactory.php

+12-4
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
use ApiPlatform\Core\Exception\InvalidArgumentException;
1515
use ApiPlatform\Core\Exception\ResourceClassNotFoundException;
1616
use ApiPlatform\Core\Metadata\Resource\ResourceMetadata;
17-
use Symfony\Component\Yaml\Parser as YamlParser;
17+
use Symfony\Component\Yaml\Exception\ParseException;
18+
use Symfony\Component\Yaml\Yaml;
1819

1920
/**
2021
* Creates a resource metadata from yml {@see Resource} configuration.
@@ -23,7 +24,6 @@
2324
*/
2425
final class YamlResourceMetadataFactory implements ResourceMetadataFactoryInterface
2526
{
26-
private $yamlParser;
2727
private $decorated;
2828
private $paths;
2929

@@ -33,13 +33,14 @@ final class YamlResourceMetadataFactory implements ResourceMetadataFactoryInterf
3333
*/
3434
public function __construct(array $paths, ResourceMetadataFactoryInterface $decorated = null)
3535
{
36-
$this->yamlParser = new YamlParser();
3736
$this->paths = $paths;
3837
$this->decorated = $decorated;
3938
}
4039

4140
/**
4241
* {@inheritdoc}
42+
*
43+
* @throws ParseException
4344
*/
4445
public function create(string $resourceClass) : ResourceMetadata
4546
{
@@ -61,7 +62,14 @@ public function create(string $resourceClass) : ResourceMetadata
6162
$metadata = null;
6263

6364
foreach ($this->paths as $path) {
64-
$resources = $this->yamlParser->parse(file_get_contents($path));
65+
try {
66+
$resources = Yaml::parse(file_get_contents($path));
67+
} catch (ParseException $parseException) {
68+
$parseException->setParsedFile($path);
69+
70+
throw $parseException;
71+
}
72+
6573
$resources = $resources['resources'] ?? $resources;
6674

6775
foreach ($resources as $resource) {

‎src/Metadata/Resource/Factory/YamlResourceNameCollectionFactory.php

+12-4
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313

1414
use ApiPlatform\Core\Exception\InvalidArgumentException;
1515
use ApiPlatform\Core\Metadata\Resource\ResourceNameCollection;
16-
use Symfony\Component\Yaml\Parser as YamlParser;
16+
use Symfony\Component\Yaml\Exception\ParseException;
17+
use Symfony\Component\Yaml\Yaml;
1718

1819
/**
1920
* Creates a resource name collection from {@see Resource} configuration files.
@@ -22,7 +23,6 @@
2223
*/
2324
final class YamlResourceNameCollectionFactory implements ResourceNameCollectionFactoryInterface
2425
{
25-
private $yamlParser;
2626
private $paths;
2727
private $decorated;
2828

@@ -32,13 +32,14 @@ final class YamlResourceNameCollectionFactory implements ResourceNameCollectionF
3232
*/
3333
public function __construct(array $paths, ResourceNameCollectionFactoryInterface $decorated = null)
3434
{
35-
$this->yamlParser = new YamlParser();
3635
$this->paths = $paths;
3736
$this->decorated = $decorated;
3837
}
3938

4039
/**
4140
* {@inheritdoc}
41+
*
42+
* @throws ParseException
4243
*/
4344
public function create() : ResourceNameCollection
4445
{
@@ -50,7 +51,14 @@ public function create() : ResourceNameCollection
5051
}
5152

5253
foreach ($this->paths as $path) {
53-
$resources = $this->yamlParser->parse(file_get_contents($path));
54+
try {
55+
$resources = Yaml::parse(file_get_contents($path));
56+
} catch (ParseException $parseException) {
57+
$parseException->setParsedFile($path);
58+
59+
throw $parseException;
60+
}
61+
5462
$resources = $resources['resources'] ?? $resources;
5563

5664
foreach ($resources as $resource) {

‎src/Metadata/schema/metadata.xsd

+16
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
<xsd:element name="itemOperation" minOccurs="0" maxOccurs="unbounded" type="attribute"/>
1818
<xsd:element name="collectionOperation" minOccurs="0" maxOccurs="unbounded" type="attribute"/>
1919
<xsd:element name="attribute" minOccurs="0" maxOccurs="unbounded" type="attribute"/>
20+
<xsd:element name="property" minOccurs="0" maxOccurs="unbounded" type="property"/>
2021
</xsd:sequence>
2122
<xsd:attribute type="xsd:string" name="class" use="required"/>
2223
<xsd:attribute type="xsd:string" name="shortName"/>
@@ -30,4 +31,19 @@
3031
</xsd:choice>
3132
<xsd:attribute type="xsd:string" name="name"/>
3233
</xsd:complexType>
34+
35+
<xsd:complexType name="property">
36+
<xsd:sequence minOccurs="0" maxOccurs="unbounded">
37+
<xsd:element name="attribute" minOccurs="0" maxOccurs="unbounded" type="attribute"/>
38+
</xsd:sequence>
39+
<xsd:attribute type="xsd:string" name="name"/>
40+
<xsd:attribute type="xsd:string" name="description"/>
41+
<xsd:attribute type="xsd:string" name="iri"/>
42+
<xsd:attribute type="xsd:boolean" name="readable"/>
43+
<xsd:attribute type="xsd:boolean" name="writable"/>
44+
<xsd:attribute type="xsd:boolean" name="readableLink"/>
45+
<xsd:attribute type="xsd:boolean" name="writableLink"/>
46+
<xsd:attribute type="xsd:boolean" name="required"/>
47+
<xsd:attribute type="xsd:boolean" name="identifier"/>
48+
</xsd:complexType>
3349
</xsd:schema>

‎tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php

+24
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,16 @@ private function getContainerBuilderProphecy()
219219
$definition = $definitionProphecy->reveal();
220220
$containerBuilderProphecy->getDefinition('api_platform.metadata.resource.metadata_factory.yaml')->willReturn($definition)->shouldBeCalled();
221221

222+
$definitionProphecy = $this->prophesize(Definition::class);
223+
$definitionProphecy->replaceArgument(0, [])->shouldBeCalled();
224+
$definition = $definitionProphecy->reveal();
225+
$containerBuilderProphecy->getDefinition('api_platform.metadata.property.name_collection_factory.yaml')->willReturn($definition)->shouldBeCalled();
226+
227+
$definitionProphecy = $this->prophesize(Definition::class);
228+
$definitionProphecy->replaceArgument(0, [])->shouldBeCalled();
229+
$definition = $definitionProphecy->reveal();
230+
$containerBuilderProphecy->getDefinition('api_platform.metadata.property.metadata_factory.yaml')->willReturn($definition)->shouldBeCalled();
231+
222232
$definitionProphecy = $this->prophesize(Definition::class);
223233
$definitionProphecy->replaceArgument(0, [])->shouldBeCalled();
224234
$definition = $definitionProphecy->reveal();
@@ -229,6 +239,16 @@ private function getContainerBuilderProphecy()
229239
$definition = $definitionProphecy->reveal();
230240
$containerBuilderProphecy->getDefinition('api_platform.metadata.resource.metadata_factory.xml')->willReturn($definition)->shouldBeCalled();
231241

242+
$definitionProphecy = $this->prophesize(Definition::class);
243+
$definitionProphecy->replaceArgument(0, [])->shouldBeCalled();
244+
$definition = $definitionProphecy->reveal();
245+
$containerBuilderProphecy->getDefinition('api_platform.metadata.property.name_collection_factory.xml')->willReturn($definition)->shouldBeCalled();
246+
247+
$definitionProphecy = $this->prophesize(Definition::class);
248+
$definitionProphecy->replaceArgument(0, [])->shouldBeCalled();
249+
$definition = $definitionProphecy->reveal();
250+
$containerBuilderProphecy->getDefinition('api_platform.metadata.property.metadata_factory.xml')->willReturn($definition)->shouldBeCalled();
251+
232252
$definitions = [
233253
'api_platform.action.documentation',
234254
'api_platform.action.placeholder',
@@ -301,9 +321,13 @@ private function getContainerBuilderProphecy()
301321
'api_platform.metadata.property.metadata_factory.property_info',
302322
'api_platform.metadata.property.metadata_factory.serializer',
303323
'api_platform.metadata.property.metadata_factory.validator',
324+
'api_platform.metadata.property.metadata_factory.xml',
325+
'api_platform.metadata.property.metadata_factory.yaml',
304326
'api_platform.metadata.property.name_collection_factory.cached',
305327
'api_platform.metadata.property.name_collection_factory.inherited',
306328
'api_platform.metadata.property.name_collection_factory.property_info',
329+
'api_platform.metadata.property.name_collection_factory.xml',
330+
'api_platform.metadata.property.name_collection_factory.yaml',
307331
'api_platform.metadata.resource.metadata_factory.annotation',
308332
'api_platform.metadata.resource.metadata_factory.cached',
309333
'api_platform.metadata.resource.metadata_factory.operation',
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
parse
2+
exception
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
resources:
2+
configdummy:
3+
class: 'ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\FileConfigDummy'
4+
properties: 'invalid'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?xml version="1.0" ?>
2+
3+
<resources>
4+
<resource class="ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\FileConfigDummy">
5+
<property name="foo">
6+
<foo>Foo</foo>
7+
</property>
8+
</resource>
9+
</resources>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
resources:
2+
configdummy:
3+
class: 'ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\FileConfigDummy'
4+
properties:
5+
'foo': 'invalid'

‎tests/Fixtures/FileConfigurations/resources.xml

+23
Original file line numberDiff line numberDiff line change
@@ -34,5 +34,28 @@
3434
<attribute name="@type">hydra:Operation</attribute>
3535
<attribute name="@hydra:title">File config Dummy</attribute>
3636
</attribute>
37+
38+
<property
39+
name="foo"
40+
description="The dummy foo"
41+
readable="true"
42+
writable="true"
43+
readableLink="false"
44+
writableLink="false"
45+
required="true"
46+
>
47+
<attribute name="foo">
48+
<attribute>Foo</attribute>
49+
</attribute>
50+
<attribute name="bar">
51+
<attribute>
52+
<attribute>Bar</attribute>
53+
</attribute>
54+
<attribute name="baz">Baz</attribute>
55+
</attribute>
56+
<attribute name="baz">Baz</attribute>
57+
</property>
58+
59+
<property name="name" description="The dummy name" />
3760
</resource>
3861
</resources>

‎tests/Fixtures/FileConfigurations/resources.yml

+16
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,19 @@ resources:
2323
'@type': 'hydra:Operation'
2424
'@hydra:title': 'File config Dummy'
2525
iri: 'someirischema'
26+
properties:
27+
'foo':
28+
description: 'The dummy foo'
29+
readable: true
30+
writable: true
31+
readableLink: false
32+
writableLink: false
33+
required: true
34+
attributes:
35+
'foo': ['Foo']
36+
'bar':
37+
0: ['Bar']
38+
'baz': 'Baz'
39+
'baz': 'Baz'
40+
'name':
41+
description: 'The dummy name'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
resources: 'invalid'

‎tests/Fixtures/TestBundle/Entity/FileConfigDummy.php

+17
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,13 @@ class FileConfigDummy
3636
*/
3737
private $name;
3838

39+
/**
40+
* @var string
41+
*
42+
* @ORM\Column
43+
*/
44+
private $foo;
45+
3946
public function getId()
4047
{
4148
return $this->id;
@@ -50,4 +57,14 @@ public function getName()
5057
{
5158
return $this->name;
5259
}
60+
61+
public function setFoo($foo)
62+
{
63+
$this->foo = $foo;
64+
}
65+
66+
public function getFoo()
67+
{
68+
return $this->foo;
69+
}
5370
}

‎tests/Fixtures/TestBundle/Resources/config/api_resources.yml

+3
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,6 @@ resources:
77
custom_operation:
88
method: 'GET'
99
controller: 'app.config_dummy_resource.action'
10+
properties:
11+
foo:
12+
description: 'The dummy foo'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
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\Tests\Metadata\Property\Factory;
13+
14+
use ApiPlatform\Core\Metadata\Property\PropertyMetadata;
15+
16+
/**
17+
* Property metadata provider for file configured factories tests.
18+
*
19+
* @author Baptiste Meyer <baptiste.meyer@gmail.com>
20+
*/
21+
abstract class FileConfigurationMetadataFactoryProvider extends \PHPUnit_Framework_TestCase
22+
{
23+
public function propertyMetadataProvider()
24+
{
25+
$metadata = [
26+
'description' => 'The dummy foo',
27+
'readable' => true,
28+
'writable' => true,
29+
'readableLink' => false,
30+
'writableLink' => false,
31+
'required' => true,
32+
'attributes' => [
33+
'foo' => [
34+
'Foo',
35+
],
36+
'bar' => [
37+
['Bar'],
38+
'baz' => 'Baz',
39+
],
40+
'baz' => 'Baz',
41+
],
42+
];
43+
44+
return [[$this->getPropertyMetadata($metadata)]];
45+
}
46+
47+
public function decoratedPropertyMetadataProvider()
48+
{
49+
$metadata = [
50+
'description' => 'The dummy foo',
51+
'readable' => true,
52+
'writable' => true,
53+
'readableLink' => true,
54+
'writableLink' => false,
55+
'required' => true,
56+
'identifier' => false,
57+
'attributes' => [
58+
'Foo',
59+
],
60+
];
61+
62+
return [[$this->getPropertyMetadata($metadata)]];
63+
}
64+
65+
private function getPropertyMetadata(array $metadata) : PropertyMetadata
66+
{
67+
$propertyMetadata = new PropertyMetadata();
68+
69+
foreach ($metadata as $propertyName => $propertyValue) {
70+
$propertyMetadata = $propertyMetadata->{'with'.ucfirst($propertyName)}($propertyValue);
71+
}
72+
73+
return $propertyMetadata;
74+
}
75+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
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\Tests\Metadata\Property\Factory;
13+
14+
use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
15+
use ApiPlatform\Core\Metadata\Property\Factory\XmlPropertyMetadataFactory;
16+
use ApiPlatform\Core\Metadata\Property\PropertyMetadata;
17+
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\FileConfigDummy;
18+
19+
/**
20+
* @author Baptiste Meyer <baptiste.meyer@gmail.com>
21+
*/
22+
class XmlPropertyMetadataFactoryTest extends FileConfigurationMetadataFactoryProvider
23+
{
24+
/**
25+
* @dataProvider propertyMetadataProvider
26+
*/
27+
public function testCreate(PropertyMetadata $expectedPropertyMetadata)
28+
{
29+
$configPath = __DIR__.'/../../../Fixtures/FileConfigurations/resources.xml';
30+
31+
$propertyMetadataFactory = new XmlPropertyMetadataFactory([$configPath]);
32+
$propertyMetadata = $propertyMetadataFactory->create(FileConfigDummy::class, 'foo');
33+
34+
$this->assertInstanceOf(PropertyMetadata::class, $propertyMetadata);
35+
$this->assertEquals($expectedPropertyMetadata, $propertyMetadata);
36+
}
37+
38+
/**
39+
* @dataProvider decoratedPropertyMetadataProvider
40+
*/
41+
public function testCreateWithParentPropertyMetadataFactory(PropertyMetadata $expectedPropertyMetadata)
42+
{
43+
$configPath = __DIR__.'/../../../Fixtures/FileConfigurations/resources.xml';
44+
45+
$decorated = $this->prophesize(PropertyMetadataFactoryInterface::class);
46+
$decorated
47+
->create(FileConfigDummy::class, 'foo', [])
48+
->willReturn(new PropertyMetadata(null, null, null, null, true, null, null, false, null, null, ['Foo']))
49+
->shouldBeCalled();
50+
51+
$propertyMetadataFactory = new XmlPropertyMetadataFactory([$configPath], $decorated->reveal());
52+
$propertyMetadata = $propertyMetadataFactory->create(FileConfigDummy::class, 'foo');
53+
54+
$this->assertInstanceOf(PropertyMetadata::class, $propertyMetadata);
55+
$this->assertEquals($expectedPropertyMetadata, $propertyMetadata);
56+
}
57+
58+
/**
59+
* @expectedException \ApiPlatform\Core\Exception\PropertyNotFoundException
60+
* @expectedExceptionMessage Property "foo" of the resource class "ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\ThisDoesNotExist" not found.
61+
*/
62+
public function testCreateWithNonexistentResource()
63+
{
64+
$configPath = __DIR__.'/../../../Fixtures/FileConfigurations/resourcenotfound.xml';
65+
66+
(new XmlPropertyMetadataFactory([$configPath]))->create(\ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\ThisDoesNotExist::class, 'foo');
67+
}
68+
69+
/**
70+
* @expectedException \ApiPlatform\Core\Exception\PropertyNotFoundException
71+
* @expectedExceptionMessage Property "bar" of the resource class "ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\FileConfigDummy" not found.
72+
*/
73+
public function testCreateWithNonexistentProperty()
74+
{
75+
$configPath = __DIR__.'/../../../Fixtures/FileConfigurations/resources.xml';
76+
77+
(new XmlPropertyMetadataFactory([$configPath]))->create(FileConfigDummy::class, 'bar');
78+
}
79+
80+
/**
81+
* @expectedException \ApiPlatform\Core\Exception\InvalidArgumentException
82+
* @expectedExceptionMessageRegExp /.+Element 'foo': This element is not expected\..+/
83+
*/
84+
public function testCreateWithInvalidXml()
85+
{
86+
$configPath = __DIR__.'/../../../Fixtures/FileConfigurations/propertyinvalid.xml';
87+
88+
(new XmlPropertyMetadataFactory([$configPath]))->create(FileConfigDummy::class, 'foo');
89+
}
90+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
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\Tests\Metadata\Property\Factory;
13+
14+
use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
15+
use ApiPlatform\Core\Metadata\Property\Factory\XmlPropertyNameCollectionFactory;
16+
use ApiPlatform\Core\Metadata\Property\PropertyNameCollection;
17+
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\FileConfigDummy;
18+
19+
/**
20+
* @author Baptiste Meyer <baptiste.meyer@gmail.com>
21+
*/
22+
class XmlPropertyNameCollectionFactoryTest extends \PHPUnit_Framework_TestCase
23+
{
24+
public function testCreate()
25+
{
26+
$configPath = __DIR__.'/../../../Fixtures/FileConfigurations/resources.xml';
27+
28+
$this->assertEquals(
29+
(new XmlPropertyNameCollectionFactory([$configPath]))->create(FileConfigDummy::class),
30+
new PropertyNameCollection(['foo', 'name'])
31+
);
32+
}
33+
34+
public function testCreateWithParentPropertyNameCollectionFactory()
35+
{
36+
$configPath = __DIR__.'/../../../Fixtures/FileConfigurations/resources.xml';
37+
38+
$decorated = $this->prophesize(PropertyNameCollectionFactoryInterface::class);
39+
$decorated
40+
->create(FileConfigDummy::class, [])
41+
->willReturn(new PropertyNameCollection(['id']))
42+
->shouldBeCalled();
43+
44+
$this->assertEquals(
45+
(new XmlPropertyNameCollectionFactory([$configPath], $decorated->reveal()))->create(FileConfigDummy::class),
46+
new PropertyNameCollection(['foo', 'name', 'id'])
47+
);
48+
}
49+
50+
/**
51+
* @expectedException \ApiPlatform\Core\Exception\ResourceClassNotFoundException
52+
* @expectedExceptionMessage The resource class "ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\ThisDoesNotExist" does not exist.
53+
*/
54+
public function testCreateWithNonexistentResource()
55+
{
56+
$configPath = __DIR__.'/../../../Fixtures/FileConfigurations/resourcenotfound.xml';
57+
58+
(new XmlPropertyNameCollectionFactory([$configPath]))->create(\ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\ThisDoesNotExist::class);
59+
}
60+
61+
/**
62+
* @expectedException \ApiPlatform\Core\Exception\InvalidArgumentException
63+
* @expectedExceptionMessageRegExp /.+Element 'foo': This element is not expected\..+/
64+
*/
65+
public function testCreateWithInvalidXml()
66+
{
67+
$configPath = __DIR__.'/../../../Fixtures/FileConfigurations/propertyinvalid.xml';
68+
69+
(new XmlPropertyNameCollectionFactory([$configPath]))->create(FileConfigDummy::class);
70+
}
71+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
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\Tests\Metadata\Property\Factory;
13+
14+
use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
15+
use ApiPlatform\Core\Metadata\Property\Factory\YamlPropertyMetadataFactory;
16+
use ApiPlatform\Core\Metadata\Property\PropertyMetadata;
17+
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\FileConfigDummy;
18+
19+
/**
20+
* @author Baptiste Meyer <baptiste.meyer@gmail.com>
21+
*/
22+
class YamlPropertyMetadataFactoryTest extends FileConfigurationMetadataFactoryProvider
23+
{
24+
/**
25+
* @dataProvider propertyMetadataProvider
26+
*/
27+
public function testCreate(PropertyMetadata $expectedPropertyMetadata)
28+
{
29+
$configPath = __DIR__.'/../../../Fixtures/FileConfigurations/resources.yml';
30+
31+
$propertyMetadataFactory = new YamlPropertyMetadataFactory([$configPath]);
32+
$propertyMetadata = $propertyMetadataFactory->create(FileConfigDummy::class, 'foo');
33+
34+
$this->assertInstanceOf(PropertyMetadata::class, $propertyMetadata);
35+
$this->assertEquals($expectedPropertyMetadata, $propertyMetadata);
36+
}
37+
38+
/**
39+
* @dataProvider decoratedPropertyMetadataProvider
40+
*/
41+
public function testCreateWithParentPropertyMetadataFactory(PropertyMetadata $expectedPropertyMetadata)
42+
{
43+
$configPath = __DIR__.'/../../../Fixtures/FileConfigurations/resources.yml';
44+
45+
$decorated = $this->prophesize(PropertyMetadataFactoryInterface::class);
46+
$decorated
47+
->create(FileConfigDummy::class, 'foo', [])
48+
->willReturn(new PropertyMetadata(null, null, null, null, true, null, null, false, null, null, ['Foo']))
49+
->shouldBeCalled();
50+
51+
$propertyMetadataFactory = new YamlPropertyMetadataFactory([$configPath], $decorated->reveal());
52+
$propertyMetadata = $propertyMetadataFactory->create(FileConfigDummy::class, 'foo');
53+
54+
$this->assertInstanceOf(PropertyMetadata::class, $propertyMetadata);
55+
$this->assertEquals($expectedPropertyMetadata, $propertyMetadata);
56+
}
57+
58+
/**
59+
* @expectedException \ApiPlatform\Core\Exception\PropertyNotFoundException
60+
* @expectedExceptionMessage Property "foo" of the resource class "ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\ThisDoesNotExist" not found.
61+
*/
62+
public function testCreateWithNonexistentResource()
63+
{
64+
$configPath = __DIR__.'/../../../Fixtures/FileConfigurations/resourcenotfound.yml';
65+
66+
(new YamlPropertyMetadataFactory([$configPath]))->create(\ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\ThisDoesNotExist::class, 'foo');
67+
}
68+
69+
/**
70+
* @expectedException \ApiPlatform\Core\Exception\PropertyNotFoundException
71+
* @expectedExceptionMessage Property "bar" of the resource class "ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\FileConfigDummy" not found.
72+
*/
73+
public function testCreateWithNonexistentProperty()
74+
{
75+
$configPath = __DIR__.'/../../../Fixtures/FileConfigurations/resources.yml';
76+
77+
(new YamlPropertyMetadataFactory([$configPath]))->create(FileConfigDummy::class, 'bar');
78+
}
79+
80+
/**
81+
* @expectedException \ApiPlatform\Core\Exception\InvalidArgumentException
82+
* @expectedExceptionMessageRegExp /"resources" setting is expected to be null or an array, string given in ".+\/\.\.\/\.\.\/\.\.\/Fixtures\/FileConfigurations\/resourcesinvalid\.yml"\./
83+
*/
84+
public function testCreateWithMalformedResourcesSetting()
85+
{
86+
$configPath = __DIR__.'/../../../Fixtures/FileConfigurations/resourcesinvalid.yml';
87+
88+
(new YamlPropertyMetadataFactory([$configPath]))->create(FileConfigDummy::class, 'foo');
89+
}
90+
91+
/**
92+
* @expectedException \ApiPlatform\Core\Exception\InvalidArgumentException
93+
* @expectedExceptionMessageRegExp /"properties" setting is expected to be null or an array, string given in ".+\/\.\.\/\.\.\/\.\.\/Fixtures\/FileConfigurations\/propertiesinvalid\.yml"\./
94+
*/
95+
public function testCreateWithMalformedPropertiesSetting()
96+
{
97+
$configPath = __DIR__.'/../../../Fixtures/FileConfigurations/propertiesinvalid.yml';
98+
99+
(new YamlPropertyMetadataFactory([$configPath]))->create(FileConfigDummy::class, 'foo');
100+
}
101+
102+
/**
103+
* @expectedException \ApiPlatform\Core\Exception\InvalidArgumentException
104+
* @expectedExceptionMessageRegExp /"foo" setting is expected to be null or an array, string given in ".+\/\.\.\/\.\.\/\.\.\/Fixtures\/FileConfigurations\/propertyinvalid\.yml"\./
105+
*/
106+
public function testCreateWithMalformedPropertySetting()
107+
{
108+
$configPath = __DIR__.'/../../../Fixtures/FileConfigurations/propertyinvalid.yml';
109+
110+
(new YamlPropertyMetadataFactory([$configPath]))->create(FileConfigDummy::class, 'foo');
111+
}
112+
113+
/**
114+
* @expectedException \ApiPlatform\Core\Exception\InvalidArgumentException
115+
* @expectedExceptionMessageRegExp /"class" setting is expected to be a string, none given in ".+\/\.\.\/\.\.\/\.\.\/Fixtures\/FileConfigurations\/resourcenoclass\.yml"\./
116+
*/
117+
public function testCreateWithoutResourceClass()
118+
{
119+
$configPath = __DIR__.'/../../../Fixtures/FileConfigurations/resourcenoclass.yml';
120+
121+
(new YamlPropertyMetadataFactory([$configPath]))->create(FileConfigDummy::class, 'foo');
122+
}
123+
124+
/**
125+
* @expectedException \Symfony\Component\Yaml\Exception\ParseException
126+
*/
127+
public function testCreateWithMalformedYaml()
128+
{
129+
$configPath = __DIR__.'/../../../Fixtures/FileConfigurations/parse_exception.yml';
130+
131+
(new YamlPropertyMetadataFactory([$configPath]))->create(FileConfigDummy::class, 'foo');
132+
}
133+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
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\Tests\Metadata\Property\Factory;
13+
14+
use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
15+
use ApiPlatform\Core\Metadata\Property\Factory\YamlPropertyNameCollectionFactory;
16+
use ApiPlatform\Core\Metadata\Property\PropertyNameCollection;
17+
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\FileConfigDummy;
18+
19+
/**
20+
* @author Baptiste Meyer <baptiste.meyer@gmail.com>
21+
*/
22+
class YamlPropertyNameCollectionFactoryTest extends \PHPUnit_Framework_TestCase
23+
{
24+
public function testCreate()
25+
{
26+
$configPath = __DIR__.'/../../../Fixtures/FileConfigurations/resources.yml';
27+
28+
$this->assertEquals(
29+
(new YamlPropertyNameCollectionFactory([$configPath]))->create(FileConfigDummy::class),
30+
new PropertyNameCollection(['foo', 'name'])
31+
);
32+
}
33+
34+
public function testCreateWithParentPropertyMetadataFactory()
35+
{
36+
$configPath = __DIR__.'/../../../Fixtures/FileConfigurations/resources.yml';
37+
38+
$decorated = $this->prophesize(PropertyNameCollectionFactoryInterface::class);
39+
$decorated
40+
->create(FileConfigDummy::class, [])
41+
->willReturn(new PropertyNameCollection(['id']))
42+
->shouldBeCalled();
43+
44+
$this->assertEquals(
45+
(new YamlPropertyNameCollectionFactory([$configPath], $decorated->reveal()))->create(FileConfigDummy::class),
46+
new PropertyNameCollection(['foo', 'name', 'id'])
47+
);
48+
}
49+
50+
/**
51+
* @expectedException \ApiPlatform\Core\Exception\ResourceClassNotFoundException
52+
* @expectedExceptionMessage The resource class "ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\ThisDoesNotExist" does not exist.
53+
*/
54+
public function testCreateWithNonexistentResource()
55+
{
56+
$configPath = __DIR__.'/../../../Fixtures/FileConfigurations/resourcenotfound.yml';
57+
58+
(new YamlPropertyNameCollectionFactory([$configPath]))->create(\ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\ThisDoesNotExist::class);
59+
}
60+
61+
/**
62+
* @expectedException \ApiPlatform\Core\Exception\InvalidArgumentException
63+
* @expectedExceptionMessageRegExp /"resources" setting is expected to be null or an array, string given in ".+\/\.\.\/\.\.\/\.\.\/Fixtures\/FileConfigurations\/resourcesinvalid\.yml"\./
64+
*/
65+
public function testCreateWithMalformedResourcesSetting()
66+
{
67+
$configPath = __DIR__.'/../../../Fixtures/FileConfigurations/resourcesinvalid.yml';
68+
69+
(new YamlPropertyNameCollectionFactory([$configPath]))->create(FileConfigDummy::class);
70+
}
71+
72+
/**
73+
* @expectedException \ApiPlatform\Core\Exception\InvalidArgumentException
74+
* @expectedExceptionMessageRegExp /"properties" setting is expected to be null or an array, string given in ".+\/\.\.\/\.\.\/\.\.\/Fixtures\/FileConfigurations\/propertiesinvalid\.yml"\./
75+
*/
76+
public function testCreateWithMalformedPropertiesSetting()
77+
{
78+
$configPath = __DIR__.'/../../../Fixtures/FileConfigurations/propertiesinvalid.yml';
79+
80+
(new YamlPropertyNameCollectionFactory([$configPath]))->create(FileConfigDummy::class);
81+
}
82+
83+
/**
84+
* @expectedException \ApiPlatform\Core\Exception\InvalidArgumentException
85+
* @expectedExceptionMessageRegExp /"foo" setting is expected to be null or an array, string given in ".+\/\.\.\/\.\.\/\.\.\/Fixtures\/FileConfigurations\/propertyinvalid\.yml"\./
86+
*/
87+
public function testCreateWithMalformedPropertySetting()
88+
{
89+
$configPath = __DIR__.'/../../../Fixtures/FileConfigurations/propertyinvalid.yml';
90+
91+
(new YamlPropertyNameCollectionFactory([$configPath]))->create(FileConfigDummy::class);
92+
}
93+
94+
/**
95+
* @expectedException \ApiPlatform\Core\Exception\InvalidArgumentException
96+
* @expectedExceptionMessageRegExp /"class" setting is expected to be a string, none given in ".+\/\.\.\/\.\.\/\.\.\/Fixtures\/FileConfigurations\/resourcenoclass\.yml"\./
97+
*/
98+
public function testCreateWithoutResourceClass()
99+
{
100+
$configPath = __DIR__.'/../../../Fixtures/FileConfigurations/resourcenoclass.yml';
101+
102+
(new YamlPropertyNameCollectionFactory([$configPath]))->create(FileConfigDummy::class);
103+
}
104+
105+
/**
106+
* @expectedException \Symfony\Component\Yaml\Exception\ParseException
107+
*/
108+
public function testCreateWithMalformedYaml()
109+
{
110+
$configPath = __DIR__.'/../../../Fixtures/FileConfigurations/parse_exception.yml';
111+
112+
(new YamlPropertyNameCollectionFactory([$configPath]))->create(FileConfigDummy::class);
113+
}
114+
}

‎tests/Metadata/Resource/Factory/FileConfigurationMetadataFactoryProvider.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
use ApiPlatform\Core\Metadata\Resource\ResourceMetadata;
1515

1616
/**
17-
* Resource metadata provider for file configurated factories tests.
17+
* Resource metadata provider for file configured factories tests.
1818
*
1919
* @author Antoine Bluchet <soyuka@gmail.com>
2020
*/

‎tests/Metadata/Resource/Factory/YamlResourceMetadataFactoryTest.php

+10
Original file line numberDiff line numberDiff line change
@@ -127,4 +127,14 @@ public function testYamlExistingParentResourceMetadataFactory(ResourceMetadata $
127127

128128
$this->assertEquals($expectedResourceMetadata, $resourceMetadata);
129129
}
130+
131+
/**
132+
* @expectedException \Symfony\Component\Yaml\Exception\ParseException
133+
*/
134+
public function testCreateWithMalformedYaml()
135+
{
136+
$configPath = __DIR__.'/../../../Fixtures/FileConfigurations/parse_exception.yml';
137+
138+
(new YamlResourceMetadataFactory([$configPath]))->create(FileConfigDummy::class);
139+
}
130140
}

‎tests/Metadata/Resource/Factory/YamlResourceNameCollectionFactoryTest.php

+10
Original file line numberDiff line numberDiff line change
@@ -55,4 +55,14 @@ public function testNoClassYamlResourceNameCollectionFactory()
5555

5656
$resourceMetadataFactory->create();
5757
}
58+
59+
/**
60+
* @expectedException \Symfony\Component\Yaml\Exception\ParseException
61+
*/
62+
public function testCreateWithMalformedYaml()
63+
{
64+
$configPath = __DIR__.'/../../../Fixtures/FileConfigurations/parse_exception.yml';
65+
66+
(new YamlResourceNameCollectionFactory([$configPath]))->create();
67+
}
5868
}

0 commit comments

Comments
 (0)
Please sign in to comment.