Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Symfony attribute describers #2112

Merged
merged 125 commits into from
Jan 15, 2024
Merged
Show file tree
Hide file tree
Changes from 95 commits
Commits
Show all changes
125 commits
Select commit Hold shift + click to select a range
7fa3df5
Add SymfonyDescriber
DjordyKoert Jul 7, 2023
d5a69ee
Add SymfonyDescriber dependency injection
DjordyKoert Jul 7, 2023
013ea83
Fix codestyle
DjordyKoert Jul 7, 2023
467476a
fixup! Fix codestyle
DjordyKoert Jul 7, 2023
4825891
Add php 8 checks
DjordyKoert Jul 7, 2023
a9ba360
Add symfony.xml loading
DjordyKoert Jul 7, 2023
b23fd1b
Temp: increase max self deprecations
DjordyKoert Jul 7, 2023
dbeb6be
Add Exception throw when invalid php version is used
DjordyKoert Jul 7, 2023
6db82c3
Fix codestyle
DjordyKoert Jul 7, 2023
cf366c8
Add describeRequestBody method
DjordyKoert Jul 7, 2023
399c05b
Use elseif
DjordyKoert Jul 7, 2023
81abaa8
Only check for php version once
DjordyKoert Jul 7, 2023
a2775af
Add SymfonyDescriberTest for MapRequestPayload
DjordyKoert Jul 7, 2023
6fc3737
Fix annotation
DjordyKoert Jul 7, 2023
f3e2638
Skip test if attributes don't exist
DjordyKoert Jul 7, 2023
95dfba7
Skip test based on php version
DjordyKoert Jul 7, 2023
7690d62
Move $mapRequestPayload type to annotation
DjordyKoert Jul 7, 2023
e1c4201
Fix annotation style
DjordyKoert Jul 7, 2023
32b59dd
Fix SymfonyDescriberTest for older symfony versions
DjordyKoert Jul 7, 2023
5356227
Remove version check
DjordyKoert Jul 7, 2023
c05b1a2
Remove usage of in_array to check for attribute
DjordyKoert Jul 14, 2023
f01d5f9
Change elseif to separate if statement
DjordyKoert Jul 14, 2023
de38727
Fix testMapRequestPayloadParamRegistersRequestBody for split up if st…
DjordyKoert Jul 14, 2023
322ef4a
Add testMapQueryParameter
DjordyKoert Jul 14, 2023
f2221bd
Fix codestyle
DjordyKoert Jul 14, 2023
00bf810
Remove newline
DjordyKoert Jul 14, 2023
f987613
Expand docs for symfony controller mapping
DjordyKoert Jul 14, 2023
1acd5dc
Add backticks
DjordyKoert Jul 14, 2023
0b2c627
Clarify docs
DjordyKoert Jul 14, 2023
123c611
Upgrade major_version to 6
DjordyKoert Jul 14, 2023
15ce29b
Upgrade versionadded_directive_min_version to 6.0
DjordyKoert Jul 14, 2023
c75b539
Revert max allowed self deprecations
DjordyKoert Jul 14, 2023
476cf22
Revert phpunit.xml.dist changes
DjordyKoert Jul 14, 2023
e7286a1
Merge remote-tracking branch 'origin/symfony-map-request-data' into s…
DjordyKoert Jul 14, 2023
44a4312
Revert "Revert max allowed self deprecations"
DjordyKoert Jul 14, 2023
b537b5b
Remove not working generator bypass and replace with iterable
DjordyKoert Jul 14, 2023
82e9705
Update testMapQueryParameter to work with 'controller' classes
DjordyKoert Jul 14, 2023
8528dff
Update testMapRequestPayload to work with 'controller' classes
DjordyKoert Jul 14, 2023
bf233a8
Remove check for MapQueryString existence
DjordyKoert Jul 14, 2023
6cc79e3
Fix codestyle
DjordyKoert Jul 14, 2023
d932817
Swap comparison order
DjordyKoert Jul 14, 2023
9370281
initial MapQueryString setup
DjordyKoert Aug 11, 2023
807779e
Move annotation describe methods to their own SymfonyAnnotationDescri…
DjordyKoert Aug 11, 2023
e319f88
Cleanup
DjordyKoert Aug 11, 2023
19834b5
Add annotation describer services
DjordyKoert Aug 11, 2023
16ad28a
Move to own test files
DjordyKoert Aug 11, 2023
2a52478
Cleanup
DjordyKoert Aug 11, 2023
9d89fdc
Call setModelRegistry on annotation describers
DjordyKoert Aug 11, 2023
3c53bb1
Use accessible values for tests
DjordyKoert Aug 11, 2023
b61f105
Add SymfonyMapQueryStringDescriberTest
DjordyKoert Aug 11, 2023
3172924
Only check availability of needed attribute
DjordyKoert Aug 11, 2023
34d6bb8
Fix message
DjordyKoert Aug 11, 2023
0a6489a
Fix styleci
DjordyKoert Aug 11, 2023
1e18c3c
Fix styleci
DjordyKoert Aug 11, 2023
65d479e
Expand SymfonyMapQueryStringDescriber to copy property data to query …
DjordyKoert Aug 12, 2023
e231847
Fix style
DjordyKoert Aug 12, 2023
7a335e3
Add missing newline
DjordyKoert Aug 12, 2023
5576e7a
Fix test php 7.2 compatability
DjordyKoert Aug 12, 2023
1fe9985
Remove annotation var name
DjordyKoert Aug 12, 2023
bdcb5a2
Fix missing values
DjordyKoert Aug 12, 2023
7544dc5
Fix missing values
DjordyKoert Aug 12, 2023
87de11c
Add DTO testing class
DjordyKoert Aug 12, 2023
28309cf
Expand SymfonyMapQueryStringDescriberTest
DjordyKoert Aug 12, 2023
8000dc1
Add SymfonyDescriberTest tests
DjordyKoert Aug 12, 2023
21bc561
Remove unused import
DjordyKoert Aug 12, 2023
924cf15
Remove trailing commas
DjordyKoert Aug 12, 2023
fa0c07c
Copy ref
DjordyKoert Aug 12, 2023
97485e8
Remove setting allowEmptyValue
DjordyKoert Aug 12, 2023
d37f119
Remove empty value test
DjordyKoert Aug 12, 2023
b8de40f
Update documentation
DjordyKoert Aug 12, 2023
d5865cc
Merge documentation instead of overwriting
DjordyKoert Aug 12, 2023
4de6671
Expand symfony controller mapping attribute documentation
DjordyKoert Aug 12, 2023
112d67d
Fix RST
DjordyKoert Aug 12, 2023
d0db0ed
Fix RST (missing blank line)
DjordyKoert Aug 12, 2023
0a4a1a6
Revert max self deprecations
DjordyKoert Aug 13, 2023
8f62e19
Use modelDescriber to describe model instead of registering all models
DjordyKoert Aug 13, 2023
81de1e4
Create weak context
DjordyKoert Aug 13, 2023
ea02d6e
Get schema from property instead of manually setting every property
DjordyKoert Aug 13, 2023
0f1a43a
Add newline at end of file
DjordyKoert Aug 13, 2023
31c06c1
Fix style
DjordyKoert Aug 13, 2023
a346209
Prevent overwriting non-default values
DjordyKoert Aug 13, 2023
703a5b2
Add functional test for MapQueryString
DjordyKoert Aug 13, 2023
a97579b
Use modifyAnnotationValue helper method instead of overwriting
DjordyKoert Aug 13, 2023
868f559
Fix incorrect name is used for query
DjordyKoert Aug 13, 2023
d4ca40a
Transform int to integer
DjordyKoert Aug 13, 2023
ec856d7
Remove allowEmptyValue
DjordyKoert Aug 13, 2023
f543584
Fix type comparison
DjordyKoert Aug 13, 2023
552070e
Fix enum not being used in test
DjordyKoert Aug 13, 2023
2155905
Add MapQueryParameter functional tests
DjordyKoert Aug 13, 2023
9e629a0
Update required statement
DjordyKoert Aug 13, 2023
f2dafb7
Set requestBody required
DjordyKoert Aug 13, 2023
37f1a4e
Add MapRequestPayload functional tests
DjordyKoert Aug 13, 2023
a7de308
Fix style
DjordyKoert Aug 13, 2023
3983578
Cleanup array format check
DjordyKoert Aug 13, 2023
752b834
Merge branch 'nelmio:master' into symfony-map-request-data
DjordyKoert Sep 8, 2023
f513fe2
Merge branch 'master' into symfony-map-request-data
DjordyKoert Jan 2, 2024
7cc3307
add required field to test
DjordyKoert Jan 2, 2024
b694819
fix baseline
DjordyKoert Jan 2, 2024
e1fc537
refactor logic to use symfony metadata instead of reflection
DjordyKoert Jan 2, 2024
f3d6af2
style fix
DjordyKoert Jan 2, 2024
c07abe0
re-add manually iterating over describers
DjordyKoert Jan 2, 2024
c86bd62
re-add unit tests
DjordyKoert Jan 2, 2024
56f2341
style fix
DjordyKoert Jan 2, 2024
1ecb9a0
remove named parameter
DjordyKoert Jan 2, 2024
035db3f
move xml load logic to describers
DjordyKoert Jan 2, 2024
cfa47e0
Revert "move xml load logic to describers"
DjordyKoert Jan 2, 2024
eb2f053
Merge branch 'master' into symfony-map-request-data
DjordyKoert Jan 5, 2024
4fd32d3
major refactor
DjordyKoert Jan 5, 2024
f8d9d8e
remove tests
DjordyKoert Jan 5, 2024
5355511
style fix
DjordyKoert Jan 5, 2024
2102421
expand symfony map attribute tests
DjordyKoert Jan 5, 2024
74df5eb
fix multiple models generated when null
DjordyKoert Jan 5, 2024
ff93889
generate proper nullable
DjordyKoert Jan 5, 2024
90ac4f5
style fix
DjordyKoert Jan 5, 2024
db9c284
remove property property from schema
DjordyKoert Jan 5, 2024
a82f5f6
style fix
DjordyKoert Jan 5, 2024
34e19d2
handle reflection exception
DjordyKoert Jan 5, 2024
ffdb8ec
rename dir
DjordyKoert Jan 5, 2024
05f66dd
style fix
DjordyKoert Jan 5, 2024
37418d5
Move MapRequestPayload describing to swagger processor
DjordyKoert Jan 5, 2024
63f7c09
fix baseline
DjordyKoert Jan 5, 2024
77da333
test overwriting to different model
DjordyKoert Jan 5, 2024
1204c32
query testing for schema overwriting
DjordyKoert Jan 5, 2024
a2f44ea
documentation update
DjordyKoert Jan 6, 2024
da56d50
Merge branch 'master' into symfony-map-request-data
DjordyKoert Jan 15, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 3 additions & 3 deletions .doctor-rst.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,10 @@ rules:

