From 1302bc7568fc1ead9304b8c11d3d2fe5c7ba76c9 Mon Sep 17 00:00:00 2001 From: Baptiste Lafontaine Date: Mon, 4 Apr 2022 11:42:44 +0200 Subject: [PATCH 1/8] Create an enum model describer (#1965) * Create an enum model describer * Bump Api-Platform Co-authored-by: Guilhem Niot --- ModelDescriber/EnumModelDescriber.php | 34 +++++++++++++++++++ Resources/config/services.xml | 4 +++ .../Functional/Controller/ApiController81.php | 7 ++++ Tests/Functional/Entity/Article81.php | 12 +++++++ Tests/Functional/Entity/ArticleType81.php | 9 +++++ Tests/Functional/FunctionalTest.php | 11 ++++++ composer.json | 2 +- 7 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 ModelDescriber/EnumModelDescriber.php create mode 100644 Tests/Functional/Entity/Article81.php create mode 100644 Tests/Functional/Entity/ArticleType81.php 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/Resources/config/services.xml b/Resources/config/services.xml index 9d2c38c..99be6ea 100644 --- a/Resources/config/services.xml +++ b/Resources/config/services.xml @@ -79,6 +79,10 @@ + + + + diff --git a/Tests/Functional/Controller/ApiController81.php b/Tests/Functional/Controller/ApiController81.php index 71f3629..b12f5cd 100644 --- a/Tests/Functional/Controller/ApiController81.php +++ b/Tests/Functional/Controller/ApiController81.php @@ -15,6 +15,7 @@ use Nelmio\ApiDocBundle\Annotation\Areas; use Nelmio\ApiDocBundle\Annotation\Model; use Nelmio\ApiDocBundle\Annotation\Security; use Nelmio\ApiDocBundle\Tests\Functional\Entity\Article; +use Nelmio\ApiDocBundle\Tests\Functional\Entity\Article81; use OpenApi\Attributes as OA; use Symfony\Component\Routing\Annotation\Route; @@ -65,4 +66,10 @@ class ApiController81 extends ApiController80 #[OA\PathParameter] string $product_id ) { } + + #[Route('/enum')] + #[OA\Response(response: '201', description: '', attachables: [new Model(type: Article81::class)])] + public function enum() + { + } } diff --git a/Tests/Functional/Entity/Article81.php b/Tests/Functional/Entity/Article81.php new file mode 100644 index 0000000..3904801 --- /dev/null +++ b/Tests/Functional/Entity/Article81.php @@ -0,0 +1,12 @@ +assertFalse($model->additionalProperties); } + + /** + * @requires PHP >= 8.1 + */ + public function testEnumSupport() + { + $model = $this->getModel('ArticleType81'); + + $this->assertSame('string', $model->type); + $this->assertCount(2, $model->enum); + } } 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", From f8c030d096babde13a450423cf9b9556ab7f54f2 Mon Sep 17 00:00:00 2001 From: Guilhem Niot Date: Wed, 13 Apr 2022 19:54:31 +0200 Subject: [PATCH 2/8] Improve "no describer found" error message (#1979) --- Model/ModelRegistry.php | 8 ++++++-- Tests/Model/ModelRegistryTest.php | 15 ++++++++++++++- 2 files changed, 20 insertions(+), 3 deletions(-) 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/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(); + } } From 35ea6f2759999e555cd88dcde0e0f3059f9971fd Mon Sep 17 00:00:00 2001 From: Valentin Salmeron Date: Thu, 21 Apr 2022 23:18:01 +0200 Subject: [PATCH 3/8] Update areas.rst (#1983) * Update areas.rst 'with_annotations' option is not mentioned in actual documentation * Slight changes * Rephrase --- Resources/doc/areas.rst | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) 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() + { + ... + } + } From 05d16e68143ac2de625431b10fffaeeb68cc9b89 Mon Sep 17 00:00:00 2001 From: Guilhem Niot Date: Thu, 21 Apr 2022 23:36:09 +0200 Subject: [PATCH 4/8] Add links to swagger php examples --- Resources/doc/index.rst | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/Resources/doc/index.rst b/Resources/doc/index.rst index ca7c633..ac86238 100644 --- a/Resources/doc/index.rst +++ b/Resources/doc/index.rst @@ -99,7 +99,7 @@ Using the bundle ---------------- You can configure global information in the bundle configuration ``documentation.info`` section (take a look at -`the OpenAPI 3.0 specification (formerly Swagger)`_ to know the available fields): +`the OpenAPI 3.0 specification (formerly Swagger)`_ to know the available fields). .. code-block:: yaml @@ -127,7 +127,12 @@ You can configure global information in the bundle configuration ``documentation .. note:: - If you're using Flex, this config is there by default. Don't forget to adapt it to your app! + If you're using Flex, this config is there by default under ``config/packages/nelmio_api_doc.yaml``. Don't forget to adapt it to your app! + +.. tip:: + + This configuration field can more generally be used to store your documentation as yaml. + You may find in the ``.yaml`` files from `SwaggerPHP examples`_. To document your routes, you can use the SwaggerPHP annotations and the ``Nelmio\ApiDocBundle\Annotation\Model`` annotation in your controllers:: @@ -174,6 +179,12 @@ To document your routes, you can use the SwaggerPHP annotations and the The normal PHPdoc block on the controller method is used for the summary and description. +.. tip:: + + Examples of using the annotations can be found in `SwaggerPHP examples`_. + However, unlike in those examples, when using this bundle you don't need to specify paths and you can easily document models as well as some + other properties described below as they can be automatically be documented using the Symfony integration. + Use models ---------- @@ -343,6 +354,7 @@ If you need more complex features, take a look at: faq security +.. _`SwaggerPHP examples`: https://github.com/zircote/swagger-php/tree/master/Examples .. _`Symfony PropertyInfo component`: https://symfony.com/doc/current/components/property_info.html .. _`willdurand/Hateoas`: https://github.com/willdurand/Hateoas .. _`BazingaHateoasBundle`: https://github.com/willdurand/BazingaHateoasBundle From 6ac4f078727ae3866b0ab8ac91c7565234042e11 Mon Sep 17 00:00:00 2001 From: Krystian Marcisz Date: Sat, 30 Apr 2022 20:25:41 +0200 Subject: [PATCH 5/8] [Docs] Add PHP Attributes examples (#1985) * [Docs] Add PHP Attributes examples * Fix namespaces --- Resources/doc/index.rst | 274 +++++++++++++++++++++++++++++----------- 1 file changed, 203 insertions(+), 71 deletions(-) diff --git a/Resources/doc/index.rst b/Resources/doc/index.rst index ac86238..a171845 100644 --- a/Resources/doc/index.rst +++ b/Resources/doc/index.rst @@ -17,6 +17,8 @@ This bundle supports *Symfony* route requirements, PHP annotations, `Swagger-Php For models, it supports the `Symfony serializer`_ , the `JMS serializer`_ and the `willdurand/Hateoas`_ library. It does also support `Symfony form`_ types. +Attributes are supported from version 4.7 and PHP 8.1. + Migrate from 3.x to 4.0 ----------------------- @@ -137,45 +139,91 @@ You can configure global information in the bundle configuration ``documentation To document your routes, you can use the SwaggerPHP annotations and the ``Nelmio\ApiDocBundle\Annotation\Model`` annotation in your controllers:: - namespace AppBundle\Controller; +.. configuration-block:: - use AppBundle\Entity\User; - use AppBundle\Entity\Reward; - use Nelmio\ApiDocBundle\Annotation\Model; - use Nelmio\ApiDocBundle\Annotation\Security; - use OpenApi\Annotations as OA; - use Symfony\Component\Routing\Annotation\Route; + .. code-block:: php-annotations + + namespace AppBundle\Controller; - class UserController - { - /** - * List the rewards of the specified user. - * - * This call takes into account all confirmed awards, but not pending or refused awards. - * - * @Route("/api/{user}/rewards", methods={"GET"}) - * @OA\Response( - * response=200, - * description="Returns the rewards of an user", - * @OA\JsonContent( - * type="array", - * @OA\Items(ref=@Model(type=Reward::class, groups={"full"})) - * ) - * ) - * @OA\Parameter( - * name="order", - * in="query", - * description="The field used to order rewards", - * @OA\Schema(type="string") - * ) - * @OA\Tag(name="rewards") - * @Security(name="Bearer") - */ - public function fetchUserRewardsAction(User $user) + use AppBundle\Entity\User; + use AppBundle\Entity\Reward; + use Nelmio\ApiDocBundle\Annotation\Model; + use Nelmio\ApiDocBundle\Annotation\Security; + use OpenApi\Annotations as OA; + use Symfony\Component\Routing\Annotation\Route; + + class UserController { - // ... + /** + * List the rewards of the specified user. + * + * This call takes into account all confirmed awards, but not pending or refused awards. + * + * @Route("/api/{user}/rewards", methods={"GET"}) + * @OA\Response( + * response=200, + * description="Returns the rewards of an user", + * @OA\JsonContent( + * type="array", + * @OA\Items(ref=@Model(type=Reward::class, groups={"full"})) + * ) + * ) + * @OA\Parameter( + * name="order", + * in="query", + * description="The field used to order rewards", + * @OA\Schema(type="string") + * ) + * @OA\Tag(name="rewards") + * @Security(name="Bearer") + */ + public function fetchUserRewardsAction(User $user) + { + // ... + } } - } + + .. code-block:: php-attributes + + namespace AppBundle\Controller; + + use AppBundle\Entity\User; + use AppBundle\Entity\Reward; + use Nelmio\ApiDocBundle\Annotation\Model; + use Nelmio\ApiDocBundle\Annotation\Security; + use OpenApi\Attributes as OA; + use Symfony\Component\Routing\Annotation\Route; + + class UserController + { + /** + * List the rewards of the specified user. + * + * This call takes into account all confirmed awards, but not pending or refused awards. + */ + #[Route('/api/{user}/rewards', methods=['GET'])] + #[OA\Response( + response: 200, + description: 'Returns the rewards of an user', + content: new OA\JsonContent( + type: 'array', + items: new OA\Items(ref: new Model(type: AlbumDto::class, groups: ['full'])) + ) + )] + #[OA\Parameter( + name: 'order', + in: 'query', + description: 'The field used to order rewards', + schema: new OA\Schema(type: 'string') + )] + #[OA\Tag(name: 'rewards')] + #[Security(name: 'Bearer')] + public function fetchUserRewardsAction(User $user) + { + // ... + } + } + The normal PHPdoc block on the controller method is used for the summary and description. @@ -199,21 +247,46 @@ This annotation has two options: * ``type`` to specify your model's type:: - /** - * @OA\Response( - * response=200, - *     @Model(type=User::class) - * ) - */ +.. configuration-block:: + + .. code-block:: php-annotations + + /** + * @OA\Response( + * response=200, + *     @Model(type=User::class) + * ) + */ + + .. code-block:: php-attributes + + #[OA\Response( + response: 200, + description: 'Successful response', + content: new Model(type: User::class) + )] * ``groups`` to specify the serialization groups used to (de)serialize your model:: - /** - * @OA\Response( - * response=200, - *     @Model(type=User::class, groups={"non_sensitive_data"}) - * ) - */ + +.. configuration-block:: + + .. code-block:: php-annotations + + /** + * @OA\Response( + * response=200, + *     @Model(type=User::class, groups={"non_sensitive_data"}) + * ) + */ + + .. code-block:: php-attributes + + #[OA\Response( + response: 200, + description: 'Successful response', + content: new Model(type: User::class, groups: ['non_sensitive_data']) + )] .. tip:: @@ -224,6 +297,10 @@ This annotation has two options: To use ``@Model`` directly within a ``@OA\Schema``, ``@OA\Items`` or ``@OA\Property``, you have to use the ``$ref`` field:: +.. configuration-block:: + + .. code-block:: php-annotations + /** * @OA\Response( * @OA\JsonContent(ref=@Model(type=User::class)) @@ -238,6 +315,23 @@ This annotation has two options: * )) */ + .. code-block:: php-attributes + + #[OA\Response( + content: new OA\JsonContent(ref: new Model(type: User::class)) + )] + /** + * or + */ + #[OA\Response( + content: new OA\XmlContent(example: new OA\Schema( + type: 'object', + properties: [ + new OA\Property(property: 'foo', ref: new Model(type: FooClass::class)) + ] + )) + )] + Symfony Form types ~~~~~~~~~~~~~~~~~~ @@ -288,7 +382,15 @@ General PHP objects .. code-block:: php - @OA\Schema(ref=@Model(type="App\Response\ItemResponse", groups=["Default"])), + .. configuration-block:: + + .. code-block:: php-annotations + + @OA\Schema(ref=@Model(type="App\Response\ItemResponse", groups=["Default"])), + + .. code-block:: php-attributes + + #[OA\Schema(ref: new Model(type: App\Response\ItemResponse::class, groups: ['Default']))] It will generate two different component schemas (ItemResponse, ItemResponse2), even though Default and blank are the same. This is by design. @@ -306,34 +408,64 @@ General PHP objects If you want to customize the documentation of an object's property, you can use ``@OA\Property``:: - use Nelmio\ApiDocBundle\Annotation\Model; - use OpenApi\Annotations as OA; - class User - { - /** - * @var int - * @OA\Property(description="The unique identifier of the user.") - */ - public $id; +.. configuration-block:: - /** - * @OA\Property(type="string", maxLength=255) - */ - public $username; + .. code-block:: php-annotations + + use Nelmio\ApiDocBundle\Annotation\Model; + use OpenApi\Annotations as OA; - /** - * @OA\Property(ref=@Model(type=User::class)) - */ - public $friend; + class User + { + /** + * @var int + * @OA\Property(description="The unique identifier of the user.") + */ + public $id; - /** - * @OA\Property(description="This is my coworker!") - */ - public setCoworker(User $coworker) { - // ... + /** + * @OA\Property(type="string", maxLength=255) + */ + public $username; + + /** + * @OA\Property(ref=@Model(type=User::class)) + */ + public $friend; + + /** + * @OA\Property(description="This is my coworker!") + */ + public setCoworker(User $coworker) { + // ... + } + } + + .. code-block:: php-attributes + + use Nelmio\ApiDocBundle\Annotation\Model; + use OpenApi\Attributes as OA; + + class User + { + /** + * @var int + */ + #[OA\Property(description: 'The unique identifier of the user.')] + public $id; + + #[OA\Property(type: 'string', maxLength: 255)] + public $username; + + #[OA\Property(ref: new Model(type: User::class))] + public $friend; + + #[OA\Property(description: 'This is my coworker!')] + public setCoworker(User $coworker) { + // ... + } } - } See the `OpenAPI 3.0 specification`__ to see all the available fields of ``@OA\Property``. From da02f3ad339437b939a6d309e9c98b62afe53c6c Mon Sep 17 00:00:00 2001 From: Christopher Davis Date: Sat, 30 Apr 2022 13:28:05 -0500 Subject: [PATCH 6/8] Stop Model Property Description When a Schema Type or Ref is Already Defined (#1978) * Return a Result Object from AnnotationsReader::updateDefinition This is so we can make a decision on whether or not a schema's type or ref has been manually defined by a user via an `@OA\Schema` annotation as something other than an object. If it has been defined, this bundle should not read model properties any further as it causes errors. I put this in AnnotationReader as it seemed the most flexible in the long run. It could have gone in `OpenApiAnnotationsReader`, but then any additional things added to `updateDefinition` could be left out of the decision down the road. This is also a convenient place to decide this once for `ObjectModelDescriber` and `JMSModelDescriber`. * Stop Model Describer if a Schema Type or Ref Has Been Defined Via the result object added in the previous commit. This lets user "short circuit" the model describers by manually defining the schema type or ref on a plain PHP object or form. For example, a collection class could be defined like this: /** * @OA\Schema(type="array", @OA\Items(ref=@Model(type=SomeEntity::class))) */ class SomeCollection implements \IteratorAggregate { } Previously the model describer would error as it tries to merge the `array` schema with the already defiend `object` schema. Now it will prefer the array schema and skip reading all the properties of the object. * Add a Documentation Bit on Stopping Property Description * Mark UpdateClassDefinitionResult as Internal --- .../Annotations/AnnotationsReader.php | 18 ++++- .../UpdateClassDefinitionResult.php | 41 +++++++++++ ModelDescriber/FormModelDescriber.php | 10 ++- ModelDescriber/JMSModelDescriber.php | 8 ++- ModelDescriber/ObjectModelDescriber.php | 10 ++- Resources/doc/faq.rst | 27 ++++++++ .../Functional/Controller/ApiController80.php | 69 +++++++++++++++++++ .../Entity/EntityWithAlternateType.php | 35 ++++++++++ .../Entity/EntityWithObjectType.php | 25 +++++++ Tests/Functional/Entity/EntityWithRef.php | 25 +++++++ .../Form/FormWithAlternateSchemaType.php | 31 +++++++++ Tests/Functional/Form/FormWithRefType.php | 31 +++++++++ Tests/Functional/FunctionalTest.php | 45 ++++++++++++ 13 files changed, 366 insertions(+), 9 deletions(-) create mode 100644 ModelDescriber/Annotations/UpdateClassDefinitionResult.php create mode 100644 Tests/Functional/Entity/EntityWithAlternateType.php create mode 100644 Tests/Functional/Entity/EntityWithObjectType.php create mode 100644 Tests/Functional/Entity/EntityWithRef.php create mode 100644 Tests/Functional/Form/FormWithAlternateSchemaType.php create mode 100644 Tests/Functional/Form/FormWithRefType.php 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); + } } From f66ab2a51633db23ad7e5ca00ce0c38067932354 Mon Sep 17 00:00:00 2001 From: Tobias Sette Date: Mon, 2 May 2022 17:39:33 -0300 Subject: [PATCH 7/8] docs(commands.rst): fix command name (#1989) --- Resources/doc/commands.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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/ From fdb2182834aa6ab7776ce3176f1f30c2d4aa417d Mon Sep 17 00:00:00 2001 From: Christoph Wieseke Date: Mon, 9 May 2022 16:31:09 +0200 Subject: [PATCH 8/8] fixed doc typo the broke the models section --- Resources/doc/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Resources/doc/index.rst b/Resources/doc/index.rst index a171845..6bc43bd 100644 --- a/Resources/doc/index.rst +++ b/Resources/doc/index.rst @@ -288,7 +288,7 @@ This annotation has two options: content: new Model(type: User::class, groups: ['non_sensitive_data']) )] - .. tip:: +.. tip:: When used at the root of ``@OA\Response`` and ``@OA\Parameter``, ``@Model`` is automatically nested in a ``@OA\Schema``.