Skip to content

Commit 00b7529

Browse files
committed
schema factory part of mtarld#6
1 parent a05ec11 commit 00b7529

File tree

1 file changed

+136
-2
lines changed

1 file changed

+136
-2
lines changed

src/JsonSchema/SchemaFactory.php

+136-2
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,15 @@
2222
use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
2323
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
2424
use ApiPlatform\Metadata\ResourceClassResolverInterface;
25+
use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
2526
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
2627
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
28+
use Symfony\Component\TypeInfo\Type\BuiltinType;
29+
use Symfony\Component\TypeInfo\Type\CollectionType;
30+
use Symfony\Component\TypeInfo\Type\CompositeTypeInterface;
31+
use Symfony\Component\TypeInfo\Type\ObjectType;
32+
use Symfony\Component\TypeInfo\Type\WrappingTypeInterface;
33+
use Symfony\Component\TypeInfo\TypeIdentifier;
2734

2835
/**
2936
* {@inheritdoc}
@@ -136,13 +143,20 @@ public function buildSchema(string $className, string $format = 'json', string $
136143
$definition['required'][] = $normalizedPropertyName;
137144
}
138145

139-
$this->buildPropertySchema($schema, $definitionName, $normalizedPropertyName, $propertyMetadata, $serializerContext, $format, $type);
146+
if (!method_exists(PropertyInfoExtractor::class, 'getType')) {
147+
$this->buildLegacyPropertySchema($schema, $definitionName, $normalizedPropertyName, $propertyMetadata, $serializerContext, $format, $type);
148+
} else {
149+
$this->buildPropertySchema($schema, $definitionName, $normalizedPropertyName, $propertyMetadata, $serializerContext, $format, $type);
150+
}
140151
}
141152

142153
return $schema;
143154
}
144155

145-
private function buildPropertySchema(Schema $schema, string $definitionName, string $normalizedPropertyName, ApiProperty $propertyMetadata, array $serializerContext, string $format, string $parentType): void
156+
/**
157+
* Builds the JSON Schema for a property using the legacy PropertyInfo component.
158+
*/
159+
private function buildLegacyPropertySchema(Schema $schema, string $definitionName, string $normalizedPropertyName, ApiProperty $propertyMetadata, array $serializerContext, string $format, string $parentType): void
146160
{
147161
$version = $schema->getVersion();
148162
if (Schema::VERSION_SWAGGER === $version || Schema::VERSION_OPENAPI === $version) {
@@ -256,6 +270,126 @@ private function buildPropertySchema(Schema $schema, string $definitionName, str
256270
$schema->getDefinitions()[$definitionName]['properties'][$normalizedPropertyName] = new \ArrayObject($propertySchema);
257271
}
258272

273+
private function buildPropertySchema(Schema $schema, string $definitionName, string $normalizedPropertyName, ApiProperty $propertyMetadata, array $serializerContext, string $format, string $parentType): void
274+
{
275+
$version = $schema->getVersion();
276+
if (Schema::VERSION_SWAGGER === $version || Schema::VERSION_OPENAPI === $version) {
277+
$additionalPropertySchema = $propertyMetadata->getOpenapiContext();
278+
} else {
279+
$additionalPropertySchema = $propertyMetadata->getJsonSchemaContext();
280+
}
281+
282+
$propertySchema = array_merge(
283+
$propertyMetadata->getSchema() ?? [],
284+
$additionalPropertySchema ?? []
285+
);
286+
287+
// @see https://github.com/api-platform/core/issues/6299
288+
if (Schema::UNKNOWN_TYPE === ($propertySchema['type'] ?? null) && isset($propertySchema['$ref'])) {
289+
unset($propertySchema['type']);
290+
}
291+
292+
$extraProperties = $propertyMetadata->getExtraProperties() ?? [];
293+
// see AttributePropertyMetadataFactory
294+
if (true === ($extraProperties[SchemaPropertyMetadataFactory::JSON_SCHEMA_USER_DEFINED] ?? false)) {
295+
// schema seems to have been declared by the user: do not override nor complete user value
296+
$schema->getDefinitions()[$definitionName]['properties'][$normalizedPropertyName] = new \ArrayObject($propertySchema);
297+
298+
return;
299+
}
300+
301+
$type = $propertyMetadata->getNativeType();
302+
$propertySchemaType = $propertySchema['type'] ?? false;
303+
$isSchemaDefined = ($propertySchema['$ref'] ?? $propertySchema['anyOf'] ?? $propertySchema['allOf'] ?? $propertySchema['oneOf'] ?? false)
304+
|| ($propertySchemaType && 'string' !== $propertySchemaType && !(\is_array($propertySchemaType) && !\in_array('string', $propertySchemaType, true)))
305+
|| (($propertySchema['format'] ?? $propertySchema['enum'] ?? false) && $propertySchemaType);
306+
307+
// Check if the type is considered "unknown" by SchemaPropertyMetadataFactory
308+
$isUnknown = Schema::UNKNOWN_TYPE === $propertySchemaType
309+
|| ('array' === $propertySchemaType && Schema::UNKNOWN_TYPE === ($propertySchema['items']['type'] ?? null))
310+
|| ('object' === $propertySchemaType && Schema::UNKNOWN_TYPE === ($propertySchema['additionalProperties']['type'] ?? null));
311+
312+
// If schema is defined and not marked as unknown, or if no type info exists, return early
313+
if (!$isUnknown && (null === $type || $isSchemaDefined)) {
314+
$schema->getDefinitions()[$definitionName]['properties'][$normalizedPropertyName] = new \ArrayObject($propertySchema);
315+
316+
return;
317+
}
318+
319+
// property schema is created in SchemaPropertyMetadataFactory, but it cannot build resource reference ($ref)
320+
// complete property schema with resource reference ($ref) if it's related to an object/resource
321+
$refs = [];
322+
$isNullable = $type?->isNullable() ?? false;
323+
324+
if ($type) {
325+
foreach ($type instanceof CompositeTypeInterface ? $type->getTypes() : [$type] as $t) {
326+
if ($t instanceof BuiltinType && TypeIdentifier::NULL === $t->getTypeIdentifier()) {
327+
continue;
328+
}
329+
330+
$valueType = $t;
331+
$isCollection = $t instanceof CollectionType;
332+
333+
if ($isCollection) {
334+
$valueType = $t->getCollectionValueType();
335+
}
336+
337+
while ($valueType instanceof WrappingTypeInterface) {
338+
$valueType = $valueType->getWrappedType();
339+
}
340+
341+
if (!$valueType instanceof ObjectType) {
342+
continue;
343+
}
344+
345+
$className = $valueType->getClassName();
346+
$subSchemaInstance = new Schema($version);
347+
$subSchemaInstance->setDefinitions($schema->getDefinitions());
348+
$subSchemaFactory = $this->schemaFactory ?: $this;
349+
$subSchemaResult = $subSchemaFactory->buildSchema($className, $format, $parentType, null, $subSchemaInstance, $serializerContext + [self::FORCE_SUBSCHEMA => true], false);
350+
if (!isset($subSchemaResult['$ref'])) {
351+
continue;
352+
}
353+
354+
if (false === $propertyMetadata->getGenId()) {
355+
$subDefinitionName = $this->definitionNameFactory->create($className, $format, $className, null, $serializerContext);
356+
if (isset($subSchemaResult->getDefinitions()[$subDefinitionName]['properties']['@id'])) {
357+
unset($subSchemaResult->getDefinitions()[$subDefinitionName]['properties']['@id']);
358+
}
359+
}
360+
361+
if ($isCollection) {
362+
$key = ($propertySchema['type'] ?? null) === 'object' ? 'additionalProperties' : 'items';
363+
if (!isset($propertySchema[$key]) || !\is_array($propertySchema[$key])) {
364+
$propertySchema[$key] = [];
365+
}
366+
$propertySchema[$key]['$ref'] = $subSchemaResult['$ref'];
367+
unset($propertySchema[$key]['type']);
368+
$refs = [];
369+
break;
370+
}
371+
372+
$refs[] = ['$ref' => $subSchemaResult['$ref']];
373+
}
374+
}
375+
376+
if (!empty($refs)) {
377+
if ($isNullable) {
378+
$refs[] = ['type' => 'null'];
379+
}
380+
381+
if (($c = \count($refs)) > 1) {
382+
$propertySchema['anyOf'] = $refs;
383+
unset($propertySchema['type'], $propertySchema['$ref']);
384+
} elseif (1 === $c) {
385+
$propertySchema['$ref'] = $refs[0]['$ref'];
386+
unset($propertySchema['type']);
387+
}
388+
}
389+
390+
$schema->getDefinitions()[$definitionName]['properties'][$normalizedPropertyName] = new \ArrayObject($propertySchema);
391+
}
392+
259393
private function getValidationGroups(Operation $operation): array
260394
{
261395
$groups = $operation->getValidationContext()['groups'] ?? [];

0 commit comments

Comments
 (0)