Skip to content

Commit e5d1f8e

Browse files
committed
read item uri template operation to get proper @type
1 parent e9333c5 commit e5d1f8e

File tree

6 files changed

+261
-5
lines changed

6 files changed

+261
-5
lines changed

src/JsonLd/Serializer/ItemNormalizer.php

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use ApiPlatform\Metadata\Exception\ItemNotFoundException;
1919
use ApiPlatform\Metadata\HttpOperation;
2020
use ApiPlatform\Metadata\IriConverterInterface;
21+
use ApiPlatform\Metadata\Operation\Factory\OperationMetadataFactoryInterface;
2122
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
2223
use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
2324
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
@@ -70,7 +71,7 @@ final class ItemNormalizer extends AbstractItemNormalizer
7071
'@vocab',
7172
];
7273

73-
public function __construct(ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, IriConverterInterface $iriConverter, ResourceClassResolverInterface $resourceClassResolver, private readonly ContextBuilderInterface $contextBuilder, ?PropertyAccessorInterface $propertyAccessor = null, ?NameConverterInterface $nameConverter = null, ?ClassMetadataFactoryInterface $classMetadataFactory = null, array $defaultContext = [], ?ResourceAccessCheckerInterface $resourceAccessChecker = null, protected ?TagCollectorInterface $tagCollector = null)
74+
public function __construct(ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, IriConverterInterface $iriConverter, ResourceClassResolverInterface $resourceClassResolver, private readonly ContextBuilderInterface $contextBuilder, ?PropertyAccessorInterface $propertyAccessor = null, ?NameConverterInterface $nameConverter = null, ?ClassMetadataFactoryInterface $classMetadataFactory = null, array $defaultContext = [], ?ResourceAccessCheckerInterface $resourceAccessChecker = null, protected ?TagCollectorInterface $tagCollector = null, private ?OperationMetadataFactoryInterface $operationMetadataFactory = null)
7475
{
7576
parent::__construct($propertyNameCollectionFactory, $propertyMetadataFactory, $iriConverter, $resourceClassResolver, $propertyAccessor, $nameConverter, $classMetadataFactory, $defaultContext, $resourceMetadataCollectionFactory, $resourceAccessChecker, $tagCollector);
7677
}
@@ -115,7 +116,9 @@ public function normalize(mixed $object, ?string $format = null, array $context
115116
$context['output']['iri'] = null;
116117
}
117118

118-
if ($this->resourceClassResolver->isResourceClass($resourceClass)) {
119+
if (isset($context['item_uri_template']) && $this->operationMetadataFactory) {
120+
$context['output']['operation'] = $this->operationMetadataFactory->create($context['item_uri_template']);
121+
} elseif ($this->resourceClassResolver->isResourceClass($resourceClass)) {
119122
$context['output']['operation'] = $this->resourceMetadataCollectionFactory->create($resourceClass)->getOperation();
120123
}
121124

@@ -141,6 +144,11 @@ public function normalize(mixed $object, ?string $format = null, array $context
141144
}
142145

143146
$operation = $context['operation'] ?? null;
147+
148+
if ($this->operationMetadataFactory && isset($context['item_uri_template']) && !$operation) {
149+
$operation = $this->operationMetadataFactory->create($context['item_uri_template']);
150+
}
151+
144152
if ($isResourceClass && !$operation) {
145153
$operation = $this->resourceMetadataCollectionFactory->create($resourceClass)->getOperation();
146154
}

src/Symfony/Bundle/Resources/config/jsonld.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
<argument>%api_platform.serializer.default_context%</argument>
3232
<argument type="service" id="api_platform.security.resource_access_checker" on-invalid="ignore" />
3333
<argument type="service" id="api_platform.http_cache.tag_collector" on-invalid="ignore" />
34+
<argument type="service" id="api_platform.metadata.operation.metadata_factory" on-invalid="ignore" />
3435

3536
<!-- Run before serializer.normalizer.json_serializable -->
3637
<tag name="serializer.normalizer" priority="-890" />
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
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\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 $recipeCategory = null;
55+
56+
public ?string $recipeCuisine = null;
57+
58+
public ?string $suitableForDiet = null;
59+
60+
public static function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
61+
{
62+
$recipe = new self();
63+
$recipe->id = '1';
64+
$recipe->name = 'Dummy Recipe';
65+
$recipe->description = 'A simple recipe for testing purposes.';
66+
$recipe->prepTime = 'PT15M';
67+
$recipe->cookTime = 'PT30M';
68+
$recipe->totalTime = 'PT45M';
69+
$recipe->recipeYield = '2 servings';
70+
$recipe->recipeCategory = ['Lunch', 'Dinner'];
71+
$recipe->recipeIngredient = ['Ingredient 1', 'Ingredient 2'];
72+
$recipe->recipeInstructions = 'Do these things.';
73+
74+
return $recipe;
75+
}
76+
}
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 <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\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 <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\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: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\ItemUriTemplateWithCollection\RecipeCollection;
2727
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue6465\Bar;
2828
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue6465\Foo;
29+
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Recipe as EntityRecipe;
2930
use ApiPlatform\Tests\SetupClassResourcesTrait;
3031
use Doctrine\ORM\EntityManagerInterface;
3132
use Doctrine\ORM\Tools\SchemaTool;
@@ -155,19 +156,72 @@ public function testItemUriTemplate(): void
155156
$this->assertJsonContains([
156157
'member' => [
157158
[
158-
'@type' => 'RecipeCollection',
159+
'@type' => 'ItemRecipe',
159160
'@id' => '/item_uri_template_recipes/1',
160161
'name' => 'Dummy Recipe',
161162
],
162163
[
163-
'@type' => 'RecipeCollection',
164+
'@type' => 'ItemRecipe',
164165
'@id' => '/item_uri_template_recipes/2',
165166
'name' => 'Dummy Recipe 2',
166167
],
167168
],
168169
]);
169170
}
170171

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' => 'ItemRecipe',
208+
'@id' => '/item_uri_template_recipes_state_option/1',
209+
'name' => 'Recipe 0',
210+
],
211+
[
212+
'@type' => 'ItemRecipe',
213+
'@id' => '/item_uri_template_recipes_state_option/2',
214+
'name' => 'Recipe 1',
215+
],
216+
[
217+
'@type' => 'ItemRecipe',
218+
'@id' => '/item_uri_template_recipes_state_option/3',
219+
'name' => 'Recipe 2',
220+
],
221+
],
222+
]);
223+
}
224+
171225
protected function setUp(): void
172226
{
173227
self::bootKernel();
@@ -180,7 +234,7 @@ protected function setUp(): void
180234
}
181235

182236
$classes = [];
183-
foreach ([Foo::class, Bar::class] as $entityClass) {
237+
foreach ([Foo::class, Bar::class, EntityRecipe::class] as $entityClass) {
184238
$classes[] = $manager->getClassMetadata($entityClass);
185239
}
186240

0 commit comments

Comments
 (0)