Merge remote-tracking branch 'origin/master' into constraint_groups

This commit is contained in:
Christopher Davis 2022-05-11 08:18:01 -05:00
commit 6b2ef45b24
24 changed files with 724 additions and 93 deletions

View File

@ -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).'[]';

View File

@ -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;
}
}

View File

@ -0,0 +1,41 @@
<?php
/*
* This file is part of the NelmioApiDocBundle package.
*
* (c) Nelmio
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Nelmio\ApiDocBundle\ModelDescriber\Annotations;
/**
* result object returned from `AnnotationReader::updateDefinition` as a way
* to pass back information about manually defined schema elements.
*
* @internal
*/
final class UpdateClassDefinitionResult
{
/**
* Whether or not the model describer shoudl continue reading class properties
* after updating the open api schema from an `OA\Schema` definition.
*
* Users may maually define a `type` or `ref` on a schema, and if that's the case
* model describers should _probably_ not describe any additional properties or try
* to merge in properties.
*/
private $shouldDescribeModelProperties;
public function __construct(bool $shouldDescribeModelProperties)
{
$this->shouldDescribeModelProperties = $shouldDescribeModelProperties;
}
public function shouldDescribeModelProperties(): bool
{
return $this->shouldDescribeModelProperties;
}
}

View File

@ -0,0 +1,34 @@
<?php
namespace Nelmio\ApiDocBundle\ModelDescriber;
use Nelmio\ApiDocBundle\Model\Model;
use OpenApi\Annotations\Schema;
use Symfony\Component\PropertyInfo\Type;
class EnumModelDescriber implements ModelDescriberInterface
{
public function describe(Model $model, Schema $schema)
{
$enumClass = $model->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);
}
}

View File

@ -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);

View File

@ -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;

View File

@ -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) {

View File

@ -80,6 +80,10 @@
<tag name="nelmio_api_doc.model_describer" />
</service>
<service id="nelmio_api_doc.model_describers.enum" class="Nelmio\ApiDocBundle\ModelDescriber\EnumModelDescriber" public="false">
<tag name="nelmio_api_doc.model_describer" priority="100"/>
</service>
<service id="nelmio_api_doc.model_describers.object_fallback" class="Nelmio\ApiDocBundle\ModelDescriber\FallbackObjectModelDescriber" public="false">
<tag name="nelmio_api_doc.model_describer" priority="-1000" />
</service>

View File

@ -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()
{
...
}
}

View File

@ -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/

View File

@ -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
<?php
use OpenApi\Annotations as OA;
use Nelmio\ApiDocBundle\Annotation\Model;
/**
* @OA\Schema(type="array", @OA\Items(ref=@Model(type=SomeEntity::class)))
*
* or define a `ref`:
* @OA\Schema(ref="#/components/schemas/SomeRef")
*/
class SomeCollection implements \IteratorAggregate
{
// ...
}

View File

@ -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
-----------------------
@ -99,7 +101,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,53 +129,110 @@ 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::
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.
.. 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
----------
@ -188,23 +247,48 @@ 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"})
* )
*/
.. tip::
.. 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::
When used at the root of ``@OA\Response`` and ``@OA\Parameter``, ``@Model`` is automatically nested
in a ``@OA\Schema``.
@ -213,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))
@ -227,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
~~~~~~~~~~~~~~~~~~
@ -277,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.
@ -295,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``.
@ -343,6 +486,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

View File

