From 7bb70f601ca96913d09e44f08a638388ff59dfab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20H=C3=A9bert?= Date: Wed, 20 Feb 2019 12:20:38 +0100 Subject: [PATCH 01/15] Add autoconfiguration for tag 'api_platform.data_transformer' update test --- .../Bundle/DependencyInjection/ApiPlatformExtension.php | 8 ++++++++ .../DependencyInjection/ApiPlatformExtensionTest.php | 5 +++++ 2 files changed, 13 insertions(+) diff --git a/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php b/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php index e19cf0e70f4..230bde361c7 100644 --- a/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php +++ b/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php @@ -25,6 +25,7 @@ use ApiPlatform\Core\DataProvider\CollectionDataProviderInterface; use ApiPlatform\Core\DataProvider\ItemDataProviderInterface; use ApiPlatform\Core\DataProvider\SubresourceDataProviderInterface; +use ApiPlatform\Core\DataTransformer\DataTransformerInterface; use ApiPlatform\Core\Exception\RuntimeException; use Doctrine\Common\Annotations\Annotation; use Doctrine\ORM\Version; @@ -150,6 +151,7 @@ public function load(array $configs, ContainerBuilder $container) $this->registerMercureConfiguration($container, $config, $loader, $useDoctrine); $this->registerMessengerConfiguration($config, $loader); $this->registerElasticsearchConfiguration($container, $config, $loader); + $this->registerDataTransformerConfiguration($container); } /** @@ -603,4 +605,10 @@ private function registerElasticsearchConfiguration(ContainerBuilder $container, $container->setParameter('api_platform.elasticsearch.hosts', $config['elasticsearch']['hosts']); $container->setParameter('api_platform.elasticsearch.mapping', $config['elasticsearch']['mapping']); } + + private function registerDataTransformerConfiguration(ContainerBuilder $container) + { + $container->registerForAutoconfiguration(DataTransformerInterface::class) + ->addTag('api_platform.data_transformer'); + } } diff --git a/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php b/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php index ca9583dc910..ab80746b646 100644 --- a/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php +++ b/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php @@ -54,6 +54,7 @@ use ApiPlatform\Core\DataProvider\CollectionDataProviderInterface; use ApiPlatform\Core\DataProvider\ItemDataProviderInterface; use ApiPlatform\Core\DataProvider\SubresourceDataProviderInterface; +use ApiPlatform\Core\DataTransformer\DataTransformerInterface; use ApiPlatform\Core\Exception\FilterValidationException; use ApiPlatform\Core\Exception\InvalidArgumentException; use ApiPlatform\Core\Exception\RuntimeException; @@ -877,6 +878,10 @@ private function getBaseContainerBuilderProphecy() ->willReturn($this->childDefinitionProphecy)->shouldBeCalledTimes(1); $this->childDefinitionProphecy->addTag('api_platform.doctrine.mongodb.aggregation_extension.collection')->shouldBeCalledTimes(1); + $containerBuilderProphecy->registerForAutoconfiguration(DataTransformerInterface::class) + ->willReturn($this->childDefinitionProphecy)->shouldBeCalledTimes(1); + $this->childDefinitionProphecy->addTag('api_platform.data_transformer')->shouldBeCalledTimes(1); + $containerBuilderProphecy->addResource(Argument::type(DirectoryResource::class))->shouldBeCalled(); $parameters = [ From 89947c930457e4e200d58ffe964adb356f057946 Mon Sep 17 00:00:00 2001 From: Ben Davies Date: Wed, 20 Feb 2019 13:06:33 +0000 Subject: [PATCH 02/15] fix error case when denormalizing relations with plain identifiers. null is returned, no exceptions are thrown --- src/Serializer/AbstractItemNormalizer.php | 11 ++-- .../Serializer/AbstractItemNormalizerTest.php | 60 ++++++++++++++++++- 2 files changed, 64 insertions(+), 7 deletions(-) diff --git a/src/Serializer/AbstractItemNormalizer.php b/src/Serializer/AbstractItemNormalizer.php index 62285fda664..d30c72a5473 100644 --- a/src/Serializer/AbstractItemNormalizer.php +++ b/src/Serializer/AbstractItemNormalizer.php @@ -436,13 +436,12 @@ protected function denormalizeRelation(string $attributeName, PropertyMetadata $ if (!\is_array($value)) { // repeat the code so that IRIs keep working with the json format if (true === $this->allowPlainIdentifiers && $this->itemDataProvider) { - try { - return $this->itemDataProvider->getItem($className, $value, null, $context + ['fetch_data' => true]); - } catch (ItemNotFoundException $e) { - throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e); - } catch (InvalidArgumentException $e) { - // Give a chance to other normalizers (e.g.: DateTimeNormalizer) + $item = $this->itemDataProvider->getItem($className, $value, null, $context + ['fetch_data' => true]); + if (null === $item) { + throw new ItemNotFoundException(sprintf('Item not found for "%s".', $value)); } + + return $item; } throw new InvalidArgumentException(sprintf( diff --git a/tests/Serializer/AbstractItemNormalizerTest.php b/tests/Serializer/AbstractItemNormalizerTest.php index b218865d9ed..4cb62247124 100644 --- a/tests/Serializer/AbstractItemNormalizerTest.php +++ b/tests/Serializer/AbstractItemNormalizerTest.php @@ -18,6 +18,7 @@ use ApiPlatform\Core\DataProvider\ItemDataProviderInterface; use ApiPlatform\Core\DataTransformer\DataTransformerInterface; use ApiPlatform\Core\Exception\InvalidArgumentException; +use ApiPlatform\Core\Exception\ItemNotFoundException; use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; use ApiPlatform\Core\Metadata\Property\PropertyMetadata; @@ -822,6 +823,63 @@ public function testChildInheritedProperty() public function testDenormalizeRelationWithPlainId() { + $relatedDummy = new RelatedDummy(); + + $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactoryProphecy->create(Dummy::class, [])->willReturn( + new PropertyNameCollection(['relatedDummy']) + )->shouldBeCalled(); + + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummy', [])->willReturn( + new PropertyMetadata( + new Type(Type::BUILTIN_TYPE_OBJECT, false, RelatedDummy::class), + '', + false, + true, + false, + false + ) + )->shouldBeCalled(); + + $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); + $propertyAccessorProphecy = $this->prophesize(PropertyAccessorInterface::class); + $propertyAccessorProphecy->setValue(Argument::type(Dummy::class), 'relatedDummy', $relatedDummy)->shouldBeCalled(); + + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolverProphecy->isResourceClass(RelatedDummy::class)->willReturn(true)->shouldBeCalled(); + + $serializerProphecy = $this->prophesize(SerializerInterface::class); + $serializerProphecy->willImplement(DenormalizerInterface::class); + + $itemDataProviderProphecy = $this->prophesize(ItemDataProviderInterface::class); + $itemDataProviderProphecy->getItem(RelatedDummy::class, 1, null, Argument::type('array'))->willReturn($relatedDummy)->shouldBeCalled(); + + $normalizer = $this->getMockForAbstractClass(AbstractItemNormalizer::class, [ + $propertyNameCollectionFactoryProphecy->reveal(), + $propertyMetadataFactoryProphecy->reveal(), + $iriConverterProphecy->reveal(), + $resourceClassResolverProphecy->reveal(), + $propertyAccessorProphecy->reveal(), + null, + null, + $itemDataProviderProphecy->reveal(), + true, + [], + null, + null, + true, + ]); + $normalizer->setSerializer($serializerProphecy->reveal()); + + $normalizer->denormalize(['relatedDummy' => 1], Dummy::class, 'jsonld'); + } + + public function testDenormalizeRelationWithPlainIdNotFound() + { + $this->expectException(ItemNotFoundException::class); + $this->expectExceptionMessage('Item not found for "1".'); + $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); $propertyNameCollectionFactoryProphecy->create(Dummy::class, [])->willReturn( new PropertyNameCollection(['relatedDummy']) @@ -849,7 +907,7 @@ public function testDenormalizeRelationWithPlainId() $serializerProphecy->willImplement(DenormalizerInterface::class); $itemDataProviderProphecy = $this->prophesize(ItemDataProviderInterface::class); - $itemDataProviderProphecy->getItem(RelatedDummy::class, 1, null, Argument::type('array'))->shouldBeCalled(); + $itemDataProviderProphecy->getItem(RelatedDummy::class, 1, null, Argument::type('array'))->willReturn(null)->shouldBeCalled(); $normalizer = $this->getMockForAbstractClass(AbstractItemNormalizer::class, [ $propertyNameCollectionFactoryProphecy->reveal(), From e6cd64c2b73476cc049554289d42a7b9f1f8dedf Mon Sep 17 00:00:00 2001 From: birkof Date: Wed, 20 Feb 2019 18:35:41 +0200 Subject: [PATCH 03/15] Fix normalization output Data Transformer for Json format --- src/Serializer/ItemNormalizer.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/Serializer/ItemNormalizer.php b/src/Serializer/ItemNormalizer.php index 0169b0dcc88..dbd15561562 100644 --- a/src/Serializer/ItemNormalizer.php +++ b/src/Serializer/ItemNormalizer.php @@ -21,6 +21,7 @@ use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; +use ApiPlatform\Core\Util\ClassInfoTrait; use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; @@ -36,6 +37,8 @@ */ class ItemNormalizer extends AbstractItemNormalizer { + use ClassInfoTrait; + private $logger; public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, IriConverterInterface $iriConverter, ResourceClassResolverInterface $resourceClassResolver, PropertyAccessorInterface $propertyAccessor = null, NameConverterInterface $nameConverter = null, ClassMetadataFactoryInterface $classMetadataFactory = null, ItemDataProviderInterface $itemDataProvider = null, bool $allowPlainIdentifiers = false, LoggerInterface $logger = null, DataTransformerInterface $dataTransformer = null, ResourceMetadataFactoryInterface $resourceMetadataFactory = null, $allowUnmappedClass = false) @@ -45,6 +48,19 @@ public function __construct(PropertyNameCollectionFactoryInterface $propertyName $this->logger = $logger ?: new NullLogger(); } + /** + * {@inheritdoc} + */ + public function normalize($object, $format = null, array $context = []) + { + $outputClass = $this->getOutputClass($this->getObjectClass($object), $context); + if (null !== $outputClass && null !== $this->dataTransformer && $this->dataTransformer->supportsTransformation($object, $outputClass, $context)) { + $object = $this->dataTransformer->transform($object, $outputClass, $context); + } + + return parent::normalize($object, $format, $context); + } + /** * {@inheritdoc} * From 1539c99ebf45802c3f0d43cbd2d62b73cea1e136 Mon Sep 17 00:00:00 2001 From: soyuka Date: Wed, 20 Feb 2019 18:32:27 +0100 Subject: [PATCH 04/15] fix jsonapi call to data transformer --- src/JsonApi/Serializer/ItemNormalizer.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/JsonApi/Serializer/ItemNormalizer.php b/src/JsonApi/Serializer/ItemNormalizer.php index 4d06d04fb2f..814b5e8c558 100644 --- a/src/JsonApi/Serializer/ItemNormalizer.php +++ b/src/JsonApi/Serializer/ItemNormalizer.php @@ -68,8 +68,8 @@ public function normalize($object, $format = null, array $context = []) } $outputClass = $this->getOutputClass($this->getObjectClass($object), $context); - if (null !== $outputClass && null !== $this->dataTransformer && $this->dataTransformer->supportsTransformation($object, $context)) { - $object = $this->dataTransformer->transform($object, $context); + if (null !== $outputClass && null !== $this->dataTransformer && $this->dataTransformer->supportsTransformation($object, $outputClass, $context)) { + $object = $this->dataTransformer->transform($object, $outputClass, $context); } // Get and populate attributes data From ef1d2ad4a833c0e2b9788420faa58e13cd00927a Mon Sep 17 00:00:00 2001 From: soyuka Date: Wed, 20 Feb 2019 18:44:05 +0100 Subject: [PATCH 05/15] Respect the RouterInterface by using a ResourceNotFoundException --- src/Bridge/Symfony/Routing/Router.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Bridge/Symfony/Routing/Router.php b/src/Bridge/Symfony/Routing/Router.php index 5524cd90d4b..6ad79658968 100644 --- a/src/Bridge/Symfony/Routing/Router.php +++ b/src/Bridge/Symfony/Routing/Router.php @@ -16,7 +16,7 @@ use ApiPlatform\Core\Api\UrlGeneratorInterface; use Symfony\Component\HttpFoundation\Exception\RequestExceptionInterface; use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\Routing\Exception\RouteNotFoundException; +use Symfony\Component\Routing\Exception\ResourceNotFoundException; use Symfony\Component\Routing\RequestContext; use Symfony\Component\Routing\RouterInterface; @@ -77,7 +77,7 @@ public function match($pathInfo) try { $context = (new RequestContext())->fromRequest($request); } catch (RequestExceptionInterface $e) { - throw new RouteNotFoundException('Invalid request context.'); + throw new ResourceNotFoundException('Invalid request context.'); } $context->setPathInfo($pathInfo); From 19157defebbb1a040b7d3e1c08a77c5179a111ac Mon Sep 17 00:00:00 2001 From: Ben Davies Date: Thu, 21 Feb 2019 14:24:03 +0000 Subject: [PATCH 06/15] add missing cache pool adapter override --- .../Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php b/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php index 230bde361c7..d20dae920f6 100644 --- a/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php +++ b/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php @@ -450,6 +450,7 @@ private function registerCacheConfiguration(ContainerBuilder $container) $container->register('api_platform.cache.route_name_resolver', ArrayAdapter::class); $container->register('api_platform.cache.identifiers_extractor', ArrayAdapter::class); $container->register('api_platform.cache.subresource_operation_factory', ArrayAdapter::class); + $container->register('api_platform.elasticsearch.cache.metadata.document', ArrayAdapter::class); } /** From 9a0e4c24be45bfd8a94ad74b0874ec2249bb93fe Mon Sep 17 00:00:00 2001 From: soyuka Date: Thu, 21 Feb 2019 10:28:17 +0100 Subject: [PATCH 07/15] DataTransformer supports with prenormalized data --- .../Symfony/Bundle/Resources/config/api.xml | 8 +- .../Bundle/Resources/config/graphql.xml | 2 +- .../Symfony/Bundle/Resources/config/hal.xml | 2 +- .../Bundle/Resources/config/jsonapi.xml | 2 +- .../Bundle/Resources/config/jsonld.xml | 2 +- src/DataTransformer/ChainDataTransformer.php | 57 ----------- src/GraphQl/Serializer/ItemNormalizer.php | 9 +- src/Hal/Serializer/ItemNormalizer.php | 5 +- src/JsonApi/Serializer/ItemNormalizer.php | 13 +-- src/JsonLd/Serializer/ItemNormalizer.php | 10 +- src/Serializer/AbstractItemNormalizer.php | 48 +++++++--- src/Serializer/ItemNormalizer.php | 15 +-- .../ApiPlatformExtensionTest.php | 2 - .../CustomInputDtoDataTransformer.php | 2 +- .../InputDtoDataTransformer.php | 17 ++-- .../RecoverPasswordInputDataTransformer.php | 2 +- .../RecoverPasswordOutputDataTransformer.php | 4 - .../Document/DummyDtoInputOutput.php | 11 ++- .../TestBundle/Entity/DummyDtoInputOutput.php | 6 +- .../GraphQl/Serializer/ItemNormalizerTest.php | 4 +- tests/Hal/Serializer/ItemNormalizerTest.php | 6 +- tests/Hydra/Serializer/ItemNormalizerTest.php | 2 +- .../JsonApi/Serializer/ItemNormalizerTest.php | 16 ++-- .../Serializer/AbstractItemNormalizerTest.php | 94 ++++++++++++++----- tests/Serializer/ItemNormalizerTest.php | 59 +++++++++++- 25 files changed, 211 insertions(+), 187 deletions(-) delete mode 100644 src/DataTransformer/ChainDataTransformer.php diff --git a/src/Bridge/Symfony/Bundle/Resources/config/api.xml b/src/Bridge/Symfony/Bundle/Resources/config/api.xml index f5d049cfa11..f1ff93b8a88 100644 --- a/src/Bridge/Symfony/Bundle/Resources/config/api.xml +++ b/src/Bridge/Symfony/Bundle/Resources/config/api.xml @@ -99,7 +99,7 @@ %api_platform.allow_plain_identifiers% null - + true @@ -287,12 +287,6 @@ - - - - - - diff --git a/src/Bridge/Symfony/Bundle/Resources/config/graphql.xml b/src/Bridge/Symfony/Bundle/Resources/config/graphql.xml index 0d9c87fa6c4..65c89d48240 100644 --- a/src/Bridge/Symfony/Bundle/Resources/config/graphql.xml +++ b/src/Bridge/Symfony/Bundle/Resources/config/graphql.xml @@ -77,7 +77,7 @@ %api_platform.allow_plain_identifiers% null - + true diff --git a/src/Bridge/Symfony/Bundle/Resources/config/hal.xml b/src/Bridge/Symfony/Bundle/Resources/config/hal.xml index c05de3f33a9..a606e613ff0 100644 --- a/src/Bridge/Symfony/Bundle/Resources/config/hal.xml +++ b/src/Bridge/Symfony/Bundle/Resources/config/hal.xml @@ -37,7 +37,7 @@ null false - + true diff --git a/src/Bridge/Symfony/Bundle/Resources/config/jsonapi.xml b/src/Bridge/Symfony/Bundle/Resources/config/jsonapi.xml index cdf78a92f1a..f9c85a66aed 100644 --- a/src/Bridge/Symfony/Bundle/Resources/config/jsonapi.xml +++ b/src/Bridge/Symfony/Bundle/Resources/config/jsonapi.xml @@ -40,7 +40,7 @@ - + true diff --git a/src/Bridge/Symfony/Bundle/Resources/config/jsonld.xml b/src/Bridge/Symfony/Bundle/Resources/config/jsonld.xml index c53482a9f4d..279b25b8b4f 100644 --- a/src/Bridge/Symfony/Bundle/Resources/config/jsonld.xml +++ b/src/Bridge/Symfony/Bundle/Resources/config/jsonld.xml @@ -26,7 +26,7 @@ - + true diff --git a/src/DataTransformer/ChainDataTransformer.php b/src/DataTransformer/ChainDataTransformer.php deleted file mode 100644 index 0e81dc88be1..00000000000 --- a/src/DataTransformer/ChainDataTransformer.php +++ /dev/null @@ -1,57 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Core\DataTransformer; - -/** - * Transforms an Input to a Resource object. - * - * @author Antoine Bluchet - */ -final class ChainDataTransformer implements DataTransformerInterface -{ - private $transformers; - - public function __construct(iterable $transformers) - { - $this->transformers = $transformers; - } - - /** - * {@inheritdoc} - */ - public function transform($object, string $to, array $context = []) - { - foreach ($this->transformers as $transformer) { - if ($transformer->supportsTransformation($object, $to, $context)) { - return $transformer->transform($object, $to, $context); - } - } - - return $object; - } - - /** - * {@inheritdoc} - */ - public function supportsTransformation($object, string $to, array $context = []): bool - { - foreach ($this->transformers as $transformer) { - if ($transformer->supportsTransformation($object, $to, $context)) { - return true; - } - } - - return false; - } -} diff --git a/src/GraphQl/Serializer/ItemNormalizer.php b/src/GraphQl/Serializer/ItemNormalizer.php index 9b5133a0e46..f19fb47b7f8 100644 --- a/src/GraphQl/Serializer/ItemNormalizer.php +++ b/src/GraphQl/Serializer/ItemNormalizer.php @@ -15,7 +15,6 @@ use ApiPlatform\Core\Metadata\Property\PropertyMetadata; use ApiPlatform\Core\Serializer\ItemNormalizer as BaseItemNormalizer; -use ApiPlatform\Core\Util\ClassInfoTrait; /** * GraphQL normalizer. @@ -24,8 +23,6 @@ */ final class ItemNormalizer extends BaseItemNormalizer { - use ClassInfoTrait; - const FORMAT = 'graphql'; const ITEM_KEY = '#item'; @@ -42,11 +39,7 @@ public function supportsNormalization($data, $format = null) */ public function normalize($object, $format = null, array $context = []) { - $outputClass = $this->getOutputClass($this->getObjectClass($object), $context); - if (null !== $outputClass && null !== $this->dataTransformer && $this->dataTransformer->supportsTransformation($object, $outputClass, $context)) { - $object = $this->dataTransformer->transform($object, $outputClass, $context); - } - + $object = $this->transformOutput($object, $context); $data = parent::normalize($object, $format, $context); $data[self::ITEM_KEY] = serialize($object); // calling serialize prevent weird normalization process done by Webonyx's GraphQL PHP diff --git a/src/Hal/Serializer/ItemNormalizer.php b/src/Hal/Serializer/ItemNormalizer.php index f05cb65533a..06d09026c5f 100644 --- a/src/Hal/Serializer/ItemNormalizer.php +++ b/src/Hal/Serializer/ItemNormalizer.php @@ -52,10 +52,7 @@ public function normalize($object, $format = null, array $context = []) $context['cache_key'] = $this->getHalCacheKey($format, $context); } - $outputClass = $this->getOutputClass($this->getObjectClass($object), $context); - if (null !== $outputClass && null !== $this->dataTransformer && $this->dataTransformer->supportsTransformation($object, $outputClass, $context)) { - $object = $this->dataTransformer->transform($object, $outputClass, $context); - } + $object = $this->transformOutput($object, $context); try { $resourceClass = $this->resourceClassResolver->getResourceClass($object, $context['resource_class'] ?? null, true); diff --git a/src/JsonApi/Serializer/ItemNormalizer.php b/src/JsonApi/Serializer/ItemNormalizer.php index 814b5e8c558..04c0f331c09 100644 --- a/src/JsonApi/Serializer/ItemNormalizer.php +++ b/src/JsonApi/Serializer/ItemNormalizer.php @@ -16,7 +16,6 @@ use ApiPlatform\Core\Api\IriConverterInterface; use ApiPlatform\Core\Api\OperationType; use ApiPlatform\Core\Api\ResourceClassResolverInterface; -use ApiPlatform\Core\DataTransformer\DataTransformerInterface; use ApiPlatform\Core\Exception\InvalidArgumentException; use ApiPlatform\Core\Exception\ItemNotFoundException; use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface; @@ -24,7 +23,6 @@ use ApiPlatform\Core\Metadata\Property\PropertyMetadata; use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; use ApiPlatform\Core\Serializer\AbstractItemNormalizer; -use ApiPlatform\Core\Util\ClassInfoTrait; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; use Symfony\Component\Serializer\NameConverter\NameConverterInterface; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; @@ -39,15 +37,13 @@ */ final class ItemNormalizer extends AbstractItemNormalizer { - use ClassInfoTrait; - const FORMAT = 'jsonapi'; private $componentsCache = []; - public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, IriConverterInterface $iriConverter, ResourceClassResolverInterface $resourceClassResolver, PropertyAccessorInterface $propertyAccessor = null, NameConverterInterface $nameConverter = null, ResourceMetadataFactoryInterface $resourceMetadataFactory, array $defaultContext = [], DataTransformerInterface $dataTransformer = null, bool $allowUnmappedClass = false) + public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, IriConverterInterface $iriConverter, ResourceClassResolverInterface $resourceClassResolver, PropertyAccessorInterface $propertyAccessor = null, NameConverterInterface $nameConverter = null, ResourceMetadataFactoryInterface $resourceMetadataFactory, array $defaultContext = [], iterable $dataTransformers = [], bool $allowUnmappedClass = false) { - parent::__construct($propertyNameCollectionFactory, $propertyMetadataFactory, $iriConverter, $resourceClassResolver, $propertyAccessor, $nameConverter, null, null, false, $defaultContext, $dataTransformer, $resourceMetadataFactory, $allowUnmappedClass); + parent::__construct($propertyNameCollectionFactory, $propertyMetadataFactory, $iriConverter, $resourceClassResolver, $propertyAccessor, $nameConverter, null, null, false, $defaultContext, $dataTransformers, $resourceMetadataFactory, $allowUnmappedClass); } /** @@ -67,10 +63,7 @@ public function normalize($object, $format = null, array $context = []) $context['cache_key'] = $this->getJsonApiCacheKey($format, $context); } - $outputClass = $this->getOutputClass($this->getObjectClass($object), $context); - if (null !== $outputClass && null !== $this->dataTransformer && $this->dataTransformer->supportsTransformation($object, $outputClass, $context)) { - $object = $this->dataTransformer->transform($object, $outputClass, $context); - } + $object = $this->transformOutput($object, $context); // Get and populate attributes data $objectAttributesData = parent::normalize($object, $format, $context); diff --git a/src/JsonLd/Serializer/ItemNormalizer.php b/src/JsonLd/Serializer/ItemNormalizer.php index 6512119fb25..013841b2909 100644 --- a/src/JsonLd/Serializer/ItemNormalizer.php +++ b/src/JsonLd/Serializer/ItemNormalizer.php @@ -15,7 +15,6 @@ use ApiPlatform\Core\Api\IriConverterInterface; use ApiPlatform\Core\Api\ResourceClassResolverInterface; -use ApiPlatform\Core\DataTransformer\DataTransformerInterface; use ApiPlatform\Core\Exception\InvalidArgumentException; use ApiPlatform\Core\JsonLd\ContextBuilderInterface; use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface; @@ -43,9 +42,9 @@ final class ItemNormalizer extends AbstractItemNormalizer private $contextBuilder; - public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, IriConverterInterface $iriConverter, ResourceClassResolverInterface $resourceClassResolver, ContextBuilderInterface $contextBuilder, PropertyAccessorInterface $propertyAccessor = null, NameConverterInterface $nameConverter = null, ClassMetadataFactoryInterface $classMetadataFactory = null, array $defaultContext = [], DataTransformerInterface $dataTransformer = null, bool $allowUnmappedClass = false) + public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, IriConverterInterface $iriConverter, ResourceClassResolverInterface $resourceClassResolver, ContextBuilderInterface $contextBuilder, PropertyAccessorInterface $propertyAccessor = null, NameConverterInterface $nameConverter = null, ClassMetadataFactoryInterface $classMetadataFactory = null, array $defaultContext = [], iterable $dataTransformers = [], bool $allowUnmappedClass = false) { - parent::__construct($propertyNameCollectionFactory, $propertyMetadataFactory, $iriConverter, $resourceClassResolver, $propertyAccessor, $nameConverter, $classMetadataFactory, null, false, $defaultContext, $dataTransformer, $resourceMetadataFactory, $allowUnmappedClass); + parent::__construct($propertyNameCollectionFactory, $propertyMetadataFactory, $iriConverter, $resourceClassResolver, $propertyAccessor, $nameConverter, $classMetadataFactory, null, false, $defaultContext, $dataTransformers, $resourceMetadataFactory, $allowUnmappedClass); $this->contextBuilder = $contextBuilder; } @@ -63,10 +62,7 @@ public function supportsNormalization($data, $format = null) */ public function normalize($object, $format = null, array $context = []) { - $outputClass = $this->getOutputClass($this->getObjectClass($object), $context); - if (null !== $outputClass && null !== $this->dataTransformer && $this->dataTransformer->supportsTransformation($object, $outputClass, $context)) { - $object = $this->dataTransformer->transform($object, $outputClass, $context); - } + $object = $this->transformOutput($object, $context); try { $resourceClass = $this->resourceClassResolver->getResourceClass($object, $context['resource_class'] ?? null, true); diff --git a/src/Serializer/AbstractItemNormalizer.php b/src/Serializer/AbstractItemNormalizer.php index d30c72a5473..e85617efc29 100644 --- a/src/Serializer/AbstractItemNormalizer.php +++ b/src/Serializer/AbstractItemNormalizer.php @@ -59,9 +59,9 @@ abstract class AbstractItemNormalizer extends AbstractObjectNormalizer protected $itemDataProvider; protected $allowPlainIdentifiers; protected $allowUnmappedClass; - protected $dataTransformer; + protected $dataTransformers = []; - public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, IriConverterInterface $iriConverter, ResourceClassResolverInterface $resourceClassResolver, PropertyAccessorInterface $propertyAccessor = null, NameConverterInterface $nameConverter = null, ClassMetadataFactoryInterface $classMetadataFactory = null, ItemDataProviderInterface $itemDataProvider = null, bool $allowPlainIdentifiers = false, array $defaultContext = [], DataTransformerInterface $dataTransformer = null, ResourceMetadataFactoryInterface $resourceMetadataFactory = null, bool $allowUnmappedClass = false) + public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, IriConverterInterface $iriConverter, ResourceClassResolverInterface $resourceClassResolver, PropertyAccessorInterface $propertyAccessor = null, NameConverterInterface $nameConverter = null, ClassMetadataFactoryInterface $classMetadataFactory = null, ItemDataProviderInterface $itemDataProvider = null, bool $allowPlainIdentifiers = false, array $defaultContext = [], iterable $dataTransformers = [], ResourceMetadataFactoryInterface $resourceMetadataFactory = null, bool $allowUnmappedClass = false) { if (!isset($defaultContext['circular_reference_handler'])) { $defaultContext['circular_reference_handler'] = function ($object) { @@ -85,7 +85,7 @@ public function __construct(PropertyNameCollectionFactoryInterface $propertyName $this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor(); $this->itemDataProvider = $itemDataProvider; $this->allowPlainIdentifiers = $allowPlainIdentifiers; - $this->dataTransformer = $dataTransformer; + $this->dataTransformers = $dataTransformers; $this->resourceMetadataFactory = $resourceMetadataFactory; $this->allowUnmappedClass = $allowUnmappedClass; } @@ -164,14 +164,12 @@ public function denormalize($data, $class, $format = null, array $context = []) $context['resource_class'] = $class; $inputClass = $this->getInputClass($class, $context); - if (null !== $inputClass) { - $data = parent::denormalize($data, $inputClass, $format, ['resource_class' => $inputClass] + $context); - - if (null !== $this->dataTransformer && $this->dataTransformer->supportsTransformation($data, $class, $context)) { - $data = $this->dataTransformer->transform($data, $class, $context); - } - - return $data; + if (null !== $inputClass && null !== $dataTransformer = $this->getDataTransformer($data, $class, $context)) { + $data = $dataTransformer->transform( + parent::denormalize($data, $inputClass, $format, ['resource_class' => $inputClass] + $context), + $class, + $context + ); } return parent::denormalize($data, $class, $format, $context); @@ -595,4 +593,32 @@ protected function normalizeRelation(PropertyMetadata $propertyMetadata, $relate return $iri; } + + /** + * Finds the first supported data transformer if any. + */ + protected function getDataTransformer($object, string $to, array $context = []): ?DataTransformerInterface + { + foreach ($this->dataTransformers as $dataTransformer) { + if ($dataTransformer->supportsTransformation($object, $to, $context)) { + return $dataTransformer; + } + } + + return null; + } + + /** + * For a given resource, it returns an output representation if any + * If not, the resource is returned. + */ + protected function transformOutput($object, array $context = []) + { + $outputClass = $this->getOutputClass($this->getObjectClass($object), $context); + if (null !== $outputClass && null !== $dataTransformer = $this->getDataTransformer($object, $outputClass, $context)) { + return $dataTransformer->transform($object, $outputClass, $context); + } + + return $object; + } } diff --git a/src/Serializer/ItemNormalizer.php b/src/Serializer/ItemNormalizer.php index dbd15561562..525c6940e6d 100644 --- a/src/Serializer/ItemNormalizer.php +++ b/src/Serializer/ItemNormalizer.php @@ -16,12 +16,10 @@ use ApiPlatform\Core\Api\IriConverterInterface; use ApiPlatform\Core\Api\ResourceClassResolverInterface; use ApiPlatform\Core\DataProvider\ItemDataProviderInterface; -use ApiPlatform\Core\DataTransformer\DataTransformerInterface; use ApiPlatform\Core\Exception\InvalidArgumentException; use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; -use ApiPlatform\Core\Util\ClassInfoTrait; use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; @@ -37,13 +35,11 @@ */ class ItemNormalizer extends AbstractItemNormalizer { - use ClassInfoTrait; - private $logger; - public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, IriConverterInterface $iriConverter, ResourceClassResolverInterface $resourceClassResolver, PropertyAccessorInterface $propertyAccessor = null, NameConverterInterface $nameConverter = null, ClassMetadataFactoryInterface $classMetadataFactory = null, ItemDataProviderInterface $itemDataProvider = null, bool $allowPlainIdentifiers = false, LoggerInterface $logger = null, DataTransformerInterface $dataTransformer = null, ResourceMetadataFactoryInterface $resourceMetadataFactory = null, $allowUnmappedClass = false) + public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, IriConverterInterface $iriConverter, ResourceClassResolverInterface $resourceClassResolver, PropertyAccessorInterface $propertyAccessor = null, NameConverterInterface $nameConverter = null, ClassMetadataFactoryInterface $classMetadataFactory = null, ItemDataProviderInterface $itemDataProvider = null, bool $allowPlainIdentifiers = false, LoggerInterface $logger = null, iterable $dataTransformers = [], ResourceMetadataFactoryInterface $resourceMetadataFactory = null, $allowUnmappedClass = false) { - parent::__construct($propertyNameCollectionFactory, $propertyMetadataFactory, $iriConverter, $resourceClassResolver, $propertyAccessor, $nameConverter, $classMetadataFactory, $itemDataProvider, $allowPlainIdentifiers, [], $dataTransformer, $resourceMetadataFactory, $allowUnmappedClass); + parent::__construct($propertyNameCollectionFactory, $propertyMetadataFactory, $iriConverter, $resourceClassResolver, $propertyAccessor, $nameConverter, $classMetadataFactory, $itemDataProvider, $allowPlainIdentifiers, [], $dataTransformers, $resourceMetadataFactory, $allowUnmappedClass); $this->logger = $logger ?: new NullLogger(); } @@ -53,12 +49,7 @@ public function __construct(PropertyNameCollectionFactoryInterface $propertyName */ public function normalize($object, $format = null, array $context = []) { - $outputClass = $this->getOutputClass($this->getObjectClass($object), $context); - if (null !== $outputClass && null !== $this->dataTransformer && $this->dataTransformer->supportsTransformation($object, $outputClass, $context)) { - $object = $this->dataTransformer->transform($object, $outputClass, $context); - } - - return parent::normalize($object, $format, $context); + return parent::normalize($this->transformOutput($object, $context), $format, $context); } /** diff --git a/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php b/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php index ab80746b646..9f09bdc2155 100644 --- a/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php +++ b/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php @@ -736,7 +736,6 @@ private function getPartialContainerBuilderProphecy() 'api_platform.cache.route_name_resolver', 'api_platform.cache.subresource_operation_factory', 'api_platform.collection_data_provider', - 'api_platform.data_transformer.chain_transformer', 'api_platform.formats_provider', 'api_platform.filter_locator', 'api_platform.filter_collection_factory', @@ -818,7 +817,6 @@ private function getPartialContainerBuilderProphecy() 'api_platform.action.post_collection' => 'api_platform.action.placeholder', 'api_platform.action.put_item' => 'api_platform.action.placeholder', 'api_platform.action.patch_item' => 'api_platform.action.placeholder', - 'api_platform.data_transformer' => 'api_platform.data_transformer.chain_transformer', 'api_platform.metadata.property.metadata_factory' => 'api_platform.metadata.property.metadata_factory.xml', 'api_platform.metadata.property.name_collection_factory' => 'api_platform.metadata.property.name_collection_factory.property_info', 'api_platform.metadata.resource.metadata_factory' => 'api_platform.metadata.resource.metadata_factory.xml', diff --git a/tests/Fixtures/TestBundle/DataTransformer/CustomInputDtoDataTransformer.php b/tests/Fixtures/TestBundle/DataTransformer/CustomInputDtoDataTransformer.php index 885ecff4743..24683213917 100644 --- a/tests/Fixtures/TestBundle/DataTransformer/CustomInputDtoDataTransformer.php +++ b/tests/Fixtures/TestBundle/DataTransformer/CustomInputDtoDataTransformer.php @@ -45,6 +45,6 @@ public function transform($object, string $to, array $context = []) */ public function supportsTransformation($object, string $to, array $context = []): bool { - return (DummyDtoCustom::class === $to || DummyDtoCustomDocument::class === $to) && $object instanceof CustomInputDto; + return (DummyDtoCustom::class === $to || DummyDtoCustomDocument::class === $to) && CustomInputDto::class === ($context['input']['class'] ?? null); } } diff --git a/tests/Fixtures/TestBundle/DataTransformer/InputDtoDataTransformer.php b/tests/Fixtures/TestBundle/DataTransformer/InputDtoDataTransformer.php index 0e51b12346e..454fc45631a 100644 --- a/tests/Fixtures/TestBundle/DataTransformer/InputDtoDataTransformer.php +++ b/tests/Fixtures/TestBundle/DataTransformer/InputDtoDataTransformer.php @@ -26,16 +26,17 @@ final class InputDtoDataTransformer implements DataTransformerInterface */ public function transform($object, string $to, array $context = []) { - if (!$object instanceof InputDto) { - throw new \InvalidArgumentException(); - } + /** + * @var \ApiPlatform\Core\Tests\Fixtures\TestBundle\Dto\InputDto + */ + $data = $object; /** * @var \ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyDtoInputOutput */ $resourceObject = $context[AbstractItemNormalizer::OBJECT_TO_POPULATE] ?? new $context['resource_class'](); - $resourceObject->str = $object->foo; - $resourceObject->num = $object->bar; + $resourceObject->str = $data->foo; + $resourceObject->num = $data->bar; return $resourceObject; } @@ -45,6 +46,10 @@ public function transform($object, string $to, array $context = []) */ public function supportsTransformation($object, string $to, array $context = []): bool { - return (DummyDtoInputOutput::class === $to || DummyDtoInputOutputDocument::class === $to) && $object instanceof InputDto; + if ($object instanceof DummyDtoInputOutput || $object instanceof DummyDtoInputOutputDocument) { + return false; + } + + return (DummyDtoInputOutput::class === $to || DummyDtoInputOutputDocument::class === $to) && (InputDto::class === $context['input']['class']); } } diff --git a/tests/Fixtures/TestBundle/DataTransformer/RecoverPasswordInputDataTransformer.php b/tests/Fixtures/TestBundle/DataTransformer/RecoverPasswordInputDataTransformer.php index ce03f105a19..55c64cac077 100644 --- a/tests/Fixtures/TestBundle/DataTransformer/RecoverPasswordInputDataTransformer.php +++ b/tests/Fixtures/TestBundle/DataTransformer/RecoverPasswordInputDataTransformer.php @@ -39,6 +39,6 @@ public function transform($data, string $to, array $context = []) */ public function supportsTransformation($data, string $to, array $context = []): bool { - return (User::class === $to || UserDocument::class === $to) && $data instanceof RecoverPasswordInput; + return (User::class === $to || UserDocument::class === $to) && RecoverPasswordInput::class === ($context['input']['class'] ?? null); } } diff --git a/tests/Fixtures/TestBundle/DataTransformer/RecoverPasswordOutputDataTransformer.php b/tests/Fixtures/TestBundle/DataTransformer/RecoverPasswordOutputDataTransformer.php index 9b9ef386ef4..0921243610f 100644 --- a/tests/Fixtures/TestBundle/DataTransformer/RecoverPasswordOutputDataTransformer.php +++ b/tests/Fixtures/TestBundle/DataTransformer/RecoverPasswordOutputDataTransformer.php @@ -25,10 +25,6 @@ final class RecoverPasswordOutputDataTransformer implements DataTransformerInter */ public function transform($object, string $to, array $context = []) { - if (!$object instanceof User) { - throw new \InvalidArgumentException(); - } - $output = new RecoverPasswordOutput(); $output->dummy = new Dummy(); $output->dummy->setId(1); diff --git a/tests/Fixtures/TestBundle/Document/DummyDtoInputOutput.php b/tests/Fixtures/TestBundle/Document/DummyDtoInputOutput.php index 12a5642ff59..66197715c9c 100644 --- a/tests/Fixtures/TestBundle/Document/DummyDtoInputOutput.php +++ b/tests/Fixtures/TestBundle/Document/DummyDtoInputOutput.php @@ -13,7 +13,6 @@ namespace ApiPlatform\Core\Tests\Fixtures\TestBundle\Document; -use ApiPlatform\Core\Annotation\ApiProperty; use ApiPlatform\Core\Annotation\ApiResource; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Dto\InputDto; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Dto\OutputDto; @@ -31,10 +30,9 @@ class DummyDtoInputOutput { /** * @var int The id - * @ApiProperty(identifier=true) - * @ODM\Id(strategy="INCREMENT", type="integer") + * @ODM\Id(strategy="INCREMENT", type="integer", nullable=true) */ - public $id; + private $id; /** * @var int The id @@ -47,4 +45,9 @@ class DummyDtoInputOutput * @ODM\Field(type="float") */ public $num; + + public function getId() + { + return $this->id; + } } diff --git a/tests/Fixtures/TestBundle/Entity/DummyDtoInputOutput.php b/tests/Fixtures/TestBundle/Entity/DummyDtoInputOutput.php index 28f0e064424..33e804441ad 100644 --- a/tests/Fixtures/TestBundle/Entity/DummyDtoInputOutput.php +++ b/tests/Fixtures/TestBundle/Entity/DummyDtoInputOutput.php @@ -13,7 +13,6 @@ namespace ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity; -use ApiPlatform\Core\Annotation\ApiProperty; use ApiPlatform\Core\Annotation\ApiResource; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Dto\InputDto; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Dto\OutputDto; @@ -31,8 +30,7 @@ class DummyDtoInputOutput { /** * @var int The id - * @ApiProperty(identifier=true) - * @ORM\Column(type="integer") + * @ORM\Column(type="integer", nullable=true) * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") */ @@ -46,7 +44,7 @@ class DummyDtoInputOutput /** * @var int - * @ORM\Column(type="decimal") + * @ORM\Column(type="float") */ public $num; } diff --git a/tests/GraphQl/Serializer/ItemNormalizerTest.php b/tests/GraphQl/Serializer/ItemNormalizerTest.php index fa7f7fe52d1..b95db9c5a65 100644 --- a/tests/GraphQl/Serializer/ItemNormalizerTest.php +++ b/tests/GraphQl/Serializer/ItemNormalizerTest.php @@ -100,7 +100,7 @@ public function testNormalize() null, false, null, - null, + [], null, true ); @@ -139,7 +139,7 @@ public function testDenormalize() null, false, null, - null, + [], null, true ); diff --git a/tests/Hal/Serializer/ItemNormalizerTest.php b/tests/Hal/Serializer/ItemNormalizerTest.php index 714a1ba9839..8d20856a537 100644 --- a/tests/Hal/Serializer/ItemNormalizerTest.php +++ b/tests/Hal/Serializer/ItemNormalizerTest.php @@ -150,7 +150,7 @@ public function testNormalize() null, false, [], - null, + [], null, true ); @@ -217,7 +217,7 @@ public function testNormalizeWithoutCache() null, false, [], - null, + [], null, true ); @@ -297,7 +297,7 @@ public function testMaxDepth() null, false, [], - null, + [], null, true ); diff --git a/tests/Hydra/Serializer/ItemNormalizerTest.php b/tests/Hydra/Serializer/ItemNormalizerTest.php index 01dd49c42ee..e34f2b8aa09 100644 --- a/tests/Hydra/Serializer/ItemNormalizerTest.php +++ b/tests/Hydra/Serializer/ItemNormalizerTest.php @@ -129,7 +129,7 @@ public function testNormalize() null, null, [], - null, + [], true ); $normalizer->setSerializer($serializerProphecy->reveal()); diff --git a/tests/JsonApi/Serializer/ItemNormalizerTest.php b/tests/JsonApi/Serializer/ItemNormalizerTest.php index 6230b369b66..7cfd4d4e4c3 100644 --- a/tests/JsonApi/Serializer/ItemNormalizerTest.php +++ b/tests/JsonApi/Serializer/ItemNormalizerTest.php @@ -140,7 +140,7 @@ public function testNormalize() new ReservedAttributeNameConverter(), $resourceMetadataFactoryProphecy->reveal(), [], - null, + [], true ); @@ -181,7 +181,7 @@ public function testNormalizeIsNotAnArray() new ReservedAttributeNameConverter(), $resourceMetadataFactory->reveal(), [], - null, + [], true ); @@ -241,7 +241,7 @@ public function testNormalizeThrowsNoSuchPropertyException() new ReservedAttributeNameConverter(), $resourceMetadataFactory->reveal(), [], - null, + [], true ); @@ -326,7 +326,7 @@ public function testDenormalize() new ReservedAttributeNameConverter(), $resourceMetadataFactory->reveal(), [], - null, + [], true ); $normalizer->setSerializer($serializerProphecy->reveal()); @@ -379,7 +379,7 @@ public function testDenormalizeUpdateOperationNotAllowed() null, $this->prophesize(ResourceMetadataFactoryInterface::class)->reveal(), [], - null, + [], true ); @@ -436,7 +436,7 @@ public function testDenormalizeCollectionIsNotArray() new ReservedAttributeNameConverter(), $resourceMetadataFactory->reveal(), [], - null, + [], true ); @@ -494,7 +494,7 @@ public function testDenormalizeCollectionWithInvalidKey() new ReservedAttributeNameConverter(), $resourceMetadataFactory->reveal(), [], - null, + [], true ); @@ -554,7 +554,7 @@ public function testDenormalizeRelationIsNotResourceLinkage() new ReservedAttributeNameConverter(), $resourceMetadataFactory->reveal(), [], - null, + [], true ); diff --git a/tests/Serializer/AbstractItemNormalizerTest.php b/tests/Serializer/AbstractItemNormalizerTest.php index 4cb62247124..b73a01b6788 100644 --- a/tests/Serializer/AbstractItemNormalizerTest.php +++ b/tests/Serializer/AbstractItemNormalizerTest.php @@ -26,6 +26,7 @@ use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; use ApiPlatform\Core\Metadata\Resource\ResourceMetadata; use ApiPlatform\Core\Serializer\AbstractItemNormalizer; +use ApiPlatform\Core\Tests\Fixtures\TestBundle\Dto\InputDto; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Dummy; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyForAdditionalFields; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyForAdditionalFieldsInput; @@ -105,7 +106,7 @@ public function testSupportNormalizationAndSupportDenormalization() null, false, [], - null, + [], null, true, ]); @@ -189,7 +190,7 @@ public function testNormalize() null, false, [], - null, + [], null, true, ]); @@ -268,7 +269,7 @@ public function testNormalizeReadableLinks() null, false, [], - null, + [], null, true, ]); @@ -346,7 +347,7 @@ public function testDenormalize() null, false, [], - null, + [], null, true, ]); @@ -385,7 +386,7 @@ public function testCanDenormalizeInputClassWithDifferentFieldsThanResourceClass (new PropertyMetadata(new Type(Type::BUILTIN_TYPE_STRING), '', true, false))->withInitializable(true) ); - $normalizer = new class($propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), $this->prophesize(IriConverterInterface::class)->reveal(), $this->prophesize(ResourceClassResolverInterface::class)->reveal(), null, null, null, null, false, [], null, null, true) extends AbstractItemNormalizer { + $normalizer = new class($propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), $this->prophesize(IriConverterInterface::class)->reveal(), $this->prophesize(ResourceClassResolverInterface::class)->reveal(), null, null, null, null, false, [], [], null, true) extends AbstractItemNormalizer { }; /** @var DummyForAdditionalFieldsInput $res */ @@ -468,7 +469,7 @@ public function testDenormalizeWritableLinks() null, false, [], - null, + [], null, true, ]); @@ -523,7 +524,7 @@ public function testBadRelationType() null, false, [], - null, + [], null, true, ]); @@ -574,7 +575,7 @@ public function testInnerDocumentNotAllowed() null, false, [], - null, + [], null, true, ]); @@ -616,7 +617,7 @@ public function testBadType() null, false, [], - null, + [], null, true, ]); @@ -656,7 +657,7 @@ public function testJsonAllowIntAsFloat() null, false, [], - null, + [], null, true, ]); @@ -713,7 +714,7 @@ public function testDenormalizeBadKeyType() null, false, [], - null, + [], null, true, ]); @@ -756,7 +757,7 @@ public function testNullable() null, false, [], - null, + [], null, true, ]); @@ -809,7 +810,7 @@ public function testChildInheritedProperty() null, false, [], - null, + [], null, true, ]); @@ -866,7 +867,7 @@ public function testDenormalizeRelationWithPlainId() $itemDataProviderProphecy->reveal(), true, [], - null, + [], null, true, ]); @@ -920,7 +921,7 @@ public function testDenormalizeRelationWithPlainIdNotFound() $itemDataProviderProphecy->reveal(), true, [], - null, + [], null, true, ]); @@ -974,7 +975,7 @@ public function testDoNotDenormalizeRelationWithPlainIdWhenPlainIdentifiersAreNo $itemDataProviderProphecy->reveal(), false, [], - null, + [], null, true, ]); @@ -1004,34 +1005,72 @@ public function testDisallowUnmappedClass() ]); } + /** + * Test case: + * 1. Request `PUT {InputDto} /recover_password` + * 2. The `AbstractItemNormalizer` denormalizes the json representation of `{InputDto}` in a `RecoverPasswordInput` + * 3. The `DataTransformer` transforms this `InputDto` in a `Dummy` + * 4. Messenger is used, we send the `Dummy` + * 5. The handler receives a `{Dummy}` json representation and tries to denormalize it + * 6. Because it has an `input`, the `AbstractItemNormalizer` tries to denormalize it as a `InputDto` which is wrong, it's a `{Dummy}`. + */ public function testNormalizationWithDataTransformer() { - $transformed = new \stdClass(); $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); - $propertyNameCollectionFactoryProphecy->create(RelatedDummy::class, [])->willReturn( + $propertyNameCollectionFactoryProphecy->create(InputDto::class, Argument::any())->willReturn( new PropertyNameCollection() )->shouldBeCalled(); + $propertyNameCollectionFactoryProphecy->create(Dummy::class, Argument::any())->willReturn( + new PropertyNameCollection(['name']) + )->shouldBeCalled(); $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'name', Argument::any())->willReturn( + new PropertyMetadata(new Type(Type::BUILTIN_TYPE_STRING), '', false, true) + )->shouldBeCalled(); $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); $propertyAccessorProphecy = $this->prophesize(PropertyAccessorInterface::class); + $propertyAccessorProphecy->setValue(Argument::type(Dummy::class), 'name', 'Dummy')->shouldBeCalled(); $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); - $serializerProphecy = $this->prophesize(SerializerInterface::class); $serializerProphecy->willImplement(DenormalizerInterface::class); - $itemDataProviderProphecy = $this->prophesize(ItemDataProviderInterface::class); - $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); + $resourceMetadataFactoryProphecy->create(Dummy::class)->willReturn(new ResourceMetadata( - 'dummy', '', '', null, null, ['input' => ['class' => RelatedDummy::class]] + 'dummy', '', '', null, null, ['input' => ['class' => InputDto::class]] )); + $jsonInput = ['foo' => 'f', 'bar' => 'b']; + $transformed = new Dummy(); + $requestContext = [ + 'operation_type' => 'collection', + 'collection_operation_name' => 'post', + 'resource_class' => Dummy::class, + 'input' => [ + 'class' => InputDto::class, + 'name' => 'InputDto', + ], + 'output' => ['class' => 'null'], + 'api_denormalize' => true, // this is added by the normalizer + ]; + + $secondJsonInput = ['name' => 'Dummy']; + $secondContext = ['api_denormalize' => true, 'resource_class' => Dummy::class]; + $secondTransformed = new Dummy(); + $secondTransformed->setName('Dummy'); + $dataTransformerProphecy = $this->prophesize(DataTransformerInterface::class); - $dataTransformerProphecy->supportsTransformation(Argument::any(), Dummy::class, Argument::any())->shouldBeCalled()->willReturn(true); - $dataTransformerProphecy->transform(Argument::any(), Dummy::class, Argument::any())->shouldBeCalled()->willReturn($transformed); + $dataTransformerProphecy->supportsTransformation($jsonInput, Dummy::class, $requestContext)->shouldBeCalled()->willReturn(true); + $dataTransformerProphecy->supportsTransformation($secondJsonInput, Dummy::class, $secondContext)->shouldBeCalled()->willReturn(false); + $dataTransformerProphecy->transform(Argument::that(function ($arg) { + return $arg instanceof InputDto; + }), Dummy::class, $requestContext)->shouldBeCalled()->willReturn($transformed); + + $secondDataTransformerProphecy = $this->prophesize(DataTransformerInterface::class); + $secondDataTransformerProphecy->supportsTransformation(Argument::any(), Dummy::class, Argument::any())->shouldBeCalled()->willReturn(false); $normalizer = $this->getMockForAbstractClass(AbstractItemNormalizer::class, [ $propertyNameCollectionFactoryProphecy->reveal(), @@ -1044,12 +1083,15 @@ public function testNormalizationWithDataTransformer() $itemDataProviderProphecy->reveal(), false, [], - $dataTransformerProphecy->reveal(), + [$dataTransformerProphecy->reveal(), $secondDataTransformerProphecy->reveal()], $resourceMetadataFactoryProphecy->reveal(), true, ]); $normalizer->setSerializer($serializerProphecy->reveal()); - $this->assertEquals($transformed, $normalizer->denormalize(['foo' => 't'], Dummy::class, 'jsonld')); + // This is step 1-3, {InputDto} to Dummy + $this->assertEquals($transformed, $normalizer->denormalize($jsonInput, Dummy::class, 'jsonld', $requestContext)); + // Messenger sends {InputDto} + $this->assertInstanceOf(Dummy::class, $normalizer->denormalize($secondJsonInput, Dummy::class, 'jsonld')); } } diff --git a/tests/Serializer/ItemNormalizerTest.php b/tests/Serializer/ItemNormalizerTest.php index 8688a91acb6..43b937edd1d 100644 --- a/tests/Serializer/ItemNormalizerTest.php +++ b/tests/Serializer/ItemNormalizerTest.php @@ -15,12 +15,14 @@ use ApiPlatform\Core\Api\IriConverterInterface; use ApiPlatform\Core\Api\ResourceClassResolverInterface; +use ApiPlatform\Core\DataTransformer\DataTransformerInterface; use ApiPlatform\Core\Exception\InvalidArgumentException; use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; use ApiPlatform\Core\Metadata\Property\PropertyMetadata; use ApiPlatform\Core\Metadata\Property\PropertyNameCollection; use ApiPlatform\Core\Serializer\ItemNormalizer; +use ApiPlatform\Core\Tests\Fixtures\TestBundle\Dto\OutputDto; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Dummy; use PHPUnit\Framework\TestCase; use Prophecy\Argument; @@ -102,7 +104,7 @@ public function testNormalize() null, false, null, - null, + [], null, true ); @@ -141,7 +143,7 @@ public function testDenormalize() null, false, null, - null, + [], null, true ); @@ -181,7 +183,7 @@ public function testDenormalizeWithIri() null, false, null, - null, + [], null, true ); @@ -219,7 +221,7 @@ public function testDenormalizeWithIdAndUpdateNotAllowed() null, false, null, - null, + [], null, true ); @@ -258,7 +260,7 @@ public function testDenormalizeWithIdAndNoResourceClass() null, false, null, - null, + [], null, true ); @@ -269,4 +271,51 @@ public function testDenormalizeWithIdAndNoResourceClass() $this->assertSame('42', $object->getId()); $this->assertSame('hello', $object->getName()); } + + public function testNormalizeWithDataTransformers() + { + $dummy = new Dummy(); + $dummy->setName('hello'); + $output = new OutputDto(); + + $propertyNameCollection = new PropertyNameCollection(['baz']); + $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactoryProphecy->create(OutputDto::class, [])->willReturn($propertyNameCollection)->shouldBeCalled(); + + $propertyMetadataFactory = new PropertyMetadata(null, null, true); + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryProphecy->create(OutputDto::class, 'baz', [])->willReturn($propertyMetadataFactory)->shouldBeCalled(); + + $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); + + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolverProphecy->getResourceClass($output, null, true)->willThrow(InvalidArgumentException::class); + + $serializerProphecy = $this->prophesize(SerializerInterface::class); + $serializerProphecy->willImplement(NormalizerInterface::class); + $serializerProphecy->normalize(null, null, Argument::type('array'))->willReturn('hello')->shouldBeCalled(); + + $dataTransformer = $this->prophesize(DataTransformerInterface::class); + $dataTransformer->supportsTransformation($dummy, OutputDto::class, Argument::any())->shouldBeCalled()->willReturn(true); + $dataTransformer->transform($dummy, OutputDto::class, Argument::any())->shouldBeCalled()->willReturn($output); + + $normalizer = new ItemNormalizer( + $propertyNameCollectionFactoryProphecy->reveal(), + $propertyMetadataFactoryProphecy->reveal(), + $iriConverterProphecy->reveal(), + $resourceClassResolverProphecy->reveal(), + null, + null, + null, + null, + false, + null, + [$dataTransformer->reveal()], + null, + true + ); + $normalizer->setSerializer($serializerProphecy->reveal()); + + $this->assertEquals(['baz' => 'hello'], $normalizer->normalize($dummy, null, ['resources' => [], 'output' => ['class' => OutputDto::class]])); + } } From 9a74e3590862033f3937cc2894f78ca59bdb4af3 Mon Sep 17 00:00:00 2001 From: soyuka Date: Fri, 22 Feb 2019 14:25:27 +0100 Subject: [PATCH 08/15] Test ContextBuilder with anonymous objects fix #2551 --- .../AnonymousContextBuilderInterface.php | 2 +- src/JsonLd/ContextBuilder.php | 2 +- src/JsonLd/Serializer/JsonLdContextTrait.php | 2 +- tests/JsonLd/ContextBuilderTest.php | 43 +++++++++++++++++++ 4 files changed, 46 insertions(+), 3 deletions(-) diff --git a/src/JsonLd/AnonymousContextBuilderInterface.php b/src/JsonLd/AnonymousContextBuilderInterface.php index 20fdaf427ed..a13ef3428ed 100644 --- a/src/JsonLd/AnonymousContextBuilderInterface.php +++ b/src/JsonLd/AnonymousContextBuilderInterface.php @@ -26,5 +26,5 @@ interface AnonymousContextBuilderInterface extends ContextBuilderInterface * Creates a JSON-LD context based on the given object. * Usually this is used with an Input or Output DTO object. */ - public function getAnonymousResourceContext($object, array $context, int $referenceType = UrlGeneratorInterface::ABS_PATH): array; + public function getAnonymousResourceContext($object, array $context = [], int $referenceType = UrlGeneratorInterface::ABS_PATH): array; } diff --git a/src/JsonLd/ContextBuilder.php b/src/JsonLd/ContextBuilder.php index 94b56d44322..204be138683 100644 --- a/src/JsonLd/ContextBuilder.php +++ b/src/JsonLd/ContextBuilder.php @@ -108,7 +108,7 @@ public function getResourceContextUri(string $resourceClass, int $referenceType /** * {@inheritdoc} */ - public function getAnonymousResourceContext($object, array $context, int $referenceType = UrlGeneratorInterface::ABS_PATH): array + public function getAnonymousResourceContext($object, array $context = [], int $referenceType = UrlGeneratorInterface::ABS_PATH): array { $id = $context['iri'] ?? '_:'.(\function_exists('spl_object_id') ? spl_object_id($object) : spl_object_hash($object)); $jsonLdContext = [ diff --git a/src/JsonLd/Serializer/JsonLdContextTrait.php b/src/JsonLd/Serializer/JsonLdContextTrait.php index cb138258ab8..10a45b91a16 100644 --- a/src/JsonLd/Serializer/JsonLdContextTrait.php +++ b/src/JsonLd/Serializer/JsonLdContextTrait.php @@ -55,6 +55,6 @@ private function createJsonLdContext(AnonymousContextBuilderInterface $contextBu $context['jsonld_has_context'] = true; - return $contextBuilder->getAnonymousResourceContext($object, $context['output']); + return $contextBuilder->getAnonymousResourceContext($object, $context['output'] ?? []); } } diff --git a/tests/JsonLd/ContextBuilderTest.php b/tests/JsonLd/ContextBuilderTest.php index a5882efc880..8680be9df7c 100644 --- a/tests/JsonLd/ContextBuilderTest.php +++ b/tests/JsonLd/ContextBuilderTest.php @@ -23,6 +23,7 @@ use ApiPlatform\Core\Metadata\Resource\Factory\ResourceNameCollectionFactoryInterface; use ApiPlatform\Core\Metadata\Resource\ResourceMetadata; use ApiPlatform\Core\Metadata\Resource\ResourceNameCollection; +use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Dummy; use PHPUnit\Framework\TestCase; use Symfony\Component\PropertyInfo\Type; @@ -126,4 +127,46 @@ public function testResourceContextWithReverse() $this->assertEquals($expected, $contextBuilder->getResourceContext($this->entityClass)); } + + public function testAnonymousResourceContext() + { + $dummy = new Dummy(); + $this->propertyNameCollectionFactoryProphecy->create(Dummy::class)->willReturn(new PropertyNameCollection(['dummyPropertyA'])); + $this->propertyMetadataFactoryProphecy->create(Dummy::class, 'dummyPropertyA')->willReturn(new PropertyMetadata(new Type(Type::BUILTIN_TYPE_STRING), 'Dummy property A', true, true, true, true, false, false, null, null, [])); + + $contextBuilder = new ContextBuilder($this->resourceNameCollectionFactoryProphecy->reveal(), $this->resourceMetadataFactoryProphecy->reveal(), $this->propertyNameCollectionFactoryProphecy->reveal(), $this->propertyMetadataFactoryProphecy->reveal(), $this->urlGeneratorProphecy->reveal()); + + $iri = '_:'.(\function_exists('spl_object_id') ? spl_object_id($dummy) : spl_object_hash($dummy)); + $expected = [ + '@context' => [ + '@vocab' => '#', + 'hydra' => 'http://www.w3.org/ns/hydra/core#', + 'dummyPropertyA' => $iri.'/dummyPropertyA', + ], + '@id' => $iri, + ]; + + $this->assertEquals($expected, $contextBuilder->getAnonymousResourceContext($dummy)); + } + + public function testAnonymousResourceContextWithIri() + { + $dummy = new Dummy(); + $this->propertyNameCollectionFactoryProphecy->create(Dummy::class)->willReturn(new PropertyNameCollection(['dummyPropertyA'])); + $this->propertyMetadataFactoryProphecy->create(Dummy::class, 'dummyPropertyA')->willReturn(new PropertyMetadata(new Type(Type::BUILTIN_TYPE_STRING), 'Dummy property A', true, true, true, true, false, false, null, null, [])); + + $contextBuilder = new ContextBuilder($this->resourceNameCollectionFactoryProphecy->reveal(), $this->resourceMetadataFactoryProphecy->reveal(), $this->propertyNameCollectionFactoryProphecy->reveal(), $this->propertyMetadataFactoryProphecy->reveal(), $this->urlGeneratorProphecy->reveal()); + + $expected = [ + '@context' => [ + '@vocab' => '#', + 'hydra' => 'http://www.w3.org/ns/hydra/core#', + 'dummyPropertyA' => '/dummies/dummyPropertyA', + ], + '@id' => '/dummies', + '@type' => 'Dummy', + ]; + + $this->assertEquals($expected, $contextBuilder->getAnonymousResourceContext($dummy, ['iri' => '/dummies', 'name' => 'Dummy'])); + } } From 955c4da8c391f42690f5bcb25db8bf2848dc075c Mon Sep 17 00:00:00 2001 From: soyuka Date: Fri, 15 Feb 2019 14:11:20 +0100 Subject: [PATCH 09/15] Make use of the new AdvancedNameConverterInterface fix #2342 Elasticsearch normalizer is experimental, changed constructor signature --- phpstan.neon | 2 + .../DataProvider/Extension/SortExtension.php | 4 +- .../Filter/AbstractSearchFilter.php | 4 +- .../DataProvider/Filter/OrderFilter.php | 4 +- .../Serializer/ItemNormalizer.php | 2 +- .../InnerFieldsNameConverter.php | 15 +++-- .../NelmioApiDoc/Parser/ApiPlatformParser.php | 2 +- .../ApiPlatformExtension.php | 4 -- .../MetadataAwareNameConverterPass.php | 40 ++++++++++++ src/Hal/Serializer/ItemNormalizer.php | 2 +- .../ConstraintViolationListNormalizer.php | 5 +- src/JsonApi/Serializer/ItemNormalizer.php | 2 +- .../ReservedAttributeNameConverter.php | 11 ++-- src/JsonLd/ContextBuilder.php | 2 +- ...tractConstraintViolationListNormalizer.php | 3 +- src/Serializer/AbstractItemNormalizer.php | 2 +- .../Extension/SortExtensionTest.php | 10 +-- .../DataProvider/Filter/MatchFilterTest.php | 9 +-- .../DataProvider/Filter/OrderFilterTest.php | 7 +- .../DataProvider/Filter/TermFilterTest.php | 9 +-- .../InnerFieldsNameConverterTest.php | 18 +++--- .../Parser/ApiPlatformParserTest.php | 2 +- .../ApiPlatformExtensionTest.php | 18 ------ .../MetadataAwareNameConverterPassTest.php | 64 +++++++++++++++++++ .../ConstraintViolationNormalizerTest.php | 2 +- .../ConstraintViolationNormalizerTest.php | 4 +- .../ConstraintViolationNormalizerTest.php | 2 +- 27 files changed, 170 insertions(+), 79 deletions(-) create mode 100644 src/Bridge/Symfony/Bundle/DependencyInjection/Compiler/MetadataAwareNameConverterPass.php create mode 100644 tests/Bridge/Symfony/Bundle/DependencyInjection/Compiler/MetadataAwareNameConverterPassTest.php diff --git a/phpstan.neon b/phpstan.neon index 5012920a113..d1ea60a22a4 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -49,3 +49,5 @@ parameters: - '#Method ApiPlatform\\Core\\Bridge\\Doctrine\\MongoDbOdm\\Filter\\(Abstract|Boolean|Date|Exists|Numeric|Order|Range|Search)Filter::isPropertyNested\(\) invoked with 2 parameters, 1 required\.#' - '#Method ApiPlatform\\Core\\Bridge\\Doctrine\\MongoDbOdm\\Filter\\(Abstract|Boolean|Date|Exists|Numeric|Order|Range|Search)Filter::splitPropertyParts\(\) invoked with 2 parameters, 1 required\.#' - '#Method ApiPlatform\\Core\\DataProvider\\CollectionDataProviderInterface::getCollection\(\) invoked with 3 parameters, 1-2 required\.#' + - '#Method Symfony\\Component\\Serializer\\NameConverter\\NameConverterInterface::normalize\(\) invoked with 3 parameters, 1 required\.#' + - '#Method Symfony\\Component\\Serializer\\NameConverter\\NameConverterInterface::normalize\(\) invoked with 4 parameters, 1 required\.#' diff --git a/src/Bridge/Elasticsearch/DataProvider/Extension/SortExtension.php b/src/Bridge/Elasticsearch/DataProvider/Extension/SortExtension.php index e82a21908e5..06ee807d54c 100644 --- a/src/Bridge/Elasticsearch/DataProvider/Extension/SortExtension.php +++ b/src/Bridge/Elasticsearch/DataProvider/Extension/SortExtension.php @@ -89,11 +89,11 @@ private function getOrder(string $resourceClass, string $property, string $direc $order = ['order' => strtolower($direction)]; if (null !== $nestedPath = $this->getNestedFieldPath($resourceClass, $property)) { - $nestedPath = null === $this->nameConverter ? $nestedPath : $this->nameConverter->normalize($nestedPath); + $nestedPath = null === $this->nameConverter ? $nestedPath : $this->nameConverter->normalize($nestedPath, $resourceClass); $order['nested'] = ['path' => $nestedPath]; } - $property = null === $this->nameConverter ? $property : $this->nameConverter->normalize($property); + $property = null === $this->nameConverter ? $property : $this->nameConverter->normalize($property, $resourceClass); return [$property => $order]; } diff --git a/src/Bridge/Elasticsearch/DataProvider/Filter/AbstractSearchFilter.php b/src/Bridge/Elasticsearch/DataProvider/Filter/AbstractSearchFilter.php index b66fff90db1..e12ee385d76 100644 --- a/src/Bridge/Elasticsearch/DataProvider/Filter/AbstractSearchFilter.php +++ b/src/Bridge/Elasticsearch/DataProvider/Filter/AbstractSearchFilter.php @@ -72,9 +72,9 @@ public function apply(array $clauseBody, string $resourceClass, ?string $operati continue; } - $property = null === $this->nameConverter ? $property : $this->nameConverter->normalize($property); + $property = null === $this->nameConverter ? $property : $this->nameConverter->normalize($property, $resourceClass, null, $context); $nestedPath = $this->getNestedFieldPath($resourceClass, $property); - $nestedPath = null === $nestedPath || null === $this->nameConverter ? $nestedPath : $this->nameConverter->normalize($nestedPath); + $nestedPath = null === $nestedPath || null === $this->nameConverter ? $nestedPath : $this->nameConverter->normalize($nestedPath, $resourceClass, null, $context); $searches[] = $this->getQuery($property, $values, $nestedPath); } diff --git a/src/Bridge/Elasticsearch/DataProvider/Filter/OrderFilter.php b/src/Bridge/Elasticsearch/DataProvider/Filter/OrderFilter.php index 2394e1b082c..a1c78f3f06d 100644 --- a/src/Bridge/Elasticsearch/DataProvider/Filter/OrderFilter.php +++ b/src/Bridge/Elasticsearch/DataProvider/Filter/OrderFilter.php @@ -70,11 +70,11 @@ public function apply(array $clauseBody, string $resourceClass, ?string $operati $order = ['order' => $direction]; if (null !== $nestedPath = $this->getNestedFieldPath($resourceClass, $property)) { - $nestedPath = null === $this->nameConverter ? $nestedPath : $this->nameConverter->normalize($nestedPath); + $nestedPath = null === $this->nameConverter ? $nestedPath : $this->nameConverter->normalize($nestedPath, $resourceClass, null, $context); $order['nested'] = ['path' => $nestedPath]; } - $property = null === $this->nameConverter ? $property : $this->nameConverter->normalize($property); + $property = null === $this->nameConverter ? $property : $this->nameConverter->normalize($property, $resourceClass, null, $context); $orders[] = [$property => $order]; } diff --git a/src/Bridge/Elasticsearch/Serializer/ItemNormalizer.php b/src/Bridge/Elasticsearch/Serializer/ItemNormalizer.php index 5f965f62da2..7dd977ffe65 100644 --- a/src/Bridge/Elasticsearch/Serializer/ItemNormalizer.php +++ b/src/Bridge/Elasticsearch/Serializer/ItemNormalizer.php @@ -84,7 +84,7 @@ public function normalize($object, $format = null, array $context = []) private function populateIdentifier(array $data, string $class): array { $identifier = $this->identifierExtractor->getIdentifierFromResourceClass($class); - $identifier = null === $this->nameConverter ? $identifier : $this->nameConverter->normalize($identifier); + $identifier = null === $this->nameConverter ? $identifier : $this->nameConverter->normalize($identifier, $class, self::FORMAT); if (!isset($data['_source'][$identifier])) { $data['_source'][$identifier] = $data['_id']; diff --git a/src/Bridge/Elasticsearch/Serializer/NameConverter/InnerFieldsNameConverter.php b/src/Bridge/Elasticsearch/Serializer/NameConverter/InnerFieldsNameConverter.php index 8f6c212a5fe..8068584c89f 100644 --- a/src/Bridge/Elasticsearch/Serializer/NameConverter/InnerFieldsNameConverter.php +++ b/src/Bridge/Elasticsearch/Serializer/NameConverter/InnerFieldsNameConverter.php @@ -13,6 +13,7 @@ namespace ApiPlatform\Core\Bridge\Elasticsearch\Serializer\NameConverter; +use Symfony\Component\Serializer\NameConverter\AdvancedNameConverterInterface; use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter; use Symfony\Component\Serializer\NameConverter\NameConverterInterface; @@ -23,7 +24,7 @@ * * @author Baptiste Meyer */ -final class InnerFieldsNameConverter implements NameConverterInterface +final class InnerFieldsNameConverter implements AdvancedNameConverterInterface { private $decorated; @@ -35,25 +36,25 @@ public function __construct(?NameConverterInterface $decorated = null) /** * {@inheritdoc} */ - public function normalize($propertyName) + public function normalize($propertyName, string $class = null, string $format = null, $context = []) { - return $this->convertInnerFields($propertyName, true); + return $this->convertInnerFields($propertyName, true, $class, $format, $context); } /** * {@inheritdoc} */ - public function denormalize($propertyName) + public function denormalize($propertyName, string $class = null, string $format = null, $context = []) { - return $this->convertInnerFields($propertyName, false); + return $this->convertInnerFields($propertyName, false, $class, $format, $context); } - private function convertInnerFields(string $propertyName, bool $normalization): string + private function convertInnerFields(string $propertyName, bool $normalization, string $class = null, string $format = null, $context = []): string { $convertedProperties = []; foreach (explode('.', $propertyName) as $decomposedProperty) { - $convertedProperties[] = $this->decorated->{$normalization ? 'normalize' : 'denormalize'}($decomposedProperty); + $convertedProperties[] = $this->decorated->{$normalization ? 'normalize' : 'denormalize'}($decomposedProperty, $class, $format, $context); } return implode('.', $convertedProperties); diff --git a/src/Bridge/NelmioApiDoc/Parser/ApiPlatformParser.php b/src/Bridge/NelmioApiDoc/Parser/ApiPlatformParser.php index e8d3c120f40..bd61de0e904 100644 --- a/src/Bridge/NelmioApiDoc/Parser/ApiPlatformParser.php +++ b/src/Bridge/NelmioApiDoc/Parser/ApiPlatformParser.php @@ -180,7 +180,7 @@ private function getPropertyMetadata(ResourceMetadata $resourceMetadata, string ($propertyMetadata->isReadable() && self::OUT_PREFIX === $io) || ($propertyMetadata->isWritable() && self::IN_PREFIX === $io) ) { - $normalizedPropertyName = $this->nameConverter ? $this->nameConverter->normalize($propertyName) : $propertyName; + $normalizedPropertyName = $this->nameConverter ? $this->nameConverter->normalize($propertyName, $resourceClass) : $propertyName; $data[$normalizedPropertyName] = $this->parseProperty($resourceMetadata, $propertyMetadata, $io, null, $visited); } } diff --git a/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php b/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php index e19cf0e70f4..981fd893635 100644 --- a/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php +++ b/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php @@ -79,10 +79,6 @@ public function prepend(ContainerBuilder $container) if (!isset($propertyInfoConfig['enabled'])) { $container->prependExtensionConfig('framework', ['property_info' => ['enabled' => true]]); } - - if (isset($serializerConfig['name_converter'])) { - $container->prependExtensionConfig('api_platform', ['name_converter' => $serializerConfig['name_converter']]); - } } /** diff --git a/src/Bridge/Symfony/Bundle/DependencyInjection/Compiler/MetadataAwareNameConverterPass.php b/src/Bridge/Symfony/Bundle/DependencyInjection/Compiler/MetadataAwareNameConverterPass.php new file mode 100644 index 00000000000..a6759824da3 --- /dev/null +++ b/src/Bridge/Symfony/Bundle/DependencyInjection/Compiler/MetadataAwareNameConverterPass.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Core\Bridge\Symfony\Bundle\DependencyInjection\Compiler; + +use ApiPlatform\Core\Exception\RuntimeException; +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; + +/** + * Injects the metadata aware name converter if available. + * + * @internal + * + * @author Antoine Bluchet + */ +final class MetadataAwareNameConverterPass implements CompilerPassInterface +{ + /** + * {@inheritdoc} + * + * @throws RuntimeException + */ + public function process(ContainerBuilder $container) + { + if (!$container->hasAlias('api_platform.name_converter') && $container->hasDefinition('serializer.name_converter.metadata_aware')) { + $container->setAlias('api_platform.name_converter', 'serializer.name_converter.metadata_aware'); + } + } +} diff --git a/src/Hal/Serializer/ItemNormalizer.php b/src/Hal/Serializer/ItemNormalizer.php index 942d5f84d5c..1814f244ceb 100644 --- a/src/Hal/Serializer/ItemNormalizer.php +++ b/src/Hal/Serializer/ItemNormalizer.php @@ -181,7 +181,7 @@ private function populateRelation(array $data, $object, string $format = null, a $relationName = $relation['name']; if ($this->nameConverter) { - $relationName = $this->nameConverter->normalize($relationName); + $relationName = $this->nameConverter->normalize($relationName, $class, $format, $context); } if ('one' === $relation['cardinality']) { diff --git a/src/JsonApi/Serializer/ConstraintViolationListNormalizer.php b/src/JsonApi/Serializer/ConstraintViolationListNormalizer.php index eba1e6ddd04..d4ad2d0bee7 100644 --- a/src/JsonApi/Serializer/ConstraintViolationListNormalizer.php +++ b/src/JsonApi/Serializer/ConstraintViolationListNormalizer.php @@ -77,15 +77,16 @@ private function getSourcePointerFromViolation(ConstraintViolationInterface $vio return 'data'; } + $class = \get_class($violation->getRoot()); $propertyMetadata = $this->propertyMetadataFactory ->create( // Im quite sure this requires some thought in case of validations over relationships - \get_class($violation->getRoot()), + $class, $fieldName ); if (null !== $this->nameConverter) { - $fieldName = $this->nameConverter->normalize($fieldName); + $fieldName = $this->nameConverter->normalize($fieldName, $class, self::FORMAT); } if (null !== $propertyMetadata->getType()->getClassName()) { diff --git a/src/JsonApi/Serializer/ItemNormalizer.php b/src/JsonApi/Serializer/ItemNormalizer.php index 8dbd25bcadb..d64cfb53cf0 100644 --- a/src/JsonApi/Serializer/ItemNormalizer.php +++ b/src/JsonApi/Serializer/ItemNormalizer.php @@ -325,7 +325,7 @@ private function getPopulatedRelations($object, string $format = null, array $co $attributeValue = $this->getAttributeValue($object, $relationshipName, $format, $context); if ($this->nameConverter) { - $relationshipName = $this->nameConverter->normalize($relationshipName); + $relationshipName = $this->nameConverter->normalize($relationshipName, $context['resource_class'], self::FORMAT, $context); } if (!$attributeValue) { diff --git a/src/JsonApi/Serializer/ReservedAttributeNameConverter.php b/src/JsonApi/Serializer/ReservedAttributeNameConverter.php index 34b1cc0a400..c11190e1065 100644 --- a/src/JsonApi/Serializer/ReservedAttributeNameConverter.php +++ b/src/JsonApi/Serializer/ReservedAttributeNameConverter.php @@ -13,6 +13,7 @@ namespace ApiPlatform\Core\JsonApi\Serializer; +use Symfony\Component\Serializer\NameConverter\AdvancedNameConverterInterface; use Symfony\Component\Serializer\NameConverter\NameConverterInterface; /** @@ -20,7 +21,7 @@ * * @author Baptiste Meyer */ -final class ReservedAttributeNameConverter implements NameConverterInterface +final class ReservedAttributeNameConverter implements AdvancedNameConverterInterface { const JSON_API_RESERVED_ATTRIBUTES = [ 'id' => '_id', @@ -40,10 +41,10 @@ public function __construct(NameConverterInterface $nameConverter = null) /** * {@inheritdoc} */ - public function normalize($propertyName) + public function normalize($propertyName, string $class = null, string $format = null, array $context = []) { if (null !== $this->nameConverter) { - $propertyName = $this->nameConverter->normalize($propertyName); + $propertyName = $this->nameConverter->normalize($propertyName, $class, $format, $context); } if (isset(self::JSON_API_RESERVED_ATTRIBUTES[$propertyName])) { @@ -56,14 +57,14 @@ public function normalize($propertyName) /** * {@inheritdoc} */ - public function denormalize($propertyName) + public function denormalize($propertyName, string $class = null, string $format = null, array $context = []) { if (\in_array($propertyName, self::JSON_API_RESERVED_ATTRIBUTES, true)) { $propertyName = substr($propertyName, 1); } if (null !== $this->nameConverter) { - $propertyName = $this->nameConverter->denormalize($propertyName); + $propertyName = $this->nameConverter->denormalize($propertyName, $class, $format, $context); } return $propertyName; diff --git a/src/JsonLd/ContextBuilder.php b/src/JsonLd/ContextBuilder.php index 9a34baf0c41..44c79d18b43 100644 --- a/src/JsonLd/ContextBuilder.php +++ b/src/JsonLd/ContextBuilder.php @@ -98,7 +98,7 @@ public function getResourceContext(string $resourceClass, int $referenceType = U continue; } - $convertedName = $this->nameConverter ? $this->nameConverter->normalize($propertyName) : $propertyName; + $convertedName = $this->nameConverter ? $this->nameConverter->normalize($propertyName, $resourceClass, self::FORMAT) : $propertyName; $jsonldContext = $propertyMetadata->getAttributes()['jsonld_context'] ?? []; if (!$id = $propertyMetadata->getIri()) { diff --git a/src/Serializer/AbstractConstraintViolationListNormalizer.php b/src/Serializer/AbstractConstraintViolationListNormalizer.php index c3d1dbbe287..593040404ca 100644 --- a/src/Serializer/AbstractConstraintViolationListNormalizer.php +++ b/src/Serializer/AbstractConstraintViolationListNormalizer.php @@ -59,8 +59,9 @@ protected function getMessagesAndViolations(ConstraintViolationListInterface $co $violations = $messages = []; foreach ($constraintViolationList as $violation) { + $class = \is_object($root = $violation->getRoot()) ? \get_class($root) : null; $violationData = [ - 'propertyPath' => $this->nameConverter ? $this->nameConverter->normalize($violation->getPropertyPath()) : $violation->getPropertyPath(), + 'propertyPath' => $this->nameConverter ? $this->nameConverter->normalize($violation->getPropertyPath(), $class, static::FORMAT) : $violation->getPropertyPath(), 'message' => $violation->getMessage(), ]; diff --git a/src/Serializer/AbstractItemNormalizer.php b/src/Serializer/AbstractItemNormalizer.php index 9f5c62b46ff..5a7f2022ea8 100644 --- a/src/Serializer/AbstractItemNormalizer.php +++ b/src/Serializer/AbstractItemNormalizer.php @@ -170,7 +170,7 @@ protected function instantiateObject(array &$data, $class, array &$context, \Ref $params = []; foreach ($constructorParameters as $constructorParameter) { $paramName = $constructorParameter->name; - $key = $this->nameConverter ? $this->nameConverter->normalize($paramName) : $paramName; + $key = $this->nameConverter ? $this->nameConverter->normalize($paramName, $class, $format, $context) : $paramName; $allowed = false === $allowedAttributes || (\is_array($allowedAttributes) && \in_array($paramName, $allowedAttributes, true)); $ignored = !$this->isAllowedAttribute($class, $paramName, $format, $context); diff --git a/tests/Bridge/Elasticsearch/DataProvider/Extension/SortExtensionTest.php b/tests/Bridge/Elasticsearch/DataProvider/Extension/SortExtensionTest.php index 481af15715e..6f36382c409 100644 --- a/tests/Bridge/Elasticsearch/DataProvider/Extension/SortExtensionTest.php +++ b/tests/Bridge/Elasticsearch/DataProvider/Extension/SortExtensionTest.php @@ -49,8 +49,8 @@ public function testApplyToCollection() $resourceMetadataFactoryProphecy->create(Foo::class)->willReturn(new ResourceMetadata(null, null, null, null, null, ['order' => ['name', 'bar' => 'desc']]))->shouldBeCalled(); $nameConverterProphecy = $this->prophesize(NameConverterInterface::class); - $nameConverterProphecy->normalize('name')->willReturn('name')->shouldBeCalled(); - $nameConverterProphecy->normalize('bar')->willReturn('bar')->shouldBeCalled(); + $nameConverterProphecy->normalize('name', Foo::class)->willReturn('name')->shouldBeCalled(); + $nameConverterProphecy->normalize('bar', Foo::class)->willReturn('bar')->shouldBeCalled(); $sortExtension = new SortExtension($resourceMetadataFactoryProphecy->reveal(), $this->prophesize(IdentifierExtractorInterface::class)->reveal(), $this->prophesize(PropertyMetadataFactoryInterface::class)->reveal(), $this->prophesize(ResourceClassResolverInterface::class)->reveal(), $nameConverterProphecy->reveal(), 'asc'); @@ -71,8 +71,8 @@ public function testApplyToCollectionWithNestedProperty() $resourceClassResolverProphecy->isResourceClass(Foo::class)->willReturn(true)->shouldBeCalled(); $nameConverterProphecy = $this->prophesize(NameConverterInterface::class); - $nameConverterProphecy->normalize('foo.bar')->willReturn('foo.bar')->shouldBeCalled(); - $nameConverterProphecy->normalize('foo')->willReturn('foo')->shouldBeCalled(); + $nameConverterProphecy->normalize('foo.bar', Foo::class)->willReturn('foo.bar')->shouldBeCalled(); + $nameConverterProphecy->normalize('foo', Foo::class)->willReturn('foo')->shouldBeCalled(); $sortExtension = new SortExtension($resourceMetadataFactoryProphecy->reveal(), $this->prophesize(IdentifierExtractorInterface::class)->reveal(), $propertyMetadataFactoryProphecy->reveal(), $resourceClassResolverProphecy->reveal(), $nameConverterProphecy->reveal(), 'asc'); @@ -88,7 +88,7 @@ public function testApplyToCollectionWithDefaultDirection() $identifierExtractorProphecy->getIdentifierFromResourceClass(Foo::class)->willReturn('id')->shouldBeCalled(); $nameConverterProphecy = $this->prophesize(NameConverterInterface::class); - $nameConverterProphecy->normalize('id')->willReturn('id')->shouldBeCalled(); + $nameConverterProphecy->normalize('id', Foo::class)->willReturn('id')->shouldBeCalled(); $sortExtension = new SortExtension($resourceMetadataFactoryProphecy->reveal(), $identifierExtractorProphecy->reveal(), $this->prophesize(PropertyMetadataFactoryInterface::class)->reveal(), $this->prophesize(ResourceClassResolverInterface::class)->reveal(), $nameConverterProphecy->reveal(), 'asc'); diff --git a/tests/Bridge/Elasticsearch/DataProvider/Filter/MatchFilterTest.php b/tests/Bridge/Elasticsearch/DataProvider/Filter/MatchFilterTest.php index 966eca00b7c..31c1e5835fa 100644 --- a/tests/Bridge/Elasticsearch/DataProvider/Filter/MatchFilterTest.php +++ b/tests/Bridge/Elasticsearch/DataProvider/Filter/MatchFilterTest.php @@ -25,6 +25,7 @@ use ApiPlatform\Core\Metadata\Property\PropertyNameCollection; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Foo; use PHPUnit\Framework\TestCase; +use Prophecy\Argument; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; use Symfony\Component\PropertyInfo\Type; use Symfony\Component\Serializer\NameConverter\NameConverterInterface; @@ -70,8 +71,8 @@ public function testApply() $propertyAccessorProphecy->getValue($foo, 'id')->willReturn(1)->shouldBeCalled(); $nameConverterProphecy = $this->prophesize(NameConverterInterface::class); - $nameConverterProphecy->normalize('id')->willReturn('id')->shouldBeCalled(); - $nameConverterProphecy->normalize('name')->willReturn('name')->shouldBeCalled(); + $nameConverterProphecy->normalize('id', Foo::class, null, Argument::type('array'))->willReturn('id')->shouldBeCalled(); + $nameConverterProphecy->normalize('name', Foo::class, null, Argument::type('array'))->willReturn('name')->shouldBeCalled(); $matchFilter = new MatchFilter( $propertyNameCollectionFactoryProphecy->reveal(), @@ -105,8 +106,8 @@ public function testApplyWithNestedProperty() $identifierExtractorProphecy->getIdentifierFromResourceClass(Foo::class)->willReturn('id')->shouldBeCalled(); $nameConverterProphecy = $this->prophesize(NameConverterInterface::class); - $nameConverterProphecy->normalize('foo.bar')->willReturn('foo.bar')->shouldBeCalled(); - $nameConverterProphecy->normalize('foo')->willReturn('foo')->shouldBeCalled(); + $nameConverterProphecy->normalize('foo.bar', Foo::class, null, Argument::type('array'))->willReturn('foo.bar')->shouldBeCalled(); + $nameConverterProphecy->normalize('foo', Foo::class, null, Argument::type('array'))->willReturn('foo')->shouldBeCalled(); $matchFilter = new MatchFilter( $this->prophesize(PropertyNameCollectionFactoryInterface::class)->reveal(), diff --git a/tests/Bridge/Elasticsearch/DataProvider/Filter/OrderFilterTest.php b/tests/Bridge/Elasticsearch/DataProvider/Filter/OrderFilterTest.php index 1622950f05d..f52a0f18378 100644 --- a/tests/Bridge/Elasticsearch/DataProvider/Filter/OrderFilterTest.php +++ b/tests/Bridge/Elasticsearch/DataProvider/Filter/OrderFilterTest.php @@ -22,6 +22,7 @@ use ApiPlatform\Core\Metadata\Property\PropertyNameCollection; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Foo; use PHPUnit\Framework\TestCase; +use Prophecy\Argument; use Symfony\Component\PropertyInfo\Type; use Symfony\Component\Serializer\NameConverter\NameConverterInterface; @@ -46,7 +47,7 @@ public function testApply() $propertyMetadataFactoryProphecy->create(Foo::class, 'name')->willReturn(new PropertyMetadata(new Type(Type::BUILTIN_TYPE_STRING)))->shouldBeCalled(); $nameConverterProphecy = $this->prophesize(NameConverterInterface::class); - $nameConverterProphecy->normalize('name')->willReturn('name')->shouldBeCalled(); + $nameConverterProphecy->normalize('name', Foo::class, null, Argument::type('array'))->willReturn('name')->shouldBeCalled(); $orderFilter = new OrderFilter( $this->prophesize(PropertyNameCollectionFactoryInterface::class)->reveal(), @@ -76,8 +77,8 @@ public function testApplyWithNestedProperty() $resourceClassResolverProphecy->isResourceClass(Foo::class)->willReturn(true)->shouldBeCalled(); $nameConverterProphecy = $this->prophesize(NameConverterInterface::class); - $nameConverterProphecy->normalize('foo.bar')->willReturn('foo.bar')->shouldBeCalled(); - $nameConverterProphecy->normalize('foo')->willReturn('foo')->shouldBeCalled(); + $nameConverterProphecy->normalize('foo.bar', Foo::class, null, Argument::type('array'))->willReturn('foo.bar')->shouldBeCalled(); + $nameConverterProphecy->normalize('foo', Foo::class, null, Argument::type('array'))->willReturn('foo')->shouldBeCalled(); $orderFilter = new OrderFilter( $this->prophesize(PropertyNameCollectionFactoryInterface::class)->reveal(), diff --git a/tests/Bridge/Elasticsearch/DataProvider/Filter/TermFilterTest.php b/tests/Bridge/Elasticsearch/DataProvider/Filter/TermFilterTest.php index 39ea9105d29..e943fa627c5 100644 --- a/tests/Bridge/Elasticsearch/DataProvider/Filter/TermFilterTest.php +++ b/tests/Bridge/Elasticsearch/DataProvider/Filter/TermFilterTest.php @@ -25,6 +25,7 @@ use ApiPlatform\Core\Metadata\Property\PropertyNameCollection; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Foo; use PHPUnit\Framework\TestCase; +use Prophecy\Argument; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; use Symfony\Component\PropertyInfo\Type; use Symfony\Component\Serializer\NameConverter\NameConverterInterface; @@ -70,8 +71,8 @@ public function testApply() $propertyAccessorProphecy->getValue($foo, 'id')->willReturn(1)->shouldBeCalled(); $nameConverterProphecy = $this->prophesize(NameConverterInterface::class); - $nameConverterProphecy->normalize('id')->willReturn('id')->shouldBeCalled(); - $nameConverterProphecy->normalize('name')->willReturn('name')->shouldBeCalled(); + $nameConverterProphecy->normalize('id', Foo::class, null, Argument::type('array'))->willReturn('id')->shouldBeCalled(); + $nameConverterProphecy->normalize('name', Foo::class, null, Argument::type('array'))->willReturn('name')->shouldBeCalled(); $termFilter = new TermFilter( $propertyNameCollectionFactoryProphecy->reveal(), @@ -105,8 +106,8 @@ public function testApplyWithNestedProperty() $identifierExtractorProphecy->getIdentifierFromResourceClass(Foo::class)->willReturn('id')->shouldBeCalled(); $nameConverterProphecy = $this->prophesize(NameConverterInterface::class); - $nameConverterProphecy->normalize('foo.bar')->willReturn('foo.bar')->shouldBeCalled(); - $nameConverterProphecy->normalize('foo')->willReturn('foo')->shouldBeCalled(); + $nameConverterProphecy->normalize('foo.bar', Foo::class, null, Argument::type('array'))->willReturn('foo.bar')->shouldBeCalled(); + $nameConverterProphecy->normalize('foo', Foo::class, null, Argument::type('array'))->willReturn('foo')->shouldBeCalled(); $termFilter = new TermFilter( $this->prophesize(PropertyNameCollectionFactoryInterface::class)->reveal(), diff --git a/tests/Bridge/Elasticsearch/Serializer/NameConverter/InnerFieldsNameConverterTest.php b/tests/Bridge/Elasticsearch/Serializer/NameConverter/InnerFieldsNameConverterTest.php index 3485d085ceb..3bebcb20eb0 100644 --- a/tests/Bridge/Elasticsearch/Serializer/NameConverter/InnerFieldsNameConverterTest.php +++ b/tests/Bridge/Elasticsearch/Serializer/NameConverter/InnerFieldsNameConverterTest.php @@ -15,23 +15,23 @@ use ApiPlatform\Core\Bridge\Elasticsearch\Serializer\NameConverter\InnerFieldsNameConverter; use PHPUnit\Framework\TestCase; -use Symfony\Component\Serializer\NameConverter\NameConverterInterface; +use Symfony\Component\Serializer\NameConverter\AdvancedNameConverterInterface; class InnerFieldsNameConverterTest extends TestCase { public function testConstruct() { self::assertInstanceOf( - NameConverterInterface::class, - new InnerFieldsNameConverter($this->prophesize(NameConverterInterface::class)->reveal()) + AdvancedNameConverterInterface::class, + new InnerFieldsNameConverter($this->prophesize(AdvancedNameConverterInterface::class)->reveal()) ); } public function testNormalize() { - $decoratedProphecy = $this->prophesize(NameConverterInterface::class); - $decoratedProphecy->normalize('fooBar')->willReturn('foo_bar')->shouldBeCalled(); - $decoratedProphecy->normalize('bazQux')->willReturn('baz_qux')->shouldBeCalled(); + $decoratedProphecy = $this->prophesize(AdvancedNameConverterInterface::class); + $decoratedProphecy->normalize('fooBar', null, null, [])->willReturn('foo_bar')->shouldBeCalled(); + $decoratedProphecy->normalize('bazQux', null, null, [])->willReturn('baz_qux')->shouldBeCalled(); $innerFieldsNameConverter = new InnerFieldsNameConverter($decoratedProphecy->reveal()); @@ -40,9 +40,9 @@ public function testNormalize() public function testDenormalize() { - $decoratedProphecy = $this->prophesize(NameConverterInterface::class); - $decoratedProphecy->denormalize('foo_bar')->willReturn('fooBar')->shouldBeCalled(); - $decoratedProphecy->denormalize('baz_qux')->willReturn('bazQux')->shouldBeCalled(); + $decoratedProphecy = $this->prophesize(AdvancedNameConverterInterface::class); + $decoratedProphecy->denormalize('foo_bar', null, null, [])->willReturn('fooBar')->shouldBeCalled(); + $decoratedProphecy->denormalize('baz_qux', null, null, [])->willReturn('bazQux')->shouldBeCalled(); $innerFieldsNameConverter = new InnerFieldsNameConverter($decoratedProphecy->reveal()); diff --git a/tests/Bridge/NelmioApiDoc/Parser/ApiPlatformParserTest.php b/tests/Bridge/NelmioApiDoc/Parser/ApiPlatformParserTest.php index 2a180420498..9472ef33851 100644 --- a/tests/Bridge/NelmioApiDoc/Parser/ApiPlatformParserTest.php +++ b/tests/Bridge/NelmioApiDoc/Parser/ApiPlatformParserTest.php @@ -439,7 +439,7 @@ public function testParseWithNameConverter() $propertyMetadataFactory = $propertyMetadataFactoryProphecy->reveal(); $nameConverterProphecy = $this->prophesize(NameConverterInterface::class); - $nameConverterProphecy->normalize('nameConverted')->willReturn('name_converted')->shouldBeCalled(); + $nameConverterProphecy->normalize('nameConverted', Dummy::class)->willReturn('name_converted')->shouldBeCalled(); $nameConverter = $nameConverterProphecy->reveal(); $apiPlatformParser = new ApiPlatformParser($resourceMetadataFactory, $propertyNameCollectionFactory, $propertyMetadataFactory, $nameConverter); diff --git a/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php b/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php index 0dc3c51934f..02671cfb87e 100644 --- a/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php +++ b/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php @@ -203,24 +203,6 @@ public function testPrependWhenNotEnabled() $this->extension->prepend($containerBuilder); } - public function testPrependWhenNameConverterIsConfigured() - { - $containerBuilderProphecy = $this->prophesize(ContainerBuilder::class); - $containerBuilderProphecy->getExtensionConfig('framework')->willReturn([0 => ['serializer' => ['enabled' => true, 'name_converter' => 'foo'], 'property_info' => ['enabled' => false]]]); - $containerBuilderProphecy->prependExtensionConfig('api_platform', ['name_converter' => 'foo'])->shouldBeCalled(); - - $this->extension->prepend($containerBuilderProphecy->reveal()); - } - - public function testNotPrependWhenNameConverterIsNotConfigured() - { - $containerBuilderProphecy = $this->prophesize(ContainerBuilder::class); - $containerBuilderProphecy->getExtensionConfig('framework')->willReturn([0 => ['serializer' => ['enabled' => true], 'property_info' => ['enabled' => false]]])->shouldBeCalled(); - $containerBuilderProphecy->prependExtensionConfig('api_platform', Argument::type('array'))->shouldNotBeCalled(); - - $this->extension->prepend($containerBuilderProphecy->reveal()); - } - public function testLoadDefaultConfig() { $containerBuilderProphecy = $this->getBaseContainerBuilderProphecy(); diff --git a/tests/Bridge/Symfony/Bundle/DependencyInjection/Compiler/MetadataAwareNameConverterPassTest.php b/tests/Bridge/Symfony/Bundle/DependencyInjection/Compiler/MetadataAwareNameConverterPassTest.php new file mode 100644 index 00000000000..52b5fa69f47 --- /dev/null +++ b/tests/Bridge/Symfony/Bundle/DependencyInjection/Compiler/MetadataAwareNameConverterPassTest.php @@ -0,0 +1,64 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Core\Tests\Bridge\Symfony\Bundle\DependencyInjection\Compiler; + +use ApiPlatform\Core\Bridge\Symfony\Bundle\DependencyInjection\Compiler\MetadataAwareNameConverterPass; +use PHPUnit\Framework\TestCase; +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; + +/** + * @author Antoine Bluchet + */ +class MetadataAwareNameConverterPassTest extends TestCase +{ + public function testProcess() + { + $pass = new MetadataAwareNameConverterPass(); + $this->assertInstanceOf(CompilerPassInterface::class, $pass); + + $containerBuilderProphecy = $this->prophesize(ContainerBuilder::class); + $containerBuilderProphecy->hasAlias('api_platform.name_converter')->shouldBeCalled()->willReturn(false); + $containerBuilderProphecy->hasDefinition('serializer.name_converter.metadata_aware')->shouldBeCalled()->willReturn(true); + $containerBuilderProphecy->setAlias('api_platform.name_converter', 'serializer.name_converter.metadata_aware')->shouldBeCalled(); + + $pass->process($containerBuilderProphecy->reveal()); + } + + public function testProcessWithNameConverter() + { + $pass = new MetadataAwareNameConverterPass(); + $this->assertInstanceOf(CompilerPassInterface::class, $pass); + + $containerBuilderProphecy = $this->prophesize(ContainerBuilder::class); + $containerBuilderProphecy->hasAlias('api_platform.name_converter')->shouldBeCalled()->willReturn(true); + $containerBuilderProphecy->hasDefinition('serializer.name_converter.metadata_aware')->shouldNotBeCalled()->willReturn(true); + $containerBuilderProphecy->setAlias('api_platform.name_converter', 'serializer.name_converter.metadata_aware')->shouldNotBeCalled(); + + $pass->process($containerBuilderProphecy->reveal()); + } + + public function testProcessWithoutMetadataAwareDefinition() + { + $pass = new MetadataAwareNameConverterPass(); + $this->assertInstanceOf(CompilerPassInterface::class, $pass); + + $containerBuilderProphecy = $this->prophesize(ContainerBuilder::class); + $containerBuilderProphecy->hasAlias('api_platform.name_converter')->shouldBeCalled()->willReturn(false); + $containerBuilderProphecy->hasDefinition('serializer.name_converter.metadata_aware')->shouldBeCalled()->willReturn(false); + $containerBuilderProphecy->setAlias('api_platform.name_converter', 'serializer.name_converter.metadata_aware')->shouldNotBeCalled(); + + $pass->process($containerBuilderProphecy->reveal()); + } +} diff --git a/tests/Hydra/Serializer/ConstraintViolationNormalizerTest.php b/tests/Hydra/Serializer/ConstraintViolationNormalizerTest.php index 7f99bc5e566..f381974a33e 100644 --- a/tests/Hydra/Serializer/ConstraintViolationNormalizerTest.php +++ b/tests/Hydra/Serializer/ConstraintViolationNormalizerTest.php @@ -46,7 +46,7 @@ public function testNormalize() $nameConverterProphecy = $this->prophesize(NameConverterInterface::class); $urlGeneratorProphecy->generate('api_jsonld_context', ['shortName' => 'ConstraintViolationList'])->willReturn('/context/foo')->shouldBeCalled(); - $nameConverterProphecy->normalize(Argument::type('string'))->will(function ($args) { + $nameConverterProphecy->normalize(Argument::type('string'), null, Argument::type('string'))->will(function ($args) { return '_'.$args[0]; }); diff --git a/tests/JsonApi/Serializer/ConstraintViolationNormalizerTest.php b/tests/JsonApi/Serializer/ConstraintViolationNormalizerTest.php index 69691af5763..3534cd49621 100644 --- a/tests/JsonApi/Serializer/ConstraintViolationNormalizerTest.php +++ b/tests/JsonApi/Serializer/ConstraintViolationNormalizerTest.php @@ -49,8 +49,8 @@ public function testNormalize() $propertyMetadataFactoryProphecy->create(Dummy::class, 'name')->willReturn(new PropertyMetadata(new Type(Type::BUILTIN_TYPE_STRING)))->shouldBeCalled(1); $nameConverterProphecy = $this->prophesize(NameConverterInterface::class); - $nameConverterProphecy->normalize('relatedDummy')->willReturn('relatedDummy')->shouldBeCalled(1); - $nameConverterProphecy->normalize('name')->willReturn('name')->shouldBeCalled(1); + $nameConverterProphecy->normalize('relatedDummy', Dummy::class, 'jsonapi')->willReturn('relatedDummy')->shouldBeCalled(1); + $nameConverterProphecy->normalize('name', Dummy::class, 'jsonapi')->willReturn('name')->shouldBeCalled(1); $dummy = new Dummy(); diff --git a/tests/Problem/Serializer/ConstraintViolationNormalizerTest.php b/tests/Problem/Serializer/ConstraintViolationNormalizerTest.php index 3861d7d11b7..3a684b238fb 100644 --- a/tests/Problem/Serializer/ConstraintViolationNormalizerTest.php +++ b/tests/Problem/Serializer/ConstraintViolationNormalizerTest.php @@ -42,7 +42,7 @@ public function testNormalize() $nameConverterProphecy = $this->prophesize(NameConverterInterface::class); $normalizer = new ConstraintViolationListNormalizer(['severity', 'anotherField1'], $nameConverterProphecy->reveal()); - $nameConverterProphecy->normalize(Argument::type('string'))->will(function ($args) { + $nameConverterProphecy->normalize(Argument::type('string'), null, Argument::type('string'))->will(function ($args) { return '_'.$args[0]; }); From f91d3d8d75870e54bd74e9ea500b7fa29d92b461 Mon Sep 17 00:00:00 2001 From: Alan Poulain Date: Wed, 20 Feb 2019 12:25:09 +0100 Subject: [PATCH 10/15] Allow limit 0 for MongoDB --- features/hydra/collection.feature | 1 - .../Extension/PaginationExtension.php | 12 +++++++--- src/Bridge/Doctrine/MongoDbOdm/Paginator.php | 24 ++++++++++++++++++- .../Extension/PaginationExtensionTest.php | 16 +++++++++---- .../Doctrine/MongoDbOdm/PaginatorTest.php | 13 ++++++++-- 5 files changed, 54 insertions(+), 12 deletions(-) diff --git a/features/hydra/collection.feature b/features/hydra/collection.feature index a18353f2104..b6f2e832131 100644 --- a/features/hydra/collection.feature +++ b/features/hydra/collection.feature @@ -396,7 +396,6 @@ Feature: Collections support } """ - @!mongodb @createSchema Scenario: Allow passing 0 to `itemsPerPage` When I send a "GET" request to "/dummies?itemsPerPage=0" diff --git a/src/Bridge/Doctrine/MongoDbOdm/Extension/PaginationExtension.php b/src/Bridge/Doctrine/MongoDbOdm/Extension/PaginationExtension.php index 26770b2fe6a..6f715bfcd3c 100644 --- a/src/Bridge/Doctrine/MongoDbOdm/Extension/PaginationExtension.php +++ b/src/Bridge/Doctrine/MongoDbOdm/Extension/PaginationExtension.php @@ -64,12 +64,18 @@ public function applyToCollection(Builder $aggregationBuilder, string $resourceC throw new RuntimeException(sprintf('The repository for "%s" must be an instance of "%s".', $resourceClass, DocumentRepository::class)); } + $resultsAggregationBuilder = $repository->createAggregationBuilder()->skip($offset); + if ($limit > 0) { + $resultsAggregationBuilder->limit($limit); + } else { + // Results have to be 0 but MongoDB does not support a limit equal to 0. + $resultsAggregationBuilder->match()->field(Paginator::LIMIT_ZERO_MARKER_FIELD)->equals(Paginator::LIMIT_ZERO_MARKER); + } + $aggregationBuilder ->facet() ->field('results')->pipeline( - $repository->createAggregationBuilder() - ->skip($offset) - ->limit($limit) + $resultsAggregationBuilder ) ->field('count')->pipeline( $repository->createAggregationBuilder() diff --git a/src/Bridge/Doctrine/MongoDbOdm/Paginator.php b/src/Bridge/Doctrine/MongoDbOdm/Paginator.php index 850d82e34e5..d525d3eb275 100644 --- a/src/Bridge/Doctrine/MongoDbOdm/Paginator.php +++ b/src/Bridge/Doctrine/MongoDbOdm/Paginator.php @@ -28,6 +28,9 @@ */ final class Paginator implements \IteratorAggregate, PaginatorInterface { + public const LIMIT_ZERO_MARKER_FIELD = '___'; + public const LIMIT_ZERO_MARKER = 'limit0'; + /** * @var Iterator */ @@ -76,7 +79,7 @@ public function __construct(Iterator $mongoDbOdmIterator, UnitOfWork $unitOfWork * skip/limit parameters of the query, the values set in the facet stage are used instead. */ $this->firstResult = $this->getStageInfo($resultsFacetInfo, '$skip'); - $this->maxResults = $this->getStageInfo($resultsFacetInfo, '$limit'); + $this->maxResults = $this->hasLimitZeroStage($resultsFacetInfo) ? 0 : $this->getStageInfo($resultsFacetInfo, '$limit'); $this->totalItems = $mongoDbOdmIterator->toArray()[0]['count'][0]['count'] ?? 0; } @@ -85,6 +88,10 @@ public function __construct(Iterator $mongoDbOdmIterator, UnitOfWork $unitOfWork */ public function getCurrentPage(): float { + if (0 >= $this->maxResults) { + return 1.; + } + return floor($this->firstResult / $this->maxResults) + 1.; } @@ -93,6 +100,10 @@ public function getCurrentPage(): float */ public function getLastPage(): float { + if (0 >= $this->maxResults) { + return 1.; + } + return ceil($this->totalItems / $this->maxResults) ?: 1.; } @@ -161,4 +172,15 @@ private function getStageInfo(array $resultsFacetInfo, string $stage): int throw new InvalidArgumentException("$stage stage was not applied to the facet stage of the aggregation pipeline."); } + + private function hasLimitZeroStage(array $resultsFacetInfo): bool + { + foreach ($resultsFacetInfo as $resultFacetInfo) { + if (self::LIMIT_ZERO_MARKER === ($resultFacetInfo['$match'][self::LIMIT_ZERO_MARKER_FIELD] ?? null)) { + return true; + } + } + + return false; + } } diff --git a/tests/Bridge/Doctrine/MongoDbOdm/Extension/PaginationExtensionTest.php b/tests/Bridge/Doctrine/MongoDbOdm/Extension/PaginationExtensionTest.php index a8fbb9b1b8d..8f0379d318f 100644 --- a/tests/Bridge/Doctrine/MongoDbOdm/Extension/PaginationExtensionTest.php +++ b/tests/Bridge/Doctrine/MongoDbOdm/Extension/PaginationExtensionTest.php @@ -14,6 +14,7 @@ namespace ApiPlatform\Core\Tests\Bridge\Doctrine\MongoDbOdm\Extension; use ApiPlatform\Core\Bridge\Doctrine\MongoDbOdm\Extension\PaginationExtension; +use ApiPlatform\Core\Bridge\Doctrine\MongoDbOdm\Paginator; use ApiPlatform\Core\DataProvider\Pagination; use ApiPlatform\Core\DataProvider\PaginatorInterface; use ApiPlatform\Core\DataProvider\PartialPaginatorInterface; @@ -24,7 +25,7 @@ use Doctrine\ODM\MongoDB\Aggregation\Builder; use Doctrine\ODM\MongoDB\Aggregation\Stage\Count; use Doctrine\ODM\MongoDB\Aggregation\Stage\Facet; -use Doctrine\ODM\MongoDB\Aggregation\Stage\Limit; +use Doctrine\ODM\MongoDB\Aggregation\Stage\Match; use Doctrine\ODM\MongoDB\Aggregation\Stage\Skip; use Doctrine\ODM\MongoDB\DocumentManager; use Doctrine\ODM\MongoDB\Iterator\Iterator; @@ -391,10 +392,15 @@ private function getPaginationExtensionResult() private function mockAggregationBuilder($expectedOffset, $expectedLimit) { - $limitProphecy = $this->prophesize(Limit::class); - $skipProphecy = $this->prophesize(Skip::class); - $skipProphecy->limit($expectedLimit)->shouldBeCalled()->willReturn($limitProphecy->reveal()); + if ($expectedLimit > 0) { + $skipProphecy->limit($expectedLimit)->shouldBeCalled(); + } else { + $matchProphecy = $this->prophesize(Match::class); + $matchProphecy->field(Paginator::LIMIT_ZERO_MARKER_FIELD)->shouldBeCalled()->willReturn($matchProphecy); + $matchProphecy->equals(Paginator::LIMIT_ZERO_MARKER)->shouldBeCalled(); + $skipProphecy->match()->shouldBeCalled()->willReturn($matchProphecy->reveal()); + } $resultsAggregationBuilderProphecy = $this->prophesize(Builder::class); $resultsAggregationBuilderProphecy->skip($expectedOffset)->shouldBeCalled()->willReturn($skipProphecy->reveal()); @@ -416,7 +422,7 @@ private function mockAggregationBuilder($expectedOffset, $expectedLimit) $this->managerRegistryProphecy->getManagerForClass('Foo')->shouldBeCalled()->willReturn($objectManagerProphecy->reveal()); $facetProphecy = $this->prophesize(Facet::class); - $facetProphecy->pipeline($limitProphecy)->shouldBeCalled()->willReturn($facetProphecy); + $facetProphecy->pipeline($skipProphecy)->shouldBeCalled()->willReturn($facetProphecy); $facetProphecy->pipeline($countProphecy)->shouldBeCalled()->willReturn($facetProphecy); $facetProphecy->field('count')->shouldBeCalled()->willReturn($facetProphecy); $facetProphecy->field('results')->shouldBeCalled()->willReturn($facetProphecy); diff --git a/tests/Bridge/Doctrine/MongoDbOdm/PaginatorTest.php b/tests/Bridge/Doctrine/MongoDbOdm/PaginatorTest.php index ea197fbbdd5..2d6277e9321 100644 --- a/tests/Bridge/Doctrine/MongoDbOdm/PaginatorTest.php +++ b/tests/Bridge/Doctrine/MongoDbOdm/PaginatorTest.php @@ -74,6 +74,15 @@ public function testInitializeWithLimitStageNotApplied() $this->getPaginatorWithMissingStage(true, true, true, true); } + public function testInitializeWithLimitZeroStageApplied() + { + $paginator = $this->getPaginator(0, 5, 0, true); + + $this->assertEquals(1, $paginator->getCurrentPage()); + $this->assertEquals(1, $paginator->getLastPage()); + $this->assertEquals(0, $paginator->getItemsPerPage()); + } + public function testInitializeWithNoCount() { $paginator = $this->getPaginatorWithNoCount(); @@ -90,7 +99,7 @@ public function testGetIterator() $this->assertSame($paginator->getIterator(), $paginator->getIterator(), 'Iterator should be cached'); } - private function getPaginator($firstResult = 1, $maxResults = 15, $totalItems = 42) + private function getPaginator($firstResult = 1, $maxResults = 15, $totalItems = 42, $limitZero = false) { $iterator = $this->prophesize(Iterator::class); $pipeline = [ @@ -98,7 +107,7 @@ private function getPaginator($firstResult = 1, $maxResults = 15, $totalItems = '$facet' => [ 'results' => [ ['$skip' => $firstResult], - ['$limit' => $maxResults], + $limitZero ? ['$match' => [Paginator::LIMIT_ZERO_MARKER_FIELD => Paginator::LIMIT_ZERO_MARKER]] : ['$limit' => $maxResults], ], 'count' => [ ['$count' => 'count'], From 6e9e9072acd72fb56db63abaa608aaf3cd32c049 Mon Sep 17 00:00:00 2001 From: Ben Davies Date: Fri, 1 Feb 2019 10:50:10 +0000 Subject: [PATCH 11/15] add feature reproducing bug --- features/main/crud_abstract.feature | 40 +++++++++++++++---- .../TestBundle/Document/AbstractDummy.php | 10 ++++- .../TestBundle/Entity/AbstractDummy.php | 9 ++++- .../config/serialization/abstract_dummy.yml | 11 +++++ 4 files changed, 60 insertions(+), 10 deletions(-) create mode 100644 tests/Fixtures/TestBundle/Resources/config/serialization/abstract_dummy.yml diff --git a/features/main/crud_abstract.feature b/features/main/crud_abstract.feature index c7c15a31821..9cfb62e57ed 100644 --- a/features/main/crud_abstract.feature +++ b/features/main/crud_abstract.feature @@ -80,8 +80,8 @@ Feature: Create-Retrieve-Update-Delete on abstract resource """ Scenario: Update a concrete resource - When I add "Content-Type" header equal to "application/ld+json" - And I send a "PUT" request to "/concrete_dummies/1" with body: + When I add "Content-Type" header equal to "application/ld+json" + And I send a "PUT" request to "/concrete_dummies/1" with body: """ { "@id": "/concrete_dummies/1", @@ -89,11 +89,11 @@ Feature: Create-Retrieve-Update-Delete on abstract resource "name": "A nice dummy" } """ - Then the response status code should be 200 - And the response should be in JSON - And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" - And the header "Content-Location" should be equal to "/concrete_dummies/1" - And the JSON should be equal to: + Then the response status code should be 200 + And the response should be in JSON + And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the header "Content-Location" should be equal to "/concrete_dummies/1" + And the JSON should be equal to: """ { "@context": "/contexts/ConcreteDummy", @@ -105,6 +105,32 @@ Feature: Create-Retrieve-Update-Delete on abstract resource } """ + Scenario: Update a concrete resource using abstract resource uri + When I add "Content-Type" header equal to "application/ld+json" + And I send a "PUT" request to "/abstract_dummies/1" with body: + """ + { + "@id": "/concrete_dummies/1", + "instance": "Become surreal", + "name": "A nicer dummy" + } + """ + Then the response status code should be 200 + And the response should be in JSON + And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the header "Content-Location" should be equal to "/concrete_dummies/1" + And the JSON should be equal to: + """ + { + "@context": "/contexts/ConcreteDummy", + "@id": "/concrete_dummies/1", + "@type": "ConcreteDummy", + "instance": "Become surreal", + "id": 1, + "name": "A nicer dummy" + } + """ + Scenario: Delete a resource When I send a "DELETE" request to "/abstract_dummies/1" Then the response status code should be 204 diff --git a/tests/Fixtures/TestBundle/Document/AbstractDummy.php b/tests/Fixtures/TestBundle/Document/AbstractDummy.php index 0d3fc8b73b2..2bd332d19c9 100644 --- a/tests/Fixtures/TestBundle/Document/AbstractDummy.php +++ b/tests/Fixtures/TestBundle/Document/AbstractDummy.php @@ -22,8 +22,14 @@ * Abstract Dummy. * * @author Jérémy Derussé - * - * @ApiResource(attributes={"filters"={"my_dummy.mongodb.search", "my_dummy.mongodb.order", "my_dummy.mongodb.date"}}) + * @ApiResource( + * itemOperations={ + * "get", + * "put", + * "delete", + * }, + * attributes={"filters"={"my_dummy.mongodb.search", "my_dummy.mongodb.order", "my_dummy.mongodb.date"}} + * ) * @ODM\Document * @ODM\InheritanceType("SINGLE_COLLECTION") * @ODM\DiscriminatorField(value="discr") diff --git a/tests/Fixtures/TestBundle/Entity/AbstractDummy.php b/tests/Fixtures/TestBundle/Entity/AbstractDummy.php index 0640dacfcd9..51e19638246 100644 --- a/tests/Fixtures/TestBundle/Entity/AbstractDummy.php +++ b/tests/Fixtures/TestBundle/Entity/AbstractDummy.php @@ -23,7 +23,14 @@ * * @author Jérémy Derussé * - * @ApiResource(attributes={"filters"={"my_dummy.search", "my_dummy.order", "my_dummy.date"}}) + * @ApiResource( + * itemOperations={ + * "get", + * "put", + * "delete", + * }, + * attributes={"filters"={"my_dummy.search", "my_dummy.order", "my_dummy.date"}} + * ) * @ORM\Entity * @ORM\InheritanceType("SINGLE_TABLE") * @ORM\DiscriminatorColumn(name="discr", type="string", length=16) diff --git a/tests/Fixtures/TestBundle/Resources/config/serialization/abstract_dummy.yml b/tests/Fixtures/TestBundle/Resources/config/serialization/abstract_dummy.yml new file mode 100644 index 00000000000..a6f77e6325f --- /dev/null +++ b/tests/Fixtures/TestBundle/Resources/config/serialization/abstract_dummy.yml @@ -0,0 +1,11 @@ +ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\AbstractDummy: + discriminator_map: + type_property: discr + mapping: + concrete: 'ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\ConcreteDummy' + +ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\AbstractDummy: + discriminator_map: + type_property: discr + mapping: + concrete: 'ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\ConcreteDummy' \ No newline at end of file From 4a79bdda232e9ae54ee6262a9fe1743853fe9bf6 Mon Sep 17 00:00:00 2001 From: Ben Davies Date: Fri, 1 Feb 2019 10:50:30 +0000 Subject: [PATCH 12/15] do not try to infer discriminator mapping if object to populate is already populated --- src/Serializer/AbstractItemNormalizer.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Serializer/AbstractItemNormalizer.php b/src/Serializer/AbstractItemNormalizer.php index b4beb8fd4bc..68257a77db4 100644 --- a/src/Serializer/AbstractItemNormalizer.php +++ b/src/Serializer/AbstractItemNormalizer.php @@ -185,6 +185,12 @@ public function denormalize($data, $class, $format = null, array $context = []) */ protected function instantiateObject(array &$data, $class, array &$context, \ReflectionClass $reflectionClass, $allowedAttributes, string $format = null) { + if (null !== $object = $this->extractObjectToPopulate($class, $context, static::OBJECT_TO_POPULATE)) { + unset($context[static::OBJECT_TO_POPULATE]); + + return $object; + } + if ($this->classDiscriminatorResolver && $mapping = $this->classDiscriminatorResolver->getMappingForClass($class)) { if (!isset($data[$mapping->getTypeProperty()])) { throw new RuntimeException(sprintf('Type property "%s" not found for the abstract object "%s"', $mapping->getTypeProperty(), $class)); @@ -199,12 +205,6 @@ protected function instantiateObject(array &$data, $class, array &$context, \Ref $reflectionClass = new \ReflectionClass($class); } - if (null !== $object = $this->extractObjectToPopulate($class, $context, static::OBJECT_TO_POPULATE)) { - unset($context[static::OBJECT_TO_POPULATE]); - - return $object; - } - $constructor = $this->getConstructor($data, $class, $context, $reflectionClass, $allowedAttributes); if ($constructor) { $constructorParameters = $constructor->getParameters(); From 2ab5c4f94414bfc72ede480c317f1d5f30c1d76d Mon Sep 17 00:00:00 2001 From: Ben Davies Date: Fri, 1 Feb 2019 10:55:29 +0000 Subject: [PATCH 13/15] always try to provide a _api_write_item_iri/Content-Location for controller results --- src/EventListener/WriteListener.php | 6 +----- tests/EventListener/WriteListenerTest.php | 9 ++++----- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/src/EventListener/WriteListener.php b/src/EventListener/WriteListener.php index 2b85fbaef07..078245f0f75 100644 --- a/src/EventListener/WriteListener.php +++ b/src/EventListener/WriteListener.php @@ -65,9 +65,6 @@ public function onKernelView(GetResponseForControllerResultEvent $event) $event->setControllerResult($persistResult ?? $controllerResult); - // Controller result must be immutable for _api_write_item_iri - // if it's class changed compared to the base class let's avoid calling the IriConverter - // especially that the Output class could be a DTO that's not referencing any route if (null === $this->iriConverter) { return; } @@ -79,8 +76,7 @@ public function onKernelView(GetResponseForControllerResultEvent $event) $hasOutput = \array_key_exists('class', $outputMetadata) && null !== $outputMetadata['class']; } - $class = \get_class($controllerResult); - if ($hasOutput && $attributes['resource_class'] === $class && $class === \get_class($event->getControllerResult())) { + if ($hasOutput) { $request->attributes->set('_api_write_item_iri', $this->iriConverter->getIriFromItem($controllerResult)); } break; diff --git a/tests/EventListener/WriteListenerTest.php b/tests/EventListener/WriteListenerTest.php index 794f15ee2fc..ddbda5aa4b5 100644 --- a/tests/EventListener/WriteListenerTest.php +++ b/tests/EventListener/WriteListenerTest.php @@ -18,9 +18,8 @@ use ApiPlatform\Core\EventListener\WriteListener; use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; use ApiPlatform\Core\Metadata\Resource\ResourceMetadata; +use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\ConcreteDummy; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Dummy; -use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyTableInheritance; -use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyTableInheritanceChild; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent; @@ -270,16 +269,16 @@ public function testOnKernelViewWithNoResourceClass() public function testOnKernelViewWithParentResourceClass() { - $dummy = new DummyTableInheritanceChild(); + $dummy = new ConcreteDummy(); $dataPersisterProphecy = $this->prophesize(DataPersisterInterface::class); $dataPersisterProphecy->supports($dummy)->willReturn(true)->shouldBeCalled(); $dataPersisterProphecy->persist($dummy)->willReturn($dummy)->shouldBeCalled(); $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); - $iriConverterProphecy->getIriFromItem($dummy)->shouldNotBeCalled(); + $iriConverterProphecy->getIriFromItem($dummy)->willReturn('/dummy/1')->shouldBeCalled(); - $request = new Request([], [], ['_api_resource_class' => DummyTableInheritance::class, '_api_item_operation_name' => 'put', '_api_persist' => true]); + $request = new Request([], [], ['_api_resource_class' => ConcreteDummy::class, '_api_item_operation_name' => 'put', '_api_persist' => true]); $request->setMethod('PUT'); $event = new GetResponseForControllerResultEvent( From 7dd4428094c9871de677fcbe402ce2ca6c9f1f55 Mon Sep 17 00:00:00 2001 From: Ben Davies Date: Fri, 1 Feb 2019 11:06:09 +0000 Subject: [PATCH 14/15] add further coverage for creating abstract resources with a discriminator --- features/main/crud_abstract.feature | 27 +++++++++++++++++++ .../TestBundle/Document/AbstractDummy.php | 7 ++--- .../TestBundle/Entity/AbstractDummy.php | 7 ++--- 3 files changed, 31 insertions(+), 10 deletions(-) diff --git a/features/main/crud_abstract.feature b/features/main/crud_abstract.feature index 9cfb62e57ed..33782d5e71a 100644 --- a/features/main/crud_abstract.feature +++ b/features/main/crud_abstract.feature @@ -135,3 +135,30 @@ Feature: Create-Retrieve-Update-Delete on abstract resource When I send a "DELETE" request to "/abstract_dummies/1" Then the response status code should be 204 And the response should be empty + + Scenario: Create a concrete resource with discriminator + When I add "Content-Type" header equal to "application/ld+json" + And I send a "POST" request to "/abstract_dummies" with body: + """ + { + "discr": "concrete", + "instance": "Concrete", + "name": "My Dummy" + } + """ + Then the response status code should be 201 + And the response should be in JSON + And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the header "Content-Location" should be equal to "/concrete_dummies/2" + And the header "Location" should be equal to "/concrete_dummies/2" + And the JSON should be equal to: + """ + { + "@context": "/contexts/ConcreteDummy", + "@id": "/concrete_dummies/2", + "@type": "ConcreteDummy", + "instance": "Concrete", + "id": 2, + "name": "My Dummy" + } + """ diff --git a/tests/Fixtures/TestBundle/Document/AbstractDummy.php b/tests/Fixtures/TestBundle/Document/AbstractDummy.php index 2bd332d19c9..decc0ba9c21 100644 --- a/tests/Fixtures/TestBundle/Document/AbstractDummy.php +++ b/tests/Fixtures/TestBundle/Document/AbstractDummy.php @@ -23,11 +23,8 @@ * * @author Jérémy Derussé * @ApiResource( - * itemOperations={ - * "get", - * "put", - * "delete", - * }, + * collectionOperations={"get", "post"}, + * itemOperations={"get", "put", "delete"}, * attributes={"filters"={"my_dummy.mongodb.search", "my_dummy.mongodb.order", "my_dummy.mongodb.date"}} * ) * @ODM\Document diff --git a/tests/Fixtures/TestBundle/Entity/AbstractDummy.php b/tests/Fixtures/TestBundle/Entity/AbstractDummy.php index 51e19638246..04b041d07cc 100644 --- a/tests/Fixtures/TestBundle/Entity/AbstractDummy.php +++ b/tests/Fixtures/TestBundle/Entity/AbstractDummy.php @@ -24,11 +24,8 @@ * @author Jérémy Derussé * * @ApiResource( - * itemOperations={ - * "get", - * "put", - * "delete", - * }, + * collectionOperations={"get", "post"}, + * itemOperations={"get", "put", "delete"}, * attributes={"filters"={"my_dummy.search", "my_dummy.order", "my_dummy.date"}} * ) * @ORM\Entity From 5f5dfa285b90ebdc3ee1c077328997edb2ee0464 Mon Sep 17 00:00:00 2001 From: Ben Davies Date: Mon, 25 Feb 2019 16:35:06 +0000 Subject: [PATCH 15/15] work around deps=low sqlite doctrine bug: https://github.com/doctrine/dbal/pull/3141/files --- features/main/crud_abstract.feature | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/features/main/crud_abstract.feature b/features/main/crud_abstract.feature index 33782d5e71a..87d6ca2578f 100644 --- a/features/main/crud_abstract.feature +++ b/features/main/crud_abstract.feature @@ -136,6 +136,7 @@ Feature: Create-Retrieve-Update-Delete on abstract resource Then the response status code should be 204 And the response should be empty + @createSchema Scenario: Create a concrete resource with discriminator When I add "Content-Type" header equal to "application/ld+json" And I send a "POST" request to "/abstract_dummies" with body: @@ -149,16 +150,16 @@ Feature: Create-Retrieve-Update-Delete on abstract resource Then the response status code should be 201 And the response should be in JSON And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" - And the header "Content-Location" should be equal to "/concrete_dummies/2" - And the header "Location" should be equal to "/concrete_dummies/2" + And the header "Content-Location" should be equal to "/concrete_dummies/1" + And the header "Location" should be equal to "/concrete_dummies/1" And the JSON should be equal to: """ { "@context": "/contexts/ConcreteDummy", - "@id": "/concrete_dummies/2", + "@id": "/concrete_dummies/1", "@type": "ConcreteDummy", "instance": "Concrete", - "id": 2, + "id": 1, "name": "My Dummy" } """