# master
versionadded_directive_major_version:
major_version: 5
major_version: 6

versionadded_directive_min_version:
min_version: '5.0'
min_version: '6.0'

deprecated_directive_major_version:
major_version: 5
Expand All @@ -71,4 +71,4 @@ whitelist:
lines:
- '.. code-block:: twig'
- '// bin/console'
- '.. code-block:: php'
- '.. code-block:: php'
12 changes: 12 additions & 0 deletions DependencyInjection/NelmioApiDocExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\ServiceLocator;
use Symfony\Component\HttpKernel\Attribute\MapQueryParameter;
use Symfony\Component\HttpKernel\Attribute\MapQueryString;
use Symfony\Component\HttpKernel\Attribute\MapRequestPayload;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\Routing\RouteCollection;

Expand Down Expand Up @@ -159,6 +162,15 @@ public function load(array $configs, ContainerBuilder $container): void
->setArgument(1, $config['media_types']);
}

if (
PHP_VERSION_ID > 80100
&& class_exists(MapRequestPayload::class)
&& class_exists(MapQueryParameter::class)
&& class_exists(MapQueryString::class)
) {
$loader->load('symfony.xml');
}

$bundles = $container->getParameter('kernel.bundles');
if (!isset($bundles['TwigBundle']) || !class_exists('Symfony\Component\Asset\Packages')) {
$container->removeDefinition('nelmio_api_doc.controller.swagger_ui');
Expand Down
24 changes: 24 additions & 0 deletions Resources/config/symfony.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">

<services>
<instanceof id="Nelmio\ApiDocBundle\RouteDescriber\SymfonyAnnotationDescriber\SymfonyAnnotationDescriber">
<tag name="nelmio_api_doc.symfony_annotation_describer" />
</instanceof>

<service id="nelmio_api_doc.symfony_annotation_describer.map_query_string" class="Nelmio\ApiDocBundle\RouteDescriber\SymfonyAnnotationDescriber\SymfonyMapQueryStringDescriber" public="false" >
<argument type="tagged_iterator" tag="nelmio_api_doc.model_describer"/>
</service>

<service id="nelmio_api_doc.symfony_annotation_describer.map_query_parameter" class="Nelmio\ApiDocBundle\RouteDescriber\SymfonyAnnotationDescriber\SymfonyMapQueryParameterDescriber" public="false" />

<service id="nelmio_api_doc.symfony_annotation_describer.map_request_payload" class="Nelmio\ApiDocBundle\RouteDescriber\SymfonyAnnotationDescriber\SymfonyMapRequestPayloadDescriber" public="false" />

<service id="nelmio_api_doc.route_describers.symfony" class="Nelmio\ApiDocBundle\RouteDescriber\SymfonyDescriber" public="false">
<tag name="nelmio_api_doc.route_describer" priority="-225" />
<argument type="tagged_iterator" tag="nelmio_api_doc.symfony_annotation_describer"/>
</service>
</services>
</container>
9 changes: 8 additions & 1 deletion Resources/doc/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ OpenAPI (Swagger) format and provides a sandbox to interactively experiment with
What's supported?
-----------------

This bundle supports *Symfony* route requirements, PHP annotations, `Swagger-Php`_ annotations,
This bundle supports *Symfony* route requirements, *Symfony* request mapping (:doc:`symfony_attributes`), PHP annotations, `Swagger-Php`_ annotations,
`FOSRestBundle`_ annotations and applications using `Api-Platform`_.

.. _`Swagger-Php`: https://github.com/zircote/swagger-php
Expand Down Expand Up @@ -239,6 +239,12 @@ The normal PHPDoc block on the controller method is used for the summary and des
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.

.. tip::

**NelmioApiDocBundle** understands **symfony's** controller attributes.
Using these attributes inside your controller allows this bundle to automatically create the necessary documentation.
More information can be found here: :doc:`symfony_attributes`.

Use Models
----------

Expand Down Expand Up @@ -576,6 +582,7 @@ If you need more complex features, take a look at:
commands
faq
security
symfony_attributes

.. _`SwaggerPHP examples`: https://github.com/zircote/swagger-php/tree/master/Examples
.. _`Symfony PropertyInfo component`: https://symfony.com/doc/current/components/property_info.html
Expand Down
159 changes: 159 additions & 0 deletions Resources/doc/symfony_attributes.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
Symfony attributes
================================

NelmioApiDocBundle has the ability to automatically create documentation from **symfony** controller attributes.

MapQueryString
-------------------------------

Using the `Symfony MapQueryString`_ attribute allows NelmioApiDocBundle to automatically generate your query parameter documentation for your endpoint from your object.

.. versionadded:: 6.3

The :class:`Symfony\\Component\\HttpKernel\\Attribute\\MapQueryString` attribute was introduced in Symfony 6.3.

Modify generated documentation
~~~~~~~

Modifying the generated documentation can easily by done in two ways, by:
* Customizing the documentation of an object's property (``#[OA\Property]`` attribute)
* Customizing the documentation of a query parameter (``#[OA\Parameter]`` attribute)

Customizing the documentation of a specific query parameter can be done by adding the ``#[OA\Parameter]`` attribute to your controller method.
Make sure that the ``in`` property is set to ``'query'`` and that the ``name`` property is set to the object's property name which you want to customize.

.. code-block:: php-attributes

#[OA\Parameter(
name: 'id',
description: 'Some additional parameter description',
in: 'query',
)]

