Skip to content

Commit b6f84b2

Browse files
committed
fix #6485
1 parent 533f07e commit b6f84b2

File tree

14 files changed

+345
-112
lines changed

14 files changed

+345
-112
lines changed

features/jsonapi/related-resouces-inclusion.feature

+4
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,9 @@ Feature: JSON API Inclusion of Related Resources
5454
}
5555
"""
5656

57+
@createSchema
5758
Scenario: Request inclusion of a non existing related resource
59+
Given there are 3 dummy property objects
5860
When I send a "GET" request to "/dummy_properties/1?include=foo"
5961
Then the response status code should be 200
6062
And the response should be in JSON
@@ -87,7 +89,9 @@ Feature: JSON API Inclusion of Related Resources
8789
}
8890
"""
8991

92+
@createSchema
9093
Scenario: Request inclusion of a related resource keeping main object properties unfiltered
94+
Given there are 3 dummy property objects
9195
When I send a "GET" request to "/dummy_properties/1?include=group&fields[group]=id,foo&fields[DummyProperty]=bar,baz"
9296
Then the response status code should be 200
9397
And the response should be in JSON

src/Hydra/JsonSchema/SchemaFactory.php

+103-38
Original file line numberDiff line numberDiff line change
@@ -37,37 +37,43 @@ final class SchemaFactory implements SchemaFactoryInterface, SchemaFactoryAwareI
3737
use SchemaUriPrefixTrait;
3838

