Skip to content

Commit 2dcddb4

Browse files
committed
Merge branch 3.0
2 parents 7f09a26 + e738785 commit 2dcddb4

23 files changed

+678
-41
lines changed

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
# Changelog
22

3+
## v3.0.7
4+
5+
### Bug fixes
6+
7+
* [27af3216f](https://github.com/api-platform/core/commit/27af3216f2beac654acb7881b52b3e2e29bf9078) fix(symfony): wire Symfony JsonEncoder if it exists (#5240)
8+
* [31215c623](https://github.com/api-platform/core/commit/31215c62365c6b9095486c307d29837e53c0357a) ci: fix mongod startup (#5248)
9+
* [55be4ca41](https://github.com/api-platform/core/commit/55be4ca41b6a97004d4be623d55bd5e7a3004b16) fix: get back return phpdoc on ProviderInterface
10+
* [6d38cd941](https://github.com/api-platform/core/commit/6d38cd94140edd573ef9b09997204ef345360880) fix(metadata): include routePrefix in default operation name (#5203) (#5252)
11+
* [b52161f](https://github.com/api-platform/core/commit/b52161f75cbfb8fd42b79db8b62e38747c84f089) perf(symfony): use default cache pool config in development environment (#5242)
12+
313
## v3.0.6
414

515
### Bug fixes

features/graphql/query.feature

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,54 @@ Feature: GraphQL query support
2020
And the JSON node "data.dummy.name" should be equal to "Dummy #1"
2121
And the JSON node "data.dummy.name_converted" should be equal to "Converted 1"
2222

23+
@createSchema
24+
Scenario: Retrieve an item with different relations to the same resource
25+
Given there are 2 multiRelationsDummy objects having each a manyToOneRelation, 2 manyToManyRelations and 3 oneToManyRelations
26+
When I send the following GraphQL request:
27+
"""
28+
{
29+
multiRelationsDummy(id: "/multi_relations_dummies/2") {
30+
id
31+
name
32+
manyToOneRelation {
33+
id
34+
name
35+
}
36+
manyToManyRelations {
37+
edges{
38+
node {
39+
id
40+
name
41+
}
42+
}
43+
}
44+
oneToManyRelations {
45+
edges{
46+
node {
47+
id
48+
name
49+
}
50+
}
51+
}
52+
}
53+
}
54+
"""
55+
Then the response status code should be 200
56+
And the response should be in JSON
57+
And the header "Content-Type" should be equal to "application/json"
58+
And the JSON node "data.multiRelationsDummy.id" should be equal to "/multi_relations_dummies/2"
59+
And the JSON node "data.multiRelationsDummy.name" should be equal to "Dummy #2"
60+
And the JSON node "data.multiRelationsDummy.manyToOneRelation.id" should not be null
61+
And the JSON node "data.multiRelationsDummy.manyToOneRelation.name" should be equal to "RelatedManyToOneDummy #2"
62+
And the JSON node "data.multiRelationsDummy.manyToManyRelations.edges" should have 2 element
63+
And the JSON node "data.multiRelationsDummy.manyToManyRelations.edges[1].node.id" should not be null
64+
And the JSON node "data.multiRelationsDummy.manyToManyRelations.edges[0].node.name" should be equal to "RelatedManyToManyDummy12"
65+
And the JSON node "data.multiRelationsDummy.manyToManyRelations.edges[1].node.name" should be equal to "RelatedManyToManyDummy22"
66+
And the JSON node "data.multiRelationsDummy.oneToManyRelations.edges" should have 3 element
67+
And the JSON node "data.multiRelationsDummy.oneToManyRelations.edges[1].node.id" should not be null
68+
And the JSON node "data.multiRelationsDummy.oneToManyRelations.edges[0].node.name" should be equal to "RelatedOneToManyDummy12"
69+
And the JSON node "data.multiRelationsDummy.oneToManyRelations.edges[2].node.name" should be equal to "RelatedOneToManyDummy32"
70+
2371
@createSchema
2472
Scenario: Retrieve a Relay Node
2573
Given there are 2 dummy objects with relatedDummy
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
Feature: Table inheritance
2+
In order to use the api with Doctrine table inheritance
3+
As a client software developer
4+
I need to be able to create resources and fetch them on the upper entity
5+
6+
Background:
7+
Given I add "Accept" header equal to "application/hal+json"
8+
And I add "Content-Type" header equal to "application/json"
9+
10+
@createSchema
11+
Scenario: Create a table inherited resource
12+
And I send a "POST" request to "/dummy_table_inheritance_children" with body:
13+
"""
14+
{
15+
"name": "foo",
16+
"nickname": "bar"
17+
}
18+
"""
19+
Then the response status code should be 201
20+
And the response should be in JSON
21+
And the header "Content-Type" should be equal to "application/hal+json; charset=utf-8"
22+
And the JSON should be equal to:
23+
"""
24+
{
25+
"_links": {
26+
"self": {
27+
"href": "/dummy_table_inheritance_children/1"
28+
}
29+
},
30+
"nickname": "bar",
31+
"id": 1,
32+
"name": "foo"
33+
}
34+
"""
35+
36+
Scenario: Get the parent entity collection
37+
When some dummy table inheritance data but not api resource child are created
38+
When I send a "GET" request to "/dummy_table_inheritances"
39+
Then the response status code should be 200
40+
And the response should be in JSON
41+
And the header "Content-Type" should be equal to "application/hal+json; charset=utf-8"
42+
And the JSON should be equal to:
43+
"""
44+
{
45+
"_links": {
46+
"self": {
47+
"href": "/dummy_table_inheritances"
48+
},
49+
"item": [
50+
{
51+
"href": "/dummy_table_inheritance_children/1"
52+
},
53+
{
54+
"href": "/dummy_table_inheritances/2"
55+
}
56+
]
57+
},
58+
"totalItems": 2,
59+
"itemsPerPage": 3,
60+
"_embedded": {
61+
"item": [
62+
{
63+
"_links": {
64+
"self": {
65+
"href": "/dummy_table_inheritance_children/1"
66+
}
67+
},
68+
"nickname": "bar",
69+
"id": 1,
70+
"name": "foo"
71+
},
72+
{
73+
"_links": {
74+
"self": {
75+
"href": "/dummy_table_inheritances/2"
76+
}
77+
},
78+
"id": 2,
79+
"name": "Foobarbaz inheritance"
80+
}
81+
]
82+
}
83+
}
84+
"""
85+
86+
87+
Scenario: Get related entity with multiple inherited children types
88+
And I send a "POST" request to "/dummy_table_inheritance_relateds" with body:
89+
"""
90+
{
91+
"children": [
92+
"/dummy_table_inheritance_children/1",
93+
"/dummy_table_inheritances/2"
94+
]
95+
}
96+
"""
97+
Then the response status code should be 201
98+
And the response should be in JSON
99+
And the header "Content-Type" should be equal to "application/hal+json; charset=utf-8"
100+
And the JSON should be equal to:
101+
"""
102+
{
103+
"_links": {
104+
"self": {
105+
"href": "/dummy_table_inheritance_relateds/1"
106+
},
107+
"children": [
108+
{
109+
"href": "/dummy_table_inheritance_children/1"
110+
},
111+
{
112+
"href": "/dummy_table_inheritances/2"
113+
}
114+
]
115+
},
116+
"_embedded": {
117+
"children": [
118+
{
119+
"_links": {
120+
"self": {
121+
"href": "/dummy_table_inheritance_children/1"
122+
}
123+
},
124+
"nickname": "bar",
125+
"id": 1,
126+
"name": "foo"
127+
},
128+
{
129+
"_links": {
130+
"self": {
131+
"href": "/dummy_table_inheritances/2"
132+
}
133+
},
134+
"id": 2,
135+
"name": "Foobarbaz inheritance"
136+
}
137+
]
138+
},
139+
"id": 1
140+
}
141+
"""

src/Doctrine/Common/State/LinksHandlerTrait.php

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,20 @@ private function getLinks(string $resourceClass, Operation $operation, array $co
3434
return $links;
3535
}
3636

37-
$newLinks = [];
37+
$newLink = null;
38+
$linkProperty = $context['linkProperty'] ?? null;
3839

3940
foreach ($links as $link) {
40-
if ($linkClass === $link->getFromClass()) {
41-
$newLinks[] = $link;
41+
if ($linkClass === $link->getFromClass() && $linkProperty === $link->getFromProperty()) {
42+
$newLink = $link;
43+
break;
4244
}
4345
}
4446

47+
if ($newLink) {
48+
return [$newLink];
49+
}
50+
4551
// Using GraphQL, it's possible that we won't find a GraphQL Operation of the same type (e.g. it is disabled).
4652
try {
4753
$resourceMetadataCollection = $this->resourceMetadataCollectionFactory->create($linkClass);
@@ -62,16 +68,17 @@ private function getLinks(string $resourceClass, Operation $operation, array $co
6268
}
6369

6470
foreach ($this->getOperationLinks($linkedOperation ?? null) as $link) {
65-
if ($resourceClass === $link->getToClass()) {
66-
$newLinks[] = $link;
71+
if ($resourceClass === $link->getToClass() && $linkProperty === $link->getFromProperty()) {
72+
$newLink = $link;
73+
break;
6774
}
6875
}
6976

70-
if (!$newLinks) {
77+
if (!$newLink) {
7178
throw new RuntimeException(sprintf('The class "%s" cannot be retrieved from "%s".', $resourceClass, $linkClass));
7279
}
7380

74-
return $newLinks;
81+
return [$newLink];
7582
}
7683

7784
private function getIdentifierValue(array &$identifiers, string $name = null): mixed

src/GraphQl/Resolver/Stage/ReadStage.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ public function __invoke(?string $resourceClass, ?string $rootClass, Operation $
8383
if (isset($source[$info->fieldName], $source[ItemNormalizer::ITEM_IDENTIFIERS_KEY], $source[ItemNormalizer::ITEM_RESOURCE_CLASS_KEY])) {
8484
$uriVariables = $source[ItemNormalizer::ITEM_IDENTIFIERS_KEY];
8585
$normalizationContext['linkClass'] = $source[ItemNormalizer::ITEM_RESOURCE_CLASS_KEY];
86+
$normalizationContext['linkProperty'] = $info->fieldName;
8687
}
8788

8889
return $this->provider->provide($operation, $uriVariables, $normalizationContext);

src/Metadata/Resource/Factory/ExtractorResourceMetadataCollectionFactory.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ final class ExtractorResourceMetadataCollectionFactory implements ResourceMetada
3131
{
3232
use OperationDefaultsTrait;
3333

34-
public function __construct(private readonly ResourceExtractorInterface $extractor, private readonly ?ResourceMetadataCollectionFactoryInterface $decorated = null, array $defaults = [], LoggerInterface $logger = null)
34+
public function __construct(private readonly ResourceExtractorInterface $extractor, private readonly ?ResourceMetadataCollectionFactoryInterface $decorated = null, array $defaults = [], LoggerInterface $logger = null, private readonly bool $graphQlEnabled = false)
3535
{
3636
$this->logger = $logger ?? new NullLogger();
3737
$this->defaults = $defaults;
@@ -85,7 +85,9 @@ private function buildResources(array $nodes, string $resourceClass): array
8585
}
8686
}
8787

88-
$resource = $this->addGraphQlOperations($node['graphQlOperations'] ?? null, $resource);
88+
if ($this->graphQlEnabled) {
89+
$resource = $this->addGraphQlOperations($node['graphQlOperations'] ?? null, $resource);
90+
}
8991

9092
$resources[] = $this->addOperations($node['operations'] ?? null, $resource);
9193
}

src/Metadata/Resource/Factory/LinkResourceMetadataCollectionFactory.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,16 +68,16 @@ private function mergeLinks(array $links, array $toMergeLinks): array
6868
{
6969
$classLinks = [];
7070
foreach ($links as $link) {
71-
$classLinks[$link->getToClass()] = $link;
71+
$classLinks[$link->getToClass().'#'.$link->getFromProperty()] = $link;
7272
}
7373

7474
foreach ($toMergeLinks as $link) {
75-
if (isset($classLinks[$link->getToClass()])) {
76-
$classLinks[$link->getToClass()] = $classLinks[$link->getToClass()]->withLink($link);
75+
if (null !== $prevLink = $classLinks[$link->getToClass().'#'.$link->getFromProperty()] ?? null) {
76+
$classLinks[$link->getToClass().'#'.$link->getFromProperty()] = $prevLink->withLink($link);
7777

7878
continue;
7979
}
80-
$classLinks[$link->getToClass()] = $link;
80+
$classLinks[$link->getToClass().'#'.$link->getFromProperty()] = $link;
8181
}
8282

8383
return array_values($classLinks);

src/Metadata/Resource/Factory/OperationDefaultsTrait.php

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -187,16 +187,28 @@ private function getOperationWithDefaults(ApiResource $resource, Operation $oper
187187
$operation = $operation->withName($operation->getRouteName());
188188
}
189189

190-
$operationName = $operation->getName() ?? sprintf(
191-
'_api_%s_%s%s',
192-
$operation->getUriTemplate() ?: $operation->getShortName(),
193-
strtolower($operation->getMethod() ?? HttpOperation::METHOD_GET),
194-
$operation instanceof CollectionOperationInterface ? '_collection' : '',
195-
);
190+
$path = ($operation->getRoutePrefix() ?? '').($operation->getUriTemplate() ?? '');
191+
$operationName = $operation->getName() ?? $this->getDefaultOperationName($operation, $resource->getClass());
196192

197193
return [
198194
$operationName,
199195
$operation,
200196
];
201197
}
198+
199+
private function getDefaultShortname(string $resourceClass): string
200+
{
201+
return (false !== $pos = strrpos($resourceClass, '\\')) ? substr($resourceClass, $pos + 1) : $resourceClass;
202+
}
203+
204+
private function getDefaultOperationName(HttpOperation $operation, string $resourceClass): string
205+
{
206+
$path = ($operation->getRoutePrefix() ?? '').($operation->getUriTemplate() ?? '');
207+
208+
return sprintf(
209+
'_api_%s_%s%s',
210+
$path ?: ($operation->getShortName() ?? $this->getDefaultShortname($resourceClass)),
211+
strtolower($operation->getMethod() ?? HttpOperation::METHOD_GET),
212+
$operation instanceof CollectionOperationInterface ? '_collection' : '');
213+
}
202214
}

src/Metadata/Resource/Factory/OperationNameResourceMetadataCollectionFactory.php

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@
1313

1414
namespace ApiPlatform\Metadata\Resource\Factory;
1515

16-
use ApiPlatform\Metadata\CollectionOperationInterface;
17-
use ApiPlatform\Metadata\HttpOperation;
1816
use ApiPlatform\Metadata\Resource\ResourceMetadataCollection;
1917

2018
/**
@@ -24,6 +22,8 @@
2422
*/
2523
final class OperationNameResourceMetadataCollectionFactory implements ResourceMetadataCollectionFactoryInterface
2624
{
25+
use OperationDefaultsTrait;
26+
2727
public function __construct(private readonly ?ResourceMetadataCollectionFactoryInterface $decorated = null)
2828
{
2929
}
@@ -52,8 +52,7 @@ public function create(string $resourceClass): ResourceMetadataCollection
5252
continue;
5353
}
5454

55-
$path = ($operation->getRoutePrefix() ?? '').($operation->getUriTemplate() ?? '');
56-
$newOperationName = sprintf('_api_%s_%s%s', $path ?: ($operation->getShortName() ?? $this->getDefaultShortname($resourceClass)), strtolower($operation->getMethod() ?? HttpOperation::METHOD_GET), $operation instanceof CollectionOperationInterface ? '_collection' : '');
55+
$newOperationName = $this->getDefaultOperationName($operation, $resourceClass);
5756
$operations->remove($operationName)->add($newOperationName, $operation->withName($newOperationName));
5857
}
5958

@@ -62,9 +61,4 @@ public function create(string $resourceClass): ResourceMetadataCollection
6261

6362
return $resourceMetadataCollection;
6463
}
65-
66-
private function getDefaultShortname(string $resourceClass): string
67-
{
68-
return (false !== $pos = strrpos($resourceClass, '\\')) ? substr($resourceClass, $pos + 1) : $resourceClass;
69-
}
7064
}

0 commit comments

Comments
 (0)