|
22 | 22 | use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
|
23 | 23 | use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
|
24 | 24 | use ApiPlatform\Metadata\ResourceClassResolverInterface;
|
| 25 | +use Symfony\Component\PropertyInfo\PropertyInfoExtractor; |
25 | 26 | use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
|
26 | 27 | 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; |
27 | 34 |
|
28 | 35 | /**
|
29 | 36 | * {@inheritdoc}
|
@@ -136,13 +143,20 @@ public function buildSchema(string $className, string $format = 'json', string $
|
136 | 143 | $definition['required'][] = $normalizedPropertyName;
|
137 | 144 | }
|
138 | 145 |
|
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 | + } |
140 | 151 | }
|
141 | 152 |
|
142 | 153 | return $schema;
|
143 | 154 | }
|
144 | 155 |
|
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 |
146 | 160 | {
|
147 | 161 | $version = $schema->getVersion();
|
148 | 162 | if (Schema::VERSION_SWAGGER === $version || Schema::VERSION_OPENAPI === $version) {
|
@@ -256,6 +270,126 @@ private function buildPropertySchema(Schema $schema, string $definitionName, str
|
256 | 270 | $schema->getDefinitions()[$definitionName]['properties'][$normalizedPropertyName] = new \ArrayObject($propertySchema);
|
257 | 271 | }
|
258 | 272 |
|
| 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 | + |
259 | 393 | private function getValidationGroups(Operation $operation): array
|
260 | 394 | {
|
261 | 395 | $groups = $operation->getValidationContext()['groups'] ?? [];
|
|
0 commit comments