Skip to content

Commit 736e2cc

Browse files
fix(metadata): operations must inherit from resource and defaults
1 parent cd3fb78 commit 736e2cc

File tree

4 files changed

+140
-9
lines changed

4 files changed

+140
-9
lines changed

src/Metadata/Resource/Factory/ExtractorResourceMetadataCollectionFactory.php

+66-8
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@
2626
use ApiPlatform\Metadata\Put;
2727
use ApiPlatform\Metadata\Resource\DeprecationMetadataTrait;
2828
use ApiPlatform\Metadata\Resource\ResourceMetadataCollection;
29+
use Psr\Log\LoggerInterface;
30+
use Psr\Log\NullLogger;
31+
use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter;
2932

3033
/**
3134
* Creates a resource metadata from {@see Resource} extractors (XML, YAML).
@@ -38,12 +41,14 @@ final class ExtractorResourceMetadataCollectionFactory implements ResourceMetada
3841
private $extractor;
3942
private $decorated;
4043
private $defaults;
44+
private $logger;
4145

42-
public function __construct(ResourceExtractorInterface $extractor, ResourceMetadataCollectionFactoryInterface $decorated = null, array $defaults = [])
46+
public function __construct(ResourceExtractorInterface $extractor, ResourceMetadataCollectionFactoryInterface $decorated = null, array $defaults = [], LoggerInterface $logger = null)
4347
{
4448
$this->extractor = $extractor;
4549
$this->decorated = $decorated;
4650
$this->defaults = $defaults;
51+
$this->logger = $logger ?? new NullLogger();
4752
}
4853

4954
/**
@@ -182,13 +187,7 @@ private function buildGraphQlOperations(?array $data, ApiResource $resource): ar
182187

183188
private function getOperationWithDefaults(ApiResource $resource, HttpOperation $operation): HttpOperation
184189
{
185-
foreach (($this->defaults['attributes'] ?? []) as $key => $value) {
186-
[$key, $value] = $this->getKeyValue($key, $value);
187-
if (null === $operation->{'get'.ucfirst($key)}()) {
188-
$operation = $operation->{'with'.ucfirst($key)}($value);
189-
}
190-
}
191-
190+
// Inherit from resource defaults
192191
foreach (get_class_methods($resource) as $methodName) {
193192
if (0 !== strpos($methodName, 'get')) {
194193
continue;
@@ -205,6 +204,65 @@ private function getOperationWithDefaults(ApiResource $resource, HttpOperation $
205204
$operation = $operation->{'with'.substr($methodName, 3)}($value);
206205
}
207206

207+
$operation = $operation->withExtraProperties(array_merge(
208+
$resource->getExtraProperties(),
209+
$operation->getExtraProperties()
210+
));
211+
212+
// Add global defaults attributes to the operation
213+
$operation = $this->addGlobalDefaults($operation);
214+
215+
if ($operation->getRouteName()) {
216+
/** @var HttpOperation $operation */
217+
$operation = $operation->withName($operation->getRouteName());
218+
}
219+
220+
// Check for name conflict
221+
if ($operation->getName() && null !== ($operations = $resource->getOperations())) {
222+
if (!$operations->has($operation->getName())) {
223+
return $operation;
224+
}
225+
226+
$this->logger->warning(sprintf('The operation "%s" already exists on the resource "%s", pick a different name or leave it empty. In the meantime we will generate a unique name.', $operation->getName(), $resource->getClass()));
227+
/** @var HttpOperation $operation */
228+
$operation = $operation->withName('');
229+
}
230+
208231
return $operation;
209232
}
233+
234+
private function addGlobalDefaults(HttpOperation $operation): HttpOperation
235+
{
236+
if (!$this->camelCaseToSnakeCaseNameConverter) {
237+
$this->camelCaseToSnakeCaseNameConverter = new CamelCaseToSnakeCaseNameConverter();
238+
}
239+
240+
$extraProperties = [];
241+
foreach ($this->defaults as $key => $value) {
242+
$upperKey = ucfirst($this->camelCaseToSnakeCaseNameConverter->denormalize($key));
243+
$getter = 'get'.$upperKey;
244+
245+
if (!method_exists($operation, $getter)) {
246+
if (!isset($extraProperties[$key])) {
247+
$extraProperties[$key] = $value;
248+
}
249+
250+
continue;
251+
}
252+
253+
$currentValue = $operation->{$getter}();
254+
255+
if (\is_array($currentValue) && $currentValue) {
256+
$operation = $operation->{'with'.$upperKey}(array_merge($value, $currentValue));
257+
}
258+
259+
if (null !== $currentValue) {
260+
continue;
261+
}
262+
263+
$operation = $operation->{'with'.$upperKey}($value);
264+
}
265+
266+
return $operation->withExtraProperties(array_merge($extraProperties, $operation->getExtraProperties()));
267+
}
210268
}

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

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
<argument type="service" id="api_platform.metadata.resource_extractor.xml" />
2222
<argument type="service" id="api_platform.metadata.resource.metadata_collection_factory.xml.inner" />
2323
<argument>%api_platform.defaults%</argument>
24+
<argument type="service" id="logger" on-invalid="null" />
2425
</service>
2526

2627
<service id="api_platform.metadata.resource.metadata_collection_factory.not_exposed_operation" class="ApiPlatform\Metadata\Resource\Factory\NotExposedOperationResourceMetadataCollectionFactory" decorates="api_platform.metadata.resource.metadata_collection_factory" decoration-priority="700" public="false">

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