MapQueryParameter
-------------------------------

Using the `Symfony MapQueryParameter`_ attribute allows NelmioApiDocBundle to automatically generate your query parameter documentation for your endpoint.

.. versionadded:: 6.3

The :class:`Symfony\\Component\\HttpKernel\\Attribute\\MapQueryParameter` attribute was introduced in Symfony 6.3.


Modify generated documentation
~~~~~~~

Customizing the documentation of the query parameter can be done by adding the ``#[OA\Parameter]`` attribute to your controller method.
Make sure that the ``in`` property is set to ``'query'`` and that the ``name`` property is set to the name of the controller method parameter.

.. code-block:: php-attributes

#[OA\Parameter(
name: 'id',
description: 'Some additional parameter description',
in: 'query',
)]

MapRequestPayload
-------------------------------

Using the `Symfony MapRequestPayload`_ attribute allows NelmioApiDocBundle to automatically generate your request body documentation for your endpoint.

.. versionadded:: 6.3

The :class:`Symfony\\Component\\HttpKernel\\Attribute\\MapRequestPayload` attribute was introduced in Symfony 6.3.


Modify generated documentation
~~~~~~~

Customizing the documentation of the request body can be done by adding the ``#[OA\RequestBody]`` attribute to your controller method.

.. code-block:: php-attributes

