From 91847abe9f9a8776adf3c2cf05f25af79a7e60fa Mon Sep 17 00:00:00 2001 From: Morgan Abraham Date: Sun, 9 Nov 2025 21:14:37 +0100 Subject: [PATCH] fix(serializer): properly handle read link parameters when generating iris --- .../ApiResourceUriVariableTransformer.php | 35 ++++++++++++ src/Symfony/Bundle/Resources/config/api.php | 7 +++ .../ApiResource/Issue7469TestResource.php | 57 +++++++++++++++++++ .../TestBundle/Document/Issue7469Dummy.php | 32 +++++++++++ .../TestBundle/Entity/Issue7469Dummy.php | 34 +++++++++++ .../Parameters/LinkProviderParameterTest.php | 43 +++++++++++++- 6 files changed, 206 insertions(+), 2 deletions(-) create mode 100644 src/Metadata/UriVariableTransformer/ApiResourceUriVariableTransformer.php create mode 100644 tests/Fixtures/TestBundle/ApiResource/Issue7469TestResource.php create mode 100644 tests/Fixtures/TestBundle/Document/Issue7469Dummy.php create mode 100644 tests/Fixtures/TestBundle/Entity/Issue7469Dummy.php diff --git a/src/Metadata/UriVariableTransformer/ApiResourceUriVariableTransformer.php b/src/Metadata/UriVariableTransformer/ApiResourceUriVariableTransformer.php new file mode 100644 index 00000000000..2ea6593c1b5 --- /dev/null +++ b/src/Metadata/UriVariableTransformer/ApiResourceUriVariableTransformer.php @@ -0,0 +1,35 @@ + + * + * 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\Metadata\UriVariableTransformer; + +use ApiPlatform\Metadata\IdentifiersExtractorInterface; +use ApiPlatform\Metadata\ResourceClassResolverInterface; +use ApiPlatform\Metadata\UriVariableTransformerInterface; + +final class ApiResourceUriVariableTransformer implements UriVariableTransformerInterface +{ + public function __construct(private readonly IdentifiersExtractorInterface $identifiersExtractor, private readonly ResourceClassResolverInterface $resourceClassResolver) + { + } + + public function transform(mixed $value, array $types, array $context = []): mixed + { + return current($this->identifiersExtractor->getIdentifiersFromItem($value)); + } + + public function supportsTransformation(mixed $value, array $types, array $context = []): bool + { + return \is_object($value) && $this->resourceClassResolver->isResourceClass($value::class); + } +} diff --git a/src/Symfony/Bundle/Resources/config/api.php b/src/Symfony/Bundle/Resources/config/api.php index f136dbe8fe2..8edb011be3a 100644 --- a/src/Symfony/Bundle/Resources/config/api.php +++ b/src/Symfony/Bundle/Resources/config/api.php @@ -169,6 +169,13 @@ $services->set('api_platform.uri_variables.transformer.date_time', 'ApiPlatform\Metadata\UriVariableTransformer\DateTimeUriVariableTransformer') ->tag('api_platform.uri_variables.transformer', ['priority' => -100]); + $services->set('api_platform.uri_variables.transformer.api_resource', 'ApiPlatform\Metadata\UriVariableTransformer\ApiResourceUriVariableTransformer') + ->args([ + service('api_platform.api.identifiers_extractor'), + service('api_platform.resource_class_resolver'), + ]) + ->tag('api_platform.uri_variables.transformer', ['priority' => -100]); + $services->alias('api_platform.iri_converter', 'api_platform.symfony.iri_converter'); $services->set('api_platform.symfony.iri_converter', 'ApiPlatform\Symfony\Routing\IriConverter') diff --git a/tests/Fixtures/TestBundle/ApiResource/Issue7469TestResource.php b/tests/Fixtures/TestBundle/ApiResource/Issue7469TestResource.php new file mode 100644 index 00000000000..29027b10633 --- /dev/null +++ b/tests/Fixtures/TestBundle/ApiResource/Issue7469TestResource.php @@ -0,0 +1,57 @@ + + * + * 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\HttpOperation; +use ApiPlatform\Metadata\Link; +use ApiPlatform\Metadata\Operation; +use ApiPlatform\State\ParameterProvider\ReadLinkParameterProvider; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue7469Dummy; + +#[ApiResource( + operations: [ + new Get( + uriTemplate: '/issue_7469_test_resources/{id}', + uriVariables: [ + 'id' => new Link( + provider: ReadLinkParameterProvider::class, + fromClass: Issue7469Dummy::class + ), + ], + provider: [self::class, 'provide'] + ), + ] +)] +final class Issue7469TestResource +{ + public int $id; + public string $dummyName; + + /** + * @param HttpOperation $operation + */ + public static function provide(Operation $operation, array $uriVariables = [], array $context = []) + { + /** @var Issue7469Dummy $dummy */ + $dummy = $operation->getUriVariables()['id']->getValue(); + + $resource = new self(); + $resource->id = $dummy->id; + $resource->dummyName = $dummy->name; + + return $resource; + } +} diff --git a/tests/Fixtures/TestBundle/Document/Issue7469Dummy.php b/tests/Fixtures/TestBundle/Document/Issue7469Dummy.php new file mode 100644 index 00000000000..99d48157779 --- /dev/null +++ b/tests/Fixtures/TestBundle/Document/Issue7469Dummy.php @@ -0,0 +1,32 @@ + + * + * 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\Document; + +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\ApiResource; +use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; + +#[ODM\Document] +#[ApiResource( + uriTemplate: '/issue_7469_dummies/{id}', +)] +class Issue7469Dummy +{ + #[ODM\Id] + #[ApiProperty(identifier: true)] + public ?string $id = null; + + #[ODM\Field(type: 'string')] + public string $name; +} diff --git a/tests/Fixtures/TestBundle/Entity/Issue7469Dummy.php b/tests/Fixtures/TestBundle/Entity/Issue7469Dummy.php new file mode 100644 index 00000000000..63657d875b1 --- /dev/null +++ b/tests/Fixtures/TestBundle/Entity/Issue7469Dummy.php @@ -0,0 +1,34 @@ + + * + * 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\Entity; + +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\ApiResource; +use Doctrine\ORM\Mapping as ORM; + +#[ORM\Entity] +#[ApiResource( + uriTemplate: '/issue_7469_dummies/{id}', +)] +class Issue7469Dummy +{ + #[ORM\Column(type: 'integer')] + #[ORM\Id] + #[ORM\GeneratedValue(strategy: 'AUTO')] + #[ApiProperty(identifier: true)] + public ?int $id = null; + + #[ORM\Column] + public string $name; +} diff --git a/tests/Functional/Parameters/LinkProviderParameterTest.php b/tests/Functional/Parameters/LinkProviderParameterTest.php index 0288bd0f684..136044de8da 100644 --- a/tests/Functional/Parameters/LinkProviderParameterTest.php +++ b/tests/Functional/Parameters/LinkProviderParameterTest.php @@ -14,11 +14,13 @@ namespace ApiPlatform\Tests\Functional\Parameters; use ApiPlatform\Symfony\Bundle\Test\ApiTestCase; +use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue7469TestResource; use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\LinkParameterProviderResource; use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\WithParameter; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Company; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Employee; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue7469Dummy; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\RelatedDummy; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\RelatedOwnedDummy; use ApiPlatform\Tests\RecreateSchemaTrait; @@ -36,7 +38,7 @@ final class LinkProviderParameterTest extends ApiTestCase */ public static function getResources(): array { - return [WithParameter::class, Dummy::class, Employee::class, Company::class, LinkParameterProviderResource::class]; + return [WithParameter::class, Dummy::class, Employee::class, Company::class, LinkParameterProviderResource::class, Issue7469TestResource::class, Issue7469Dummy::class]; } /** @@ -44,7 +46,7 @@ public static function getResources(): array */ protected function setUp(): void { - $this->recreateSchema([Dummy::class, RelatedOwnedDummy::class, RelatedDummy::class, Employee::class, Company::class]); + $this->recreateSchema([Dummy::class, RelatedOwnedDummy::class, RelatedDummy::class, Employee::class, Company::class, Issue7469Dummy::class]); } public function testReadDummyProviderFromQueryParameter(): void @@ -183,4 +185,41 @@ public function testUriVariableHasDummy(): void 'dummy' => '/dummies/1', ]); } + + public function testCollectionIdIsCorrect(): void + { + $container = static::getContainer(); + if ('mongodb' === $container->getParameter('kernel.environment')) { + $this->markTestSkipped(); + } + + $manager = $this->getManager(); + $dummy = new Dummy(); + $dummy->setName('hi'); + $manager->persist($dummy); + $manager->flush(); + + self::createClient()->request('GET', '/link_parameter_provider_resources/'.$dummy->getId()); + + $this->assertJsonContains([ + '@id' => '/link_parameter_provider_resources/1', + ]); + } + + public function testIssue7469IriGenerationFailsForLinkedResource(): void + { + $container = static::getContainer(); + if ('mongodb' === $container->getParameter('kernel.environment')) { + $this->markTestSkipped(); + } + + $manager = $this->getManager(); + $issue7469Dummy = new Issue7469Dummy(); + $issue7469Dummy->name = 'Linked Dummy'; + $manager->persist($issue7469Dummy); + $manager->flush(); + + $r = self::createClient()->request('GET', '/issue_7469_test_resources/'.$issue7469Dummy->id, ['headers' => ['Accept' => 'application/ld+json']]); + $this->assertResponseIsSuccessful(); + } }