Skip to content

Commit 2224ff1

Browse files
committed
feat: support polymorphism
1 parent effc99e commit 2224ff1

File tree

14 files changed

+998
-24
lines changed

14 files changed

+998
-24
lines changed

features/hal/table_inheritance.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ Feature: Table inheritance
7575
"href": "/dummy_table_inheritances/2"
7676
}
7777
},
78+
"swaggerThanParent": true,
7879
"id": 2,
7980
"name": "Foobarbaz inheritance"
8081
}

features/main/table_inheritance.feature

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -548,6 +548,9 @@ Feature: Table inheritance
548548
"type": "string",
549549
"pattern": "^single item$"
550550
},
551+
"bar": {
552+
"type": ["string", "null"]
553+
},
551554
"fooz": {
552555
"type": "string",
553556
"pattern": "fooz"

src/JsonSchema/SchemaFactory.php

Lines changed: 125 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
use ApiPlatform\Metadata\ResourceClassResolverInterface;
2525
use ApiPlatform\Metadata\Util\TypeHelper;
2626
use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
27+
use Symfony\Component\Serializer\Attribute\DiscriminatorMap;
2728
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
2829
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
2930
use Symfony\Component\TypeInfo\Type\BuiltinType;
@@ -160,6 +161,8 @@ public function buildSchema(string $className, string $format = 'json', string $
160161
}
161162
}
162163

164+
$this->buildDiscriminatorSchema($schema, $definitions, $definitionName, $definition, $inputOrOutputClass, $format, $type, $version, $options, $serializerContext, $isJsonMergePatch);
165+
163166
return $schema;
164167
}
165168