#[OA\RequestBody(
groups: ["create"],
)

Complete example
----------------------

.. code-block:: php-attributes

class UserQuery
{
public int $userId;
}

.. code-block:: php-attributes

use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert;

class UserDto
{
#[Groups(["default", "create", "update"])]
#[Assert\NotBlank(groups: ["default", "create"])]
public string $username;
}

.. code-block:: php-attributes

namespace AppBundle\Controller;

use AppBundle\UserDTO;
use AppBundle\UserQuery;
use OpenApi\Attributes as OA;
use Symfony\Component\Routing\Annotation\Route;

class UserController
{
/**
* Find user with MapQueryString.
*/
#[Route('/api/users', methods: ['GET'])]
#[OA\Parameter(
name: 'userId',
description: 'Id of the user to find',
in: 'query',
)]
public function findUser(#[MapQueryString] UserQuery $userQuery)
{
// ...
}

/**
* Find user with MapQueryParameter.
*/
#[Route('/api/users/v2', methods: ['GET'])]
#[OA\Parameter(
name: 'userId',
description: 'Id of the user to find',
in: 'query',
)]
public function findUserV2(#[MapQueryParameter] int $userId)
{
// ...
}

/**
* Create a new user.
*/
#[Route('/api/users', methods: ['POST'])]
#[OA\RequestBody(
groups: ['create'],
)]
public function createUser(#[MapRequestPayload] UserDTO $user)
{
// ...
}
}

