Skip to content

Commit 7c76fb8

Browse files
authored
fix(jsonld): read identifier with itemUriTemplate (#7517)
1 parent 3df65aa commit 7c76fb8

File tree

5 files changed

+301
-5
lines changed

5 files changed

+301
-5
lines changed

src/Metadata/IdentifiersExtractor.php

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,9 @@ public function getIdentifiersFromItem(object $item, ?Operation $operation = nul
6262
return $this->getIdentifiersFromOperation($item, $operation, $context);
6363
}
6464

65+
/**
66+
* @param array<string, mixed> $context
67+
*/
6568
private function getIdentifiersFromOperation(object $item, Operation $operation, array $context = []): array
6669
{
6770
if ($operation instanceof HttpOperation) {
@@ -75,24 +78,26 @@ private function getIdentifiersFromOperation(object $item, Operation $operation,
7578
if (1 < (is_countable($link->getIdentifiers()) ? \count($link->getIdentifiers()) : 0)) {
7679
$compositeIdentifiers = [];
7780
foreach ($link->getIdentifiers() as $identifier) {
78-
$compositeIdentifiers[$identifier] = $this->getIdentifierValue($item, $link->getFromClass() ?? $operation->getClass(), $identifier, $link->getParameterName());
81+
$compositeIdentifiers[$identifier] = $this->getIdentifierValue($item, $link->getFromClass() ?? $operation->getClass(), $identifier, $link->getParameterName(), null, $context, $operation);
7982
}
8083

8184
$identifiers[$link->getParameterName()] = CompositeIdentifierParser::stringify($compositeIdentifiers);
8285
continue;
8386
}
8487

8588
$parameterName = $link->getParameterName();
86-
$identifiers[$parameterName] = $this->getIdentifierValue($item, $link->getFromClass() ?? $operation->getClass(), $link->getIdentifiers()[0] ?? $k, $parameterName, $link->getToProperty());
89+
$identifiers[$parameterName] = $this->getIdentifierValue($item, $link->getFromClass() ?? $operation->getClass(), $link->getIdentifiers()[0] ?? $k, $parameterName, $link->getToProperty(), $context, $operation);
8790
}
8891

8992
return $identifiers;
9093
}
9194

9295
/**
9396
* Gets the value of the given class property.
97+
*
98+
* @param array<string, mixed> $context
9499
*/
95-
private function getIdentifierValue(object $item, string $class, string $property, string $parameterName, ?string $toProperty = null): float|bool|int|string
100+
private function getIdentifierValue(object $item, string $class, string $property, string $parameterName, ?string $toProperty, array $context, Operation $operation): float|bool|int|string
96101
{
97102
if ($item instanceof $class) {
98103
try {
@@ -102,6 +107,15 @@ private function getIdentifierValue(object $item, string $class, string $propert
102107
}
103108
}
104109

110+
// ItemUriTemplate is defined on a collection and we read the identifier alghough the PHP class may be different
111+
if (isset($context['item_uri_template']) && $operation->getClass() === $class) {
112+
try {
113+
return $this->resolveIdentifierValue($this->propertyAccessor->getValue($item, $property), $parameterName);
114+
} catch (NoSuchPropertyException $e) {
115+
throw new RuntimeException(\sprintf('Could not retrieve identifier "%s" for class "%s" using itemUriTemplate "%s". Check that the property exists and is accessible.', $property, $class, $context['item_uri_template']), $e->getCode(), $e);
116+
}
117+
}
118+
105119
if ($toProperty) {
106120
return $this->resolveIdentifierValue($this->propertyAccessor->getValue($item, "$toProperty.$property"), $parameterName);
107121
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
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\ItemUriTemplateWithCollection;
15+
16+
use ApiPlatform\Doctrine\Orm\State\Options;
17+
use ApiPlatform\Metadata\Get;
18+
use ApiPlatform\Metadata\Operation;
19+
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Recipe as EntityRecipe;
20+
21+
#[Get(
22+
uriTemplate: '/item_uri_template_recipes/{id}{._format}',
23+
shortName: 'ItemRecipe',
24+
uriVariables: ['id'],
25+
provider: [self::class, 'provide'],
26+
openapi: false
27+
)]
28+
#[Get(
29+
uriTemplate: '/item_uri_template_recipes_state_option/{id}{._format}',
30+
shortName: 'ItemRecipe',
31+
uriVariables: ['id'],
32+
openapi: false,
33+
stateOptions: new Options(entityClass: EntityRecipe::class)
34+
)]
35+
class Recipe
36+
{
37+
public ?string $id;
38+
public ?string $name = null;
39+
40+
public ?string $description = null;
41+
42+
public ?string $author = null;
43+
44+
public ?array $recipeIngredient = [];
45+
46+
public ?string $recipeInstructions = null;
47+
48+
public ?string $prepTime = null;
49+
50+
public ?string $cookTime = null;
51+
52+
public ?string $totalTime = null;
53+
54+
public ?string $recipeCuisine = null;
55+
56+
public ?string $suitableForDiet = null;
57+
58+
public static function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
59+
{
60+
$recipe = new self();
61+
$recipe->id = '1';
62+
$recipe->name = 'Dummy Recipe';
63+
$recipe->description = 'A simple recipe for testing purposes.';
64+
$recipe->prepTime = 'PT15M';
65+
$recipe->cookTime = 'PT30M';
66+
$recipe->totalTime = 'PT45M';
67+
$recipe->recipeIngredient = ['Ingredient 1', 'Ingredient 2'];
68+
$recipe->recipeInstructions = 'Do these things.';
69+
70+
return $recipe;
71+
}
72+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
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\ItemUriTemplateWithCollection;
15+
16+
use ApiPlatform\Doctrine\Orm\State\Options;
17+
use ApiPlatform\Metadata\GetCollection;
18+
use ApiPlatform\Metadata\Operation;
19+
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Recipe as EntityRecipe;
20+
21+
#[GetCollection(
22+
uriTemplate: '/item_uri_template_recipes{._format}',
23+
provider: [self::class, 'provide'],
24+
openapi: false,
25+
shortName: 'CollectionRecipe',
26+
itemUriTemplate: '/item_uri_template_recipes/{id}{._format}',
27+
normalizationContext: ['hydra_prefix' => false],
28+
)]
29+
#[GetCollection(
30+
uriTemplate: '/item_uri_template_recipes_state_option{._format}',
31+
openapi: false,
32+
shortName: 'CollectionRecipe',
33+
itemUriTemplate: '/item_uri_template_recipes_state_option/{id}{._format}',
34+
stateOptions: new Options(entityClass: EntityRecipe::class),
35+
normalizationContext: ['hydra_prefix' => false],
36+
)]
37+
class RecipeCollection
38+
{
39+
public ?string $id;
40+
public ?string $name = null;
41+
42+
public static function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
43+
{
44+
$recipe = new self();
45+
$recipe->id = '1';
46+
$recipe->name = 'Dummy Recipe';
47+
48+
$recipe2 = new self();
49+
$recipe2->id = '2';
50+
$recipe2->name = 'Dummy Recipe 2';
51+
52+
return [$recipe, $recipe2];
53+
}
54+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
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\Entity;
15+
16+
use Doctrine\ORM\Mapping as ORM;
17+
18+
#[ORM\Entity]
19+
class Recipe
20+
{
21+
#[ORM\Id]
22+
#[ORM\GeneratedValue]
23+
#[ORM\Column(type: 'integer')]
24+
private ?int $id = null;
25+
26+
#[ORM\Column(type: 'string', nullable: true)]
27+
public ?string $name = null;
28+
29+
#[ORM\Column(type: 'text', nullable: true)]
30+
public ?string $description = null;
31+
32+
#[ORM\Column(type: 'string', nullable: true)]
33+
public ?string $author = null;
34+
35+
#[ORM\Column(type: 'json', nullable: true)]
36+
public ?array $recipeIngredient = [];
37+
38+
#[ORM\Column(type: 'text', nullable: true)]
39+
public ?string $recipeInstructions = null;
40+
41+
#[ORM\Column(type: 'string', nullable: true)]
42+
public ?string $prepTime = null;
43+
44+
#[ORM\Column(type: 'string', nullable: true)]
45+
public ?string $cookTime = null;
46+
47+
#[ORM\Column(type: 'string', nullable: true)]
48+
public ?string $totalTime = null;
49+
50+
#[ORM\Column(type: 'string', nullable: true)]
51+
public ?string $recipeCategory = null;
52+
53+
#[ORM\Column(type: 'string', nullable: true)]
54+
public ?string $recipeCuisine = null;
55+
56+
#[ORM\Column(type: 'string', nullable: true)]
57+
public ?string $suitableForDiet = null;
58+
59+
public function getId(): ?int
60+
{
61+
return $this->id;
62+
}
63+
}

tests/Functional/JsonLdTest.php

Lines changed: 95 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,11 @@
2222
use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue7298\ImageModuleResource;
2323
use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue7298\PageResource;
2424
use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue7298\TitleModuleResource;
25+
use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\ItemUriTemplateWithCollection\Recipe;
26+
use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\ItemUriTemplateWithCollection\RecipeCollection;
2527
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue6465\Bar;
2628
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue6465\Foo;
29+
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Recipe as EntityRecipe;
2730
use ApiPlatform\Tests\SetupClassResourcesTrait;
2831
use Doctrine\ORM\EntityManagerInterface;
2932
use Doctrine\ORM\Tools\SchemaTool;
@@ -39,7 +42,20 @@ class JsonLdTest extends ApiTestCase
3942
*/
4043
public static function getResources(): array
4144
{
42-
return [Foo::class, Bar::class, JsonLdContextOutput::class, GenIdFalse::class, AggregateRating::class, LevelFirst::class, LevelThird::class, PageResource::class, TitleModuleResource::class, ImageModuleResource::class];
45+
return [
46+
Foo::class,
47+
Bar::class,
48+
JsonLdContextOutput::class,
49+
GenIdFalse::class,
50+
AggregateRating::class,
51+
LevelFirst::class,
52+
LevelThird::class,
53+
PageResource::class,
54+
TitleModuleResource::class,
55+
ImageModuleResource::class,
56+
Recipe::class,
57+
RecipeCollection::class,
58+
];
4359
}
4460

4561
/**
@@ -129,6 +145,83 @@ public function testIssue7298(): void
129145
]);
130146
}
131147

148+
public function testItemUriTemplate(): void
149+
{
150+
self::createClient()->request(
151+
'GET',
152+
'/item_uri_template_recipes',
153+
);
154+
$this->assertResponseIsSuccessful();
155+
156+
$this->assertJsonContains([
157+
'member' => [
158+
[
159+
'@type' => 'RecipeCollection',
160+
'@id' => '/item_uri_template_recipes/1',
161+
'name' => 'Dummy Recipe',
162+
],
163+
[
164+
'@type' => 'RecipeCollection',
165+
'@id' => '/item_uri_template_recipes/2',
166+
'name' => 'Dummy Recipe 2',
167+
],
168+
],
169+
]);
170+
}
171+
172+
public function testItemUriTemplateWithStateOption(): void
173+
{
174+
$container = static::getContainer();
175+
$registry = $container->get('doctrine');
176+
$manager = $registry->getManager();
177+
for ($i = 0; $i < 10; ++$i) {
178+
$recipe = new EntityRecipe();
179+
$recipe->name = "Recipe $i";
180+
$recipe->description = "Description of recipe $i";
181+
$recipe->author = "Author $i";
182+
$recipe->recipeIngredient = [
183+
"Ingredient 1 for recipe $i",
184+
"Ingredient 2 for recipe $i",
185+
];
186+
$recipe->recipeInstructions = "Instructions for recipe $i";
187+
$recipe->prepTime = '10 minutes';
188+
$recipe->cookTime = '20 minutes';
189+
$recipe->totalTime = '30 minutes';
190+
$recipe->recipeCategory = "Category $i";
191+
$recipe->recipeCuisine = "Cuisine $i";
192+
$recipe->suitableForDiet = "Diet $i";
193+
194+
$manager->persist($recipe);
195+
}
196+
$manager->flush();
197+
198+
self::createClient()->request(
199+
'GET',
200+
'/item_uri_template_recipes_state_option',
201+
);
202+
$this->assertResponseIsSuccessful();
203+
204+
$this->assertJsonContains([
205+
'member' => [
206+
[
207+
'@type' => 'Recipe',
208+
'@id' => '/item_uri_template_recipes_state_option/1',
209+
'name' => 'Recipe 0',
210+
],
211+
[
212+
'@type' => 'Recipe',
213+
'@id' => '/item_uri_template_recipes_state_option/2',
214+
'name' => 'Recipe 1',
215+
],
216+
[
217+
'@type' => 'Recipe',
218+
'@id' => '/item_uri_template_recipes_state_option/3',
219+
'name' => 'Recipe 2',
220+
],
221+
],
222+
]);
223+
}
224+
132225
protected function setUp(): void
133226
{
134227
self::bootKernel();
@@ -141,7 +234,7 @@ protected function setUp(): void
141234
}
142235

143236
$classes = [];
144-
foreach ([Foo::class, Bar::class] as $entityClass) {
237+
foreach ([Foo::class, Bar::class, EntityRecipe::class] as $entityClass) {
145238
$classes[] = $manager->getClassMetadata($entityClass);
146239
}
147240

0 commit comments

Comments
 (0)