diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ce9cb96450..cdaebd63f18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ * OpenAPI: Fix error when schema is empty (#4051) * OpenAPI: Do not set scheme to oauth2 when generating securitySchemes (#4073) * OpenAPI: Fix missing `$ref` when no `type` is used in context (#4076) +* GraphQL: Fix "Resource class cannot be determined." error when a null iterable field is returned (#4092) ## 2.6.2 diff --git a/features/graphql/query.feature b/features/graphql/query.feature index 149d39eaef1..fc8441da537 100644 --- a/features/graphql/query.feature +++ b/features/graphql/query.feature @@ -59,6 +59,23 @@ Feature: GraphQL query support And the JSON node "data.dummy.jsonData.bar" should be equal to 5 And the JSON node "data.dummy.arrayData[2]" should be equal to baz + Scenario: Retrieve an item with an iterable null field + Given there are 2 dummy with null JSON objects + When I send the following GraphQL request: + """ + { + withJsonDummy(id: "/with_json_dummies/2") { + id + json + } + } + """ + 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 "data.withJsonDummy.id" should be equal to "/with_json_dummies/2" + And the JSON node "data.withJsonDummy.json" should be null + Scenario: Retrieve an item through a GraphQL query with variables When I have the following GraphQL request: """ diff --git a/src/GraphQl/Resolver/Factory/ItemResolverFactory.php b/src/GraphQl/Resolver/Factory/ItemResolverFactory.php index 1b257b94f69..353834b397b 100644 --- a/src/GraphQl/Resolver/Factory/ItemResolverFactory.php +++ b/src/GraphQl/Resolver/Factory/ItemResolverFactory.php @@ -59,7 +59,7 @@ public function __invoke(?string $resourceClass = null, ?string $rootClass = nul { return function (?array $source, array $args, $context, ResolveInfo $info) use ($resourceClass, $rootClass, $operationName) { // Data already fetched and normalized (field or nested resource) - if (isset($source[$info->fieldName])) { + if ($source && array_key_exists($info->fieldName, $source)) { return $source[$info->fieldName]; } diff --git a/tests/Behat/DoctrineContext.php b/tests/Behat/DoctrineContext.php index a62255ed6b5..bbbc943c763 100644 --- a/tests/Behat/DoctrineContext.php +++ b/tests/Behat/DoctrineContext.php @@ -80,6 +80,7 @@ use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\ThirdLevel as ThirdLevelDocument; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\UrlEncodedId as UrlEncodedIdDocument; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\User as UserDocument; +use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\WithJsonDummy as WithJsonDummyDocument; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\AbsoluteUrlDummy; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\AbsoluteUrlRelationDummy; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Address; @@ -154,6 +155,7 @@ use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\UrlEncodedId; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\User; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\UuidIdentifierDummy; +use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\WithJsonDummy; use Behat\Behat\Context\Context; use Behat\Gherkin\Node\PyStringNode; use Doctrine\ODM\MongoDB\DocumentManager; @@ -573,6 +575,21 @@ public function thereAreDummyObjectsWithJsonData(int $nb) $this->manager->flush(); } + /** + * @Given there are :nb dummy with null JSON objects + */ + public function thereAreDummyWithNullJsonObjects(int $nb) + { + for ($i = 1; $i <= $nb; ++$i) { + $dummy = $this->buildWithJsonDummy(); + $dummy->json = null; + + $this->manager->persist($dummy); + } + + $this->manager->flush(); + } + /** * @Given there are :nb dummy objects with relatedDummy and its thirdLevel * @Given there is :nb dummy object with relatedDummy and its thirdLevel @@ -2190,4 +2207,12 @@ private function buildCustomMultipleIdentifierDummy() { return $this->isOrm() ? new CustomMultipleIdentifierDummy() : new CustomMultipleIdentifierDummyDocument(); } + + /** + * @return WithJsonDummy|WithJsonDummyDocument + */ + private function buildWithJsonDummy() + { + return $this->isOrm() ? new WithJsonDummy() : new WithJsonDummyDocument(); + } } diff --git a/tests/Fixtures/TestBundle/Document/WithJsonDummy.php b/tests/Fixtures/TestBundle/Document/WithJsonDummy.php new file mode 100644 index 00000000000..5c33e4ee42c --- /dev/null +++ b/tests/Fixtures/TestBundle/Document/WithJsonDummy.php @@ -0,0 +1,43 @@ + + * + * 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\Fixtures\TestBundle\Document; + +use ApiPlatform\Core\Annotation\ApiResource; +use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; + +/** + * @ApiResource + * @ODM\Document + */ +class WithJsonDummy +{ + /** + * @var int + * + * @ODM\Id(strategy="INCREMENT", type="int", nullable=true) + */ + private $id; + + /** + * @var ?array + * + * @ODM\Field(type="hash", nullable=true) + */ + public $json; + + public function getId(): int + { + return $this->id; + } +} diff --git a/tests/Fixtures/TestBundle/Entity/WithJsonDummy.php b/tests/Fixtures/TestBundle/Entity/WithJsonDummy.php new file mode 100644 index 00000000000..b3998f858b7 --- /dev/null +++ b/tests/Fixtures/TestBundle/Entity/WithJsonDummy.php @@ -0,0 +1,45 @@ + + * + * 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\Fixtures\TestBundle\Entity; + +use ApiPlatform\Core\Annotation\ApiResource; +use Doctrine\ORM\Mapping as ORM; + +/** + * @ApiResource + * @ORM\Entity + */ +class WithJsonDummy +{ + /** + * @var int + * + * @ORM\Column(type="integer", nullable=true) + * @ORM\Id + * @ORM\GeneratedValue(strategy="AUTO") + */ + private $id; + + /** + * @var ?array + * + * @ORM\Column(type="json", nullable=true) + */ + public $json; + + public function getId(): int + { + return $this->id; + } +} diff --git a/tests/GraphQl/Resolver/Factory/ItemResolverFactoryTest.php b/tests/GraphQl/Resolver/Factory/ItemResolverFactoryTest.php index 69cc1b31576..06d2c66f627 100644 --- a/tests/GraphQl/Resolver/Factory/ItemResolverFactoryTest.php +++ b/tests/GraphQl/Resolver/Factory/ItemResolverFactoryTest.php @@ -118,6 +118,15 @@ public function testResolveNested(): void $this->assertSame(['already_serialized'], ($this->itemResolverFactory)('resourceClass')($source, [], null, $info)); } + public function testResolveNestedNullValue(): void + { + $source = ['nestedNullValue' => null]; + $info = $this->prophesize(ResolveInfo::class)->reveal(); + $info->fieldName = 'nestedNullValue'; + + $this->assertNull(($this->itemResolverFactory)('resourceClass')($source, [], null, $info)); + } + public function testResolveBadReadStageItem(): void { $resourceClass = 'stdClass';