Skip to content

Commit a47e36c

Browse files
authored
fix(state): scope ReadLinkParameterProvider to current Link's class (#7943)
1 parent cc0ae12 commit a47e36c

5 files changed

Lines changed: 249 additions & 15 deletions

File tree

src/State/ParameterProvider/ReadLinkParameterProvider.php

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -105,11 +105,13 @@ public function provide(Parameter $parameter, array $parameters = [], array $con
105105
}
106106

107107
/**
108-
* @return array<string, string>
108+
* @return array<string, mixed>
109109
*/
110110
private function getUriVariables(mixed $value, Parameter $parameter, Operation $operation): array
111111
{
112-
$extraProperties = $parameter->getExtraProperties();
112+
if (\is_array($value)) {
113+
return $value;
114+
}
113115

114116
if ($operation instanceof HttpOperation) {
115117
$links = $operation->getUriVariables();
@@ -119,24 +121,30 @@ private function getUriVariables(mixed $value, Parameter $parameter, Operation $
119121
$links = [];
120122
}
121123

122-
if (!\is_array($value)) {
123-
$uriVariables = [];
124+
$extraProperties = $parameter->getExtraProperties();
125+
$linkClass = $parameter instanceof Link
126+
? ($parameter->getFromClass() ?? $parameter->getToClass())
127+
: null;
128+
129+
$fallbackKey = null;
130+
foreach ($links as $key => $link) {
131+
if (!\is_string($key)) {
132+
$key = $link->getParameterName() ?? $extraProperties['uri_variable'] ?? $link->getFromProperty();
133+
}
124134

125-
foreach ($links as $key => $link) {
126-
if (!\is_string($key)) {
127-
$key = $link->getParameterName() ?? $extraProperties['uri_variable'] ?? $link->getFromProperty();
128-
}
135+
if (!$key || !\is_string($key)) {
136+
continue;
137+
}
129138

130-
if (!$key || !\is_string($key)) {
131-
continue;
132-
}
139+
$linkFromClass = $link instanceof Link ? ($link->getFromClass() ?? $link->getToClass()) : null;
133140

134-
$uriVariables[$key] = $value;
141+
if (null !== $linkClass && $linkFromClass === $linkClass) {
142+
return [$key => $value];
135143
}
136144

137-
return $uriVariables;
145+
$fallbackKey ??= $key;
138146
}
139147

140-
return $value;
148+
return null === $fallbackKey ? [] : [$fallbackKey => $value];
141149
}
142150
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
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\Metadata\ApiResource;
17+
use ApiPlatform\Metadata\Get;
18+
use ApiPlatform\Metadata\Link;
19+
use ApiPlatform\Metadata\Operation;
20+
21+
#[ApiResource(
22+
operations: [
23+
new Get(
24+
uriTemplate: '/issue7939_foos/{fooId}/bars/{id}',
25+
uriVariables: [
26+
'fooId' => new Link(fromClass: Issue7939FooResource::class, toProperty: 'foo'),
27+
'id' => new Link(fromClass: self::class),
28+
],
29+
provider: [self::class, 'provide'],
30+
),
31+
],
32+
)]
33+
final class Issue7939BarResource
34+
{
35+
private const PARENTS = ['B' => 'F2'];
36+
37+
public string $id = '';
38+
public ?Issue7939FooResource $foo = null;
39+
40+
public static function parentOf(string $barId): ?string
41+
{
42+
return self::PARENTS[$barId] ?? null;
43+
}
44+
45+
public static function provide(Operation $operation, array $uriVariables = [])
46+
{
47+
$id = (string) ($uriVariables['id'] ?? '');
48+
$parent = self::parentOf($id);
49+
50+
if (null === $parent) {
51+
return null;
52+
}
53+
54+
$bar = new self();
55+
$bar->id = $id;
56+
$foo = new Issue7939FooResource();
57+
$foo->id = $parent;
58+
$bar->foo = $foo;
59+
60+
return $bar;
61+
}
62+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
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\Metadata\ApiResource;
17+
use ApiPlatform\Metadata\Get;
18+
use ApiPlatform\Metadata\Link;
19+
use ApiPlatform\Metadata\Operation;
20+
use ApiPlatform\Metadata\Parameter;
21+
use ApiPlatform\State\ParameterProvider\ReadLinkParameterProvider;
22+
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
23+
24+
#[ApiResource(
25+
operations: [
26+
new Get(
27+
uriTemplate: '/issue7939_foos/{fooId}/bars/{barId}/baz',
28+
uriVariables: [
29+
'fooId' => new Link(fromClass: Issue7939FooResource::class),
30+
'barId' => new Link(
31+
fromClass: Issue7939BarResource::class,
32+
identifiers: ['id'],
33+
provider: ReadLinkParameterProvider::class,
34+
),
35+
],
36+
provider: [self::class, 'provide'],
37+
),
38+
new Get(
39+
uriTemplate: '/issue7939_foos/{fooId}/bars/{barId}/baz_strict',
40+
uriVariables: [
41+
'fooId' => new Link(
42+
fromClass: Issue7939FooResource::class,
43+
provider: [self::class, 'validateParent'],
44+
),
45+
'barId' => new Link(
46+
fromClass: Issue7939BarResource::class,
47+
identifiers: ['id'],
48+
provider: ReadLinkParameterProvider::class,
49+
),
50+
],
51+
provider: [self::class, 'provide'],
52+
),
53+
],
54+
)]
55+
final class Issue7939BazResource
56+
{
57+
public string $id = '1';
58+
public string $barId = '';
59+
public string $fooId = '';
60+
61+
public static function provide(Operation $operation, array $uriVariables = [])
62+
{
63+
$r = new self();
64+
$r->fooId = (string) ($uriVariables['fooId'] ?? '');
65+
$r->barId = (string) ($uriVariables['barId'] ?? '');
66+
67+
return $r;
68+
}
69+
70+
public static function validateParent(Parameter $parameter, array $values = [], array $context = []): ?Operation
71+
{
72+
$barId = (string) ($values['barId'] ?? '');
73+
$fooId = (string) ($values['fooId'] ?? '');
74+
75+
if (Issue7939BarResource::parentOf($barId) !== $fooId) {
76+
throw new NotFoundHttpException('Bar does not belong to the requested Foo.');
77+
}
78+
79+
return $context['operation'] ?? null;
80+
}
81+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
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\Metadata\ApiResource;
17+
use ApiPlatform\Metadata\Get;
18+
use ApiPlatform\Metadata\Operation;
19+
20+
#[ApiResource(
21+
operations: [
22+
new Get(
23+
uriTemplate: '/issue7939_foos/{id}',
24+
provider: [self::class, 'provide'],
25+
),
26+
],
27+
)]
28+
final class Issue7939FooResource
29+
{
30+
public string $id = '';
31+
32+
public static function provide(Operation $operation, array $uriVariables = [])
33+
{
34+
$r = new self();
35+
$r->id = (string) ($uriVariables['id'] ?? '');
36+
37+
return $r;
38+
}
39+
}

