diff --git a/src/Serializer/AbstractItemNormalizer.php b/src/Serializer/AbstractItemNormalizer.php index 159e0967268..0042511fe2a 100644 --- a/src/Serializer/AbstractItemNormalizer.php +++ b/src/Serializer/AbstractItemNormalizer.php @@ -220,7 +220,7 @@ public function denormalize(mixed $data, string $class, ?string $format = null, throw new LogicException('Cannot denormalize the input because the injected serializer is not a denormalizer'); } - unset($context['input'], $context['operation'], $context['operation_name']); + unset($context['input'], $context['operation'], $context['operation_name'], $context['uri_variables']); $context['resource_class'] = $inputClass; try { diff --git a/src/Symfony/Routing/IriConverter.php b/src/Symfony/Routing/IriConverter.php index 2f727e508e6..9ca94fa0673 100644 --- a/src/Symfony/Routing/IriConverter.php +++ b/src/Symfony/Routing/IriConverter.php @@ -78,6 +78,7 @@ public function getResourceFromIri(string $iri, array $context = [], ?Operation throw new InvalidArgumentException(sprintf('No resource associated to "%s".', $iri)); } + // uri_variables come from the Request context and may not be available foreach ($context['uri_variables'] ?? [] as $key => $value) { if (!isset($parameters[$key]) || $parameters[$key] !== (string) $value) { throw new InvalidArgumentException(sprintf('The iri "%s" does not reference the correct resource.', $iri)); diff --git a/tests/Fixtures/TestBundle/Entity/Issue6465/Bar.php b/tests/Fixtures/TestBundle/Entity/Issue6465/Bar.php new file mode 100644 index 00000000000..c9bd992566f --- /dev/null +++ b/tests/Fixtures/TestBundle/Entity/Issue6465/Bar.php @@ -0,0 +1,31 @@ + + * + * 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\Issue6465; + +use ApiPlatform\Metadata\ApiResource; +use Doctrine\ORM\Mapping as ORM; + +#[ORM\Entity()] +#[ApiResource(shortName: 'Bar6465')] +#[ORM\Table(name: 'bar6465')] +class Bar +{ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column] + public ?int $id = null; + + #[ORM\Column] + public string $title = ''; +} diff --git a/tests/Fixtures/TestBundle/Entity/Issue6465/CustomInput.php b/tests/Fixtures/TestBundle/Entity/Issue6465/CustomInput.php new file mode 100644 index 00000000000..3f99290d8bb --- /dev/null +++ b/tests/Fixtures/TestBundle/Entity/Issue6465/CustomInput.php @@ -0,0 +1,22 @@ + + * + * 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\Issue6465; + +use Symfony\Component\Validator\Constraints\NotBlank; + +class CustomInput +{ + #[NotBlank] + public Bar $bar; +} diff --git a/tests/Fixtures/TestBundle/Entity/Issue6465/CustomOutput.php b/tests/Fixtures/TestBundle/Entity/Issue6465/CustomOutput.php new file mode 100644 index 00000000000..49f700721a4 --- /dev/null +++ b/tests/Fixtures/TestBundle/Entity/Issue6465/CustomOutput.php @@ -0,0 +1,21 @@ + + * + * 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\Issue6465; + +class CustomOutput +{ + public function __construct(public string $title) + { + } +} diff --git a/tests/Fixtures/TestBundle/Entity/Issue6465/Foo.php b/tests/Fixtures/TestBundle/Entity/Issue6465/Foo.php new file mode 100644 index 00000000000..359b22e6605 --- /dev/null +++ b/tests/Fixtures/TestBundle/Entity/Issue6465/Foo.php @@ -0,0 +1,58 @@ + + * + * 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\Issue6465; + +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\Link; +use ApiPlatform\Metadata\Post; +use Doctrine\ORM\Mapping as ORM; + +#[ORM\Entity()] +#[ORM\Table(name: 'foo6465')] +#[ApiResource( + shortName: 'Foo6465', + operations: [ + new Get(), + new GetCollection(), + new Post(), + new Post( + uriTemplate: '/foo/{id}/validate', + uriVariables: ['id' => new Link(fromClass: Foo::class)], + status: 200, + input: CustomInput::class, + output: CustomOutput::class, + processor: [self::class, 'process'], + ), + ] +)] +class Foo +{ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column] + public ?int $id = null; + + #[ORM\Column(length: 255)] + public ?string $title = null; + + /** + * @param CustomInput $data + */ + public static function process($data): CustomOutput + { + return new CustomOutput($data->bar->title); + } +} diff --git a/tests/Functional/JsonLd.php b/tests/Functional/JsonLd.php new file mode 100644 index 00000000000..da76c723e76 --- /dev/null +++ b/tests/Functional/JsonLd.php @@ -0,0 +1,91 @@ + + * + * 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\Functional; + +use ApiPlatform\Symfony\Bundle\Test\ApiTestCase; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue6465\Bar; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue6465\Foo; +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\Tools\SchemaTool; + +class JsonLd extends ApiTestCase +{ + /** + * The input DTO denormalizes an existing Doctrine entity. + */ + public function testIssue6465(): void + { + $container = static::getContainer(); + if ('mongodb' === $container->getParameter('kernel.environment')) { + $this->markTestSkipped(); + } + + $response = self::createClient()->request('POST', '/foo/1/validate', [ + 'json' => ['bar' => '/bar6465s/2'], + ]); + + $res = $response->toArray(); + $this->assertEquals('Bar two', $res['title']); + } + + protected function setUp(): void + { + self::bootKernel(); + + $container = static::getContainer(); + $registry = $container->get('doctrine'); + $manager = $registry->getManager(); + if (!$manager instanceof EntityManagerInterface) { + return; + } + + $classes = []; + foreach ([Foo::class, Bar::class] as $entityClass) { + $classes[] = $manager->getClassMetadata($entityClass); + } + + $schemaTool = new SchemaTool($manager); + @$schemaTool->createSchema($classes); + + $foo = new Foo(); + $foo->title = 'Foo'; + $manager->persist($foo); + $bar = new Bar(); + $bar->title = 'Bar one'; + $manager->persist($bar); + $bar2 = new Bar(); + $bar2->title = 'Bar two'; + $manager->persist($bar2); + $manager->flush(); + } + + protected function tearDown(): void + { + $container = static::getContainer(); + $registry = $container->get('doctrine'); + $manager = $registry->getManager(); + if (!$manager instanceof EntityManagerInterface) { + return; + } + + $classes = []; + foreach ([Foo::class, Bar::class] as $entityClass) { + $classes[] = $manager->getClassMetadata($entityClass); + } + + $schemaTool = new SchemaTool($manager); + @$schemaTool->dropSchema($classes); + parent::tearDown(); + } +}