diff --git a/Model/ModelRegistry.php b/Model/ModelRegistry.php
index db346fb..7631d0b 100644
--- a/Model/ModelRegistry.php
+++ b/Model/ModelRegistry.php
@@ -100,7 +100,11 @@ final class ModelRegistry
}
if (null === $schema) {
- throw new \LogicException(sprintf('Schema of type "%s" can\'t be generated, no describer supports it.', $this->typeToString($model->getType())));
+ $errorMessage = sprintf('Schema of type "%s" can\'t be generated, no describer supports it.', $this->typeToString($model->getType()));
+ if (Type::BUILTIN_TYPE_OBJECT === $model->getType()->getBuiltinType() && !class_exists($className = $model->getType()->getClassName())) {
+ $errorMessage .= sprintf(' Class "\\%s" does not exist, did you forget a use statement, or typed it wrong?', $className);
+ }
+ throw new \LogicException($errorMessage);
}
}
}
@@ -174,7 +178,7 @@ final class ModelRegistry
private function typeToString(Type $type): string
{
if (Type::BUILTIN_TYPE_OBJECT === $type->getBuiltinType()) {
- return $type->getClassName();
+ return '\\'.$type->getClassName();
} elseif ($type->isCollection()) {
if (null !== $collectionType = $this->getCollectionValueType($type)) {
return $this->typeToString($collectionType).'[]';
diff --git a/ModelDescriber/Annotations/AnnotationsReader.php b/ModelDescriber/Annotations/AnnotationsReader.php
index 3fbcbfb..85f496d 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
@@ -44,10 +45,14 @@ class 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
@@ -61,4 +66,15 @@ class AnnotationsReader
$this->phpDocReader->updateProperty($reflection, $property);
$this->symfonyConstraintAnnotationReader->updateProperty($reflection, $property, $serializationGroups);
}
+
+ /**
+ * 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/EnumModelDescriber.php b/ModelDescriber/EnumModelDescriber.php
new file mode 100644
index 0000000..41fba6e
--- /dev/null
+++ b/ModelDescriber/EnumModelDescriber.php
@@ -0,0 +1,34 @@
+getType()->getClassName();
+
+ $enums = [];
+ foreach ($enumClass::cases() as $enumCase) {
+ $enums[] = $enumCase->value;
+ }
+
+ $schema->type = is_subclass_of($enumClass, \IntBackedEnum::class) ? 'int' : 'string';
+ $schema->enum = $enums;
+ }
+
+ public function supports(Model $model): bool
+ {
+ if (!function_exists('enum_exists')) {
+ return false;
+ }
+
+ return Type::BUILTIN_TYPE_OBJECT === $model->getType()->getBuiltinType()
+ && enum_exists($model->getType()->getClassName())
+ && is_subclass_of($model->getType()->getClassName(), \BackedEnum::class);
+ }
+}
diff --git a/ModelDescriber/FormModelDescriber.php b/ModelDescriber/FormModelDescriber.php
index 370f8a6..587b3a1 100644
--- a/ModelDescriber/FormModelDescriber.php
+++ b/ModelDescriber/FormModelDescriber.php
@@ -69,8 +69,6 @@ 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(
@@ -79,7 +77,13 @@ final class FormModelDescriber implements ModelDescriberInterface, ModelRegistry
$this->mediaTypes,
$this->useValidationGroups
);
- $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 9dc2c4e..fd59df7 100644
--- a/ModelDescriber/JMSModelDescriber.php
+++ b/ModelDescriber/JMSModelDescriber.php
@@ -80,14 +80,18 @@ 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,
$this->useValidationGroups
);
- $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 55c8115..925ea5c 100644
--- a/ModelDescriber/ObjectModelDescriber.php
+++ b/ModelDescriber/ObjectModelDescriber.php
@@ -62,8 +62,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;
@@ -79,7 +77,13 @@ class ObjectModelDescriber implements ModelDescriberInterface, ModelRegistryAwar
$this->mediaTypes,
$this->useValidationGroups
);
- $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/config/services.xml b/Resources/config/services.xml
index bfc845c..b107302 100644
--- a/Resources/config/services.xml
+++ b/Resources/config/services.xml
@@ -80,6 +80,10 @@
+
+
+
+
diff --git a/Resources/doc/areas.rst b/Resources/doc/areas.rst
index da2e43c..e1f0f08 100644
--- a/Resources/doc/areas.rst
+++ b/Resources/doc/areas.rst
@@ -32,6 +32,7 @@ You can define areas which will each generates a different documentation:
store:
# Includes routes with names containing 'store'
name_patterns: [ store ]
+
Your main documentation is under the ``default`` area. It's the one shown when accessing ``/api/doc``.
@@ -52,3 +53,38 @@ Then update your routing to be able to access your different documentations:
# defaults: { _controller: nelmio_api_doc.controller.swagger }
That's all! You can now access ``/api/doc/internal``, ``/api/doc/commercial`` and ``/api/doc/store``.
+
+Use annotations to filter documented routes in each area
+--------------------------------------------------------
+
+You can use the `@Areas` annotation inside your controllers to define your routes' areas.
+
+First, you need to define which areas will use the`@Areas` annotations to filter
+the routes that should be documented:
+
+.. code-block:: yaml
+
+ nelmio_api_doc:
+ areas:
+ default:
+ path_patterns: [ ^/api ]
+ internal:
+ with_annotations: true
+
+Then add the annotation before your controller or action::
+
+ use Nelmio\Annotations as Nelmio;
+
+ /**
+ * @Nelmio\Areas({"internal"}) => All actions in this controller are documented under the 'internal' area
+ */
+ class MyController
+ {
+ /**
+ * @Nelmio\Areas({"internal"}) => This action is documented under the 'internal' area
+ */
+ public function index()
+ {
+ ...
+ }
+ }
diff --git a/Resources/doc/commands.rst b/Resources/doc/commands.rst
index 839a122..67ab295 100644
--- a/Resources/doc/commands.rst
+++ b/Resources/doc/commands.rst
@@ -5,7 +5,7 @@ A command is provided in order to dump the documentation in ``json``, ``yaml`` o
.. code-block:: bash
- $ php app/console api:doc:dump [--format="..."]
+ $ php app/console nelmio:apidoc:dump [--format="..."]
The ``--format`` option allows to choose the format (default is: ``json``).
@@ -14,20 +14,20 @@ without whitespace, use the ``--no-pretty`` option.
.. code-block:: bash
- $ php app/console api:doc:dump --format=json > json-pretty-formatted.json
- $ php app/console api:doc:dump --format=json --no-pretty > json-no-pretty.json
+ $ php app/console nelmio:apidoc:dump --format=json > json-pretty-formatted.json
+ $ php app/console nelmio:apidoc:dump --format=json --no-pretty > json-no-pretty.json
Every format can override API url. Useful if static documentation is not hosted on API url:
.. code-block:: bash
- $ php app/console api:doc:dump --format=yaml --server-url "http://example.com/api" > api.yaml
+ $ php app/console nelmio:apidoc:dump --format=yaml --server-url "http://example.com/api" > api.yaml
For example to generate a static version of your documentation you can use:
.. code-block:: bash
- $ php app/console api:doc:dump --format=html > api.html
+ $ php app/console nelmio:apidoc:dump --format=html > api.html
By default, the generated HTML will add the sandbox feature.
If you want to generate a static version of your documentation without sandbox,
@@ -40,6 +40,6 @@ or configure UI configuration, use the ``--html-config`` option.
.. code-block:: bash
- $ php app/console api:doc:dump --format=html --html-config '{"assets_mode":"offline","server_url":"https://example.com","swagger_ui_config":{"supportedSubmitMethods":[]}}' > api.html
+ $ php app/console nelmio:apidoc:dump --format=html --html-config '{"assets_mode":"offline","server_url":"https://example.com","swagger_ui_config":{"supportedSubmitMethods":[]}}' > api.html
.. _`configure Swagger UI`: https://swagger.io/docs/open-source-tools/swagger-ui/usage/configuration/
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 8d53598..38afb94 100644
--- a/Tests/Functional/FunctionalTest.php
+++ b/Tests/Functional/FunctionalTest.php
@@ -624,4 +624,60 @@ class FunctionalTest extends WebTestCase
$this->assertFalse($model->additionalProperties);
}
+
+ /**
+ * @requires PHP >= 8.1
+ */
+ public function testEnumSupport()
+ {
+ $model = $this->getModel('ArticleType81');
+
+ $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);
+ }
}
diff --git a/Tests/Model/ModelRegistryTest.php b/Tests/Model/ModelRegistryTest.php
index ef7fd85..fdbbc36 100644
--- a/Tests/Model/ModelRegistryTest.php
+++ b/Tests/Model/ModelRegistryTest.php
@@ -234,7 +234,20 @@ class ModelRegistryTest extends TestCase
{
return [
[new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true), 'mixed[]'],
- [new Type(Type::BUILTIN_TYPE_OBJECT, false, self::class), self::class],
+ [new Type(Type::BUILTIN_TYPE_OBJECT, false, self::class), '\\'.self::class],
];
}
+
+ public function testUnsupportedTypeExceptionWithNonExistentClass()
+ {
+ $className = DoesNotExist::class;
+ $type = new Type(Type::BUILTIN_TYPE_OBJECT, false, $className);
+
+ $this->expectException('\LogicException');
+ $this->expectExceptionMessage(sprintf('Schema of type "\%s" can\'t be generated, no describer supports it. Class "\Nelmio\ApiDocBundle\Tests\Model\DoesNotExist" does not exist, did you forget a use statement, or typed it wrong?', $className));
+
+ $registry = new ModelRegistry([], new OA\OpenApi([]));
+ $registry->register(new Model($type));
+ $registry->registerSchemas();
+ }
}
diff --git a/composer.json b/composer.json
index d69570a..0d12f33 100644
--- a/composer.json
+++ b/composer.json
@@ -48,7 +48,7 @@
"symfony/twig-bundle": "^4.4|^5.2|^6.0",
"symfony/validator": "^4.4|^5.2|^6.0",
- "api-platform/core": "^2.4",
+ "api-platform/core": "^2.6.8",
"friendsofsymfony/rest-bundle": "^2.8|^3.0",
"willdurand/hateoas-bundle": "^1.0|^2.0",
"jms/serializer-bundle": "^2.3|^3.0|^4.0",