tests/Functional/Parameters/LinkProviderParameterTest.php

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515

1616
use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
1717
use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue7469TestResource;
18+
use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue7939BarResource;
19+
use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue7939BazResource;
20+
use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue7939FooResource;
1821
use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\LinkParameterProviderResource;
1922
use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\WithParameter;
2023
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Company;
@@ -40,7 +43,7 @@ final class LinkProviderParameterTest extends ApiTestCase
4043
*/
4144
public static function getResources(): array
4245
{
43-
return [WithParameter::class, Dummy::class, Employee::class, Company::class, LinkParameterProviderResource::class, Issue7469TestResource::class, Issue7469Dummy::class, Pairing::class, Plan::class];
46+
return [WithParameter::class, Dummy::class, Employee::class, Company::class, LinkParameterProviderResource::class, Issue7469TestResource::class, Issue7469Dummy::class, Pairing::class, Plan::class, Issue7939FooResource::class, Issue7939BarResource::class, Issue7939BazResource::class];
4447
}
4548

4649
/**
@@ -236,6 +239,47 @@ public function testSecurityLinkWithDifferentFromClassDoesNotBreakDoctrine(): vo
236239
]);
237240
}
238241

242+
/**
243+
* @see https://github.com/api-platform/core/issues/7939
244+
*/
245+
public function testReadLinkParameterProviderResolvesNestedUriVariables(): void
246+
{
247+
$container = static::getContainer();
248+
if ('mongodb' === $container->getParameter('kernel.environment')) {
249+
$this->markTestSkipped();
250+
}
251+
252+
$response = self::createClient()->request('GET', '/issue7939_foos/F/bars/B/baz');
253+
self::assertResponseStatusCodeSame(200);
254+
self::assertJsonContains([
255+
'fooId' => 'F',
256+
'barId' => 'B',
257+
]);
258+
}
259+
260+
/**
261+
* @see https://github.com/api-platform/core/issues/7939
262+
*/
263+
public function testParentLinkProviderEnforcesParentScope(): void
264+
{
265+
$container = static::getContainer();
266+
if ('mongodb' === $container->getParameter('kernel.environment')) {
267+
$this->markTestSkipped();
268+
}
269+
270+
$client = self::createClient();
271+
272+
$client->request('GET', '/issue7939_foos/F2/bars/B/baz_strict');
273+
self::assertResponseStatusCodeSame(200);
274+
self::assertJsonContains([
275+
'fooId' => 'F2',
276+
'barId' => 'B',
277+
]);
278+
279+
$client->request('GET', '/issue7939_foos/F1/bars/B/baz_strict');
280+
self::assertResponseStatusCodeSame(404);
281+
}
282+
239283
public function testIssue7469IriGenerationFailsForLinkedResource(): void
240284
{
241285
$container = static::getContainer();

0 commit comments

Comments
 (0)