Skip to content

Commit f3c811d

Browse files
authored
fix(hydra): add base schema to item of a collection (#7444)
1 parent 6c26740 commit f3c811d

File tree

3 files changed

+93
-49
lines changed

3 files changed

+93
-49
lines changed

src/Hydra/JsonSchema/SchemaFactory.php

Lines changed: 50 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -117,69 +117,35 @@ public function buildSchema(string $className, string $format = 'jsonld', string
117117
$format = 'json';
118118
}
119119

120-
if ('jsonld' !== $format) {
120+
if ('jsonld' !== $format || !$this->isResourceClass($className)) {
121121
return $this->schemaFactory->buildSchema($className, $format, $type, $operation, $schema, $serializerContext, $forceCollection);
122122
}
123-
if (!$this->isResourceClass($className)) {
124-
$operation = null;
125-
$inputOrOutputClass = null;
126-
$serializerContext ??= [];
127-
} else {
128-
$operation = $this->findOperation($className, $type, $operation, $serializerContext, $format);
129-
$inputOrOutputClass = $this->findOutputClass($className, $type, $operation, $serializerContext);
130-
$serializerContext ??= $this->getSerializerContext($operation, $type);
131-
}
123+
124+
$operation = $this->findOperation($className, $type, $operation, $serializerContext, $format);
125+
$inputOrOutputClass = $this->findOutputClass($className, $type, $operation, $serializerContext);
126+
$serializerContext ??= $this->getSerializerContext($operation, $type);
132127

133128
if (null === $inputOrOutputClass) {
134129
// input or output disabled
135130
return $this->schemaFactory->buildSchema($className, $format, $type, $operation, $schema, $serializerContext, $forceCollection);
136131
}
137132

138-
$definitionName = $this->definitionNameFactory->create($className, $format, $inputOrOutputClass, $operation, $serializerContext);
139-
140-
// JSON-LD is slightly different then JSON:API or HAL
141-
// All the references that are resources must also be in JSON-LD therefore combining
142-
// the HydraItemBaseSchema and the JSON schema is harder (unless we loop again through all relationship)
143-
// The less intensive path is to compute the jsonld schemas, then to combine in an allOf
144133
$schema = $this->schemaFactory->buildSchema($className, 'jsonld', $type, $operation, $schema, $serializerContext, $forceCollection);
145134
$definitions = $schema->getDefinitions();
146-
147135
$prefix = $this->getSchemaUriPrefix($schema->getVersion());
148136
$collectionKey = $schema->getItemsDefinitionKey();
149-
$key = $schema->getRootDefinitionKey() ?? $collectionKey;
150137

151138
if (!$collectionKey) {
152-
if ($this->transformed[$definitionName] ?? false) {
153-
return $schema;
154-
}
155-
156-
$hasNoId = Schema::TYPE_OUTPUT === $type && false === ($serializerContext['gen_id'] ?? true);
157-
$baseName = self::ITEM_BASE_SCHEMA_NAME;
158-
159-
if ($hasNoId) {
160-
$baseName = self::ITEM_WITHOUT_ID_BASE_SCHEMA_NAME;
139+
$definitionName = $schema->getRootDefinitionKey() ?? $this->definitionNameFactory->create($className, $format, $inputOrOutputClass, $operation, $serializerContext);
140+
$this->decorateItemDefinition($definitionName, $definitions, $prefix, $type, $serializerContext);
141+
142+
if (isset($definitions[$definitionName])) {
143+
$currentDefinitions = $schema->getDefinitions();
144+
$schema->exchangeArray([]); // Clear the schema
145+
$schema['$ref'] = $prefix.$definitionName;
146+
$schema->setDefinitions($currentDefinitions);
161147
}
162148

163-
if (!isset($definitions[$baseName])) {
164-
$definitions[$baseName] = $hasNoId ? self::ITEM_BASE_SCHEMA_WITHOUT_ID : self::ITEM_BASE_SCHEMA_WITH_ID;
165-
}
166-
167-
$allOf = new \ArrayObject(['allOf' => [
168-
['$ref' => $prefix.$baseName],
169-
$definitions[$key],
170-
]]);
171-
172-
if (isset($definitions[$key]['description'])) {
173-
$allOf['description'] = $definitions[$key]['description'];
174-
}
175-
176-
$definitions[$definitionName] = $allOf;
177-
unset($definitions[$definitionName]['allOf'][1]['description']);
178-
179-
$schema['$ref'] = $prefix.$definitionName;
180-
181-
$this->transformed[$definitionName] = true;
182-
183149
return $schema;
184150
}
185151

@@ -206,11 +172,11 @@ public function buildSchema(string $className, string $format = 'jsonld', string
206172
'type' => 'object',
207173
'required' => [
208174
$hydraPrefix.'member',
209-
'items' => ['type' => 'object'],
210175
],
211176
'properties' => [
212177
$hydraPrefix.'member' => [
213178
'type' => 'array',
179+
'items' => ['type' => 'object'],
214180
],
215181
$hydraPrefix.'totalItems' => [
216182
'type' => 'integer',
@@ -276,6 +242,7 @@ public function buildSchema(string $className, string $format = 'jsonld', string
276242
];
277243
}
278244

245+
$definitionName = $this->definitionNameFactory->create($className, $format, $inputOrOutputClass, $operation, $serializerContext);
279246
$schema['type'] = 'object';
280247
$schema['description'] = "$definitionName collection.";
281248
$schema['allOf'] = [
@@ -293,6 +260,10 @@ public function buildSchema(string $className, string $format = 'jsonld', string
293260

294261
unset($schema['items']);
295262

263+
if (isset($definitions[$collectionKey])) {
264+
$this->decorateItemDefinition($collectionKey, $definitions, $prefix, $type, $serializerContext);
265+
}
266+
296267
return $schema;
297268
}
298269

@@ -302,4 +273,35 @@ public function setSchemaFactory(SchemaFactoryInterface $schemaFactory): void
302273
$this->schemaFactory->setSchemaFactory($schemaFactory);
303274
}
304275
}
276+
277+
private function decorateItemDefinition(string $definitionName, \ArrayObject $definitions, string $prefix, string $type, ?array $serializerContext): void
278+
{
279+
if (!isset($definitions[$definitionName]) || ($this->transformed[$definitionName] ?? false)) {
280+
return;
281+
}
282+
283+
$hasNoId = Schema::TYPE_OUTPUT === $type && false === ($serializerContext['gen_id'] ?? true);
284+
$baseName = self::ITEM_BASE_SCHEMA_NAME;
285+
if ($hasNoId) {
286+
$baseName = self::ITEM_WITHOUT_ID_BASE_SCHEMA_NAME;
287+
}
288+
289+
if (!isset($definitions[$baseName])) {
290+
$definitions[$baseName] = $hasNoId ? self::ITEM_BASE_SCHEMA_WITHOUT_ID : self::ITEM_BASE_SCHEMA_WITH_ID;
291+
}
292+
293+
$allOf = new \ArrayObject(['allOf' => [
294+
['$ref' => $prefix.$baseName],
295+
$definitions[$definitionName],
296+
]]);
297+
298+
if (isset($definitions[$definitionName]['description'])) {
299+
$allOf['description'] = $definitions[$definitionName]['description'];
300+
}
301+
302+
$definitions[$definitionName] = $allOf;
303+
unset($definitions[$definitionName]['allOf'][1]['description']);
304+
305+
$this->transformed[$definitionName] = true;
306+
}
305307
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
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\Issue7426;
15+
16+
use ApiPlatform\Metadata\ApiProperty;
17+
use ApiPlatform\Metadata\GetCollection;
18+
use Symfony\Component\Serializer\Attribute\Groups;
19+
20+
#[GetCollection(
21+
normalizationContext: ['groups' => ['boat:read']],
22+
)]
23+
class Boat
24+
{
25+
#[ApiProperty(identifier: true)]
26+
#[Groups(['boat:read'])]
27+
public int $id;
28+
29+
#[Groups(['boat:read'])]
30+
public string $name;
31+
}

tests/Functional/JsonSchema/JsonLdJsonSchemaTest.php

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@
1414
namespace ApiPlatform\Tests\Functional\JsonSchema;
1515

1616
use ApiPlatform\JsonSchema\SchemaFactoryInterface;
17+
use ApiPlatform\Metadata\Operation\Factory\OperationMetadataFactoryInterface;
1718
use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
19+
use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue7426\Boat;
1820
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue5793\BagOfTests;
1921
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue6212\Nest;
2022
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\RelatedDummy;
@@ -27,6 +29,7 @@ final class JsonLdJsonSchemaTest extends ApiTestCase
2729
use SetupClassResourcesTrait;
2830

2931
protected SchemaFactoryInterface $schemaFactory;
32+
protected OperationMetadataFactoryInterface $operationMetadataFactory;
3033

3134
protected static ?bool $alwaysBootKernel = false;
3235

@@ -35,13 +38,14 @@ final class JsonLdJsonSchemaTest extends ApiTestCase
3538
*/
3639
public static function getResources(): array
3740
{
38-
return [RelatedDummy::class, ThirdLevel::class, RelatedToDummyFriend::class];
41+
return [RelatedDummy::class, ThirdLevel::class, RelatedToDummyFriend::class, Boat::class];
3942
}
4043

4144
protected function setUp(): void
4245
{
4346
parent::setUp();
4447
$this->schemaFactory = self::getContainer()->get('api_platform.json_schema.schema_factory');
48+
$this->operationMetadataFactory = self::getContainer()->get('api_platform.metadata.operation.metadata_factory');
4549
}
4650

4751
public function testSubSchemaJsonLd(): void
@@ -169,4 +173,11 @@ public function testArraySchemaWithMultipleUnionTypes(): void
169173

170174
$this->assertArrayHasKey('Nest.jsonld', $schema['definitions']);
171175
}
176+
177+
public function testSchemaWithoutGetOperation(): void
178+
{
179+
$schema = $this->schemaFactory->buildSchema(Boat::class, 'jsonld', 'output', $this->operationMetadataFactory->create('_api_/boats{._format}_get_collection'));
180+
181+
$this->assertEquals(['$ref' => '#/definitions/HydraItemBaseSchema'], $schema->getDefinitions()['Boat.jsonld-boat.read']['allOf'][0]);
182+
}
172183
}

0 commit comments

Comments
 (0)