Skip to content

Commit 2b5031d

Browse files
committed
Merge 4.3
2 parents 6cac23a + 8586a80 commit 2b5031d

7 files changed

Lines changed: 137 additions & 14 deletions

File tree

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@
142142
"jangregor/phpstan-prophecy": "^2.1.11",
143143
"justinrainbow/json-schema": "^6.5.2",
144144
"laravel/framework": "^11.0 || ^12.0 || ^13.0",
145-
"mcp/sdk": ">=0.4 <1.0",
145+
"mcp/sdk": "^0.6",
146146
"orchestra/testbench": "^10.9 || ^11.0",
147147
"phpspec/prophecy-phpunit": "^2.2",
148148
"phpstan/extension-installer": "^1.1",

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;

src/Mcp/Capability/Registry/Loader.php

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
use Mcp\Capability\Registry\Loader\LoaderInterface;
2424
use Mcp\Capability\RegistryInterface;
2525
use Mcp\Schema\Annotations;
26-
use Mcp\Schema\Resource;
2726
use Mcp\Schema\ResourceDefinition;
2827
use Mcp\Schema\Tool;
2928
use Mcp\Schema\ToolAnnotations;
@@ -73,15 +72,12 @@ public function load(RegistryInterface $registry): void
7372
outputSchema: $outputSchema,
7473
),
7574
self::HANDLER,
76-
true,
7775
);
7876
}
7977

8078
if ($mcp instanceof McpResource) {
81-
// mcp/sdk 0.6 renamed Mcp\Schema\Resource to ResourceDefinition; support both while symfony/mcp-bundle still pins ^0.5
82-
$resourceClass = class_exists(ResourceDefinition::class) ? ResourceDefinition::class : Resource::class;
8379
$registry->registerResource(
84-
new $resourceClass(
80+
new ResourceDefinition(
8581
uri: $mcp->getUri(),
8682
name: $mcp->getName(),
8783
description: $mcp->getDescription(),
@@ -92,7 +88,6 @@ public function load(RegistryInterface $registry): void
9288
meta: $mcp->getMeta()
9389
),
9490
self::HANDLER,
95-
true,
9691
);
9792
}
9893
}

src/Mcp/Tests/Capability/Registry/LoaderTest.php

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,6 @@ class: \stdClass::class,
7171
return true;
7272
}),
7373
Loader::HANDLER,
74-
true,
7574
);
7675

7776
$loader = new Loader($nameCollectionFactory, $metadataCollectionFactory, $schemaFactory);
@@ -114,7 +113,6 @@ class: \stdClass::class,
114113
return true;
115114
}),
116115
Loader::HANDLER,
117-
true,
118116
);
119117

120118
$loader = new Loader($nameCollectionFactory, $metadataCollectionFactory, $schemaFactory);
@@ -154,7 +152,6 @@ class: \stdClass::class,
154152
return true;
155153
}),
156154
Loader::HANDLER,
157-
true,
158155
);
159156

160157
$loader = new Loader($nameCollectionFactory, $metadataCollectionFactory, $schemaFactory);

src/Mcp/composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
"php": ">=8.2",
3131
"api-platform/metadata": "^4.3",
3232
"api-platform/json-schema": "^4.3",
33-
"mcp/sdk": ">=0.4 <1.0",
33+
"mcp/sdk": "^0.6",
3434
"symfony/object-mapper": "^7.4 || ^8.0",
3535
"symfony/polyfill-php85": "^1.32"
3636
},
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)