diff --git a/CHANGELOG.md b/CHANGELOG.md index 749279a1155..0643d4d3f8b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -108,6 +108,13 @@ api_platform: form: ['multipart/form-data'] ``` +## v3.2.20 + +### Bug fixes + +* [90c9fb31a](https://github.com/api-platform/core/commit/90c9fb31a322a2c7891fbbafb75d60b09fd67772) fix(symfony): register api_error route (#6281) +* [d061c3811](https://github.com/api-platform/core/commit/d061c381120b858c471157dbdccbb5084a39fb04) fix(graphql): increment graphql normalizer priority (#6283) + ## v3.2.19 ### Bug fixes diff --git a/composer.json b/composer.json index 0cc729fe56d..5f1c863939c 100644 --- a/composer.json +++ b/composer.json @@ -108,6 +108,7 @@ "doctrine/orm": "<2.14.0", "doctrine/mongodb-odm": "<2.4", "doctrine/persistence": "<1.3", + "symfony/framework-bundle": "6.4.6 || 7.0.6", "symfony/var-exporter": "<6.1.1", "phpunit/phpunit": "<9.5", "phpspec/prophecy": "<1.15", diff --git a/features/graphql/query.feature b/features/graphql/query.feature index 2021d9039a7..cbf2bdfb892 100644 --- a/features/graphql/query.feature +++ b/features/graphql/query.feature @@ -22,7 +22,7 @@ Feature: GraphQL query support @createSchema Scenario: Retrieve an item with different relations to the same resource - Given there are 2 multiRelationsDummy objects having each a manyToOneRelation, 2 manyToManyRelations, 3 oneToManyRelations and 4 embeddedRelations + Given there are 2 multiRelationsDummy objects having each 1 manyToOneRelation, 2 manyToManyRelations, 3 oneToManyRelations and 4 embeddedRelations When I send the following GraphQL request: """ { @@ -33,6 +33,10 @@ Feature: GraphQL query support id name } + manyToOneResolveRelation { + id + name + } manyToManyRelations { edges{ node { @@ -70,7 +74,7 @@ Feature: GraphQL query support @createSchema Scenario: Retrieve embedded collections - Given there are 2 multiRelationsDummy objects having each a manyToOneRelation, 2 manyToManyRelations, 3 oneToManyRelations and 4 embeddedRelations + Given there are 2 multiRelationsDummy objects having each 1 manyToOneRelation, 2 manyToManyRelations, 3 oneToManyRelations and 4 embeddedRelations When I send the following GraphQL request: """ { @@ -81,6 +85,10 @@ Feature: GraphQL query support id name } + manyToOneResolveRelation { + id + name + } manyToManyRelations { edges{ node { @@ -113,10 +121,13 @@ Feature: GraphQL query support 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/json" + And the JSON node "errors" should not exist And the JSON node "data.multiRelationsDummy.id" should be equal to "/multi_relations_dummies/2" And the JSON node "data.multiRelationsDummy.name" should be equal to "Dummy #2" And the JSON node "data.multiRelationsDummy.manyToOneRelation.id" should not be null And the JSON node "data.multiRelationsDummy.manyToOneRelation.name" should be equal to "RelatedManyToOneDummy #2" + And the JSON node "data.multiRelationsDummy.manyToOneResolveRelation.id" should not be null + And the JSON node "data.multiRelationsDummy.manyToOneResolveRelation.name" should be equal to "RelatedManyToOneResolveDummy #2" And the JSON node "data.multiRelationsDummy.manyToManyRelations.edges" should have 2 element And the JSON node "data.multiRelationsDummy.manyToManyRelations.edges[1].node.id" should not be null And the JSON node "data.multiRelationsDummy.manyToManyRelations.edges[0].node.name" should match "#RelatedManyToManyDummy(1|2)2#" @@ -135,6 +146,65 @@ Feature: GraphQL query support And the JSON node "data.multiRelationsDummy.nestedPaginatedCollection.edges[2].node.name" should be equal to "NestedPaginatedDummy3" And the JSON node "data.multiRelationsDummy.nestedPaginatedCollection.edges[3].node.name" should be equal to "NestedPaginatedDummy4" + @createSchema + Scenario: Retrieve an item with different relations (all unset) + Given there are 2 multiRelationsDummy objects having each 0 manyToOneRelation, 0 manyToManyRelations, 0 oneToManyRelations and 0 embeddedRelations + When I send the following GraphQL request: + """ + { + multiRelationsDummy(id: "/multi_relations_dummies/2") { + id + name + manyToOneRelation { + id + name + } + manyToOneResolveRelation { + id + name + } + manyToManyRelations { + edges{ + node { + id + name + } + } + } + oneToManyRelations { + edges{ + node { + id + name + } + } + } + nestedCollection { + name + } + nestedPaginatedCollection { + edges{ + node { + name + } + } + } + } + } + """ + 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/json" + And the JSON node "errors" should not exist + And the JSON node "data.multiRelationsDummy.id" should be equal to "/multi_relations_dummies/2" + And the JSON node "data.multiRelationsDummy.name" should be equal to "Dummy #2" + And the JSON node "data.multiRelationsDummy.manyToOneRelation" should be null + And the JSON node "data.multiRelationsDummy.manyToOneResolveRelation" should be null + And the JSON node "data.multiRelationsDummy.manyToManyRelations.edges" should have 0 element + And the JSON node "data.multiRelationsDummy.oneToManyRelations.edges" should have 0 element + And the JSON node "data.multiRelationsDummy.nestedCollection" should have 0 element + And the JSON node "data.multiRelationsDummy.nestedPaginatedCollection.edges" should have 0 element + @createSchema @!mongodb Scenario: Retrieve an item with child relation to the same resource Given there are tree dummies diff --git a/src/GraphQl/Resolver/Factory/CollectionResolverFactory.php b/src/GraphQl/Resolver/Factory/CollectionResolverFactory.php index a25e477c1f2..5f7e31f2af0 100644 --- a/src/GraphQl/Resolver/Factory/CollectionResolverFactory.php +++ b/src/GraphQl/Resolver/Factory/CollectionResolverFactory.php @@ -20,6 +20,7 @@ use ApiPlatform\GraphQl\Resolver\Stage\SerializeStageInterface; use ApiPlatform\Metadata\GraphQl\Operation; use ApiPlatform\Metadata\GraphQl\Query; +use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Metadata\Util\CloneTrait; use ApiPlatform\State\Pagination\ArrayPaginator; use GraphQL\Type\Definition\ResolveInfo; @@ -40,7 +41,7 @@ public function __construct(private readonly ReadStageInterface $readStage, priv { } - public function __invoke(?string $resourceClass = null, ?string $rootClass = null, ?Operation $operation = null): callable + public function __invoke(?string $resourceClass = null, ?string $rootClass = null, ?Operation $operation = null, ?PropertyMetadataFactoryInterface $propertyMetadataFactory = null): callable { return function (?array $source, array $args, $context, ResolveInfo $info) use ($resourceClass, $rootClass, $operation): ?array { // If authorization has failed for a relation field (e.g. via ApiProperty security), the field is not present in the source: null can be returned directly to ensure the collection isn't in the response. diff --git a/src/GraphQl/Resolver/Factory/ItemMutationResolverFactory.php b/src/GraphQl/Resolver/Factory/ItemMutationResolverFactory.php index 999ea9a51ae..ffe1e3d04c2 100644 --- a/src/GraphQl/Resolver/Factory/ItemMutationResolverFactory.php +++ b/src/GraphQl/Resolver/Factory/ItemMutationResolverFactory.php @@ -24,6 +24,7 @@ use ApiPlatform\GraphQl\Resolver\Stage\WriteStageInterface; use ApiPlatform\Metadata\DeleteOperationInterface; use ApiPlatform\Metadata\GraphQl\Operation; +use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Metadata\Util\ClassInfoTrait; use ApiPlatform\Metadata\Util\CloneTrait; use GraphQL\Type\Definition\ResolveInfo; @@ -44,7 +45,7 @@ public function __construct(private readonly ReadStageInterface $readStage, priv { } - public function __invoke(?string $resourceClass = null, ?string $rootClass = null, ?Operation $operation = null): callable + public function __invoke(?string $resourceClass = null, ?string $rootClass = null, ?Operation $operation = null, ?PropertyMetadataFactoryInterface $propertyMetadataFactory = null): callable { return function (?array $source, array $args, $context, ResolveInfo $info) use ($resourceClass, $rootClass, $operation): ?array { if (null === $resourceClass || null === $operation) { diff --git a/src/GraphQl/Resolver/Factory/ItemResolverFactory.php b/src/GraphQl/Resolver/Factory/ItemResolverFactory.php index a1652b9bca7..45820aaeed4 100644 --- a/src/GraphQl/Resolver/Factory/ItemResolverFactory.php +++ b/src/GraphQl/Resolver/Factory/ItemResolverFactory.php @@ -20,6 +20,7 @@ use ApiPlatform\GraphQl\Resolver\Stage\SerializeStageInterface; use ApiPlatform\Metadata\GraphQl\Operation; use ApiPlatform\Metadata\GraphQl\Query; +use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Metadata\Util\ClassInfoTrait; use ApiPlatform\Metadata\Util\CloneTrait; use GraphQL\Type\Definition\ResolveInfo; @@ -41,7 +42,7 @@ public function __construct(private readonly ReadStageInterface $readStage, priv { } - public function __invoke(?string $resourceClass = null, ?string $rootClass = null, ?Operation $operation = null): callable + public function __invoke(?string $resourceClass = null, ?string $rootClass = null, ?Operation $operation = null, ?PropertyMetadataFactoryInterface $propertyMetadataFactory = null): callable { return function (?array $source, array $args, $context, ResolveInfo $info) use ($resourceClass, $rootClass, $operation) { // Data already fetched and normalized (field or nested resource) diff --git a/src/GraphQl/Resolver/Factory/ItemSubscriptionResolverFactory.php b/src/GraphQl/Resolver/Factory/ItemSubscriptionResolverFactory.php index 0335a6ac232..b182ba528ff 100644 --- a/src/GraphQl/Resolver/Factory/ItemSubscriptionResolverFactory.php +++ b/src/GraphQl/Resolver/Factory/ItemSubscriptionResolverFactory.php @@ -19,6 +19,7 @@ use ApiPlatform\GraphQl\Subscription\MercureSubscriptionIriGeneratorInterface; use ApiPlatform\GraphQl\Subscription\SubscriptionManagerInterface; use ApiPlatform\Metadata\GraphQl\Operation; +use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Metadata\Util\ClassInfoTrait; use ApiPlatform\Metadata\Util\CloneTrait; use GraphQL\Type\Definition\ResolveInfo; @@ -37,7 +38,7 @@ public function __construct(private readonly ReadStageInterface $readStage, priv { } - public function __invoke(?string $resourceClass = null, ?string $rootClass = null, ?Operation $operation = null): callable + public function __invoke(?string $resourceClass = null, ?string $rootClass = null, ?Operation $operation = null, ?PropertyMetadataFactoryInterface $propertyMetadataFactory = null): callable { return function (?array $source, array $args, $context, ResolveInfo $info) use ($resourceClass, $rootClass, $operation): ?array { if (null === $resourceClass || null === $operation) { diff --git a/src/GraphQl/Resolver/Factory/ResolverFactory.php b/src/GraphQl/Resolver/Factory/ResolverFactory.php index 64faed799dc..6577c95521d 100644 --- a/src/GraphQl/Resolver/Factory/ResolverFactory.php +++ b/src/GraphQl/Resolver/Factory/ResolverFactory.php @@ -17,6 +17,7 @@ use ApiPlatform\Metadata\GraphQl\Mutation; use ApiPlatform\Metadata\GraphQl\Operation; use ApiPlatform\Metadata\GraphQl\Query; +use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\State\Pagination\ArrayPaginator; use ApiPlatform\State\ProcessorInterface; use ApiPlatform\State\ProviderInterface; @@ -30,21 +31,30 @@ public function __construct( ) { } - public function __invoke(?string $resourceClass = null, ?string $rootClass = null, ?Operation $operation = null): callable + public function __invoke(?string $resourceClass = null, ?string $rootClass = null, ?Operation $operation = null, ?PropertyMetadataFactoryInterface $propertyMetadataFactory = null): callable { - return function (?array $source, array $args, $context, ResolveInfo $info) use ($resourceClass, $rootClass, $operation) { - // Data already fetched and normalized (field or nested resource) - if ($body = $source[$info->fieldName] ?? null) { + return function (?array $source, array $args, $context, ResolveInfo $info) use ($resourceClass, $rootClass, $operation, $propertyMetadataFactory) { + if (\array_key_exists($info->fieldName, $source ?? [])) { + $body = $source[$info->fieldName]; + // special treatment for nested resources without a resolver/provider if ($operation instanceof Query && $operation->getNested() && !$operation->getResolver() && (!$operation->getProvider() || NoopProvider::class === $operation->getProvider())) { - return $this->resolve($source, $args, $info, $rootClass, $operation, new ArrayPaginator($body, 0, \count($body))); + return \is_array($body) ? $this->resolve( + $source, + $args, + $info, + $rootClass, + $operation, + new ArrayPaginator($body, 0, \count($body)) + ) : $body; } - return $body; - } - - if (null === $resourceClass && \array_key_exists($info->fieldName, $source ?? [])) { - return $body; + $propertyMetadata = $rootClass ? $propertyMetadataFactory?->create($rootClass, $info->fieldName) : null; + $type = $propertyMetadata?->getBuiltinTypes()[0] ?? null; + // Data already fetched and normalized (field or nested resource) + if ($body || null === $resourceClass || ($type && !$type->isCollection())) { + return $body; + } } // If authorization has failed for a relation field (e.g. via ApiProperty security), the field is not present in the source: null can be returned directly to ensure the collection isn't in the response. diff --git a/src/GraphQl/Resolver/Factory/ResolverFactoryInterface.php b/src/GraphQl/Resolver/Factory/ResolverFactoryInterface.php index c65e8f16682..4fced141822 100644 --- a/src/GraphQl/Resolver/Factory/ResolverFactoryInterface.php +++ b/src/GraphQl/Resolver/Factory/ResolverFactoryInterface.php @@ -14,6 +14,7 @@ namespace ApiPlatform\GraphQl\Resolver\Factory; use ApiPlatform\Metadata\GraphQl\Operation; +use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; /** * Builds a GraphQL resolver. @@ -22,5 +23,5 @@ */ interface ResolverFactoryInterface { - public function __invoke(?string $resourceClass = null, ?string $rootClass = null, ?Operation $operation = null): callable; + public function __invoke(?string $resourceClass = null, ?string $rootClass = null, ?Operation $operation = null, ?PropertyMetadataFactoryInterface $propertyMetadataFactory = null): callable; } diff --git a/src/GraphQl/Tests/Resolver/Factory/ResolverFactoryTest.php b/src/GraphQl/Tests/Resolver/Factory/ResolverFactoryTest.php index 574f8d1ef83..ee0be4aef60 100644 --- a/src/GraphQl/Tests/Resolver/Factory/ResolverFactoryTest.php +++ b/src/GraphQl/Tests/Resolver/Factory/ResolverFactoryTest.php @@ -14,9 +14,11 @@ namespace ApiPlatform\GraphQl\Tests\Resolver\Factory; use ApiPlatform\GraphQl\Resolver\Factory\ResolverFactory; +use ApiPlatform\Metadata\ApiProperty; use ApiPlatform\Metadata\GraphQl\Mutation; use ApiPlatform\Metadata\GraphQl\Operation; use ApiPlatform\Metadata\GraphQl\Query; +use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\State\ProcessorInterface; use ApiPlatform\State\ProviderInterface; use GraphQL\Type\Definition\ResolveInfo; @@ -38,11 +40,13 @@ public function testGraphQlResolver(?string $resourceClass = null, ?string $root $provider->expects($this->once())->method('provide')->with($providedOperation ?: $operation, [], $context)->willReturn($body); $processor = $this->createMock(ProcessorInterface::class); $processor->expects($this->once())->method('process')->with($body, $processedOperation ?: $operation, [], $context)->willReturn($returnValue); + $propertyMetadataFactory = $this->createMock(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactory->expects($this->once())->method('create')->with($rootClass, 'test')->willReturn(new ApiProperty(schema: ['type' => 'array'])); $resolveInfo = $this->createMock(ResolveInfo::class); $resolveInfo->fieldName = 'test'; $resolverFactory = new ResolverFactory($provider, $processor); - $this->assertEquals($resolverFactory->__invoke($resourceClass, $rootClass, $operation)(['test' => null], [], [], $resolveInfo), $returnValue); + $this->assertEquals($resolverFactory->__invoke($resourceClass, $rootClass, $operation, $propertyMetadataFactory)(['test' => null], [], [], $resolveInfo), $returnValue); } public static function graphQlQueries(): array diff --git a/src/GraphQl/Type/FieldsBuilder.php b/src/GraphQl/Type/FieldsBuilder.php index f708c6b9f84..0d1042e48ea 100644 --- a/src/GraphQl/Type/FieldsBuilder.php +++ b/src/GraphQl/Type/FieldsBuilder.php @@ -458,7 +458,7 @@ private function getResourceFieldConfiguration(?string $property, ?string $field if ($isStandardGraphqlType || $input) { $resolve = null; } else { - $resolve = ($this->itemResolverFactory)($resourceClass, $rootResource, $resourceOperation); + $resolve = ($this->itemResolverFactory)($resourceClass, $rootResource, $resourceOperation, $this->propertyMetadataFactory); } } else { if ($isStandardGraphqlType || $input) { diff --git a/src/Symfony/Bundle/Resources/config/graphql.xml b/src/Symfony/Bundle/Resources/config/graphql.xml index 9edae7441f5..14bf60b8405 100644 --- a/src/Symfony/Bundle/Resources/config/graphql.xml +++ b/src/Symfony/Bundle/Resources/config/graphql.xml @@ -218,7 +218,7 @@ - + diff --git a/src/Symfony/Bundle/Resources/config/routing/api.xml b/src/Symfony/Bundle/Resources/config/routing/api.xml index e4524f69a2c..9eb63d600fc 100644 --- a/src/Symfony/Bundle/Resources/config/routing/api.xml +++ b/src/Symfony/Bundle/Resources/config/routing/api.xml @@ -13,16 +13,4 @@ index - - - api_platform.action.not_exposed - 500 - - \d+ - - - - api_platform.action.not_exposed - - diff --git a/src/Symfony/Bundle/Resources/config/routing/errors.xml b/src/Symfony/Bundle/Resources/config/routing/errors.xml new file mode 100644 index 00000000000..574515ce305 --- /dev/null +++ b/src/Symfony/Bundle/Resources/config/routing/errors.xml @@ -0,0 +1,18 @@ + + + + + + api_platform.action.not_exposed + 500 + + \d+ + + + + api_platform.action.not_exposed + + diff --git a/src/Symfony/GraphQl/Resolver/Factory/DataCollectorResolverFactory.php b/src/Symfony/GraphQl/Resolver/Factory/DataCollectorResolverFactory.php index 9091ab3dc9e..72bf61223cc 100644 --- a/src/Symfony/GraphQl/Resolver/Factory/DataCollectorResolverFactory.php +++ b/src/Symfony/GraphQl/Resolver/Factory/DataCollectorResolverFactory.php @@ -15,6 +15,7 @@ use ApiPlatform\GraphQl\Resolver\Factory\ResolverFactoryInterface; use ApiPlatform\Metadata\GraphQl\Operation; +use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use GraphQL\Type\Definition\ResolveInfo; use Symfony\Component\HttpFoundation\RequestStack; @@ -24,7 +25,7 @@ public function __construct(private readonly ResolverFactoryInterface $resolverF { } - public function __invoke(?string $resourceClass = null, ?string $rootClass = null, ?Operation $operation = null): callable + public function __invoke(?string $resourceClass = null, ?string $rootClass = null, ?Operation $operation = null, ?PropertyMetadataFactoryInterface $propertyMetadataFactory = null): callable { return function (?array $source, array $args, $context, ResolveInfo $info) use ($resourceClass, $rootClass, $operation) { if ($this->requestStack && null !== $request = $this->requestStack->getCurrentRequest()) { diff --git a/src/Symfony/Routing/ApiLoader.php b/src/Symfony/Routing/ApiLoader.php index 0d30198622b..832c068a78f 100644 --- a/src/Symfony/Routing/ApiLoader.php +++ b/src/Symfony/Routing/ApiLoader.php @@ -125,6 +125,7 @@ public function supports(mixed $resource, ?string $type = null): bool private function loadExternalFiles(RouteCollection $routeCollection): void { $routeCollection->addCollection($this->fileLoader->load('genid.xml')); + $routeCollection->addCollection($this->fileLoader->load('errors.xml')); if ($this->entrypointEnabled) { $routeCollection->addCollection($this->fileLoader->load('api.xml')); diff --git a/tests/Behat/DoctrineContext.php b/tests/Behat/DoctrineContext.php index d27b1b219e1..8894e737d37 100644 --- a/tests/Behat/DoctrineContext.php +++ b/tests/Behat/DoctrineContext.php @@ -69,6 +69,7 @@ use ApiPlatform\Tests\Fixtures\TestBundle\Document\MultiRelationsNested as MultiRelationsNestedDocument; use ApiPlatform\Tests\Fixtures\TestBundle\Document\MultiRelationsNestedPaginated as MultiRelationsNestedPaginatedDocument; use ApiPlatform\Tests\Fixtures\TestBundle\Document\MultiRelationsRelatedDummy as MultiRelationsRelatedDummyDocument; +use ApiPlatform\Tests\Fixtures\TestBundle\Document\MultiRelationsResolveDummy as MultiRelationsResolveDummyDocument; use ApiPlatform\Tests\Fixtures\TestBundle\Document\MusicGroup as MusicGroupDocument; use ApiPlatform\Tests\Fixtures\TestBundle\Document\NetworkPathDummy as NetworkPathDummyDocument; use ApiPlatform\Tests\Fixtures\TestBundle\Document\NetworkPathRelationDummy as NetworkPathRelationDummyDocument; @@ -165,6 +166,7 @@ use ApiPlatform\Tests\Fixtures\TestBundle\Entity\MultiRelationsNested; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\MultiRelationsNestedPaginated; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\MultiRelationsRelatedDummy; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\MultiRelationsResolveDummy; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\MusicGroup; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\NetworkPathDummy; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\NetworkPathRelationDummy; @@ -809,17 +811,24 @@ public function thereAreDummyObjectsWithRelatedDummies(int $nb, int $nbrelated): } /** - * @Given there are :nb multiRelationsDummy objects having each a manyToOneRelation, :nbmtmr manyToManyRelations, :nbotmr oneToManyRelations and :nber embeddedRelations + * @Given there are :nb multiRelationsDummy objects having each :nbmtor manyToOneRelation, :nbmtmr manyToManyRelations, :nbotmr oneToManyRelations and :nber embeddedRelations */ - public function thereAreMultiRelationsDummyObjectsHavingEachAManyToOneRelationManyToManyRelationsOneToManyRelationsAndEmbeddedRelations(int $nb, int $nbmtmr, int $nbotmr, int $nber): void + public function thereAreMultiRelationsDummyObjectsHavingEachAManyToOneRelationManyToManyRelationsOneToManyRelationsAndEmbeddedRelations(int $nb, int $nbmtor, int $nbmtmr, int $nbotmr, int $nber): void { for ($i = 1; $i <= $nb; ++$i) { $relatedDummy = $this->buildMultiRelationsRelatedDummy(); $relatedDummy->name = 'RelatedManyToOneDummy #'.$i; + $resolveDummy = $this->buildMultiRelationsResolveDummy(); + $resolveDummy->name = 'RelatedManyToOneResolveDummy #'.$i; + $dummy = $this->buildMultiRelationsDummy(); $dummy->name = 'Dummy #'.$i; - $dummy->setManyToOneRelation($relatedDummy); + + if ($nbmtor) { + $dummy->setManyToOneRelation($relatedDummy); + $dummy->setManyToOneResolveRelation($resolveDummy); + } for ($j = 1; $j <= $nbmtmr; ++$j) { $manyToManyItem = $this->buildMultiRelationsRelatedDummy(); @@ -855,6 +864,7 @@ public function thereAreMultiRelationsDummyObjectsHavingEachAManyToOneRelationMa $dummy->setNestedPaginatedCollection($nestedPaginated); $this->manager->persist($relatedDummy); + $this->manager->persist($resolveDummy); $this->manager->persist($dummy); } $this->manager->flush(); @@ -2668,6 +2678,11 @@ private function buildMultiRelationsNestedPaginated(): MultiRelationsNestedPagin return $this->isOrm() ? new MultiRelationsNestedPaginated() : new MultiRelationsNestedPaginatedDocument(); } + private function buildMultiRelationsResolveDummy(): MultiRelationsResolveDummy|MultiRelationsResolveDummyDocument + { + return $this->isOrm() ? new MultiRelationsResolveDummy() : new MultiRelationsResolveDummyDocument(); + } + private function buildMusicGroup(): MusicGroup|MusicGroupDocument { return $this->isOrm() ? new MusicGroup() : new MusicGroupDocument(); diff --git a/tests/Fixtures/TestBundle/Document/MultiRelationsDummy.php b/tests/Fixtures/TestBundle/Document/MultiRelationsDummy.php index 19733044e50..fbddd76ccda 100644 --- a/tests/Fixtures/TestBundle/Document/MultiRelationsDummy.php +++ b/tests/Fixtures/TestBundle/Document/MultiRelationsDummy.php @@ -38,6 +38,9 @@ class MultiRelationsDummy #[ODM\ReferenceOne(targetDocument: MultiRelationsRelatedDummy::class, storeAs: 'id', nullable: true)] public ?MultiRelationsRelatedDummy $manyToOneRelation = null; + #[ODM\ReferenceOne(targetDocument: MultiRelationsResolveDummy::class, storeAs: 'id', nullable: true)] + public ?MultiRelationsResolveDummy $manyToOneResolveRelation = null; + /** @var Collection */ #[ODM\ReferenceMany(targetDocument: MultiRelationsRelatedDummy::class, storeAs: 'id', nullable: true)] public Collection $manyToManyRelations; @@ -77,6 +80,18 @@ public function setManyToOneRelation(?MultiRelationsRelatedDummy $relatedMultiUs $this->manyToOneRelation = $relatedMultiUsedDummy; } + public function getManyToOneResolveRelation(): ?MultiRelationsResolveDummy + { + return $this->manyToOneResolveRelation; + } + + public function setManyToOneResolveRelation(?MultiRelationsResolveDummy $manyToOneResolveRelation): self + { + $this->manyToOneResolveRelation = $manyToOneResolveRelation; + + return $this; + } + public function addManyToManyRelation(MultiRelationsRelatedDummy $relatedMultiUsedDummy): void { $this->manyToManyRelations->add($relatedMultiUsedDummy); diff --git a/tests/Fixtures/TestBundle/Document/MultiRelationsResolveDummy.php b/tests/Fixtures/TestBundle/Document/MultiRelationsResolveDummy.php new file mode 100644 index 00000000000..d8f8199bf19 --- /dev/null +++ b/tests/Fixtures/TestBundle/Document/MultiRelationsResolveDummy.php @@ -0,0 +1,53 @@ + + * + * 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\Document; + +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\GraphQl\Query; +use ApiPlatform\Metadata\GraphQl\QueryCollection; +use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; + +/** + * Dummy used in different kind of relations in the same resource. + * + * @author Thomas Helmrich + */ +#[ApiResource(graphQlOperations: [new Query(resolver: 'app.graphql.query_resolver.multi_relations_custom_item', read: false), new QueryCollection(resolver: 'app.graphql.query_resolver.multi_relations_collection', read: false)])] +#[ODM\Document] +class MultiRelationsResolveDummy +{ + #[ODM\Id(strategy: 'INCREMENT', type: 'int')] + private ?int $id = null; + + #[ODM\Field(type: 'string', nullable: true)] + public ?string $name; + + #[ODM\ReferenceOne(targetDocument: MultiRelationsDummy::class, inversedBy: 'oneToManyResolveRelations', nullable: true, storeAs: 'id')] + private ?MultiRelationsDummy $oneToManyResolveRelation; + + public function getId(): ?int + { + return $this->id; + } + + public function getOneToManyResolveRelation(): ?MultiRelationsDummy + { + return $this->oneToManyResolveRelation; + } + + public function setOneToManyResolveRelation(?MultiRelationsDummy $oneToManyResolveRelation): void + { + $this->oneToManyResolveRelation = $oneToManyResolveRelation; + } +} diff --git a/tests/Fixtures/TestBundle/Entity/MultiRelationsDummy.php b/tests/Fixtures/TestBundle/Entity/MultiRelationsDummy.php index cfc23adc998..f07edcd9313 100644 --- a/tests/Fixtures/TestBundle/Entity/MultiRelationsDummy.php +++ b/tests/Fixtures/TestBundle/Entity/MultiRelationsDummy.php @@ -40,6 +40,9 @@ class MultiRelationsDummy #[ORM\ManyToOne(targetEntity: MultiRelationsRelatedDummy::class)] public ?MultiRelationsRelatedDummy $manyToOneRelation = null; + #[ORM\ManyToOne(targetEntity: MultiRelationsResolveDummy::class)] + public ?MultiRelationsResolveDummy $manyToOneResolveRelation = null; + /** @var Collection */ #[ORM\ManyToMany(targetEntity: MultiRelationsRelatedDummy::class)] public Collection $manyToManyRelations; @@ -79,6 +82,18 @@ public function setManyToOneRelation(?MultiRelationsRelatedDummy $relatedMultiUs $this->manyToOneRelation = $relatedMultiUsedDummy; } + public function getManyToOneResolveRelation(): ?MultiRelationsResolveDummy + { + return $this->manyToOneResolveRelation; + } + + public function setManyToOneResolveRelation(?MultiRelationsResolveDummy $manyToOneResolveRelation): self + { + $this->manyToOneResolveRelation = $manyToOneResolveRelation; + + return $this; + } + public function addManyToManyRelation(MultiRelationsRelatedDummy $relatedMultiUsedDummy): void { $this->manyToManyRelations->add($relatedMultiUsedDummy); diff --git a/tests/Fixtures/TestBundle/Entity/MultiRelationsResolveDummy.php b/tests/Fixtures/TestBundle/Entity/MultiRelationsResolveDummy.php new file mode 100644 index 00000000000..f724803e219 --- /dev/null +++ b/tests/Fixtures/TestBundle/Entity/MultiRelationsResolveDummy.php @@ -0,0 +1,55 @@ + + * + * 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; + +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\GraphQl\Query; +use ApiPlatform\Metadata\GraphQl\QueryCollection; +use Doctrine\ORM\Mapping as ORM; + +/** + * Dummy used in different kind of relations in the same resource. + * + * @author Thomas Helmrich + */ +#[ApiResource(graphQlOperations: [new Query(resolver: 'app.graphql.query_resolver.multi_relations_custom_item', read: false), new QueryCollection(resolver: 'app.graphql.query_resolver.multi_relations_collection', read: false)])] +#[ORM\Entity] +class MultiRelationsResolveDummy +{ + #[ORM\Column(type: 'integer')] + #[ORM\Id] + #[ORM\GeneratedValue(strategy: 'AUTO')] + private ?int $id = null; + + #[ORM\Column(nullable: true)] + public ?string $name; + + #[ORM\ManyToOne(targetEntity: MultiRelationsDummy::class, inversedBy: 'oneToManyResolveRelations')] + private ?MultiRelationsDummy $oneToManyResolveRelation = null; + + public function getId(): ?int + { + return $this->id; + } + + public function getOneToManyResolveRelation(): ?MultiRelationsDummy + { + return $this->oneToManyResolveRelation; + } + + public function setOneToManyResolveRelation(?MultiRelationsDummy $oneToManyResolveRelation): void + { + $this->oneToManyResolveRelation = $oneToManyResolveRelation; + } +} diff --git a/tests/Fixtures/TestBundle/GraphQl/Resolver/DummyCustomQueryNotRetrievedItemDocumentResolver.php b/tests/Fixtures/TestBundle/GraphQl/Resolver/DummyCustomQueryNotRetrievedItemDocumentResolver.php index cbf5d8e9be4..16c7b12bf5e 100644 --- a/tests/Fixtures/TestBundle/GraphQl/Resolver/DummyCustomQueryNotRetrievedItemDocumentResolver.php +++ b/tests/Fixtures/TestBundle/GraphQl/Resolver/DummyCustomQueryNotRetrievedItemDocumentResolver.php @@ -15,7 +15,6 @@ use ApiPlatform\GraphQl\Resolver\QueryItemResolverInterface; use ApiPlatform\Tests\Fixtures\TestBundle\Document\DummyCustomQuery as DummyCustomQueryDocument; -use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyCustomQuery; /** * Resolver for dummy item custom query. @@ -24,7 +23,7 @@ */ class DummyCustomQueryNotRetrievedItemDocumentResolver implements QueryItemResolverInterface { - public function __invoke(?object $item, array $context): DummyCustomQuery|DummyCustomQueryDocument + public function __invoke(?object $item, array $context): DummyCustomQueryDocument { if (null === $item) { $item = new DummyCustomQueryDocument(); diff --git a/tests/Fixtures/TestBundle/GraphQl/Resolver/DummyCustomQueryNotRetrievedItemResolver.php b/tests/Fixtures/TestBundle/GraphQl/Resolver/DummyCustomQueryNotRetrievedItemResolver.php index 3d9ce628dee..fad1088d953 100644 --- a/tests/Fixtures/TestBundle/GraphQl/Resolver/DummyCustomQueryNotRetrievedItemResolver.php +++ b/tests/Fixtures/TestBundle/GraphQl/Resolver/DummyCustomQueryNotRetrievedItemResolver.php @@ -14,7 +14,6 @@ namespace ApiPlatform\Tests\Fixtures\TestBundle\GraphQl\Resolver; use ApiPlatform\GraphQl\Resolver\QueryItemResolverInterface; -use ApiPlatform\Tests\Fixtures\TestBundle\Document\DummyCustomQuery as DummyCustomQueryDocument; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyCustomQuery; /** @@ -24,7 +23,7 @@ */ class DummyCustomQueryNotRetrievedItemResolver implements QueryItemResolverInterface { - public function __invoke(?object $item, array $context): DummyCustomQuery|DummyCustomQueryDocument + public function __invoke(?object $item, array $context): DummyCustomQuery { if (null === $item) { $item = new DummyCustomQuery(); diff --git a/tests/Fixtures/TestBundle/GraphQl/Resolver/MultiRelationsResolveQueryItemResolver.php b/tests/Fixtures/TestBundle/GraphQl/Resolver/MultiRelationsResolveQueryItemResolver.php new file mode 100644 index 00000000000..9559d33a3c8 --- /dev/null +++ b/tests/Fixtures/TestBundle/GraphQl/Resolver/MultiRelationsResolveQueryItemResolver.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\GraphQl\Resolver; + +use ApiPlatform\GraphQl\Resolver\QueryItemResolverInterface; +use ApiPlatform\Tests\Fixtures\TestBundle\Document\MultiRelationsResolveDummy as MultiRelationsResolveDummyDocument; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\MultiRelationsResolveDummy; + +/** + * Resolver for dummy item custom query. + * + * @author Lukas Lücke + */ +class MultiRelationsResolveQueryItemResolver implements QueryItemResolverInterface +{ + public function __invoke(?object $item, array $context): MultiRelationsResolveDummy|MultiRelationsResolveDummyDocument + { + return $context['source']['manyToOneResolveRelation']; + } +} diff --git a/tests/Fixtures/app/config/config_common.yml b/tests/Fixtures/app/config/config_common.yml index 7bd2b6b82a5..d9edd6f3308 100644 --- a/tests/Fixtures/app/config/config_common.yml +++ b/tests/Fixtures/app/config/config_common.yml @@ -301,6 +301,11 @@ services: tags: - name: 'api_platform.graphql.resolver' + app.graphql.query_resolver.multi_relations_custom_item: + class: 'ApiPlatform\Tests\Fixtures\TestBundle\GraphQl\Resolver\MultiRelationsResolveQueryItemResolver' + tags: + - { name: 'api_platform.graphql.resolver' } + app.graphql.mutation_resolver.upload_media_object: class: 'ApiPlatform\Tests\Fixtures\TestBundle\GraphQl\Resolver\UploadMediaObjectResolver' tags: diff --git a/tests/Fixtures/app/config/config_mongodb.yml b/tests/Fixtures/app/config/config_mongodb.yml index 8f42de9e3ab..975a11aab8d 100644 --- a/tests/Fixtures/app/config/config_mongodb.yml +++ b/tests/Fixtures/app/config/config_mongodb.yml @@ -110,6 +110,11 @@ services: tags: - name: 'api_platform.graphql.resolver' + app.graphql.query_resolver.multi_relations_custom_item_document: + class: 'ApiPlatform\Tests\Fixtures\TestBundle\GraphQl\Resolver\MultiRelationsResolveQueryItemResolver' + tags: + - { name: 'api_platform.graphql.resolver' } + app.graphql.mutation_resolver.dummy_custom_only_persist_document: class: 'ApiPlatform\Tests\Fixtures\TestBundle\GraphQl\Resolver\SumOnlyPersistDocumentMutationResolver' public: false diff --git a/tests/Symfony/Routing/ApiLoaderTest.php b/tests/Symfony/Routing/ApiLoaderTest.php index 6766527825f..0c9ba60f018 100644 --- a/tests/Symfony/Routing/ApiLoaderTest.php +++ b/tests/Symfony/Routing/ApiLoaderTest.php @@ -213,6 +213,7 @@ public function testApiLoaderWithPrefix(): void $prefixedPath = $prefix.$path; + $this->assertNotNull($routeCollection->get('api_errors')); $this->assertEquals( $this->getRoute( $prefixedPath,