Skip to content

fix(metadata): operations must inherit from resource and defaults #5194

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Nov 17, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we find a way to remove this dependency? Or at least create an interface to abstract it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should be done in a separate PR as it's used at least 7 times in traits and classes. It's hardcoded (new CamelCaseToSnakeCaseNameConverter()), using an interface would require a dependency injection behavior.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think there's a point doing an abstraction for this.


/**
* 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()));
}
}