diff --git a/features/graphql/query.feature b/features/graphql/query.feature index 69fcdf40326..9629a759460 100644 --- a/features/graphql/query.feature +++ b/features/graphql/query.feature @@ -677,3 +677,50 @@ Feature: GraphQL query support Then the response status code should be 200 And the header "Content-Type" should be equal to "application/json" And the JSON node "data.getSecurityAfterResolver.name" should be equal to "test" + + @createSchema + @!mongodb + Scenario: Retrieve an item with a subResource collection within an DTO using stateOptions entityClass with an GraphQL query + Given there is an issue6590 foo object with 2 bar sub objects + When I send the following GraphQL request: + """ + { + issue6590OrmFoo(id: "/issue6590_orm_foos/1") { + id + bars { + 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 should be equal to: + """ + { + "data": { + "issue6590OrmFoo": { + "id": "/issue6590_orm_foos/1", + "bars": { + "edges": [ + { + "node": { + "name": "bar1" + } + }, + { + "node": { + "name": "bar2" + } + } + ] + } + } + } + } + """ diff --git a/src/Doctrine/Orm/State/LinksHandlerTrait.php b/src/Doctrine/Orm/State/LinksHandlerTrait.php index 9a088a4e87a..4355a747ba8 100644 --- a/src/Doctrine/Orm/State/LinksHandlerTrait.php +++ b/src/Doctrine/Orm/State/LinksHandlerTrait.php @@ -40,7 +40,7 @@ private function handleLinks(QueryBuilder $queryBuilder, array $identifiers, Que $doctrineClassMetadata = $manager->getClassMetadata($entityClass); $alias = $queryBuilder->getRootAliases()[0]; - $links = $this->getLinks($entityClass, $operation, $context); + $links = $this->getLinks($context['resource_class'] ?? $entityClass, $operation, $context); if (!$links) { return; diff --git a/tests/Behat/DoctrineContext.php b/tests/Behat/DoctrineContext.php index 4639644beb4..7e0e1e94f1b 100644 --- a/tests/Behat/DoctrineContext.php +++ b/tests/Behat/DoctrineContext.php @@ -160,6 +160,8 @@ 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\Issue6590\Bar as Issue6590BarDummy; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue6590\Foo as Issue6590FooDummy; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\LinkHandledDummy; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\MaxDepthDummy; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\MultiRelationsDummy; @@ -2325,6 +2327,27 @@ public function thereAreIssue6039Users(): void $this->manager->flush(); } + /** + * @Given there is an issue6590 foo object with 2 bar sub objects + */ + public function thereIsAIssue6590DtoFooObjectWith2BarSubObjects(): void + { + $foo = new Issue6590FooDummy(); + $this->manager->persist($foo); + + $bar1 = new Issue6590BarDummy(); + $bar1->setName('bar1'); + $bar1->setFoo($foo); + $this->manager->persist($bar1); + + $bar2 = new Issue6590BarDummy(); + $bar2->setName('bar2'); + $bar2->setFoo($foo); + $this->manager->persist($bar2); + + $this->manager->flush(); + } + private function isOrm(): bool { return null !== $this->schemaTool; diff --git a/tests/Fixtures/TestBundle/ApiResource/Issue6590/OrmBarResource.php b/tests/Fixtures/TestBundle/ApiResource/Issue6590/OrmBarResource.php new file mode 100644 index 00000000000..db547f5d420 --- /dev/null +++ b/tests/Fixtures/TestBundle/ApiResource/Issue6590/OrmBarResource.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\Tests\Fixtures\TestBundle\ApiResource\Issue6590; + +use ApiPlatform\Doctrine\Orm\State\Options; +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\GraphQl\Query; +use ApiPlatform\Metadata\GraphQl\QueryCollection; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue6590\Bar; +use ApiPlatform\Tests\Fixtures\TestBundle\State\Issue6590\BarResourceProvider; + +#[ApiResource( + shortName: 'Issue6590OrmBar', + operations: [], + graphQlOperations: [ + new Query(), + new QueryCollection(), + ], + provider: BarResourceProvider::class, + stateOptions: new Options(entityClass: Bar::class) +)] +class OrmBarResource +{ + #[ApiProperty(identifier: true)] + public int $id; + + public string $name; +} diff --git a/tests/Fixtures/TestBundle/ApiResource/Issue6590/OrmFooResource.php b/tests/Fixtures/TestBundle/ApiResource/Issue6590/OrmFooResource.php new file mode 100644 index 00000000000..c0227572c87 --- /dev/null +++ b/tests/Fixtures/TestBundle/ApiResource/Issue6590/OrmFooResource.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\ApiResource\Issue6590; + +use ApiPlatform\Doctrine\Orm\State\Options; +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\GraphQl\Query; +use ApiPlatform\Metadata\GraphQl\QueryCollection; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue6590\Foo; +use ApiPlatform\Tests\Fixtures\TestBundle\State\Issue6590\FooResourceProvider; + +#[ApiResource( + shortName: 'Issue6590OrmFoo', + operations: [], + graphQlOperations: [ + new Query(), + new QueryCollection(), + ], + provider: FooResourceProvider::class, + stateOptions: new Options(entityClass: Foo::class) +)] +class OrmFooResource +{ + #[ApiProperty(identifier: true)] + public int $id; + + /** + * @var OrmBarResource[] + */ + public array $bars; + + public function addBar(OrmBarResource $bar): void + { + $this->bars[] = $bar; + } + + public function removeBar(OrmBarResource $bar): void + { + unset($this->bars[array_search($bar, $this->bars, true)]); + } +} diff --git a/tests/Fixtures/TestBundle/Entity/Issue6590/Bar.php b/tests/Fixtures/TestBundle/Entity/Issue6590/Bar.php new file mode 100644 index 00000000000..7da419cc18b --- /dev/null +++ b/tests/Fixtures/TestBundle/Entity/Issue6590/Bar.php @@ -0,0 +1,61 @@ + + * + * 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\Issue6590; + +use Doctrine\ORM\Mapping as ORM; + +#[ORM\Entity()] +#[ORM\Table(name: 'bar6590')] +class Bar +{ + #[ORM\Id] + #[ORM\Column(type: 'integer')] + #[ORM\GeneratedValue(strategy: 'AUTO')] + private int $id; + + #[ORM\Column] + private string $name; + + #[ORM\ManyToOne(targetEntity: Foo::class, inversedBy: 'bars')] + private ?Foo $foo = null; + + public function getId(): int + { + return $this->id; + } + + public function getName(): string + { + return $this->name; + } + + public function setName(string $name): self + { + $this->name = $name; + + return $this; + } + + public function getFoo(): ?Foo + { + return $this->foo; + } + + public function setFoo(?Foo $foo): self + { + $this->foo = $foo; + + return $this; + } +} diff --git a/tests/Fixtures/TestBundle/Entity/Issue6590/Foo.php b/tests/Fixtures/TestBundle/Entity/Issue6590/Foo.php new file mode 100644 index 00000000000..d930611b1e9 --- /dev/null +++ b/tests/Fixtures/TestBundle/Entity/Issue6590/Foo.php @@ -0,0 +1,59 @@ + + * + * 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\Issue6590; + +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Doctrine\ORM\Mapping as ORM; + +#[ORM\Entity] +#[ORM\Table(name: 'foo6590')] +class Foo +{ + #[ORM\Id] + #[ORM\Column(type: 'integer')] + #[ORM\GeneratedValue(strategy: 'AUTO')] + private int $id; + + #[ORM\OneToMany(targetEntity: Bar::class, mappedBy: 'foo')] + private Collection $bars; + + public function __construct() + { + $this->bars = new ArrayCollection(); + } + + public function getId(): int + { + return $this->id; + } + + /** + * @return Collection + */ + public function getBars(): Collection + { + return $this->bars; + } + + /** + * @param Collection $bars + */ + public function setBars(Collection $bars): self + { + $this->bars = $bars; + + return $this; + } +} diff --git a/tests/Fixtures/TestBundle/State/Issue6590/BarResourceProvider.php b/tests/Fixtures/TestBundle/State/Issue6590/BarResourceProvider.php new file mode 100644 index 00000000000..474f40745ca --- /dev/null +++ b/tests/Fixtures/TestBundle/State/Issue6590/BarResourceProvider.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\Tests\Fixtures\TestBundle\State\Issue6590; + +use ApiPlatform\Doctrine\Orm\Paginator; +use ApiPlatform\Metadata\CollectionOperationInterface; +use ApiPlatform\Metadata\Operation; +use ApiPlatform\State\Pagination\TraversablePaginator; +use ApiPlatform\State\ProviderInterface; +use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue6590\OrmBarResource; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue6590\Bar as BarEntity; + +class BarResourceProvider implements ProviderInterface +{ + public function __construct( + private readonly ProviderInterface $itemProvider, + private readonly ProviderInterface $collectionProvider, + ) { + } + + public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null + { + if ($operation instanceof CollectionOperationInterface) { + $entities = $this->collectionProvider->provide($operation, $uriVariables, $context); + \assert($entities instanceof Paginator); + + $resources = []; + foreach ($entities as $entity) { + $resources[] = $this->getResource($entity); + } + + return new TraversablePaginator( + new \ArrayIterator($resources), + $entities->getCurrentPage(), + $entities->getItemsPerPage(), + $entities->getTotalItems() + ); + } + + $entity = $this->itemProvider->provide($operation, $uriVariables, $context); + + return $this->getResource($entity); + } + + protected function getResource(BarEntity $entity): OrmBarResource + { + $resource = new OrmBarResource(); + $resource->id = $entity->getId(); + $resource->name = $entity->getName(); + + return $resource; + } +} diff --git a/tests/Fixtures/TestBundle/State/Issue6590/FooResourceProvider.php b/tests/Fixtures/TestBundle/State/Issue6590/FooResourceProvider.php new file mode 100644 index 00000000000..4bbbb782cda --- /dev/null +++ b/tests/Fixtures/TestBundle/State/Issue6590/FooResourceProvider.php @@ -0,0 +1,71 @@ + + * + * 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\State\Issue6590; + +use ApiPlatform\Doctrine\Orm\Paginator; +use ApiPlatform\Metadata\CollectionOperationInterface; +use ApiPlatform\Metadata\Operation; +use ApiPlatform\State\Pagination\TraversablePaginator; +use ApiPlatform\State\ProviderInterface; +use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue6590\OrmBarResource; +use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue6590\OrmFooResource; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue6590\Foo as FooEntity; + +class FooResourceProvider implements ProviderInterface +{ + public function __construct( + private readonly ProviderInterface $itemProvider, + private readonly ProviderInterface $collectionProvider, + ) { + } + + public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null + { + if ($operation instanceof CollectionOperationInterface) { + $entities = $this->collectionProvider->provide($operation, $uriVariables, $context); + \assert($entities instanceof Paginator); + + $resources = []; + foreach ($entities as $entity) { + $resources[] = $this->getResource($entity); + } + + return new TraversablePaginator( + new \ArrayIterator($resources), + $entities->getCurrentPage(), + $entities->getItemsPerPage(), + $entities->getTotalItems() + ); + } + + $entity = $this->itemProvider->provide($operation, $uriVariables, $context); + + return $this->getResource($entity); + } + + protected function getResource(FooEntity $entity): OrmFooResource + { + $resource = new OrmFooResource(); + $resource->id = $entity->getId(); + + foreach ($entity->getBars() as $barEntity) { + $barResource = new OrmBarResource(); + $barResource->id = $barEntity->getId(); + $barResource->name = $barEntity->getName(); + $resource->bars[] = $barResource; + } + + return $resource; + } +} diff --git a/tests/Fixtures/app/config/config_doctrine.yml b/tests/Fixtures/app/config/config_doctrine.yml index 929fb6289ec..fb9172f5501 100644 --- a/tests/Fixtures/app/config/config_doctrine.yml +++ b/tests/Fixtures/app/config/config_doctrine.yml @@ -145,3 +145,19 @@ services: parent: 'api_platform.doctrine.orm.order_filter' arguments: [ { id: 'ASC', foo: 'DESC' } ] tags: [ 'api_platform.filter' ] + + ApiPlatform\Tests\Fixtures\TestBundle\State\Issue6590\FooResourceProvider: + class: 'ApiPlatform\Tests\Fixtures\TestBundle\State\Issue6590\FooResourceProvider' + tags: + - name: 'api_platform.state_provider' + arguments: + $itemProvider: '@ApiPlatform\Doctrine\Orm\State\ItemProvider' + $collectionProvider: '@ApiPlatform\Doctrine\Orm\State\CollectionProvider' + + ApiPlatform\Tests\Fixtures\TestBundle\State\Issue6590\BarResourceProvider: + class: 'ApiPlatform\Tests\Fixtures\TestBundle\State\Issue6590\BarResourceProvider' + tags: + - name: 'api_platform.state_provider' + arguments: + $itemProvider: '@ApiPlatform\Doctrine\Orm\State\ItemProvider' + $collectionProvider: '@ApiPlatform\Doctrine\Orm\State\CollectionProvider'