Skip to content

Commit

Permalink
fix(metadata): operations must inherit from resource and defaults
Browse files Browse the repository at this point in the history
  • Loading branch information
vincentchalamon committed Nov 17, 2022
1 parent cd3fb78 commit 2b2d468
Show file tree
Hide file tree
Showing 4 changed files with 141 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@
use ApiPlatform\Metadata\Put;
use ApiPlatform\Metadata\Resource\DeprecationMetadataTrait;
use ApiPlatform\Metadata\Resource\ResourceMetadataCollection;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter;

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

public function __construct(ResourceExtractorInterface $extractor, ResourceMetadataCollectionFactoryInterface $decorated = null, array $defaults = [])
public function __construct(ResourceExtractorInterface $extractor, ResourceMetadataCollectionFactoryInterface $decorated = null, array $defaults = [], LoggerInterface $logger = null)
{
$this->extractor = $extractor;
$this->decorated = $decorated;
$this->defaults = $defaults;
$this->logger = $logger ?? new NullLogger();
}

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

private function getOperationWithDefaults(ApiResource $resource, HttpOperation $operation): HttpOperation
{
foreach (($this->defaults['attributes'] ?? []) as $key => $value) {
[$key, $value] = $this->getKeyValue($key, $value);
if (null === $operation->{'get'.ucfirst($key)}()) {
$operation = $operation->{'with'.ucfirst($key)}($value);
}
}

// Inherit from resource defaults
foreach (get_class_methods($resource) as $methodName) {
if (0 !== strpos($methodName, 'get')) {
continue;
Expand All @@ -205,6 +204,65 @@ private function getOperationWithDefaults(ApiResource $resource, HttpOperation $
$operation = $operation->{'with'.substr($methodName, 3)}($value);
}

$operation = $operation->withExtraProperties(array_merge(
$resource->getExtraProperties(),
$operation->getExtraProperties()
));

// Add global defaults attributes to the operation
$operation = $this->addGlobalDefaults($operation);

if ($operation->getRouteName()) {
/** @var HttpOperation $operation */
$operation = $operation->withName($operation->getRouteName());
}

// Check for name conflict
if ($operation->getName() && null !== ($operations = $resource->getOperations())) {
if (!$operations->has($operation->getName())) {
return $operation;
}

$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()));
/** @var HttpOperation $operation */
$operation = $operation->withName('');
}

return $operation;
}

private function addGlobalDefaults(HttpOperation $operation): HttpOperation
{
if (!$this->camelCaseToSnakeCaseNameConverter) {
$this->camelCaseToSnakeCaseNameConverter = new CamelCaseToSnakeCaseNameConverter();
}

$extraProperties = [];
foreach ($this->defaults as $key => $value) {
$upperKey = ucfirst($this->camelCaseToSnakeCaseNameConverter->denormalize($key));
$getter = 'get'.$upperKey;

if (!method_exists($operation, $getter)) {
if (!isset($extraProperties[$key])) {
$extraProperties[$key] = $value;
}

continue;
}

$currentValue = $operation->{$getter}();

if (\is_array($currentValue) && $currentValue) {
$operation = $operation->{'with'.$upperKey}(array_merge($value, $currentValue));
}

if (null !== $currentValue) {
continue;
}

$operation = $operation->{'with'.$upperKey}($value);
}

return $operation->withExtraProperties(array_merge($extraProperties, $operation->getExtraProperties()));
}
}
1 change: 1 addition & 0 deletions src/Symfony/Bundle/Resources/config/metadata/resource.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
<argument type="service" id="api_platform.metadata.resource_extractor.xml" />
<argument type="service" id="api_platform.metadata.resource.metadata_collection_factory.xml.inner" />
<argument>%api_platform.defaults%</argument>
<argument type="service" id="logger" on-invalid="null" />
</service>