3939
private const ITEM_BASE_SCHEMA_NAME = 'HydraItemBaseSchema';
40+
private const ITEM_BASE_SCHEMA_OUTPUT_NAME = 'HydraOutputBaseSchema';
4041
private const COLLECTION_BASE_SCHEMA_NAME = 'HydraCollectionBaseSchema';
4142
private const BASE_PROP = [
42-
'readOnly' => true,
4343
'type' => 'string',
4444
];
4545
private const BASE_PROPS = [
4646
'@id' => self::BASE_PROP,
4747
'@type' => self::BASE_PROP,
4848
];
49-
private const BASE_ROOT_PROPS = [
50-
'@context' => [
51-
'readOnly' => true,
52-
'oneOf' => [
53-
['type' => 'string'],
54-
[
55-
'type' => 'object',
56-
'properties' => [
57-
'@vocab' => [
58-
'type' => 'string',
59-
],
60-
'hydra' => [
61-
'type' => 'string',
62-
'enum' => [ContextBuilder::HYDRA_NS],
49+
private const ITEM_BASE_SCHEMA = [
50+
'type' => 'object',
51+
'properties' => [
52+
'@context' => [
53+
'oneOf' => [
54+
['type' => 'string'],
55+
[
56+
'type' => 'object',
57+
'properties' => [
58+
'@vocab' => [
59+
'type' => 'string',
60+
],
61+
'hydra' => [
62+
'type' => 'string',
63+
'enum' => [ContextBuilder::HYDRA_NS],
64+
],
6365
],
66+
'required' => ['@vocab', 'hydra'],
67+
'additionalProperties' => true,
6468
],
65-
'required' => ['@vocab', 'hydra'],
66-
'additionalProperties' => true,
6769
],
68-
],
70+
] + self::BASE_PROPS,
6971
],
70-
] + self::BASE_PROPS;
72+
];
73+
74+
private const ITEM_BASE_SCHEMA_OUTPUT = [
75+
'required' => ['@id', '@type'],
76+
] + self::ITEM_BASE_SCHEMA;
7177

7278
/**
7379
* @param array<string, mixed> $defaultContext
@@ -106,47 +112,74 @@ public function buildSchema(string $className, string $format = 'jsonld', string
106112
$inputOrOutputClass = $this->findOutputClass($className, $type, $operation, $serializerContext);
107113
$serializerContext ??= $this->getSerializerContext($operation, $type);
108114
}
115+
109116
if (null === $inputOrOutputClass) {
110117
// input or output disabled
111118
return $this->schemaFactory->buildSchema($className, $format, $type, $operation, $schema, $serializerContext, $forceCollection);
112119
}
113120

114-
$schema = $this->schemaFactory->buildSchema($className, 'json', $type, $operation, $schema, $serializerContext, $forceCollection);
121+
if ($schema) {
122+
$definitions = $schema->getDefinitions();
123+
$jsonDefinitionName = $this->definitionNameFactory->create($className, 'json', $className, $operation, $serializerContext);
124+
125+
if (!isset($definitions[$jsonDefinitionName])) {
126+
$schema = $this->schemaFactory->buildSchema($className, 'json', $type, $operation, $schema, $serializerContext, $forceCollection);
127+
}
128+
} else {
129+
$schema = $this->schemaFactory->buildSchema($className, 'json', $type, $operation, $schema, $serializerContext, $forceCollection);
130+
}
131+
115132
$definitionName = $this->definitionNameFactory->create($className, $format, $className, $operation, $serializerContext);
116133
$definitions = $schema->getDefinitions();
117-
$prefix = $this->getSchemaUriPrefix($schema->getVersion());
118-
$collectionKey = $schema->getItemsDefinitionKey();
119134

120-
// Already computed
121-
if (!$collectionKey && isset($definitions[$definitionName])) {
122-
$schema['$ref'] = $prefix.$definitionName;
135+
$addJsonLdBaseSchema = false;
123136

124-
return $schema;
137+
if (!isset($definitions[$definitionName])) {
138+
$addJsonLdBaseSchema = true;
139+
// only compute json-ld references, skip the scalar properties as they're inherited from the json format
140+
$schema = $this->schemaFactory->buildSchema($className, 'jsonld', $type, $operation, $schema, [self::COMPUTE_REFERENCES => true] + $serializerContext, $forceCollection);
125141
}
126142

127-
$key = $schema->getRootDefinitionKey() ?? $collectionKey;
143+
$prefix = $this->getSchemaUriPrefix($schema->getVersion());
144+
$collectionKey = $schema->getItemsDefinitionKey();
128145

129-
if (!isset($definitions[self::ITEM_BASE_SCHEMA_NAME])) {
130-
$definitions[self::ITEM_BASE_SCHEMA_NAME] = ['type' => 'object', 'properties' => self::BASE_ROOT_PROPS];
146+
$key = $schema->getRootDefinitionKey() ?? $collectionKey;
147+
$name = Schema::TYPE_OUTPUT === $type ? self::ITEM_BASE_SCHEMA_NAME : self::ITEM_BASE_SCHEMA_OUTPUT_NAME;
148+
if (!isset($definitions[$name])) {
149+
$definitions[$name] = Schema::TYPE_OUTPUT === $type ? self::ITEM_BASE_SCHEMA_OUTPUT : self::ITEM_BASE_SCHEMA;
131150
}
132151

133-
$definitions[$definitionName] = [
134-
'allOf' => [
135-
['$ref' => $prefix.self::ITEM_BASE_SCHEMA_NAME],
152+
if (!$collectionKey && isset($definitions[$definitionName])) {
153+
if (!$addJsonLdBaseSchema) {
154+
$schema['$ref'] = $prefix.$definitionName;
155+
156+
return $schema;
157+
}
158+
159+
$allOf = [
160+
['$ref' => $prefix.$name],
136161
['$ref' => $prefix.$key],
137-
],
138-
];
162+
];
139163

140-
if (isset($definitions[$key]['description'])) {
141-
$definitions[$definitionName]['description'] = $definitions[$key]['description'];
142-
}
164+
// if there're no properties, we did not compute any json-ld specific reference
165+
if (isset($definitions[$definitionName]['properties'])) {
166+
$allOf[] = $definitions[$definitionName];
167+
}
168+
169+
$definitions[$definitionName] = new \ArrayObject([
170+
'allOf' => $allOf,
171+
]);
143172

144-
if (!$collectionKey) {
173+
$schema->setDefinitions($definitions);
145174
$schema['$ref'] = $prefix.$definitionName;
146175

147176
return $schema;
148177
}
149178

179+
if (isset($definitions[$key]['description'])) {
180+
$definitions[$definitionName]['description'] = $definitions[$key]['description'];
181+
}
182+
150183
// handle hydra:Collection
151184
if (($schema['type'] ?? '') === 'array') {
152185
$hydraPrefix = $this->getHydraPrefix($serializerContext + $this->defaultContext);
@@ -266,4 +299,36 @@ public function setSchemaFactory(SchemaFactoryInterface $schemaFactory): void
266299
$this->schemaFactory->setSchemaFactory($schemaFactory);
267300
}
268301
}
302+
303+
private function collectRefs(array|\ArrayObject $baseFormatSchema, $prefix)
304+
{
305+
if (!$key = $this->getSubSchemaKey($baseFormatSchema)) {
306+
return null;
307+
}
308+
309+
foreach ($baseFormatSchema[$key] as $k => $s) {
310+
if (isset($s['$ref'])) {
311+
dd($s['$ref'], $prefix);
312+
}
313+
314+
if (!$s instanceof \ArrayObject) {
315+
continue;
316+
}
317+
318+
$this->collectRefs($s, $prefix);
319+
}
320+
321+
return [];
322+
}
323+
324+
private function getSubSchemaKey(array|\ArrayObject $subSchema): ?string
325+
{
326+
foreach (['properties', 'items', 'allOf', 'anyOf', 'oneOf'] as $key) {
327+
if (isset($subSchema[$key])) {
328+
return $key;
329+
}
330+
}
331+
332+
return null;
333+
}
269334
}

src/JsonApi/JsonSchema/SchemaFactory.php

+10-5
Original file line numberDiff line numberDiff line change
@@ -322,15 +322,20 @@ private function buildDefinitionPropertiesSchema(string $key, string $className,
322322
}
323323

324324
if ($required = $definitions[$key]['required'] ?? null) {
325-
foreach ($required as $require) {
326-
if (isset($replacement['attributes']['properties'][$require])) {
327-
$replacement['attributes']['required'][] = $require;
328-
continue;
329-
}
325+
foreach ($required as $i => $require) {
330326
if (isset($relationships[$require])) {
331327
$replacement['relationships']['required'][] = $require;
328+
unset($required[$i]);
332329
}
333330
}
331+
332+
$replacement['attributes'] = [
333+
'allOf' => [
334+
$replacement['attributes'],
335+
['type' => 'object', 'required' => $required],
336+
],
337+
];
338+
334339
unset($definitions[$key]['required']);
335340
}
336341

src/JsonSchema/Metadata/Property/Factory/SchemaPropertyMetadataFactory.php

+2-4
Original file line numberDiff line numberDiff line change
@@ -535,10 +535,8 @@ private function getLegacyClassType(?string $className, bool $nullable, ?bool $r
535535
];
536536
}
537537

538-
if ($className && !$isResourceClass) {
539-
return ['type' => 'object'];
540-
}
541-
538+
// When this is set, we compute the schema at SchemaFactory::buildPropertySchema as it
539+
// will end up being a $ref to another class schema, we don't have enough informations here
542540
return ['type' => Schema::UNKNOWN_TYPE];
543541
}
544542

0 commit comments

Comments
 (0)