Skip to content

Commit f393574

Browse files
mrossardsoyuka
andauthored
fix: the stateOptions::entityClass should be used when present while building Links (#5550)
* fix: the stateOptions::entityClass should be used when present while building Links Fixes #5510 * proper fix --------- Co-authored-by: Manuel Rossard <manuel.rossard@u-bordeaux.fr> Co-authored-by: soyuka <soyuka@users.noreply.github.com>
1 parent 60082d7 commit f393574

File tree

8 files changed

+190
-18
lines changed

8 files changed

+190
-18
lines changed

features/main/crud_uri_variables.feature

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,3 +200,15 @@ Feature: Uri Variables
200200
When I add "Content-Type" header equal to "application/ld+json"
201201
And I send a "GET" request to "/companies/1/employees/2"
202202
Then the response status code should be 404
203+
204+
@!mongodb
205+
Scenario: Get all EntityClassAndCustomProviderResources
206+
Given there are 1 separated entities
207+
When I send a "GET" request to "/entityClassAndCustomProviderResources"
208+
Then the response status code should be 200
209+
210+
@!mongodb
211+
Scenario: Get one EntityClassAndCustomProviderResource
212+
Given there are 1 separated entities
213+
When I send a "GET" request to "/entityClassAndCustomProviderResources/1"
214+
Then the response status code should be 200

src/Api/IdentifiersExtractor.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ private function getIdentifiersFromOperation(object $item, Operation $operation,
8484
}
8585

8686
$parameterName = $link->getParameterName();
87-
$identifiers[$parameterName] = $this->getIdentifierValue($item, $link->getFromClass(), $link->getIdentifiers()[0], $parameterName, $link->getToProperty());
87+
$identifiers[$parameterName] = $this->getIdentifierValue($item, $link->getFromClass() ?? $operation->getClass(), $link->getIdentifiers()[0], $parameterName, $link->getToProperty());
8888
}
8989

9090
return $identifiers;

src/Doctrine/Orm/State/LinksHandlerTrait.php

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
use ApiPlatform\Doctrine\Common\State\LinksHandlerTrait as CommonLinksHandlerTrait;
1717
use ApiPlatform\Doctrine\Orm\Util\QueryNameGenerator;
18+
use ApiPlatform\Metadata\Link;
1819
use ApiPlatform\Metadata\Operation;
1920
use Doctrine\ORM\Mapping\ClassMetadataInfo;
2021
use Doctrine\ORM\QueryBuilder;
@@ -49,12 +50,17 @@ private function handleLinks(QueryBuilder $queryBuilder, array $identifiers, Que
4950
continue;
5051
}
5152

52-
$fromClassMetadata = $manager->getClassMetadata($link->getFromClass());
53+
$fromClass = $link->getFromClass();
54+
if (!$this->managerRegistry->getManagerForClass($fromClass)) {
55+
$fromClass = $this->getLinkFromClass($link, $operation);
56+
}
57+
58+
$fromClassMetadata = $manager->getClassMetadata($fromClass);
5359
$identifierProperties = $link->getIdentifiers();
5460
$hasCompositeIdentifiers = 1 < \count($identifierProperties);
5561