<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">
Expand Down
1 change: 1 addition & 0 deletions src/Symfony/Bundle/Resources/config/metadata/yaml.xml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
<argument type="service" id="api_platform.metadata.resource_extractor.yaml" />
<argument type="service" id="api_platform.metadata.resource.metadata_collection_factory.yaml.inner" />
<argument>%api_platform.defaults%</argument>
<argument type="service" id="logger" on-invalid="null" />
</service>

<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">
Expand Down
74 changes: 73 additions & 1 deletion tests/Metadata/Extractor/ResourceMetadataCompatibilityTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
use ApiPlatform\Tests\Metadata\Extractor\Adapter\YamlResourceAdapter;
use PHPUnit\Framework\AssertionFailedError;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter;

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

private const RESOURCE_CLASS = Comment::class;
private const SHORT_NAME = 'Comment';
private const DEFAULTS = [
'route_prefix' => '/v1',
];
private const FIXTURES = [
null,
[
Expand Down Expand Up @@ -208,6 +212,10 @@ final class ResourceMetadataCompatibilityTest extends TestCase
'priority' => 200,
'extraProperties' => [
'foo' => 'bar',
'custom_property' => 'Lorem ipsum dolor sit amet',
'another_custom_property' => [
'Lorem ipsum' => 'Dolor sit amet',
],
],
],
],
Expand Down Expand Up @@ -329,6 +337,10 @@ final class ResourceMetadataCompatibilityTest extends TestCase
'priority' => 200,
'extraProperties' => [
'foo' => 'bar',
'custom_property' => 'Lorem ipsum dolor sit amet',
'another_custom_property' => [
'Lorem ipsum' => 'Dolor sit amet',
],
],
],
[
Expand Down Expand Up @@ -420,7 +432,7 @@ public function testValidMetadata(string $extractorClass, ResourceAdapterInterfa

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

private function getOperationWithDefaults(ApiResource $resource, HttpOperation $operation): HttpOperation
{
// Inherit from resource defaults
foreach (get_class_methods($resource) as $methodName) {
if (0 !== strpos($methodName, 'get')) {
continue;
Expand All @@ -611,6 +624,65 @@ private function getOperationWithDefaults(ApiResource $resource, HttpOperation $
$operation = $operation->{'with'.substr($methodName, 3)}($value);
}

$operation = $operation->withExtraProperties(array_merge(
$resource->getExtraProperties(),
$operation->getExtraProperties()
));

// Add global defaults attributes to the operation
$operation = $this->addGlobalDefaults($operation);

if ($operation->getRouteName()) {
/** @var HttpOperation $operation */
$operation = $operation->withName($operation->getRouteName());
}

// Check for name conflict
if ($operation->getName() && null !== ($operations = $resource->getOperations())) {
if (!$operations->has($operation->getName())) {
return $operation;
}

/** @var HttpOperation $operation */
$operation = $operation->withName('');
}

return $operation;
}

private function addGlobalDefaults(HttpOperation $operation): HttpOperation
{
if (!$this->camelCaseToSnakeCaseNameConverter) {
$this->camelCaseToSnakeCaseNameConverter = new CamelCaseToSnakeCaseNameConverter();
}

$extraProperties = [];
foreach (self::DEFAULTS as $key => $value) {
$upperKey = ucfirst($this->camelCaseToSnakeCaseNameConverter->denormalize($key));
$getter = 'get'.$upperKey;

if (!method_exists($operation, $getter)) {
if (!isset($extraProperties[$key])) {
$extraProperties[$key] = $value;
}

continue;
}

$currentValue = $operation->{$getter}();

/* @phpstan-ignore-next-line */
if (\is_array($currentValue) && $currentValue && \is_array($value) && $value) {
$operation = $operation->{'with'.$upperKey}(array_merge($value, $currentValue));
}

if (null !== $currentValue) {
continue;
}

$operation = $operation->{'with'.$upperKey}($value);
}

return $operation->withExtraProperties(array_merge($extraProperties, $operation->getExtraProperties()));
}
}

0 comments on commit 2b2d468

Please sign in to comment.