diff --git a/ModelDescriber/Annotations/AnnotationsReader.php b/ModelDescriber/Annotations/AnnotationsReader.php index 62641ab..261bd84 100644 --- a/ModelDescriber/Annotations/AnnotationsReader.php +++ b/ModelDescriber/Annotations/AnnotationsReader.php @@ -14,6 +14,7 @@ namespace Nelmio\ApiDocBundle\ModelDescriber\Annotations; use Doctrine\Common\Annotations\Reader; use Nelmio\ApiDocBundle\Model\ModelRegistry; use OpenApi\Annotations as OA; +use OpenApi\Generator; /** * @internal @@ -37,10 +38,14 @@ class AnnotationsReader $this->symfonyConstraintAnnotationReader = new SymfonyConstraintAnnotationReader($annotationsReader); } - public function updateDefinition(\ReflectionClass $reflectionClass, OA\Schema $schema): void + public function updateDefinition(\ReflectionClass $reflectionClass, OA\Schema $schema): UpdateClassDefinitionResult { $this->openApiAnnotationsReader->updateSchema($reflectionClass, $schema); $this->symfonyConstraintAnnotationReader->setSchema($schema); + + return new UpdateClassDefinitionResult( + $this->shouldDescribeModelProperties($schema) + ); } public function getPropertyName($reflection, string $default): string @@ -54,4 +59,15 @@ class AnnotationsReader $this->phpDocReader->updateProperty($reflection, $property); $this->symfonyConstraintAnnotationReader->updateProperty($reflection, $property); } + + /** + * if an objects schema type and ref are undefined OR the object was manually + * defined as an object, then we're good to do the normal describe flow of + * class properties. + */ + private function shouldDescribeModelProperties(OA\Schema $schema): bool + { + return (Generator::UNDEFINED === $schema->type || 'object' === $schema->type) + && Generator::UNDEFINED === $schema->ref; + } } diff --git a/ModelDescriber/Annotations/UpdateClassDefinitionResult.php b/ModelDescriber/Annotations/UpdateClassDefinitionResult.php new file mode 100644 index 0000000..3a3945c --- /dev/null +++ b/ModelDescriber/Annotations/UpdateClassDefinitionResult.php @@ -0,0 +1,41 @@ +shouldDescribeModelProperties = $shouldDescribeModelProperties; + } + + public function shouldDescribeModelProperties(): bool + { + return $this->shouldDescribeModelProperties; + } +} diff --git a/ModelDescriber/FormModelDescriber.php b/ModelDescriber/FormModelDescriber.php index 093422b..7a398a8 100644 --- a/ModelDescriber/FormModelDescriber.php +++ b/ModelDescriber/FormModelDescriber.php @@ -63,12 +63,16 @@ final class FormModelDescriber implements ModelDescriberInterface, ModelRegistry throw new \LogicException('You need to enable forms in your application to use a form as a model.'); } - $schema->type = 'object'; - $class = $model->getType()->getClassName(); $annotationsReader = new AnnotationsReader($this->doctrineReader, $this->modelRegistry, $this->mediaTypes); - $annotationsReader->updateDefinition(new \ReflectionClass($class), $schema); + $classResult = $annotationsReader->updateDefinition(new \ReflectionClass($class), $schema); + + if (!$classResult->shouldDescribeModelProperties()) { + return; + } + + $schema->type = 'object'; $form = $this->formFactory->create($class, null, $model->getOptions() ?? []); $this->parseForm($schema, $form); diff --git a/ModelDescriber/JMSModelDescriber.php b/ModelDescriber/JMSModelDescriber.php index 60a57ac..0a65c26 100644 --- a/ModelDescriber/JMSModelDescriber.php +++ b/ModelDescriber/JMSModelDescriber.php @@ -73,9 +73,13 @@ class JMSModelDescriber implements ModelDescriberInterface, ModelRegistryAwareIn throw new \InvalidArgumentException(sprintf('No metadata found for class %s.', $className)); } - $schema->type = 'object'; $annotationsReader = new AnnotationsReader($this->doctrineReader, $this->modelRegistry, $this->mediaTypes); - $annotationsReader->updateDefinition(new \ReflectionClass($className), $schema); + $classResult = $annotationsReader->updateDefinition(new \ReflectionClass($className), $schema); + + if (!$classResult->shouldDescribeModelProperties()) { + return; + } + $schema->type = 'object'; $isJmsV1 = null !== $this->namingStrategy; diff --git a/ModelDescriber/ObjectModelDescriber.php b/ModelDescriber/ObjectModelDescriber.php index 21f8d03..e10dba6 100644 --- a/ModelDescriber/ObjectModelDescriber.php +++ b/ModelDescriber/ObjectModelDescriber.php @@ -58,8 +58,6 @@ class ObjectModelDescriber implements ModelDescriberInterface, ModelRegistryAwar public function describe(Model $model, OA\Schema $schema) { - $schema->type = 'object'; - $class = $model->getType()->getClassName(); $schema->_context->class = $class; @@ -70,7 +68,13 @@ class ObjectModelDescriber implements ModelDescriberInterface, ModelRegistryAwar $reflClass = new \ReflectionClass($class); $annotationsReader = new AnnotationsReader($this->doctrineReader, $this->modelRegistry, $this->mediaTypes); - $annotationsReader->updateDefinition($reflClass, $schema); + $classResult = $annotationsReader->updateDefinition($reflClass, $schema); + + if (!$classResult->shouldDescribeModelProperties()) { + return; + } + + $schema->type = 'object'; $discriminatorMap = $this->getAnnotation($reflClass, DiscriminatorMap::class); if ($discriminatorMap && Generator::UNDEFINED === $schema->discriminator) { diff --git a/Resources/doc/faq.rst b/Resources/doc/faq.rst index 6398a41..439dfb5 100644 --- a/Resources/doc/faq.rst +++ b/Resources/doc/faq.rst @@ -210,3 +210,30 @@ A: Use ``disable_default_routes`` config in your area. areas: default: disable_default_routes: true + +Overriding a Form or Plain PHP Object Schema Type +------------------------------------------------- + +Q: I'd like to define a PHP object or form with a type other any ``object``, how +do I do that? + +A: By using the ``@OA\Schema`` annotation or attribute with a ``type`` or ``ref``. +Note, however, that a ``type="object"`` will still read all a models properties. + +.. code-block:: php + + add('ignored', InputType::class, [ + 'required' => false, + ]); + } +} diff --git a/Tests/Functional/Form/FormWithRefType.php b/Tests/Functional/Form/FormWithRefType.php new file mode 100644 index 0000000..7904230 --- /dev/null +++ b/Tests/Functional/Form/FormWithRefType.php @@ -0,0 +1,31 @@ +add('ignored', InputType::class, [ + 'required' => false, + ]); + } +} diff --git a/Tests/Functional/FunctionalTest.php b/Tests/Functional/FunctionalTest.php index 6ef59df..617a98d 100644 --- a/Tests/Functional/FunctionalTest.php +++ b/Tests/Functional/FunctionalTest.php @@ -635,4 +635,49 @@ class FunctionalTest extends WebTestCase $this->assertSame('string', $model->type); $this->assertCount(2, $model->enum); } + + public function testEntitiesWithOverriddenSchemaTypeDoNotReadOtherProperties() + { + $model = $this->getModel('EntityWithAlternateType'); + + $this->assertSame('array', $model->type); + $this->assertSame('string', $model->items->type); + $this->assertSame(Generator::UNDEFINED, $model->properties); + } + + public function testEntitiesWithRefInSchemaDoNoReadOtherProperties() + { + $model = $this->getModel('EntityWithRef'); + + $this->assertSame(Generator::UNDEFINED, $model->type); + $this->assertSame('#/components/schemas/Test', $model->ref); + $this->assertSame(Generator::UNDEFINED, $model->properties); + } + + public function testEntitiesWithObjectTypeStillReadProperties() + { + $model = $this->getModel('EntityWithObjectType'); + + $this->assertSame('object', $model->type); + $this->assertCount(1, $model->properties); + $property = Util::getProperty($model, 'notIgnored'); + $this->assertSame('string', $property->type); + } + + public function testFormsWithOverriddenSchemaTypeDoNotReadOtherProperties() + { + $model = $this->getModel('FormWithAlternateSchemaType'); + + $this->assertSame('string', $model->type); + $this->assertSame(Generator::UNDEFINED, $model->properties); + } + + public function testFormWithRefInSchemaDoNoReadOtherProperties() + { + $model = $this->getModel('FormWithRefType'); + + $this->assertSame(Generator::UNDEFINED, $model->type); + $this->assertSame('#/components/schemas/Test', $model->ref); + $this->assertSame(Generator::UNDEFINED, $model->properties); + } }