@ -17,11 +17,16 @@ use Nelmio\ApiDocBundle\Annotation\Operation;
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\EntityWithAlternateType;
use Nelmio\ApiDocBundle\Tests\Functional\Entity\EntityWithObjectType;
use Nelmio\ApiDocBundle\Tests\Functional\Entity\EntityWithRef;
use Nelmio\ApiDocBundle\Tests\Functional\Entity\SymfonyConstraints;
use Nelmio\ApiDocBundle\Tests\Functional\Entity\SymfonyConstraintsWithValidationGroups;
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\FormWithAlternateSchemaType;
use Nelmio\ApiDocBundle\Tests\Functional\Form\FormWithRefType;
use Nelmio\ApiDocBundle\Tests\Functional\Form\UserType;
use OpenApi\Annotations as OA;
use Symfony\Component\Routing\Annotation\Route;
@ -272,4 +277,68 @@ class ApiController80
public function symfonyConstraintsWithGroupsAction()
{
}
/**
* @Route("/alternate-entity-type", methods={"GET", "POST"})
*
* @OA\Get(operationId="alternate-entity-type")
* @OA\Response(response=200, description="success", @OA\JsonContent(
* ref=@Model(type=EntityWithAlternateType::class),
* ))
*/
public function alternateEntityType()
{
}
/**
* @Route("/entity-with-ref", methods={"GET", "POST"})
*
* @OA\Get(operationId="entity-with-ref")
* @OA\Response(response=200, description="success", @OA\JsonContent(
* ref=@Model(type=EntityWithRef::class),
* ))
*/
public function entityWithRef()
{
}
/**
* @Route("/entity-with-object-type", methods={"GET", "POST"})
*
* @OA\Get(operationId="entity-with-object-type")
* @OA\Response(response=200, description="success", @OA\JsonContent(
* ref=@Model(type=EntityWithObjectType::class),
* ))
*/
public function entityWithObjectType()
{
}
/**
* @Route("/form-with-alternate-type", methods={"POST"})
* @OA\Response(
* response="204",
* description="Operation automatically detected",
* ),
* @OA\RequestBody(
* @Model(type=FormWithAlternateSchemaType::class))
* )
*/
public function formWithAlternateSchemaType()
{
}
/**
* @Route("/form-with-ref-type", methods={"POST"})
* @OA\Response(
* response="204",
* description="Operation automatically detected",
* ),
* @OA\RequestBody(
* @Model(type=FormWithRefType::class))
* )
*/
public function formWithRefSchemaType()
{
}
}

View File

@ -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()
{
}
}

View File

@ -0,0 +1,12 @@
<?php
namespace Nelmio\ApiDocBundle\Tests\Functional\Entity;
class Article81
{
public function __construct(
public readonly int $id,
public readonly ArticleType81 $type,
) {
}
}

View File

@ -0,0 +1,9 @@
<?php
namespace Nelmio\ApiDocBundle\Tests\Functional\Entity;
enum ArticleType81: string
{
case DRAFT = 'draft';
case FINAL = 'final';
}

View File

@ -0,0 +1,35 @@
<?php
/*
* This file is part of the NelmioApiDocBundle package.
*
* (c) Nelmio
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Nelmio\ApiDocBundle\Tests\Functional\Entity;
use ArrayIterator;
use IteratorAggregate;
use OpenApi\Annotations as OA;
/**
* @OA\Schema(type="array", @OA\Items(type="string"))
*/
class EntityWithAlternateType implements IteratorAggregate
{
/**
* @var string
*/
public $ignored = 'this property should be ignored because of the annotation above';
public function getIterator(): ArrayIterator
{
return new ArrayIterator([
'abc',
'def',
]);
}
}

View File

@ -0,0 +1,25 @@
<?php
/*
* This file is part of the NelmioApiDocBundle package.
*
* (c) Nelmio
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Nelmio\ApiDocBundle\Tests\Functional\Entity;
use OpenApi\Annotations as OA;
/**
* @OA\Schema(type="object")
*/
class EntityWithObjectType
{
/**
* @var string
*/
public $notIgnored = 'this should be read';
}

View File

@ -0,0 +1,25 @@
<?php
/*
* This file is part of the NelmioApiDocBundle package.
*
* (c) Nelmio
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Nelmio\ApiDocBundle\Tests\Functional\Entity;
use OpenApi\Annotations as OA;
/**
* @OA\Schema(ref="#/components/schemas/Test")
*/
class EntityWithRef
{
/**
* @var string
*/
public $ignored = 'this property should be ignored because of the annotation above';
}

View File

@ -0,0 +1,31 @@
<?php
/*
* This file is part of the NelmioApiDocBundle package.
*
* (c) Nelmio
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Nelmio\ApiDocBundle\Tests\Functional\Form;
use OpenApi\Annotations as OA;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\InputType;
use Symfony\Component\Form\FormBuilderInterface;
/**
* @OA\Schema(type="string")
*/
class FormWithAlternateSchemaType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('ignored', InputType::class, [
'required' => false,
]);
}
}

View File

@ -0,0 +1,31 @@
<?php
/*
* This file is part of the NelmioApiDocBundle package.
*
* (c) Nelmio
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Nelmio\ApiDocBundle\Tests\Functional\Form;
use OpenApi\Annotations as OA;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\InputType;
use Symfony\Component\Form\FormBuilderInterface;
/**
* @OA\Schema(ref="#/components/schemas/Test")
*/
class FormWithRefType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('ignored', InputType::class, [
'required' => false,
]);
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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",