5662
if (!$link->getFromProperty() && !$link->getToProperty()) {
57-
$currentAlias = $link->getFromClass() === $entityClass ? $alias : $queryNameGenerator->generateJoinAlias($alias);
63+
$currentAlias = $fromClass === $entityClass ? $alias : $queryNameGenerator->generateJoinAlias($alias);
5864

5965
foreach ($identifierProperties as $identifierProperty) {
6066
$placeholder = $queryNameGenerator->generateParameterName($identifierProperty);
@@ -85,7 +91,7 @@ private function handleLinks(QueryBuilder $queryBuilder, array $identifiers, Que
8591

8692
$property = $associationMapping['mappedBy'] ?? $joinProperties[0];
8793
$select = isset($associationMapping['mappedBy']) ? "IDENTITY($joinAlias.$property)" : "$joinAlias.$property";
88-
$expressions["$previousAlias.{$property}"] = "SELECT $select FROM {$link->getFromClass()} $nextAlias INNER JOIN $nextAlias.{$associationMapping['fieldName']} $joinAlias WHERE ".implode(' AND ', $whereClause);
94+
$expressions["$previousAlias.{$property}"] = "SELECT $select FROM {$fromClass} $nextAlias INNER JOIN $nextAlias.{$associationMapping['fieldName']} $joinAlias WHERE ".implode(' AND ', $whereClause);
8995
$previousAlias = $nextAlias;
9096
continue;
9197
}
@@ -95,7 +101,7 @@ private function handleLinks(QueryBuilder $queryBuilder, array $identifiers, Que
95101
$queryBuilder->innerJoin("$previousAlias.".$associationMapping['mappedBy'], $joinAlias);
96102
} else {
97103
$queryBuilder->join(
98-
$link->getFromClass(),
104+
$fromClass,
99105
$joinAlias,
100106
'WITH',
101107
"$previousAlias.{$previousJoinProperties[0]} = $joinAlias.{$associationMapping['fieldName']}"
@@ -143,4 +149,29 @@ private function handleLinks(QueryBuilder $queryBuilder, array $identifiers, Que
143149
$queryBuilder->andWhere($clause.str_repeat(')', $i));
144150
}
145151
}
152+
153+
private function getLinkFromClass(Link $link, Operation $operation): string
154+
{
155+
$fromClass = $link->getFromClass();
156+
if ($fromClass === $operation->getClass() && $entityClass = $this->getStateOptionsEntityClass($operation)) {
157+
return $entityClass;
158+
}
159+
160+
$operation = $this->resourceMetadataCollectionFactory->create($fromClass)->getOperation();
161+
162+
if ($entityClass = $this->getStateOptionsEntityClass($operation)) {
163+
return $entityClass;
164+
}
165+
166+
throw new \Exception('Can not found a doctrine class for this link.');
167+
}
168+
169+
private function getStateOptionsEntityClass(Operation $operation): ?string
170+
{
171+
if (($options = $operation->getStateOptions()) && $options instanceof Options && $entityClass = $options->getEntityClass()) {
172+
return $entityClass;
173+
}
174+
175+
return null;
176+
}
146177
}

src/Metadata/Resource/Factory/UriTemplateResourceMetadataCollectionFactory.php

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

1414
namespace ApiPlatform\Metadata\Resource\Factory;
1515

16-
use ApiPlatform\Doctrine\Orm\State\Options;
1716
use ApiPlatform\Metadata\ApiResource;
1817
use ApiPlatform\Metadata\CollectionOperationInterface;
1918
use ApiPlatform\Metadata\HttpOperation;
@@ -182,14 +181,9 @@ private function configureUriVariables(ApiResource|HttpOperation $operation): Ap
182181
continue;
183182
}
184183

185-
$entityClass = $operation->getClass();
186-
if (($options = $operation->getStateOptions()) && $options instanceof Options && $options->getEntityClass()) {
187-
$entityClass = $options->getEntityClass();
188-
}
189-
190184
$newUriVariables[$variable] = (new Link())
191-
->withFromClass($entityClass)
192-
->withIdentifiers([property_exists($entityClass, $variable) ? $variable : 'id'])
185+
->withFromClass($operation->getClass())
186+
->withIdentifiers([property_exists($operation->getClass(), $variable) ? $variable : 'id'])
193187
->withParameterName($variable);
194188
}
195189

tests/Doctrine/Orm/State/ItemProviderTest.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,9 @@ public function testGetSubresourceFromProperty(): void
267267
Employee::class => $employeeClassMetadataProphecy->reveal(),
268268
]);
269269

