Skip to content

Commit

Permalink
BAP-22864: Add possibility to customize request data normalization fo…
Browse files Browse the repository at this point in the history
…r API subresources (#39970)
  • Loading branch information
vsoroka authored Dec 8, 2024
1 parent d3a4b20 commit 023acf6
Show file tree
Hide file tree
Showing 8 changed files with 162 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
abstract class AbstractNormalizeRequestData implements ProcessorInterface
{
protected const ROOT_POINTER = '';
protected const CLASS_FIELD_NAME = 'class';
protected const ID_FIELD_NAME = 'id';

protected ValueNormalizer $valueNormalizer;
protected EntityIdTransformerRegistry $entityIdTransformerRegistry;
Expand Down Expand Up @@ -126,13 +128,12 @@ protected function normalizeRelationshipItem(
$entityId = $data[JsonApiDoc::ID];
if (str_contains($entityClass, '\\')) {
if ($this->isAcceptableTargetClass($entityClass, $associationMetadata)) {
$targetMetadata = $associationMetadata?->getTargetMetadata();
$entityId = $this->normalizeEntityId(
$this->buildPath($path, 'id'),
$this->buildPath($path, self::ID_FIELD_NAME),
$this->buildPointer($pointer, JsonApiDoc::ID),
$entityClass,
$entityId,
$targetMetadata
$associationMetadata?->getTargetMetadata()
);
} else {
$this->addValidationError(Constraint::ENTITY_TYPE, $this->buildPointer($pointer, JsonApiDoc::TYPE))
Expand All @@ -141,8 +142,8 @@ protected function normalizeRelationshipItem(
}

return [
'class' => $entityClass,
'id' => $entityId
self::CLASS_FIELD_NAME => $entityClass,
self::ID_FIELD_NAME => $entityId
];
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
*/
abstract class AbstractNormalizeRequestData implements ProcessorInterface
{
protected const CLASS_FIELD_NAME = 'class';
protected const ID_FIELD_NAME = 'id';

protected EntityIdTransformerRegistry $entityIdTransformerRegistry;
protected ?FormContext $context = null;
protected ?string $requestDataItemKey = null;
Expand Down Expand Up @@ -76,8 +79,8 @@ protected function normalizeRelationId(
EntityMetadata $metadata
): array {
return [
'class' => $entityClass,
'id' => $this->normalizeEntityId($propertyPath, $entityId, $metadata)
self::CLASS_FIELD_NAME => $entityClass,
self::ID_FIELD_NAME => $this->normalizeEntityId($propertyPath, $entityId, $metadata)
];
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,25 @@
*/
class NormalizeRequestData extends AbstractNormalizeRequestData
{
public const OPERATION_NAME = 'normalize_request_data';

#[\Override]
public function process(ContextInterface $context): void
{
/** @var ChangeSubresourceContext $context */

if ($context->isProcessed(self::OPERATION_NAME)) {
// the request data are already normalized
return;
}

$requestData = $context->getRequestData();
if ($context->getRequestMetadata()?->hasIdentifierFields()) {
if (\array_key_exists(JsonApiDoc::DATA, $requestData)) {
$data = $requestData[JsonApiDoc::DATA];
if (!\is_array($data)) {
// the request data were not validated
throw new RuntimeException(sprintf(
throw new RuntimeException(\sprintf(
'The "%s" top-section of the request data should be an array.',
JsonApiDoc::DATA
));
Expand Down Expand Up @@ -55,5 +62,6 @@ public function process(ContextInterface $context): void
} elseif (\array_key_exists(JsonApiDoc::META, $requestData)) {
$context->setRequestData($requestData[JsonApiDoc::META]);
}
$context->setProcessed(self::OPERATION_NAME);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

namespace Oro\Bundle\ApiBundle\Processor\Subresource\ChangeSubresource;

use Doctrine\Common\Collections\ArrayCollection;
use Oro\Bundle\ApiBundle\Config\EntityDefinitionConfig;
use Oro\Bundle\ApiBundle\Processor\Subresource\ChangeSubresourceContext;
use Oro\Bundle\ApiBundle\Util\ConfigUtil;
use Oro\Component\ChainProcessor\ContextInterface;
use Oro\Component\ChainProcessor\ProcessorInterface;
use Symfony\Component\PropertyAccess\Exception\AccessException;
Expand Down Expand Up @@ -38,16 +40,25 @@ public function process(ContextInterface $context): void

private function getAssociationData(ChangeSubresourceContext $context): mixed
{
$entityFieldName = $this->getEntityFieldName($context->getAssociationName(), $context->getParentConfig());
if (ConfigUtil::IGNORE_PROPERTY_PATH === $entityFieldName) {
return $this->createEmptyAssociationData($context);
}

try {
return $this->propertyAccessor->getValue(
$context->getParentEntity(),
$this->getEntityFieldName($context->getAssociationName(), $context->getParentConfig())
);
return $this->propertyAccessor->getValue($context->getParentEntity(), $entityFieldName);
} catch (AccessException) {
return $this->createObject($context->getRequestClassName());
return $this->createEmptyAssociationData($context);
}
}

private function createEmptyAssociationData(ChangeSubresourceContext $context): object
{
return $context->isCollection()
? new ArrayCollection()
: $this->createObject($context->getRequestClassName());
}

private function getEntityFieldName(string $fieldName, ?EntityDefinitionConfig $config): string
{
if (null === $config) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,22 @@
*/
class NormalizeRequestData extends AbstractNormalizeRequestData
{
public const OPERATION_NAME = 'normalize_request_data';

#[\Override]
public function process(ContextInterface $context): void
{
/** @var ChangeSubresourceContext $context */

if ($context->isProcessed(self::OPERATION_NAME)) {
// the request data are already normalized
return;
}

$metadata = $context->getRequestMetadata();
if (null === $metadata) {
$context->setProcessed(self::OPERATION_NAME);

return;
}

Expand Down Expand Up @@ -45,5 +54,6 @@ public function process(ContextInterface $context): void
} finally {
$this->context = null;
}
$context->setProcessed(self::OPERATION_NAME);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,21 @@ protected function createAssociationMetadata(
return $associationMetadata;
}

public function testProcessWhenRequestDataAlreadyNormalized(): void
{
$inputData = ['foo' => 'bar'];

$this->entityIdTransformer->expects(self::never())
->method('reverseTransform');

$this->context->setRequestData($inputData);
$this->context->setProcessed(NormalizeRequestData::OPERATION_NAME);
$this->processor->process($this->context);

self::assertEquals($inputData, $this->context->getRequestData());
self::assertTrue($this->context->isProcessed(NormalizeRequestData::OPERATION_NAME));
}

public function testProcessForAlreadyNormalizedData(): void
{
$inputData = ['foo' => 'bar'];
Expand All @@ -78,6 +93,7 @@ public function testProcessForAlreadyNormalizedData(): void

self::assertSame($inputData, $this->context->getRequestData());
self::assertSame([], $this->context->getNotResolvedIdentifiers());
self::assertTrue($this->context->isProcessed(NormalizeRequestData::OPERATION_NAME));
}

public function testProcessWithoutMetadata(): void
Expand Down Expand Up @@ -109,6 +125,7 @@ public function testProcessWithoutMetadata(): void

self::assertEquals($inputData, $this->context->getRequestData());
self::assertSame([], $this->context->getNotResolvedIdentifiers());
self::assertTrue($this->context->isProcessed(NormalizeRequestData::OPERATION_NAME));
}

/**
Expand Down Expand Up @@ -210,6 +227,7 @@ public function testProcessWithMetadata(): void

self::assertEquals($expectedData, $this->context->getRequestData());
self::assertSame([], $this->context->getNotResolvedIdentifiers());
self::assertTrue($this->context->isProcessed(NormalizeRequestData::OPERATION_NAME));
}

public function testProcessNoAttributes(): void
Expand Down Expand Up @@ -258,6 +276,7 @@ public function testProcessNoAttributes(): void

self::assertEquals($expectedData, $this->context->getRequestData());
self::assertSame([], $this->context->getNotResolvedIdentifiers());
self::assertTrue($this->context->isProcessed(NormalizeRequestData::OPERATION_NAME));
}

public function testProcessWithInvalidEntityTypes(): void
Expand Down Expand Up @@ -330,6 +349,7 @@ public function testProcessWithInvalidEntityTypes(): void
$this->context->getErrors()
);
self::assertSame([], $this->context->getNotResolvedIdentifiers());
self::assertTrue($this->context->isProcessed(NormalizeRequestData::OPERATION_NAME));
}

public function testProcessWithNotAcceptableEntityTypes(): void
Expand Down Expand Up @@ -411,6 +431,7 @@ public function testProcessWithNotAcceptableEntityTypes(): void
$this->context->getErrors()
);
self::assertSame([], $this->context->getNotResolvedIdentifiers());
self::assertTrue($this->context->isProcessed(NormalizeRequestData::OPERATION_NAME));
}

public function testProcessWithEmptyAcceptableEntityTypes(): void
Expand Down Expand Up @@ -487,6 +508,7 @@ public function testProcessWithEmptyAcceptableEntityTypes(): void
self::assertFalse($this->context->hasErrors());
self::assertEquals($expectedData, $this->context->getRequestData());
self::assertSame([], $this->context->getNotResolvedIdentifiers());
self::assertTrue($this->context->isProcessed(NormalizeRequestData::OPERATION_NAME));
}

public function testProcessWithEmptyAcceptableEntityTypesShouldBeRejected(): void
Expand Down Expand Up @@ -575,6 +597,7 @@ public function testProcessWithEmptyAcceptableEntityTypesShouldBeRejected(): voi
$this->context->getErrors()
);
self::assertSame([], $this->context->getNotResolvedIdentifiers());
self::assertTrue($this->context->isProcessed(NormalizeRequestData::OPERATION_NAME));
}

public function testProcessWithInvalidIdentifiers(): void
Expand Down Expand Up @@ -660,6 +683,7 @@ public function testProcessWithInvalidIdentifiers(): void
$this->context->getErrors()
);
self::assertSame([], $this->context->getNotResolvedIdentifiers());
self::assertTrue($this->context->isProcessed(NormalizeRequestData::OPERATION_NAME));
}

public function testProcessWithNotResolvedIdentifiers(): void
Expand Down Expand Up @@ -744,6 +768,7 @@ public function testProcessWithNotResolvedIdentifiers(): void
],
$this->context->getNotResolvedIdentifiers()
);
self::assertTrue($this->context->isProcessed(NormalizeRequestData::OPERATION_NAME));
}

public function testProcessShouldNotNormalizeIdOfIncludedEntity(): void
Expand Down Expand Up @@ -791,6 +816,7 @@ public function testProcessShouldNotNormalizeIdOfIncludedEntity(): void

self::assertEquals($expectedData, $this->context->getRequestData());
self::assertSame([], $this->context->getNotResolvedIdentifiers());
self::assertTrue($this->context->isProcessed(NormalizeRequestData::OPERATION_NAME));
}

public function testProcessShouldNotNormalizeIdOfIncludedPrimaryEntity(): void
Expand Down Expand Up @@ -838,6 +864,7 @@ public function testProcessShouldNotNormalizeIdOfIncludedPrimaryEntity(): void

self::assertEquals($expectedData, $this->context->getRequestData());
self::assertSame([], $this->context->getNotResolvedIdentifiers());
self::assertTrue($this->context->isProcessed(NormalizeRequestData::OPERATION_NAME));
}

public function testProcessForEntityThatDoesNotHaveIdentifierFields(): void
Expand All @@ -850,6 +877,7 @@ public function testProcessForEntityThatDoesNotHaveIdentifierFields(): void

self::assertSame($requestData['meta'], $this->context->getRequestData());
self::assertSame([], $this->context->getNotResolvedIdentifiers());
self::assertTrue($this->context->isProcessed(NormalizeRequestData::OPERATION_NAME));
}

public function testProcessForEntityThatDoesNotHaveIdentifierFieldsAndNoMetaSectionInRequestData(): void
Expand All @@ -862,5 +890,6 @@ public function testProcessForEntityThatDoesNotHaveIdentifierFieldsAndNoMetaSect

self::assertSame($requestData, $this->context->getRequestData());
self::assertSame([], $this->context->getNotResolvedIdentifiers());
self::assertTrue($this->context->isProcessed(NormalizeRequestData::OPERATION_NAME));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Oro\Bundle\ApiBundle\Tests\Unit\Processor\Subresource\ChangeSubresource;

use Doctrine\Common\Collections\ArrayCollection;
use Oro\Bundle\ApiBundle\Config\EntityDefinitionConfig;
use Oro\Bundle\ApiBundle\Processor\Subresource\ChangeSubresource\PrepareFormData;
use Oro\Bundle\ApiBundle\Tests\Unit\Processor\Subresource\ChangeSubresourceProcessorTestCase;
Expand Down Expand Up @@ -118,7 +119,7 @@ public function testProcessWhenAssociationDataExistButNoParentEntityConfig(): vo
);
}

public function testProcessWhenAssociationDataDoesNotExist(): void
public function testProcessWhenToOneAssociationDataDoesNotExist(): void
{
$parentEntity = new User();
$associationName = 'notExisting';
Expand All @@ -135,9 +136,66 @@ public function testProcessWhenAssociationDataDoesNotExist(): void
$this->processor->process($this->context);

self::assertEquals([$associationName => $requestData], $this->context->getRequestData());
self::assertEquals(
[$associationName => new \stdClass()],
$this->context->getResult()
);
self::assertEquals([$associationName => new \stdClass()], $this->context->getResult());
}

public function testProcessWhenToManyAssociationDataDoesNotExist(): void
{
$parentEntity = new User();
$associationName = 'notExisting';
$requestData = ['key' => 'value'];

$parentConfig = new EntityDefinitionConfig();
$parentConfig->set(ConfigUtil::REQUEST_TARGET_CLASS, \stdClass::class);
$parentConfig->addField($associationName);

$this->context->setParentEntity($parentEntity);
$this->context->setAssociationName($associationName);
$this->context->setRequestData($requestData);
$this->context->setParentConfig($parentConfig);
$this->context->setIsCollection(true);
$this->processor->process($this->context);

self::assertEquals([$associationName => $requestData], $this->context->getRequestData());
self::assertEquals([$associationName => new ArrayCollection()], $this->context->getResult());
}

public function testProcessForComputedToOneAssociation(): void
{
$associationName = 'someAssociation';
$requestData = ['key' => 'value'];

$parentConfig = new EntityDefinitionConfig();
$parentConfig->set(ConfigUtil::REQUEST_TARGET_CLASS, \stdClass::class);
$parentConfig->addField($associationName)->setPropertyPath(ConfigUtil::IGNORE_PROPERTY_PATH);

$this->context->setParentEntity(new User());
$this->context->setAssociationName($associationName);
$this->context->setRequestData($requestData);
$this->context->setParentConfig($parentConfig);
$this->processor->process($this->context);

self::assertEquals([$associationName => $requestData], $this->context->getRequestData());
self::assertEquals([$associationName => new \stdClass()], $this->context->getResult());
}

public function testProcessForComputedToManyAssociation(): void
{
$associationName = 'someAssociation';
$requestData = ['key' => 'value'];

$parentConfig = new EntityDefinitionConfig();
$parentConfig->set(ConfigUtil::REQUEST_TARGET_CLASS, \stdClass::class);
$parentConfig->addField($associationName)->setPropertyPath(ConfigUtil::IGNORE_PROPERTY_PATH);

$this->context->setParentEntity(new User());
$this->context->setAssociationName($associationName);
$this->context->setRequestData($requestData);
$this->context->setParentConfig($parentConfig);
$this->context->setIsCollection(true);
$this->processor->process($this->context);

self::assertEquals([$associationName => $requestData], $this->context->getRequestData());
self::assertEquals([$associationName => new ArrayCollection()], $this->context->getResult());
}
}
Loading

0 comments on commit 023acf6

Please sign in to comment.