diff --git a/ModelDescriber/ApplyOpenApiDiscriminatorTrait.php b/ModelDescriber/ApplyOpenApiDiscriminatorTrait.php new file mode 100644 index 0000000..3c9446d --- /dev/null +++ b/ModelDescriber/ApplyOpenApiDiscriminatorTrait.php @@ -0,0 +1,60 @@ + $typeMap the map of $discriminatorProperty values to their + * types + */ + protected function applyOpenApiDiscriminator( + Model $model, + OA\Schema $schema, + ModelRegistry $modelRegistry, + string $discriminatorProperty, + array $typeMap + ): void { + $schema->oneOf = []; + $schema->discriminator = new OA\Discriminator([]); + $schema->discriminator->propertyName = $discriminatorProperty; + $schema->discriminator->mapping = []; + foreach ($typeMap as $propertyValue => $className) { + $oneOfSchema = new OA\Schema([]); + $oneOfSchema->ref = $modelRegistry->register(new Model( + new Type(Type::BUILTIN_TYPE_OBJECT, false, $className), + $model->getGroups(), + $model->getOptions() + )); + $schema->oneOf[] = $oneOfSchema; + $schema->discriminator->mapping[$propertyValue] = $oneOfSchema->ref; + } + } +} diff --git a/ModelDescriber/ObjectModelDescriber.php b/ModelDescriber/ObjectModelDescriber.php index fc86439..5260da8 100644 --- a/ModelDescriber/ObjectModelDescriber.php +++ b/ModelDescriber/ObjectModelDescriber.php @@ -22,11 +22,13 @@ use Nelmio\ApiDocBundle\PropertyDescriber\PropertyDescriberInterface; use OpenApi\Annotations as OA; use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface; use Symfony\Component\PropertyInfo\Type; +use Symfony\Component\Serializer\Annotation\DiscriminatorMap; use Symfony\Component\Serializer\NameConverter\NameConverterInterface; class ObjectModelDescriber implements ModelDescriberInterface, ModelRegistryAwareInterface { use ModelRegistryAwareTrait; + use ApplyOpenApiDiscriminatorTrait; /** @var PropertyInfoExtractorInterface */ private $propertyInfo; @@ -71,6 +73,17 @@ class ObjectModelDescriber implements ModelDescriberInterface, ModelRegistryAwar $annotationsReader = new AnnotationsReader($this->doctrineReader, $this->modelRegistry, $this->mediaTypes); $annotationsReader->updateDefinition($reflClass, $schema); + $discriminatorMap = $this->doctrineReader->getClassAnnotation($reflClass, DiscriminatorMap::class); + if ($discriminatorMap && OA\UNDEFINED === $schema->discriminator) { + $this->applyOpenApiDiscriminator( + $model, + $schema, + $this->modelRegistry, + $discriminatorMap->getTypeProperty(), + $discriminatorMap->getMapping() + ); + } + $propertyInfoProperties = $this->propertyInfo->getProperties($class, $context); if (null === $propertyInfoProperties) { diff --git a/Tests/Functional/Controller/ApiController.php b/Tests/Functional/Controller/ApiController.php index 9018102..4a40897 100644 --- a/Tests/Functional/Controller/ApiController.php +++ b/Tests/Functional/Controller/ApiController.php @@ -18,6 +18,7 @@ use Nelmio\ApiDocBundle\Annotation\Security; use Nelmio\ApiDocBundle\Tests\Functional\Entity\Article; use Nelmio\ApiDocBundle\Tests\Functional\Entity\CompoundEntity; use Nelmio\ApiDocBundle\Tests\Functional\Entity\SymfonyConstraints; +use Nelmio\ApiDocBundle\Tests\Functional\Entity\SymfonyDiscriminator; use Nelmio\ApiDocBundle\Tests\Functional\Entity\User; use Nelmio\ApiDocBundle\Tests\Functional\Form\DummyType; use Nelmio\ApiDocBundle\Tests\Functional\Form\UserType; @@ -222,4 +223,13 @@ class ApiController public function compoundEntityAction() { } + + /** + * @Route("/discriminator-mapping", methods={"GET", "POST"}) + * + * @OA\Response(response=200, description="Worked well!", @Model(type=SymfonyDiscriminator::class)) + */ + public function discriminatorMappingAction() + { + } } diff --git a/Tests/Functional/Entity/SymfonyDiscriminator.php b/Tests/Functional/Entity/SymfonyDiscriminator.php new file mode 100644 index 0000000..cd4e562 --- /dev/null +++ b/Tests/Functional/Entity/SymfonyDiscriminator.php @@ -0,0 +1,28 @@ +assertNotHasProperty('protectedField', $model); $this->assertNotHasProperty('protected', $model); } + + public function testModelsWithDiscriminatorMapAreLoadedWithOpenApiPolymorphism() + { + $model = $this->getModel('SymfonyDiscriminator'); + + $this->assertInstanceOf(OA\Discriminator::class, $model->discriminator); + $this->assertSame('type', $model->discriminator->propertyName); + $this->assertCount(2, $model->discriminator->mapping); + $this->assertArrayHasKey('one', $model->discriminator->mapping); + $this->assertArrayHasKey('two', $model->discriminator->mapping); + $this->assertNotSame(OA\UNDEFINED, $model->oneOf); + $this->assertCount(2, $model->oneOf); + } + + public function testDiscriminatorMapLoadsChildrenModels() + { + // get model does its own assertions + $this->getModel('SymfonyDiscriminatorOne'); + $this->getModel('SymfonyDiscriminatorTwo'); + } } diff --git a/Tests/ModelDescriber/ApplyOpenApiDiscriminatorTraitTest.php b/Tests/ModelDescriber/ApplyOpenApiDiscriminatorTraitTest.php new file mode 100644 index 0000000..51df5e4 --- /dev/null +++ b/Tests/ModelDescriber/ApplyOpenApiDiscriminatorTraitTest.php @@ -0,0 +1,87 @@ + 123]; + + private $schema; + + private $model; + + public function testApplyAddsDiscriminatorProperty() + { + $this->applyOpenApiDiscriminator($this->model, $this->schema, $this->modelRegistry, 'type', [ + 'one' => 'FirstType', + 'two' => 'SecondType', + ]); + + $this->assertInstanceOf(OA\Discriminator::class, $this->schema->discriminator); + $this->assertSame('type', $this->schema->discriminator->propertyName); + $this->assertArrayHasKey('one', $this->schema->discriminator->mapping); + $this->assertSame( + $this->modelRegistry->register($this->createModel('FirstType')), + $this->schema->discriminator->mapping['one'] + ); + $this->assertArrayHasKey('two', $this->schema->discriminator->mapping); + $this->assertSame( + $this->modelRegistry->register($this->createModel('SecondType')), + $this->schema->discriminator->mapping['two'] + ); + } + + public function testApplyAddsOneOfFieldToSchema() + { + $this->applyOpenApiDiscriminator($this->model, $this->schema, $this->modelRegistry, 'type', [ + 'one' => 'FirstType', + 'two' => 'SecondType', + ]); + + $this->assertNotSame(OA\UNDEFINED, $this->schema->oneOf); + $this->assertCount(2, $this->schema->oneOf); + $this->assertSame( + $this->modelRegistry->register($this->createModel('FirstType')), + $this->schema->oneOf[0]->ref + ); + $this->assertSame( + $this->modelRegistry->register($this->createModel('SecondType')), + $this->schema->oneOf[1]->ref + ); + } + + protected function setUp(): void + { + $this->schema = new OA\Schema([]); + $this->model = $this->createModel(__CLASS__); + $this->modelRegistry = new ModelRegistry([], new OA\OpenApi([])); + } + + private function createModel(string $className): Model + { + return new Model( + new Type(Type::BUILTIN_TYPE_OBJECT, false, $className), + self::GROUPS, + self::OPTIONS + ); + } +}