Disclaimer
----------------------

Make sure to use at least php 8 (annotations) to make use of this functionality

.. _`Symfony MapQueryString`: https://symfony.com/doc/current/controller.html#mapping-the-whole-query-string
.. _`Symfony MapQueryParameter`: https://symfony.com/doc/current/controller.html#mapping-query-parameters-individually
.. _`Symfony MapRequestPayload`: https://symfony.com/doc/current/controller.html#mapping-request-payload
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

namespace Nelmio\ApiDocBundle\RouteDescriber\SymfonyAnnotationDescriber;

use OpenApi\Annotations as OA;
use ReflectionParameter;

interface SymfonyAnnotationDescriber
{
public function supports(ReflectionParameter $parameter): bool;

public function describe(OA\OpenApi $api, OA\Operation $operation, ReflectionParameter $parameter): void;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php

declare(strict_types=1);

namespace Nelmio\ApiDocBundle\RouteDescriber\SymfonyAnnotationDescriber;

use OpenApi\Annotations as OA;
use OpenApi\Generator;
use ReflectionParameter;

final class SymfonyAnnotationHelper
{
/**
* @param class-string<T> $attribute
*
* @return T|null
*
* @template T of object
*/
public static function getAttribute(ReflectionParameter $parameter, string $attribute): ?object
{
if ($attribute = $parameter->getAttributes($attribute, \ReflectionAttribute::IS_INSTANCEOF)) {
return $attribute[0]->newInstance();
}

return null;
}

public static function describeCommonSchemaFromParameter(OA\Schema $schema, ReflectionParameter $parameter): void
{
if ($parameter->isDefaultValueAvailable()) {
self::modifyAnnotationValue($schema, 'default', $parameter->getDefaultValue());
}

if ($parameter->getType()->isBuiltin()) {
$type = $parameter->getType()->getName();

if (in_array($type, ['int', 'integer'], true)) {
$type = 'integer';
}

self::modifyAnnotationValue($schema, 'type', $type);
}
}

public static function modifyAnnotationValue(OA\AbstractAnnotation $parameter, string $property, $value): void
{
if (!Generator::isDefault($parameter->{$property})) {
return;
}

$parameter->{$property} = $value;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

declare(strict_types=1);

namespace Nelmio\ApiDocBundle\RouteDescriber\SymfonyAnnotationDescriber;

use Nelmio\ApiDocBundle\OpenApiPhp\Util;
use OpenApi\Annotations as OA;
use ReflectionParameter;
use Symfony\Component\HttpKernel\Attribute\MapQueryParameter;

final class SymfonyMapQueryParameterDescriber implements SymfonyAnnotationDescriber
{
public function supports(ReflectionParameter $parameter): bool
{
if (!SymfonyAnnotationHelper::getAttribute($parameter, MapQueryParameter::class)) {
return false;
}

return $parameter->hasType();
}

public function describe(OA\OpenApi $api, OA\Operation $operation, ReflectionParameter $parameter): void
{
$attribute = SymfonyAnnotationHelper::getAttribute($parameter, MapQueryParameter::class);

$operationParameter = Util::getOperationParameter($operation, $attribute->name ?? $parameter->getName(), 'query');

SymfonyAnnotationHelper::modifyAnnotationValue($operationParameter, 'required', !($parameter->isDefaultValueAvailable() || $parameter->allowsNull()));

/** @var OA\Schema $schema */
$schema = Util::getChild($operationParameter, OA\Schema::class);

if (FILTER_VALIDATE_REGEXP === $attribute->filter) {
SymfonyAnnotationHelper::modifyAnnotationValue($schema, 'pattern', $attribute->options['regexp']);
}

SymfonyAnnotationHelper::describeCommonSchemaFromParameter($schema, $parameter);
}
}