+1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
<argument type="service" id="api_platform.metadata.resource_extractor.yaml" />
2525
<argument type="service" id="api_platform.metadata.resource.metadata_collection_factory.yaml.inner" />
2626
<argument>%api_platform.defaults%</argument>
27+
<argument type="service" id="logger" on-invalid="null" />
2728
</service>
2829

2930
<service id="api_platform.metadata.property.metadata_factory.yaml" class="ApiPlatform\Metadata\Property\Factory\ExtractorPropertyMetadataFactory" decorates="api_platform.metadata.property.metadata_factory" decoration-priority="20" public="false">

tests/Metadata/Extractor/ResourceMetadataCompatibilityTest.php

+72-1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
use ApiPlatform\Tests\Metadata\Extractor\Adapter\YamlResourceAdapter;
3838
use PHPUnit\Framework\AssertionFailedError;
3939
use PHPUnit\Framework\TestCase;
40+
use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter;
4041

4142
/**
4243
* Ensures XML and YAML mappings are fully compatible with ApiResource.
@@ -49,6 +50,9 @@ final class ResourceMetadataCompatibilityTest extends TestCase
4950

5051
private const RESOURCE_CLASS = Comment::class;
5152
private const SHORT_NAME = 'Comment';
53+
private const DEFAULTS = [
54+
'route_prefix' => '/v1',
55+
];
5256
private const FIXTURES = [
5357
null,
5458
[
@@ -208,6 +212,10 @@ final class ResourceMetadataCompatibilityTest extends TestCase
208212
'priority' => 200,
209213
'extraProperties' => [
210214
'foo' => 'bar',
215+
'custom_property' => 'Lorem ipsum dolor sit amet',
216+
'another_custom_property' => [
217+
'Lorem ipsum' => 'Dolor sit amet',
218+
],
211219
],
212220
],
213221
],
@@ -329,6 +337,10 @@ final class ResourceMetadataCompatibilityTest extends TestCase
329337
'priority' => 200,
330338
'extraProperties' => [
331339
'foo' => 'bar',
340+
'custom_property' => 'Lorem ipsum dolor sit amet',
341+
'another_custom_property' => [
342+
'Lorem ipsum' => 'Dolor sit amet',
343+
],
332344
],
333345
],
334346
[
@@ -420,7 +432,7 @@ public function testValidMetadata(string $extractorClass, ResourceAdapterInterfa
420432

421433
try {
422434
$extractor = new $extractorClass($adapter(self::RESOURCE_CLASS, $parameters, self::FIXTURES));
423-
$factory = new ExtractorResourceMetadataCollectionFactory($extractor);
435+
$factory = new ExtractorResourceMetadataCollectionFactory($extractor, null, self::DEFAULTS);
424436
$collection = $factory->create(self::RESOURCE_CLASS);
425437
} catch (\Exception $exception) {
426438
throw new AssertionFailedError('Failed asserting that the schema is valid according to '.ApiResource::class, 0, $exception);
@@ -595,6 +607,7 @@ private function withGraphQlOperations(array $values, ?array $fixtures): array
595607

596608
private function getOperationWithDefaults(ApiResource $resource, HttpOperation $operation): HttpOperation
597609
{
610+
// Inherit from resource defaults
598611
foreach (get_class_methods($resource) as $methodName) {
599612
if (0 !== strpos($methodName, 'get')) {
600613
continue;
@@ -611,6 +624,64 @@ private function getOperationWithDefaults(ApiResource $resource, HttpOperation $
611624
$operation = $operation->{'with'.substr($methodName, 3)}($value);
612625
}
613626

627+
$operation = $operation->withExtraProperties(array_merge(
628+
$resource->getExtraProperties(),
629+
$operation->getExtraProperties()
630+
));
631+
632+
// Add global defaults attributes to the operation
633+
$operation = $this->addGlobalDefaults($operation);
634+
635+
if ($operation->getRouteName()) {
636+
/** @var HttpOperation $operation */
637+
$operation = $operation->withName($operation->getRouteName());
638+
}
639+
640+
// Check for name conflict
641+
if ($operation->getName() && null !== ($operations = $resource->getOperations())) {
642+
if (!$operations->has($operation->getName())) {
643+
return $operation;
644+
}
645+
646+
/** @var HttpOperation $operation */
647+
$operation = $operation->withName('');
648+
}
649+
614650
return $operation;
615651
}
652+
653+
private function addGlobalDefaults(HttpOperation $operation): HttpOperation
654+
{
655+
if (!$this->camelCaseToSnakeCaseNameConverter) {
656+
$this->camelCaseToSnakeCaseNameConverter = new CamelCaseToSnakeCaseNameConverter();
657+
}
658+
659+
$extraProperties = [];
660+
foreach (self::DEFAULTS as $key => $value) {
661+
$upperKey = ucfirst($this->camelCaseToSnakeCaseNameConverter->denormalize($key));
662+
$getter = 'get'.$upperKey;
663+
664+
if (!method_exists($operation, $getter)) {
665+
if (!isset($extraProperties[$key])) {
666+
$extraProperties[$key] = $value;
667+
}
668+
669+
continue;
670+
}
671+
672+
$currentValue = $operation->{$getter}();
673+
674+
if (\is_array($currentValue) && $currentValue && \is_array($value) && $value) {
675+
$operation = $operation->{'with'.$upperKey}(array_merge($value, $currentValue));
676+
}
677+
678+
if (null !== $currentValue) {
679+
continue;
680+
}
681+
682+
$operation = $operation->{'with'.$upperKey}($value);
683+
}
684+
685+
return $operation->withExtraProperties(array_merge($extraProperties, $operation->getExtraProperties()));
686+
}
616687
}

0 commit comments

Comments
 (0)