From e3d26c9bafc77126516f0450e4be57d3a3f68141 Mon Sep 17 00:00:00 2001 From: Antoine Bluchet Date: Fri, 29 Mar 2024 13:16:50 +0100 Subject: [PATCH 1/6] test: decorate name converter as symfony interface changed (#6261) * test: decorate name converter as symfony interface changed * test --- .github/workflows/guides.yaml | 4 +++- docs/composer.json | 6 +++--- docs/config/packages/framework.yaml | 2 -- .../Serializer/NameConverter/CustomConverter.php | 14 +++++++++++--- .../Serializer/NameConverter/CustomConverter.php | 14 +++++++++++--- .../Serializer/NameConverter/CustomConverter.php | 14 +++++++++++--- 6 files changed, 39 insertions(+), 15 deletions(-) diff --git a/.github/workflows/guides.yaml b/.github/workflows/guides.yaml index 41413e23626..1b2b483b915 100644 --- a/.github/workflows/guides.yaml +++ b/.github/workflows/guides.yaml @@ -42,7 +42,9 @@ jobs: restore-keys: ${{ runner.os }}-composer- - name: Install project dependencies working-directory: docs - run: composer install --no-interaction --no-progress --ansi && composer require webonyx/graphql-php + run: | + composer update --no-interaction --no-progress --ansi + cp -r ../src ./vendor/api-platform/core/ - name: Test guides working-directory: docs env: diff --git a/docs/composer.json b/docs/composer.json index 9a376542c2d..65b7f198709 100644 --- a/docs/composer.json +++ b/docs/composer.json @@ -34,7 +34,8 @@ "zenstruck/foundry": "^1.31", "symfony/http-client": "^6.4 || ^7.0", "symfony/browser-kit": "^6.4 || ^7.0", - "justinrainbow/json-schema": "^5.2" + "justinrainbow/json-schema": "^5.2", + "webonyx/graphql-php": "^15.11" }, "config": { "allow-plugins": { @@ -43,6 +44,5 @@ }, "require-dev": { "phpunit/phpunit": "^10" - }, - "minimum-stability": "dev" + } } diff --git a/docs/config/packages/framework.yaml b/docs/config/packages/framework.yaml index 582a3b5972c..8b31359ee27 100644 --- a/docs/config/packages/framework.yaml +++ b/docs/config/packages/framework.yaml @@ -7,9 +7,7 @@ api_platform: json: ['application/json'] docs_formats: jsonopenapi: ['application/vnd.openapi+json'] - event_listeners_backward_compatibility_layer: false keep_legacy_inflector: false defaults: extra_properties: - rfc_7807_compliant_errors: true standard_put: true diff --git a/src/GraphQl/Tests/Fixtures/Serializer/NameConverter/CustomConverter.php b/src/GraphQl/Tests/Fixtures/Serializer/NameConverter/CustomConverter.php index 28a97625c20..3c4efde93d9 100644 --- a/src/GraphQl/Tests/Fixtures/Serializer/NameConverter/CustomConverter.php +++ b/src/GraphQl/Tests/Fixtures/Serializer/NameConverter/CustomConverter.php @@ -14,20 +14,28 @@ namespace ApiPlatform\GraphQl\Tests\Fixtures\Serializer\NameConverter; use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter; +use Symfony\Component\Serializer\NameConverter\NameConverterInterface; /** * Custom converter that will only convert a property named "nameConverted" * with the same logic as Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter. */ -class CustomConverter extends CamelCaseToSnakeCaseNameConverter +class CustomConverter implements NameConverterInterface { + private NameConverterInterface $nameConverter; + + public function __construct() + { + $this->nameConverter = new CamelCaseToSnakeCaseNameConverter(); + } + public function normalize(string $propertyName): string { - return 'nameConverted' === $propertyName ? parent::normalize($propertyName) : $propertyName; + return 'nameConverted' === $propertyName ? $this->nameConverter->normalize($propertyName) : $propertyName; } public function denormalize(string $propertyName): string { - return 'name_converted' === $propertyName ? parent::denormalize($propertyName) : $propertyName; + return 'name_converted' === $propertyName ? $this->nameConverter->denormalize($propertyName) : $propertyName; } } diff --git a/src/Serializer/Tests/Fixtures/Serializer/NameConverter/CustomConverter.php b/src/Serializer/Tests/Fixtures/Serializer/NameConverter/CustomConverter.php index 72332e4f0cb..4e940ffa6dd 100644 --- a/src/Serializer/Tests/Fixtures/Serializer/NameConverter/CustomConverter.php +++ b/src/Serializer/Tests/Fixtures/Serializer/NameConverter/CustomConverter.php @@ -14,20 +14,28 @@ namespace ApiPlatform\Serializer\Tests\Fixtures\Serializer\NameConverter; use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter; +use Symfony\Component\Serializer\NameConverter\NameConverterInterface; /** * Custom converter that will only convert a property named "nameConverted" * with the same logic as Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter. */ -final class CustomConverter extends CamelCaseToSnakeCaseNameConverter +class CustomConverter implements NameConverterInterface { + private NameConverterInterface $nameConverter; + + public function __construct() + { + $this->nameConverter = new CamelCaseToSnakeCaseNameConverter(); + } + public function normalize(string $propertyName): string { - return 'nameConverted' === $propertyName ? parent::normalize($propertyName) : $propertyName; + return 'nameConverted' === $propertyName ? $this->nameConverter->normalize($propertyName) : $propertyName; } public function denormalize(string $propertyName): string { - return 'name_converted' === $propertyName ? parent::denormalize($propertyName) : $propertyName; + return 'name_converted' === $propertyName ? $this->nameConverter->denormalize($propertyName) : $propertyName; } } diff --git a/tests/Fixtures/TestBundle/Serializer/NameConverter/CustomConverter.php b/tests/Fixtures/TestBundle/Serializer/NameConverter/CustomConverter.php index e3c0e8f4556..e2837b7228c 100644 --- a/tests/Fixtures/TestBundle/Serializer/NameConverter/CustomConverter.php +++ b/tests/Fixtures/TestBundle/Serializer/NameConverter/CustomConverter.php @@ -14,20 +14,28 @@ namespace ApiPlatform\Tests\Fixtures\TestBundle\Serializer\NameConverter; use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter; +use Symfony\Component\Serializer\NameConverter\NameConverterInterface; /** * Custom converter that will only convert a property named "nameConverted" * with the same logic as Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter. */ -class CustomConverter extends CamelCaseToSnakeCaseNameConverter +class CustomConverter implements NameConverterInterface { + private NameConverterInterface $nameConverter; + + public function __construct() + { + $this->nameConverter = new CamelCaseToSnakeCaseNameConverter(); + } + public function normalize(string $propertyName): string { - return 'nameConverted' === $propertyName ? parent::normalize($propertyName) : $propertyName; + return 'nameConverted' === $propertyName ? $this->nameConverter->normalize($propertyName) : $propertyName; } public function denormalize(string $propertyName): string { - return 'name_converted' === $propertyName ? parent::denormalize($propertyName) : $propertyName; + return 'name_converted' === $propertyName ? $this->nameConverter->denormalize($propertyName) : $propertyName; } } From 0154fbf00635b46e151c917378e5fe4d76dc1a83 Mon Sep 17 00:00:00 2001 From: Antoine Bluchet Date: Fri, 29 Mar 2024 14:58:54 +0100 Subject: [PATCH 2/6] fix(elasticsearch): wrong namespace for stateOptions (#6260) --- src/Metadata/Extractor/XmlResourceExtractor.php | 12 +++++++----- src/Metadata/Extractor/YamlResourceExtractor.php | 6 ++++-- .../Extractor/ResourceMetadataCompatibilityTest.php | 6 ++---- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/Metadata/Extractor/XmlResourceExtractor.php b/src/Metadata/Extractor/XmlResourceExtractor.php index 6115bde4e41..bdb22f5304d 100644 --- a/src/Metadata/Extractor/XmlResourceExtractor.php +++ b/src/Metadata/Extractor/XmlResourceExtractor.php @@ -13,10 +13,10 @@ namespace ApiPlatform\Metadata\Extractor; +use ApiPlatform\Elasticsearch\State\Options; use ApiPlatform\Metadata\Exception\InvalidArgumentException; use ApiPlatform\Metadata\GetCollection; use ApiPlatform\Metadata\Post; -use ApiPlatform\Metadata\Tests\Fixtures\StateOptions; use ApiPlatform\OpenApi\Model\ExternalDocumentation; use ApiPlatform\OpenApi\Model\Operation as OpenApiOperation; use ApiPlatform\OpenApi\Model\Parameter; @@ -451,10 +451,12 @@ private function buildStateOptions(\SimpleXMLElement $resource): ?OptionsInterfa } $elasticsearchOptions = $stateOptions->elasticsearchOptions ?? null; if ($elasticsearchOptions) { - return new StateOptions( - isset($elasticsearchOptions['index']) ? (string) $elasticsearchOptions['index'] : null, - isset($elasticsearchOptions['type']) ? (string) $elasticsearchOptions['type'] : null, - ); + if (class_exists(Options::class)) { + return new Options( + isset($elasticsearchOptions['index']) ? (string) $elasticsearchOptions['index'] : null, + isset($elasticsearchOptions['type']) ? (string) $elasticsearchOptions['type'] : null, + ); + } } return null; diff --git a/src/Metadata/Extractor/YamlResourceExtractor.php b/src/Metadata/Extractor/YamlResourceExtractor.php index ea8812a0355..71a8403d0e8 100644 --- a/src/Metadata/Extractor/YamlResourceExtractor.php +++ b/src/Metadata/Extractor/YamlResourceExtractor.php @@ -13,10 +13,10 @@ namespace ApiPlatform\Metadata\Extractor; +use ApiPlatform\Elasticsearch\State\Options; use ApiPlatform\Metadata\Exception\InvalidArgumentException; use ApiPlatform\Metadata\GetCollection; use ApiPlatform\Metadata\Post; -use ApiPlatform\Metadata\Tests\Fixtures\StateOptions; use ApiPlatform\OpenApi\Model\ExternalDocumentation; use ApiPlatform\OpenApi\Model\Operation as OpenApiOperation; use ApiPlatform\OpenApi\Model\Parameter; @@ -410,7 +410,9 @@ private function buildStateOptions(array $resource): ?OptionsInterface $configuration = reset($stateOptions); switch (key($stateOptions)) { case 'elasticsearchOptions': - return new StateOptions($configuration['index'] ?? null, $configuration['type'] ?? null); + if (class_exists(Options::class)) { + return new Options($configuration['index'] ?? null, $configuration['type'] ?? null); + } } return null; diff --git a/src/Metadata/Tests/Extractor/ResourceMetadataCompatibilityTest.php b/src/Metadata/Tests/Extractor/ResourceMetadataCompatibilityTest.php index 0dce9014fa9..ae2609ac847 100644 --- a/src/Metadata/Tests/Extractor/ResourceMetadataCompatibilityTest.php +++ b/src/Metadata/Tests/Extractor/ResourceMetadataCompatibilityTest.php @@ -36,12 +36,10 @@ use ApiPlatform\Metadata\Tests\Extractor\Adapter\XmlResourceAdapter; use ApiPlatform\Metadata\Tests\Extractor\Adapter\YamlResourceAdapter; use ApiPlatform\Metadata\Tests\Fixtures\ApiResource\Comment; -use ApiPlatform\Metadata\Tests\Fixtures\StateOptions; use ApiPlatform\Metadata\Util\CamelCaseToSnakeCaseNameConverter; use ApiPlatform\OpenApi\Model\ExternalDocumentation; use ApiPlatform\OpenApi\Model\Operation as OpenApiOperation; use ApiPlatform\OpenApi\Model\RequestBody; -use ApiPlatform\State\OptionsInterface; use PHPUnit\Framework\AssertionFailedError; use PHPUnit\Framework\TestCase; use Symfony\Component\WebLink\Link; @@ -720,7 +718,7 @@ private function withGraphQlOperations(array $values, ?array $fixtures): array return $operations; } - private function withStateOptions(array $values): ?OptionsInterface + private function withStateOptions(array $values) { if (!$values) { return null; @@ -733,7 +731,7 @@ private function withStateOptions(array $values): ?OptionsInterface $configuration = reset($values); switch (key($values)) { case 'elasticsearchOptions': - return new StateOptions($configuration['index'] ?? null, $configuration['type'] ?? null); + return null; } throw new \LogicException(sprintf('Unsupported "%s" state options.', key($values))); From 88d88ed2c41cca89da4ef2185d58adb2ea91d20c Mon Sep 17 00:00:00 2001 From: Antoine Bluchet Date: Fri, 29 Mar 2024 15:02:44 +0100 Subject: [PATCH 3/6] fix(doctrine): wrong return type without name converter #6079 (#6254) --- src/Doctrine/Odm/Filter/AbstractFilter.php | 2 +- src/Doctrine/Orm/Filter/AbstractFilter.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Doctrine/Odm/Filter/AbstractFilter.php b/src/Doctrine/Odm/Filter/AbstractFilter.php index 40cecd1400f..d1b30add62a 100644 --- a/src/Doctrine/Odm/Filter/AbstractFilter.php +++ b/src/Doctrine/Odm/Filter/AbstractFilter.php @@ -86,7 +86,7 @@ protected function isPropertyEnabled(string $property, string $resourceClass): b protected function denormalizePropertyName(string|int $property): string { if (!$this->nameConverter instanceof NameConverterInterface) { - return $property; + return (string) $property; } return implode('.', array_map($this->nameConverter->denormalize(...), explode('.', (string) $property))); diff --git a/src/Doctrine/Orm/Filter/AbstractFilter.php b/src/Doctrine/Orm/Filter/AbstractFilter.php index c1deb1c0a90..0ad569e5145 100644 --- a/src/Doctrine/Orm/Filter/AbstractFilter.php +++ b/src/Doctrine/Orm/Filter/AbstractFilter.php @@ -80,7 +80,7 @@ protected function isPropertyEnabled(string $property, string $resourceClass): b protected function denormalizePropertyName(string|int $property): string { if (!$this->nameConverter instanceof NameConverterInterface) { - return $property; + return (string) $property; } return implode('.', array_map($this->nameConverter->denormalize(...), explode('.', (string) $property))); From cf20566af66f65c90f9067ddf8ce44377b9db007 Mon Sep 17 00:00:00 2001 From: soyuka Date: Fri, 29 Mar 2024 15:20:23 +0100 Subject: [PATCH 4/6] docs: use exception compatibility flag --- docs/config/packages/framework.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/config/packages/framework.yaml b/docs/config/packages/framework.yaml index 8b31359ee27..c0aac2ebbb3 100644 --- a/docs/config/packages/framework.yaml +++ b/docs/config/packages/framework.yaml @@ -10,4 +10,5 @@ api_platform: keep_legacy_inflector: false defaults: extra_properties: + rfc_7807_compliant_errors: true standard_put: true From 4adc0752471910a1dca104c1ac8d9c1dbebf268e Mon Sep 17 00:00:00 2001 From: Antoine Bluchet Date: Fri, 29 Mar 2024 16:02:24 +0100 Subject: [PATCH 5/6] fix: multiple error routes #6214 (#6263) --- src/Hydra/Serializer/DocumentationNormalizer.php | 6 ++++++ .../Resource/Factory/OperationDefaultsTrait.php | 2 +- .../UriTemplateResourceMetadataCollectionFactory.php | 2 +- src/State/ApiResource/Error.php | 9 +++++++++ src/Symfony/Bundle/Resources/config/routing/api.xml | 11 +++++++++++ src/Symfony/Routing/IriConverter.php | 4 +++- .../Validator/Exception/ValidationException.php | 7 +++++++ 7 files changed, 38 insertions(+), 3 deletions(-) diff --git a/src/Hydra/Serializer/DocumentationNormalizer.php b/src/Hydra/Serializer/DocumentationNormalizer.php index c8e0c725ed9..2d2bbbb9c8f 100644 --- a/src/Hydra/Serializer/DocumentationNormalizer.php +++ b/src/Hydra/Serializer/DocumentationNormalizer.php @@ -20,6 +20,7 @@ use ApiPlatform\Metadata\ApiProperty; use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\CollectionOperationInterface; +use ApiPlatform\Metadata\ErrorResource; use ApiPlatform\Metadata\HttpOperation; use ApiPlatform\Metadata\Operation; use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; @@ -29,6 +30,7 @@ use ApiPlatform\Metadata\ResourceClassResolverInterface; use ApiPlatform\Metadata\UrlGeneratorInterface; use ApiPlatform\Serializer\CacheableSupportsMethodInterface; +use ApiPlatform\Symfony\Validator\Exception\ValidationException; use Symfony\Component\PropertyInfo\Type; use Symfony\Component\Serializer\NameConverter\NameConverterInterface; use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; @@ -60,6 +62,10 @@ public function normalize(mixed $object, ?string $format = null, array $context $resourceMetadataCollection = $this->resourceMetadataFactory->create($resourceClass); $resourceMetadata = $resourceMetadataCollection[0]; + if ($resourceMetadata instanceof ErrorResource && ValidationException::class === $resourceMetadata->getClass()) { + continue; + } + $shortName = $resourceMetadata->getShortName(); $prefixedShortName = $resourceMetadata->getTypes()[0] ?? "#$shortName"; $this->populateEntrypointProperties($resourceMetadata, $shortName, $prefixedShortName, $entrypointProperties, $resourceMetadataCollection); diff --git a/src/Metadata/Resource/Factory/OperationDefaultsTrait.php b/src/Metadata/Resource/Factory/OperationDefaultsTrait.php index 1abf1b1c6d6..8814c5b9214 100644 --- a/src/Metadata/Resource/Factory/OperationDefaultsTrait.php +++ b/src/Metadata/Resource/Factory/OperationDefaultsTrait.php @@ -202,7 +202,7 @@ private function getOperationWithDefaults(ApiResource $resource, Operation $oper throw new RuntimeException(sprintf('Operation should be an instance of "%s"', HttpOperation::class)); } - if ($operation->getRouteName()) { + if (!$operation->getName() && $operation->getRouteName()) { /** @var HttpOperation $operation */ $operation = $operation->withName($operation->getRouteName()); } diff --git a/src/Metadata/Resource/Factory/UriTemplateResourceMetadataCollectionFactory.php b/src/Metadata/Resource/Factory/UriTemplateResourceMetadataCollectionFactory.php index f75e3ef2962..888711b4c84 100644 --- a/src/Metadata/Resource/Factory/UriTemplateResourceMetadataCollectionFactory.php +++ b/src/Metadata/Resource/Factory/UriTemplateResourceMetadataCollectionFactory.php @@ -75,7 +75,7 @@ public function create(string $resourceClass): ResourceMetadataCollection $operation = $operation->withName($routeName); } - $operations->add($routeName, $operation); + $operations->add($operation->getName(), $operation); continue; } diff --git a/src/State/ApiResource/Error.php b/src/State/ApiResource/Error.php index 3141026c0d2..98740214822 100644 --- a/src/State/ApiResource/Error.php +++ b/src/State/ApiResource/Error.php @@ -33,6 +33,7 @@ operations: [ new Operation( name: '_api_errors_problem', + routeName: 'api_errors', outputFormats: ['json' => ['application/problem+json']], normalizationContext: [ 'groups' => ['jsonproblem'], @@ -42,6 +43,7 @@ ), new Operation( name: '_api_errors_hydra', + routeName: 'api_errors', outputFormats: ['jsonld' => ['application/problem+json']], normalizationContext: [ 'groups' => ['jsonld'], @@ -52,6 +54,7 @@ ), new Operation( name: '_api_errors_jsonapi', + routeName: 'api_errors', outputFormats: ['jsonapi' => ['application/vnd.api+json']], normalizationContext: [ 'groups' => ['jsonapi'], @@ -59,6 +62,10 @@ 'rfc_7807_compliant_errors' => true, ], ), + new Operation( + name: '_api_errors', + routeName: 'api_errors' + ), ], provider: 'api_platform.state.error_provider', graphQlOperations: [] @@ -120,12 +127,14 @@ public static function createFromException(\Exception|\Throwable $exception, int } #[Ignore] + #[ApiProperty(readable: false)] public function getHeaders(): array { return $this->headers; } #[Ignore] + #[ApiProperty(readable: false)] public function getStatusCode(): int { return $this->status; diff --git a/src/Symfony/Bundle/Resources/config/routing/api.xml b/src/Symfony/Bundle/Resources/config/routing/api.xml index 7c9cbdd7f26..e4524f69a2c 100644 --- a/src/Symfony/Bundle/Resources/config/routing/api.xml +++ b/src/Symfony/Bundle/Resources/config/routing/api.xml @@ -14,4 +14,15 @@ index + + api_platform.action.not_exposed + 500 + + \d+ + + + + api_platform.action.not_exposed + + diff --git a/src/Symfony/Routing/IriConverter.php b/src/Symfony/Routing/IriConverter.php index 266774c2bff..9de95f35f87 100644 --- a/src/Symfony/Routing/IriConverter.php +++ b/src/Symfony/Routing/IriConverter.php @@ -186,7 +186,9 @@ private function generateSymfonyRoute(object|string $resource, int $referenceTyp } try { - return $this->router->generate($operation->getName(), $identifiers, $operation->getUrlGenerationStrategy() ?? $referenceType); + $routeName = $operation instanceof HttpOperation ? ($operation->getRouteName() ?? $operation->getName()) : $operation->getName(); + + return $this->router->generate($routeName, $identifiers, $operation->getUrlGenerationStrategy() ?? $referenceType); } catch (RoutingExceptionInterface $e) { throw new InvalidArgumentException(sprintf('Unable to generate an IRI for the item of type "%s"', $operation->getClass()), $e->getCode(), $e); } diff --git a/src/Symfony/Validator/Exception/ValidationException.php b/src/Symfony/Validator/Exception/ValidationException.php index cc0cd251499..612da05897b 100644 --- a/src/Symfony/Validator/Exception/ValidationException.php +++ b/src/Symfony/Validator/Exception/ValidationException.php @@ -40,6 +40,7 @@ operations: [ new ErrorOperation( name: '_api_validation_errors_problem', + routeName: 'api_validation_errors', outputFormats: ['json' => ['application/problem+json']], normalizationContext: ['groups' => ['json'], 'skip_null_values' => true, @@ -47,6 +48,7 @@ ]), new ErrorOperation( name: '_api_validation_errors_hydra', + routeName: 'api_validation_errors', outputFormats: ['jsonld' => ['application/problem+json']], links: [new Link(rel: ContextBuilderInterface::JSONLD_NS.'error', href: 'http://www.w3.org/ns/hydra/error')], normalizationContext: [ @@ -57,9 +59,14 @@ ), new ErrorOperation( name: '_api_validation_errors_jsonapi', + routeName: 'api_validation_errors', outputFormats: ['jsonapi' => ['application/vnd.api+json']], normalizationContext: ['groups' => ['jsonapi'], 'skip_null_values' => true, 'rfc_7807_compliant_errors' => true] ), + new ErrorOperation( + name: '_api_validation_errors', + routeName: 'api_validation_errors' + ), ], graphQlOperations: [] )] From db50a46c16689b7fb4db79b1d4ef7e1ad094d358 Mon Sep 17 00:00:00 2001 From: Antoine Bluchet Date: Fri, 29 Mar 2024 16:02:48 +0100 Subject: [PATCH 6/6] fix(doctrine): stateOptions force resource class on collection (#6255) fixes #6039 --- .../issue6039/entity_class_option.feature | 12 +++++++ .../AbstractCollectionNormalizer.php | 4 +++ tests/Behat/DoctrineContext.php | 17 ++++++++++ .../ApiResource/Issue5648/DummyResource.php | 6 +++- .../ApiResource/Issue6039/UserApi.php | 25 +++++++++++++++ .../Entity/Issue6039/Issue6039EntityUser.php | 31 +++++++++++++++++++ 6 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 features/doctrine/issue6039/entity_class_option.feature create mode 100644 tests/Fixtures/TestBundle/ApiResource/Issue6039/UserApi.php create mode 100644 tests/Fixtures/TestBundle/Entity/Issue6039/Issue6039EntityUser.php diff --git a/features/doctrine/issue6039/entity_class_option.feature b/features/doctrine/issue6039/entity_class_option.feature new file mode 100644 index 00000000000..18609626a2e --- /dev/null +++ b/features/doctrine/issue6039/entity_class_option.feature @@ -0,0 +1,12 @@ +Feature: Test entity class option on collections + In order to retrieve a collections of resources mapped to a DTO automatically + As a client software developer + + @createSchema + @!mongodb + Scenario: Get collection + Given there are issue6039 users + And I add "Accept" header equal to "application/ld+json" + When I send a "GET" request to "/issue6039_user_apis" + Then the response status code should be 200 + And the JSON node "hydra:member[0].bar" should not exist diff --git a/src/Serializer/AbstractCollectionNormalizer.php b/src/Serializer/AbstractCollectionNormalizer.php index 96d2eb06c7f..ff1aa7f1a07 100644 --- a/src/Serializer/AbstractCollectionNormalizer.php +++ b/src/Serializer/AbstractCollectionNormalizer.php @@ -103,6 +103,10 @@ public function normalize(mixed $object, ?string $format = null, array $context $paginationData = $this->getPaginationData($object, $collectionContext); $childContext = $this->createOperationContext($collectionContext, $resourceClass); + if (isset($collectionContext['force_resource_class'])) { + $childContext['force_resource_class'] = $collectionContext['force_resource_class']; + } + $itemsData = $this->getItemsData($object, $format, $childContext); return array_merge_recursive($data, $paginationData, $itemsData); diff --git a/tests/Behat/DoctrineContext.php b/tests/Behat/DoctrineContext.php index 01df0994a91..b83f0b63810 100644 --- a/tests/Behat/DoctrineContext.php +++ b/tests/Behat/DoctrineContext.php @@ -155,6 +155,7 @@ use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue5722\Event; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue5722\ItemLog; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue5735\Group; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue6039\Issue6039EntityUser; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\LinkHandledDummy; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\MaxDepthDummy; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\MultiRelationsDummy; @@ -2270,6 +2271,22 @@ public function thereIsADummyEntityWithAMappedSuperclass(): void $this->manager->flush(); } + /** + * @Given there are issue6039 users + */ + public function thereAreIssue6039Users(): void + { + $entity = new Issue6039EntityUser(); + $entity->name = 'test'; + $entity->bar = 'test'; + $this->manager->persist($entity); + $entity = new Issue6039EntityUser(); + $entity->name = 'test2'; + $entity->bar = 'test'; + $this->manager->persist($entity); + $this->manager->flush(); + } + private function isOrm(): bool { return null !== $this->schemaTool; diff --git a/tests/Fixtures/TestBundle/ApiResource/Issue5648/DummyResource.php b/tests/Fixtures/TestBundle/ApiResource/Issue5648/DummyResource.php index 21674757fc3..62b86ff4684 100644 --- a/tests/Fixtures/TestBundle/ApiResource/Issue5648/DummyResource.php +++ b/tests/Fixtures/TestBundle/ApiResource/Issue5648/DummyResource.php @@ -21,10 +21,11 @@ use ApiPlatform\Metadata\GetCollection; use ApiPlatform\Metadata\Link; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\RelatedDummy; #[ApiResource( operations: [ - new GetCollection(uriTemplate: '/dummy_resource_with_custom_filter', itemUriTemplate: '/dummy_resource_with_custom_filter/{id}'), + new GetCollection(uriTemplate: '/dummy_resource_with_custom_filter{._format}', itemUriTemplate: '/dummy_resource_with_custom_filter/{id}'), new Get(uriTemplate: '/dummy_resource_with_custom_filter/{id}', uriVariables: ['id' => new Link(fromClass: Dummy::class)]), ], stateOptions: new Options(entityClass: Dummy::class) @@ -37,5 +38,8 @@ class DummyResource public string $name; + /** + * @var RelatedDummy[] + */ public array $relatedDummies; } diff --git a/tests/Fixtures/TestBundle/ApiResource/Issue6039/UserApi.php b/tests/Fixtures/TestBundle/ApiResource/Issue6039/UserApi.php new file mode 100644 index 00000000000..42174b7f3f0 --- /dev/null +++ b/tests/Fixtures/TestBundle/ApiResource/Issue6039/UserApi.php @@ -0,0 +1,25 @@ + + * + * 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\Tests\Fixtures\TestBundle\ApiResource\Issue6039; + +use ApiPlatform\Doctrine\Orm\State\Options; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue6039\Issue6039EntityUser; + +#[GetCollection(shortName: 'Issue6039UserApi', stateOptions: new Options(entityClass: Issue6039EntityUser::class))] +class UserApi +{ + public string $id; + public string $name; +} diff --git a/tests/Fixtures/TestBundle/Entity/Issue6039/Issue6039EntityUser.php b/tests/Fixtures/TestBundle/Entity/Issue6039/Issue6039EntityUser.php new file mode 100644 index 00000000000..0b00bfa5064 --- /dev/null +++ b/tests/Fixtures/TestBundle/Entity/Issue6039/Issue6039EntityUser.php @@ -0,0 +1,31 @@ + + * + * 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\Tests\Fixtures\TestBundle\Entity\Issue6039; + +use Doctrine\ORM\Mapping as ORM; + +#[ORM\Entity] +class Issue6039EntityUser +{ + #[ORM\Column(type: 'integer')] + #[ORM\Id] + #[ORM\GeneratedValue(strategy: 'AUTO')] + public ?int $id = null; + + #[ORM\Column] + public string $name; + + #[ORM\Column] + public string $bar; +}