@@ -169,16 +172,7 @@ public function buildSchema(string $className, string $format = 'json', string $
169172
private function buildLegacyPropertySchema(Schema $schema, string $definitionName, string $normalizedPropertyName, ApiProperty $propertyMetadata, array $serializerContext, string $format, string $parentType): void
170173
{
171174
$version = $schema->getVersion();
172-
if (Schema::VERSION_SWAGGER === $version || Schema::VERSION_OPENAPI === $version) {
173-
$additionalPropertySchema = $propertyMetadata->getOpenapiContext();
174-
} else {
175-
$additionalPropertySchema = $propertyMetadata->getJsonSchemaContext();
176-
}
177-
178-
$propertySchema = array_merge(
179-
$propertyMetadata->getSchema() ?? [],
180-
$additionalPropertySchema ?? []
181-
);
175+
$propertySchema = $this->getBasePropertySchema($propertyMetadata, $version);
182176

183177
// @see https://github.com/api-platform/core/issues/6299
184178
if (Schema::UNKNOWN_TYPE === ($propertySchema['type'] ?? null) && isset($propertySchema['$ref'])) {
@@ -300,16 +294,7 @@ private function buildLegacyPropertySchema(Schema $schema, string $definitionNam
300294
private function buildPropertySchema(Schema $schema, string $definitionName, string $normalizedPropertyName, ApiProperty $propertyMetadata, array $serializerContext, string $format, string $parentType): void
301295
{
302296
$version = $schema->getVersion();
303-
if (Schema::VERSION_SWAGGER === $version || Schema::VERSION_OPENAPI === $version) {
304-
$additionalPropertySchema = $propertyMetadata->getOpenapiContext();
305-
} else {
306-
$additionalPropertySchema = $propertyMetadata->getJsonSchemaContext();
307-
}
308-
309-
$propertySchema = array_merge(
310-
$propertyMetadata->getSchema() ?? [],
311-
$additionalPropertySchema ?? []
312-
);
297+
$propertySchema = $this->getBasePropertySchema($propertyMetadata, $version);
313298

314299
$extraProperties = $propertyMetadata->getExtraProperties();
315300
// see AttributePropertyMetadataFactory
@@ -566,4 +551,124 @@ private function getSchemaValue(array $schema, string $key): array|string|null
566551

567552
return $schema[$key] ?? $schema['allOf'][0][$key] ?? $schema['anyOf'][0][$key] ?? $schema['oneOf'][0][$key] ?? null;
568553
}
554+
555+
private function getBasePropertySchema(ApiProperty $propertyMetadata, string $version): array
556+
{
557+
if (Schema::VERSION_SWAGGER === $version || Schema::VERSION_OPENAPI === $version) {
558+
$additionalPropertySchema = $propertyMetadata->getOpenapiContext();
559+
} else {
560+
$additionalPropertySchema = $propertyMetadata->getJsonSchemaContext();
561+
}
562+
563+
return array_merge(
564+
$propertyMetadata->getSchema() ?? [],
565+
$additionalPropertySchema ?? []
566+
);
567+
}
568+
569+
/**
570+
* @return array<string, mixed>|null
571+
*/
572+
private function buildSubclassPropertySchema(Schema $schema, ApiProperty $propertyMetadata): ?array
573+
{
574+
$propertySchema = $this->getBasePropertySchema($propertyMetadata, $schema->getVersion());
575+
576+
return $propertySchema ?: null;
577+
}
578+
579+
/**
580+
* Builds polymorphic schema (oneOf + discriminator) when the class has a Symfony DiscriminatorMap attribute.
581+
*/
582+
private function buildDiscriminatorSchema(Schema $schema, \ArrayObject $definitions, string $definitionName, \ArrayObject $definition, string $inputOrOutputClass, string $format, string $type, string $version, array $options, array $serializerContext, bool $isJsonMergePatch): void
583+
{
584+
$reflectionClass = new \ReflectionClass($inputOrOutputClass);
585+
586+
$discriminatorMapAttributes = $reflectionClass->getAttributes(DiscriminatorMap::class);
587+
if (!$discriminatorMapAttributes) {
588+
return;
589+
}
590+
591+
$discriminatorMapInstance = $discriminatorMapAttributes[0]->newInstance();
592+
593+
$discriminatorReflection = new \ReflectionObject($discriminatorMapInstance);
594+
$typeProperty = $discriminatorReflection->getProperty('typeProperty');
595+
$mappingProperty = $discriminatorReflection->getProperty('mapping');
596+
597+
$typeProperty = $typeProperty->getValue($discriminatorMapInstance);
598+
$mapping = $mappingProperty->getValue($discriminatorMapInstance);
599+
600+
if (!$mapping) {
601+
return;
602+
}
603+
604+
$uriPrefix = $this->getSchemaUriPrefix($version);
605+
$oneOf = [];
606+
$discriminatorMapping = [];
607+
608+
$parentPropertyNames = [];
609+
foreach ($this->propertyNameCollectionFactory->create($inputOrOutputClass, $options) as $propertyName) {
610+
$parentPropertyNames[$propertyName] = true;
611+
}
612+
613+
foreach ($mapping as $typeValue => $subClassName) {
614+
$subDefinitionName = $this->definitionNameFactory->create($subClassName, $format, $subClassName, null, $serializerContext + ['schema_type' => $type]);
615+
616+
if (isset($definitions[$subDefinitionName])) {
617+
$oneOf[] = ['$ref' => $uriPrefix.$subDefinitionName];
618+
$discriminatorMapping[$typeValue] = $uriPrefix.$subDefinitionName;
619+
continue;
620+
}
621+
622+
/** @var \ArrayObject<string, array<string, mixed>> $subclassProperties */
623+
$subclassProperties = new \ArrayObject();
624+
625+
foreach ($this->propertyNameCollectionFactory->create($subClassName, $options) as $propertyName) {
626+
if (isset($parentPropertyNames[$propertyName])) {
627+
continue;
628+
}
629+
630+
$propertyMetadata = $this->propertyMetadataFactory->create($subClassName, $propertyName, $options);
631+
632+
if (false === $propertyMetadata->isReadable() && false === $propertyMetadata->isWritable()) {
633+
continue;
634+
}
635+
636+
$normalizedPropertyName = $this->nameConverter ? $this->nameConverter->normalize($propertyName, $subClassName, $format, $serializerContext) : $propertyName;
637+
if ($propertyMetadata->isRequired() && !$isJsonMergePatch) {
638+
$definition['required'][] = $normalizedPropertyName;
639+
}
640+
641+
if ($propertySchema = $this->buildSubclassPropertySchema($schema, $propertyMetadata)) {
642+
$subclassProperties[$normalizedPropertyName] = $propertySchema;
643+
}
644+
}
645+
646+
$allOf = [
647+
['$ref' => $uriPrefix.$definitionName],
648+
];
649+
650+
if (\count($subclassProperties) > 0) {
651+
$extra = ['type' => 'object', 'properties' => $subclassProperties->getArrayCopy()];
652+
if (isset($definition['required']) && \count($definition['required']) > 0) {
653+
$extra['required'] = $definition['required'];
654+
}
655+
$allOf[] = $extra;
656+
}
657+
658+
$subDefinition = new \ArrayObject(['allOf' => new \ArrayObject($allOf)]);
659+
$definitions[$subDefinitionName] = $subDefinition;
660+
661+
$oneOf[] = ['$ref' => $uriPrefix.$subDefinitionName];
662+
$discriminatorMapping[$typeValue] = $uriPrefix.$subDefinitionName;
663+
}
664+
665+
if (\count($oneOf) > 0) {
666+
$definition['oneOf'] = $oneOf;
667+
$normalizedTypeProperty = $this->nameConverter ? $this->nameConverter->normalize($typeProperty, $inputOrOutputClass, $format, $serializerContext) : $typeProperty;
668+
$definition['discriminator'] = [
669+
'propertyName' => $normalizedTypeProperty,
670+
'mapping' => $discriminatorMapping,
671+
];
672+
}
673+
}
569674
}

src/Serializer/AbstractItemNormalizer.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -461,17 +461,17 @@ protected function extractAttributes(object $object, ?string $format = null, arr
461461
*/
462462
protected function getAllowedAttributes(string|object $classOrObject, array $context, bool $attributesAsString = false): array|bool
463463
{
464-
if (!$this->resourceClassResolver->isResourceClass($context['resource_class'])) {
464+
$contextResourceClass = $context['resource_class'];
465+
if (!$this->resourceClassResolver->isResourceClass($contextResourceClass)) {
465466
return parent::getAllowedAttributes($classOrObject, $context, $attributesAsString);
466467
}
467468

468-
$resourceClass = $this->resourceClassResolver->getResourceClass(null, $context['resource_class']); // fix for abstract classes and interfaces
469469
$options = $this->getFactoryOptions($context);
470-
$propertyNames = $this->propertyNameCollectionFactory->create($resourceClass, $options);
470+
$propertyNames = $this->propertyNameCollectionFactory->create($contextResourceClass, $options);
471471

472472
$allowedAttributes = [];
473473
foreach ($propertyNames as $propertyName) {
474-
$propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $propertyName, $options);
474+
$propertyMetadata = $this->propertyMetadataFactory->create($contextResourceClass, $propertyName, $options);
475475

476476
if (
477477
$this->isAllowedAttribute($classOrObject, $propertyName, null, $context)

0 commit comments

Comments
 (0)