diff --git a/src/State/ParameterProvider/ReadLinkParameterProvider.php b/src/State/ParameterProvider/ReadLinkParameterProvider.php index 9b64676d5c..906eb0ac9d 100644 --- a/src/State/ParameterProvider/ReadLinkParameterProvider.php +++ b/src/State/ParameterProvider/ReadLinkParameterProvider.php @@ -105,11 +105,13 @@ public function provide(Parameter $parameter, array $parameters = [], array $con } /** - * @return array + * @return array */ private function getUriVariables(mixed $value, Parameter $parameter, Operation $operation): array { - $extraProperties = $parameter->getExtraProperties(); + if (\is_array($value)) { + return $value; + } if ($operation instanceof HttpOperation) { $links = $operation->getUriVariables(); @@ -119,24 +121,30 @@ private function getUriVariables(mixed $value, Parameter $parameter, Operation $ $links = []; } - if (!\is_array($value)) { - $uriVariables = []; + $extraProperties = $parameter->getExtraProperties(); + $linkClass = $parameter instanceof Link + ? ($parameter->getFromClass() ?? $parameter->getToClass()) + : null; + + $fallbackKey = null; + foreach ($links as $key => $link) { + if (!\is_string($key)) { + $key = $link->getParameterName() ?? $extraProperties['uri_variable'] ?? $link->getFromProperty(); + } - foreach ($links as $key => $link) { - if (!\is_string($key)) { - $key = $link->getParameterName() ?? $extraProperties['uri_variable'] ?? $link->getFromProperty(); - } + if (!$key || !\is_string($key)) { + continue; + } - if (!$key || !\is_string($key)) { - continue; - } + $linkFromClass = $link instanceof Link ? ($link->getFromClass() ?? $link->getToClass()) : null; - $uriVariables[$key] = $value; + if (null !== $linkClass && $linkFromClass === $linkClass) { + return [$key => $value]; } - return $uriVariables; + $fallbackKey ??= $key; } - return $value; + return null === $fallbackKey ? [] : [$fallbackKey => $value]; } } diff --git a/tests/Fixtures/TestBundle/ApiResource/Issue7939BarResource.php b/tests/Fixtures/TestBundle/ApiResource/Issue7939BarResource.php new file mode 100644 index 0000000000..485c9e75ae --- /dev/null +++ b/tests/Fixtures/TestBundle/ApiResource/Issue7939BarResource.php @@ -0,0 +1,62 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Tests\Fixtures\TestBundle\ApiResource; + +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\Link; +use ApiPlatform\Metadata\Operation; + +#[ApiResource( + operations: [ + new Get( + uriTemplate: '/issue7939_foos/{fooId}/bars/{id}', + uriVariables: [ + 'fooId' => new Link(fromClass: Issue7939FooResource::class, toProperty: 'foo'), + 'id' => new Link(fromClass: self::class), + ], + provider: [self::class, 'provide'], + ), + ], +)] +final class Issue7939BarResource +{ + private const PARENTS = ['B' => 'F2']; + + public string $id = ''; + public ?Issue7939FooResource $foo = null; + + public static function parentOf(string $barId): ?string + { + return self::PARENTS[$barId] ?? null; + } + + public static function provide(Operation $operation, array $uriVariables = []) + { + $id = (string) ($uriVariables['id'] ?? ''); + $parent = self::parentOf($id); + + if (null === $parent) { + return null; + } + + $bar = new self(); + $bar->id = $id; + $foo = new Issue7939FooResource(); + $foo->id = $parent; + $bar->foo = $foo; + + return $bar; + } +} diff --git a/tests/Fixtures/TestBundle/ApiResource/Issue7939BazResource.php b/tests/Fixtures/TestBundle/ApiResource/Issue7939BazResource.php new file mode 100644 index 0000000000..51f49c569c --- /dev/null +++ b/tests/Fixtures/TestBundle/ApiResource/Issue7939BazResource.php @@ -0,0 +1,81 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Tests\Fixtures\TestBundle\ApiResource; + +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\Link; +use ApiPlatform\Metadata\Operation; +use ApiPlatform\Metadata\Parameter; +use ApiPlatform\State\ParameterProvider\ReadLinkParameterProvider; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; + +#[ApiResource( + operations: [ + new Get( + uriTemplate: '/issue7939_foos/{fooId}/bars/{barId}/baz', + uriVariables: [ + 'fooId' => new Link(fromClass: Issue7939FooResource::class), + 'barId' => new Link( + fromClass: Issue7939BarResource::class, + identifiers: ['id'], + provider: ReadLinkParameterProvider::class, + ), + ], + provider: [self::class, 'provide'], + ), + new Get( + uriTemplate: '/issue7939_foos/{fooId}/bars/{barId}/baz_strict', + uriVariables: [ + 'fooId' => new Link( + fromClass: Issue7939FooResource::class, + provider: [self::class, 'validateParent'], + ), + 'barId' => new Link( + fromClass: Issue7939BarResource::class, + identifiers: ['id'], + provider: ReadLinkParameterProvider::class, + ), + ], + provider: [self::class, 'provide'], + ), + ], +)] +final class Issue7939BazResource +{ + public string $id = '1'; + public string $barId = ''; + public string $fooId = ''; + + public static function provide(Operation $operation, array $uriVariables = []) + { + $r = new self(); + $r->fooId = (string) ($uriVariables['fooId'] ?? ''); + $r->barId = (string) ($uriVariables['barId'] ?? ''); + + return $r; + } + + public static function validateParent(Parameter $parameter, array $values = [], array $context = []): ?Operation + { + $barId = (string) ($values['barId'] ?? ''); + $fooId = (string) ($values['fooId'] ?? ''); + + if (Issue7939BarResource::parentOf($barId) !== $fooId) { + throw new NotFoundHttpException('Bar does not belong to the requested Foo.'); + } + + return $context['operation'] ?? null; + } +} diff --git a/tests/Fixtures/TestBundle/ApiResource/Issue7939FooResource.php b/tests/Fixtures/TestBundle/ApiResource/Issue7939FooResource.php new file mode 100644 index 0000000000..670179b82c --- /dev/null +++ b/tests/Fixtures/TestBundle/ApiResource/Issue7939FooResource.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Tests\Fixtures\TestBundle\ApiResource; + +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\Operation; + +#[ApiResource( + operations: [ + new Get( + uriTemplate: '/issue7939_foos/{id}', + provider: [self::class, 'provide'], + ), + ], +)] +final class Issue7939FooResource +{ + public string $id = ''; + + public static function provide(Operation $operation, array $uriVariables = []) + { + $r = new self(); + $r->id = (string) ($uriVariables['id'] ?? ''); + + return $r; + } +} diff --git a/tests/Functional/Parameters/LinkProviderParameterTest.php b/tests/Functional/Parameters/LinkProviderParameterTest.php index 9702582119..cd7ef7d5a4 100644 --- a/tests/Functional/Parameters/LinkProviderParameterTest.php +++ b/tests/Functional/Parameters/LinkProviderParameterTest.php @@ -15,6 +15,9 @@ use ApiPlatform\Symfony\Bundle\Test\ApiTestCase; use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue7469TestResource; +use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue7939BarResource; +use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue7939BazResource; +use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue7939FooResource; use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\LinkParameterProviderResource; use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\WithParameter; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Company; @@ -40,7 +43,7 @@ final class LinkProviderParameterTest extends ApiTestCase */ public static function getResources(): array { - return [WithParameter::class, Dummy::class, Employee::class, Company::class, LinkParameterProviderResource::class, Issue7469TestResource::class, Issue7469Dummy::class, Pairing::class, Plan::class]; + 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]; } /** @@ -236,6 +239,47 @@ public function testSecurityLinkWithDifferentFromClassDoesNotBreakDoctrine(): vo ]); } + /** + * @see https://github.com/api-platform/core/issues/7939 + */ + public function testReadLinkParameterProviderResolvesNestedUriVariables(): void + { + $container = static::getContainer(); + if ('mongodb' === $container->getParameter('kernel.environment')) { + $this->markTestSkipped(); + } + + $response = self::createClient()->request('GET', '/issue7939_foos/F/bars/B/baz'); + self::assertResponseStatusCodeSame(200); + self::assertJsonContains([ + 'fooId' => 'F', + 'barId' => 'B', + ]); + } + + /** + * @see https://github.com/api-platform/core/issues/7939 + */ + public function testParentLinkProviderEnforcesParentScope(): void + { + $container = static::getContainer(); + if ('mongodb' === $container->getParameter('kernel.environment')) { + $this->markTestSkipped(); + } + + $client = self::createClient(); + + $client->request('GET', '/issue7939_foos/F2/bars/B/baz_strict'); + self::assertResponseStatusCodeSame(200); + self::assertJsonContains([ + 'fooId' => 'F2', + 'barId' => 'B', + ]); + + $client->request('GET', '/issue7939_foos/F1/bars/B/baz_strict'); + self::assertResponseStatusCodeSame(404); + } + public function testIssue7469IriGenerationFailsForLinkedResource(): void { $container = static::getContainer();