Skip to content

Commit 9777141

Browse files
authored
fix(doctrine): exclude self-reference relation links from GraphQL root item lookup (#8314)
1 parent 0d9bcde commit 9777141

3 files changed

Lines changed: 134 additions & 3 deletions

File tree

src/Doctrine/Common/State/LinksHandlerTrait.php

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,12 @@ private function getLinks(string $resourceClass, Operation $operation, array $co
3636

3737
if (!($linkClass = $context['linkClass'] ?? false)) {
3838
// Root item lookup: GraphQl Query.links carries relation links (for nested traversal)
39-
// and the identifier-self link. Keep only the identifier-self / self-references so
40-
// handleLinks applies WHERE id=X without consuming identifiers via relation joins.
39+
// and the identifier-self link. Keep only the identifier-self link so handleLinks
40+
// applies WHERE id=X. A relation link always carries a fromProperty/toProperty (even a
41+
// self-reference whose toClass equals the resource class, e.g. a ManyToOne to self), so
42+
// excluding those prevents consuming the identifier via a bogus relation join.
4143
if ($operation instanceof GraphQlOperation) {
42-
return array_values(array_filter($links, static fn ($l) => null === $l->getToClass() || $l->getToClass() === $resourceClass));
44+
return array_values(array_filter($links, static fn ($l) => !$l->getFromProperty() && !$l->getToProperty() && (null === $l->getToClass() || $l->getToClass() === $resourceClass)));
4345
}
4446

4547
return $links;
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <dunglas@gmail.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Tests\Fixtures\TestBundle\Entity\GraphQlMappedSelfReference;
15+
16+
use ApiPlatform\Metadata\ApiResource;
17+
use ApiPlatform\Metadata\GraphQl\Query;
18+
use ApiPlatform\Metadata\GraphQl\QueryCollection;
19+
use Doctrine\ORM\Mapping as ORM;
20+
21+
#[ApiResource(
22+
operations: [],
23+
graphQlOperations: [
24+
new Query(),
25+
new QueryCollection(),
26+
]
27+
)]
28+
#[ORM\Entity]
29+
class MappedSelfReference
30+
{
31+
#[ORM\Id]
32+
#[ORM\Column(type: 'integer')]
33+
#[ORM\GeneratedValue(strategy: 'AUTO')]
34+
public ?int $id = null;
35+
36+
#[ORM\Column(type: 'string')]
37+
public ?string $name = null;
38+
39+
// A real Doctrine association pointing back to the same resource class. Its
40+
// link has toClass === resourceClass, so the GraphQL root-item filter must
41+
// not mistake it for the identifier-self link and emit a bogus self-join.
42+
#[ORM\ManyToOne(targetEntity: self::class)]
43+
#[ORM\JoinColumn(name: 'parent_id', referencedColumnName: 'id', nullable: true)]
44+
public ?self $parent = null;
45+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <dunglas@gmail.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Tests\Functional\GraphQl;
15+
16+
use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
17+
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\GraphQlMappedSelfReference\MappedSelfReference;
18+
use ApiPlatform\Tests\RecreateSchemaTrait;
19+
use ApiPlatform\Tests\SetupClassResourcesTrait;
20+
21+
/**
22+
* A GraphQL item query on a self-referencing entity (a mapped ManyToOne pointing
23+
* back to the same resource class) must apply WHERE id=X only. The relation link
24+
* has toClass === resourceClass like the identifier-self link, but it must not
25+
* survive the root-item filter, otherwise handleLinks emits a bogus self-join
26+
* plus an extra WHERE condition and the item always resolves to null.
27+
*
28+
* @see https://github.com/api-platform/core/issues/8305
29+
*/
30+
final class MappedSelfReferenceTest extends ApiTestCase
31+
{
32+
use RecreateSchemaTrait;
33+
use SetupClassResourcesTrait;
34+
35+
protected static ?bool $alwaysBootKernel = false;
36+
37+
/**
38+
* @return class-string[]
39+
*/
40+
public static function getResources(): array
41+
{
42+
return [MappedSelfReference::class];
43+
}
44+
45+
public function testItemQueryOnMappedSelfReferenceReturnsTheRecord(): void
46+
{
47+
if ($this->isMongoDB()) {
48+
$this->markTestSkipped('MappedSelfReference is ORM-only.');
49+
}
50+
51+
$this->recreateSchema([MappedSelfReference::class]);
52+
53+
$manager = $this->getManager();
54+
$root = new MappedSelfReference();
55+
$root->name = 'Root';
56+
$manager->persist($root);
57+
$child = new MappedSelfReference();
58+
$child->name = 'Child';
59+
$child->parent = $root;
60+
$manager->persist($child);
61+
$manager->flush();
62+
63+
// The child's parent is the root, not itself: the bogus self-join
64+
// (WHERE parent.id = child.id) would never match, so this proves the
65+
// self-reference link is excluded from the root item lookup.
66+
$iri = '/mapped_self_references/'.$child->id;
67+
68+
$response = self::createClient()->request('POST', '/graphql', ['json' => [
69+
'query' => <<<GRAPHQL
70+
{
71+
mappedSelfReference(id: "{$iri}") {
72+
id
73+
name
74+
}
75+
}
76+
GRAPHQL,
77+
]]);
78+
79+
$this->assertResponseIsSuccessful();
80+
$json = $response->toArray(false);
81+
$this->assertArrayNotHasKey('errors', $json, json_encode($json['errors'] ?? null));
82+
$this->assertSame('Child', $json['data']['mappedSelfReference']['name']);
83+
}
84+
}

0 commit comments

Comments
 (0)