270+
$managerProphecy = $this->prophesize(EntityManagerInterface::class);
271+
$managerRegistryProphecy->getManagerForClass(Employee::class)->willReturn($managerProphecy->reveal());
272+
270273
/** @var HttpOperation */
271274
$operation = (new Get())->withUriVariables([
272275
'employeeId' => (new Link())->withFromClass(Employee::class)
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
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\ApiResource;
15+
16+
use ApiPlatform\Doctrine\Orm\State\Options;
17+
use ApiPlatform\Metadata\ApiProperty;
18+
use ApiPlatform\Metadata\ApiResource;
19+
use ApiPlatform\Metadata\Get;
20+
use ApiPlatform\Metadata\GetCollection;
21+
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\SeparatedEntity;
22+
use ApiPlatform\Tests\Fixtures\TestBundle\State\EntityClassAndCustomProviderResourceProvider;
23+
24+
#[ApiResource(
25+
operations: [
26+
new Get(
27+
uriTemplate: '/entityClassAndCustomProviderResources/{id}',
28+
uriVariables: ['id']
29+
),
30+
new GetCollection(
31+
uriTemplate: '/entityClassAndCustomProviderResources'
32+
),
33+
],
34+
provider: EntityClassAndCustomProviderResourceProvider::class,
35+
stateOptions: new Options(entityClass: SeparatedEntity::class)
36+
)]
37+
class EntityClassAndCustomProviderResource
38+
{
39+
#[ApiProperty(identifier: true)]
40+
public ?int $id;
41+
42+
public ?string $value;
43+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
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\State;
15+
16+
use ApiPlatform\Doctrine\Orm\State\CollectionProvider;
17+
use ApiPlatform\Doctrine\Orm\State\ItemProvider;
18+
use ApiPlatform\Doctrine\Orm\State\Options;
19+
use ApiPlatform\Exception\ItemNotFoundException;
20+
use ApiPlatform\Metadata\CollectionOperationInterface;
21+
use ApiPlatform\Metadata\Operation;
22+
use ApiPlatform\State\ProviderInterface;
23+
use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\EntityClassAndCustomProviderResource;
24+
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\SeparatedEntity;
25+
26+
class EntityClassAndCustomProviderResourceProvider implements ProviderInterface
27+
{
28+
/**
29+
* Should probably be ProviderInterface for both with a binding in services.yaml in a real app.
30+
*/
31+
public function __construct(
32+
private readonly ItemProvider $itemProvider,
33+
private readonly CollectionProvider $collectionProvider
34+
) {
35+
}
36+
37+
/**
38+
* @return EntityClassAndCustomProviderResource[]|EntityClassAndCustomProviderResource|null
39+
*/
40+
public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
41+
{
42+
$operation = ($stateOptions = $operation->getStateOptions()) instanceof Options ? $operation->withClass($stateOptions->getEntityClass()) : $operation;
43+
if ($operation instanceof CollectionOperationInterface) {
44+
$data = $this->collectionProvider->provide(
45+
$operation,
46+
$uriVariables,
47+
$context);
48+
49+
$processed = [];
50+
51+
foreach ($data as $item) {
52+
$processed[] = $this->transform($item);
53+
}
54+
55+
return $processed;
56+
}
57+
58+
$data = $this->itemProvider->provide(
59+
$operation,
60+
$uriVariables,
61+
$context
62+
);
63+
64+
if (null === $data) {
65+
throw new ItemNotFoundException();
66+
}
67+
68+
return $this->transform($data);
69+
}
70+
71+
/**
72+
* Would do more in a real app...
73+
*/
74+
private function transform(SeparatedEntity $data): EntityClassAndCustomProviderResource
75+
{
76+
$resource = new EntityClassAndCustomProviderResource();
77+
$resource->id = $data->id;
78+
$resource->value = $data->value;
79+
80+
return $resource;
81+
}
82+
}

tests/Fixtures/app/config/config_doctrine.yml

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -75,27 +75,27 @@ services:
7575
tags:
7676
- name: 'api_platform.state_provider'
7777

78-
ApiPlatform\Tests\Fixtures\TestBundle\State\DummyDtoInputOutputProvider:
78+
ApiPlatform\Tests\Fixtures\TestBundle\State\DummyDtoInputOutputProvider:
7979
arguments:
8080
$decorated: '@ApiPlatform\Doctrine\Orm\State\ItemProvider'
8181
tags:
8282
- name: 'api_platform.state_provider'
8383

84-
ApiPlatform\Tests\Fixtures\TestBundle\State\DummyDtoNoInputsProvider:
84+
ApiPlatform\Tests\Fixtures\TestBundle\State\DummyDtoNoInputsProvider:
8585
arguments:
8686
$itemProvider: '@ApiPlatform\Doctrine\Orm\State\ItemProvider'
8787
$collectionProvider: '@ApiPlatform\Doctrine\Orm\State\CollectionProvider'
8888
tags:
8989
- name: 'api_platform.state_provider'
9090

91-
ApiPlatform\Tests\Fixtures\TestBundle\State\CustomOutputDtoProvider:
91+
ApiPlatform\Tests\Fixtures\TestBundle\State\CustomOutputDtoProvider:
9292
arguments:
9393
$itemProvider: '@ApiPlatform\Doctrine\Orm\State\ItemProvider'
9494
$collectionProvider: '@ApiPlatform\Doctrine\Orm\State\CollectionProvider'
9595
tags:
9696
- name: 'api_platform.state_provider'
9797

98-
ApiPlatform\Tests\Fixtures\TestBundle\State\DummyDtoInputOutputProcessor:
98+
ApiPlatform\Tests\Fixtures\TestBundle\State\DummyDtoInputOutputProcessor:
9999
arguments:
100100
$registry: '@doctrine'
101101
tags:
@@ -107,9 +107,16 @@ services:
107107
tags:
108108
- name: 'api_platform.state_processor'
109109

110-
ApiPlatform\Tests\Fixtures\TestBundle\State\CustomInputDtoProcessor:
110+
ApiPlatform\Tests\Fixtures\TestBundle\State\CustomInputDtoProcessor:
111111
arguments:
112112
$decorated: '@ApiPlatform\Doctrine\Common\State\PersistProcessor'
113113
tags:
114114
- name: 'api_platform.state_processor'
115115

116+
ApiPlatform\Tests\Fixtures\TestBundle\State\EntityClassAndCustomProviderResourceProvider:
117+
class: 'ApiPlatform\Tests\Fixtures\TestBundle\State\EntityClassAndCustomProviderResourceProvider'
118+
tags:
119+
- { name: 'api_platform.state_provider' }
120+
arguments:
121+
$itemProvider: '@ApiPlatform\Doctrine\Orm\State\ItemProvider'
122+
$collectionProvider: '@ApiPlatform\Doctrine\Orm\State\CollectionProvider'

0 commit comments

Comments
 (0)