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 all 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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
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'
51 changes: 51 additions & 0 deletions DependencyInjection/NelmioApiDocExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@
use Nelmio\ApiDocBundle\ModelDescriber\BazingaHateoasModelDescriber;
use Nelmio\ApiDocBundle\ModelDescriber\JMSModelDescriber;
use Nelmio\ApiDocBundle\ModelDescriber\ModelDescriberInterface;
use Nelmio\ApiDocBundle\Processors\MapQueryStringProcessor;
use Nelmio\ApiDocBundle\Processors\MapRequestPayloadProcessor;
use Nelmio\ApiDocBundle\RouteDescriber\RouteArgumentDescriber;
use Nelmio\ApiDocBundle\RouteDescriber\RouteArgumentDescriber\RouteArgumentDescriberInterface;
use Nelmio\ApiDocBundle\RouteDescriber\RouteArgumentDescriber\SymfonyMapQueryParameterDescriber;
use Nelmio\ApiDocBundle\RouteDescriber\RouteArgumentDescriber\SymfonyMapQueryStringDescriber;
use Nelmio\ApiDocBundle\RouteDescriber\RouteArgumentDescriber\SymfonyMapRequestPayloadDescriber;
use Nelmio\ApiDocBundle\Routing\FilteredRouteCollectionBuilder;
use OpenApi\Generator;
use Symfony\Component\Config\FileLocator;
Expand All @@ -32,6 +39,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 @@ -170,6 +180,47 @@ public function load(array $configs, ContainerBuilder $container): void
->setArgument(1, $config['media_types']);
}

if (PHP_VERSION_ID > 80100) {
// Add autoconfiguration for route argument describer
$container->registerForAutoconfiguration(RouteArgumentDescriberInterface::class)
->addTag('nelmio_api_doc.route_argument_describer');

$container->register('nelmio_api_doc.route_describers.route_argument', RouteArgumentDescriber::class)
->setPublic(false)
->addTag('nelmio_api_doc.route_describer', ['priority' => -225])
->setArguments([
new Reference('argument_metadata_factory'),
new TaggedIteratorArgument('nelmio_api_doc.route_argument_describer'),
])
;

if (class_exists(MapQueryString::class)) {
$container->register('nelmio_api_doc.route_argument_describer.map_query_string', SymfonyMapQueryStringDescriber::class)
->setPublic(false)
->addTag('nelmio_api_doc.route_argument_describer', ['priority' => 0]);

$container->register('nelmio_api_doc.swagger.processor.map_query_string', MapQueryStringProcessor::class)
->setPublic(false)
->addTag('nelmio_api_doc.swagger.processor', ['priority' => 0]);
}

if (class_exists(MapRequestPayload::class)) {
$container->register('nelmio_api_doc.route_argument_describer.map_request_payload', SymfonyMapRequestPayloadDescriber::class)
->setPublic(false)
->addTag('nelmio_api_doc.route_argument_describer', ['priority' => 0]);

$container->register('nelmio_api_doc.swagger.processor.map_request_payload', MapRequestPayloadProcessor::class)
->setPublic(false)
->addTag('nelmio_api_doc.swagger.processor', ['priority' => 0]);
}

if (class_exists(MapQueryParameter::class)) {
$container->register('nelmio_api_doc.route_argument_describer.map_query_parameter', SymfonyMapQueryParameterDescriber::class)
->setPublic(false)
->addTag('nelmio_api_doc.route_argument_describer', ['priority' => 0]);
}
}

$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
12 changes: 12 additions & 0 deletions OpenApiPhp/Util.php
Original file line number Diff line number Diff line change
Expand Up @@ -504,4 +504,16 @@ function ($value) {
$class::$_nested
));
}

/**
* Helper method to modify an annotation value only if its value has not yet been set.
*/
public static function modifyAnnotationValue(OA\AbstractAnnotation $parameter, string $property, $value): void
{
if (!Generator::isDefault($parameter->{$property})) {
return;
}

$parameter->{$property} = $value;
}
}
80 changes: 80 additions & 0 deletions Processors/MapQueryStringProcessor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<?php

declare(strict_types=1);

namespace Nelmio\ApiDocBundle\Processors;

use Nelmio\ApiDocBundle\OpenApiPhp\Util;
use Nelmio\ApiDocBundle\RouteDescriber\RouteArgumentDescriber\SymfonyMapQueryStringDescriber;
use OpenApi\Analysis;
use OpenApi\Annotations as OA;
use OpenApi\Generator;
use OpenApi\Processors\ProcessorInterface;
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;

/**
* A processor that adds query parameters to operations that have a MapQueryString attribute.
* A processor is used to ensure that a Model has been created.
*
* @see SymfonyMapQueryStringDescriber
*/
final class MapQueryStringProcessor implements ProcessorInterface
{
public function __invoke(Analysis $analysis)
{
/** @var OA\Operation[] $operations */
$operations = $analysis->getAnnotationsOfType(OA\Operation::class);

foreach ($operations as $operation) {
if (!isset($operation->_context->{SymfonyMapQueryStringDescriber::CONTEXT_ARGUMENT_METADATA})) {
continue;
}

$argumentMetaData = $operation->_context->{SymfonyMapQueryStringDescriber::CONTEXT_ARGUMENT_METADATA};
if (!$argumentMetaData instanceof ArgumentMetadata) {
throw new \LogicException(sprintf('MapQueryString ArgumentMetaData not found for operation "%s"', $operation->operationId));
}

$modelRef = $operation->_context->{SymfonyMapQueryStringDescriber::CONTEXT_MODEL_REF};
if (!isset($modelRef)) {
throw new \LogicException(sprintf('MapQueryString Model reference not found for operation "%s"', $operation->operationId));
}

$nativeModelName = str_replace(OA\Components::SCHEMA_REF, '', $modelRef);

$schemaModel = Util::getSchema($analysis->openapi, $nativeModelName);

// There are no properties to map to query parameters
if (Generator::UNDEFINED === $schemaModel->properties) {
return;
}

$isModelOptional = $argumentMetaData->hasDefaultValue() || $argumentMetaData->isNullable();

foreach ($schemaModel->properties as $property) {
$operationParameter = Util::getOperationParameter($operation, $property->property, 'query');

// Remove incompatible properties
$propertyVars = get_object_vars($property);
unset($propertyVars['property']);

$schema = new OA\Schema($propertyVars);

Util::modifyAnnotationValue($operationParameter, 'schema', $schema);
Util::modifyAnnotationValue($operationParameter, 'name', $property->property);
Util::modifyAnnotationValue($operationParameter, 'description', $schema->description);
Util::modifyAnnotationValue($operationParameter, 'required', $schema->required);
Util::modifyAnnotationValue($operationParameter, 'deprecated', $schema->deprecated);
Util::modifyAnnotationValue($operationParameter, 'example', $schema->example);

if ($isModelOptional) {
Util::modifyAnnotationValue($operationParameter, 'required', false);
} elseif (is_array($schemaModel->required) && in_array($property->property, $schemaModel->required, true)) {
Util::modifyAnnotationValue($operationParameter, 'required', true);
} else {
Util::modifyAnnotationValue($operationParameter, 'required', false);
}
}
}
}
}
99 changes: 99 additions & 0 deletions Processors/MapRequestPayloadProcessor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<?php

declare(strict_types=1);

namespace Nelmio\ApiDocBundle\Processors;

use Nelmio\ApiDocBundle\OpenApiPhp\Util;
use Nelmio\ApiDocBundle\RouteDescriber\RouteArgumentDescriber\SymfonyMapRequestPayloadDescriber;
use OpenApi\Analysis;
use OpenApi\Annotations as OA;
use OpenApi\Processors\ProcessorInterface;
use Symfony\Component\HttpKernel\Attribute\MapRequestPayload;
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;

/**
* A processor that adds query parameters to operations that have a MapRequestPayload attribute.
* A processor is used to ensure that a Model has been created.
*
* @see SymfonyMapRequestPayloadDescriber
*/
final class MapRequestPayloadProcessor implements ProcessorInterface
{
public function __invoke(Analysis $analysis)
{
/** @var OA\Operation[] $operations */
$operations = $analysis->getAnnotationsOfType(OA\Operation::class);

foreach ($operations as $operation) {
if (!isset($operation->_context->{SymfonyMapRequestPayloadDescriber::CONTEXT_ARGUMENT_METADATA})) {
continue;
}

$argumentMetaData = $operation->_context->{SymfonyMapRequestPayloadDescriber::CONTEXT_ARGUMENT_METADATA};
if (!$argumentMetaData instanceof ArgumentMetadata) {
throw new \LogicException(sprintf('MapRequestPayload ArgumentMetaData not found for operation "%s"', $operation->operationId));
}

/** @var MapRequestPayload $attribute */
if (!$attribute = $argumentMetaData->getAttributes(MapRequestPayload::class, ArgumentMetadata::IS_INSTANCEOF)[0] ?? null) {
throw new \LogicException(sprintf('Operation "%s" does not contain attribute of "%s', $operation->operationId, MapRequestPayload::class));
}

$modelRef = $operation->_context->{SymfonyMapRequestPayloadDescriber::CONTEXT_MODEL_REF};
if (!isset($modelRef)) {
throw new \LogicException(sprintf('MapRequestPayload Model reference not found for operation "%s"', $operation->operationId));
}

/** @var OA\RequestBody $requestBody */
$requestBody = Util::getChild($operation, OA\RequestBody::class);
Util::modifyAnnotationValue($requestBody, 'required', !($argumentMetaData->hasDefaultValue() || $argumentMetaData->isNullable()));

$formats = $attribute->acceptFormat;
if (!is_array($formats)) {
$formats = [$attribute->acceptFormat ?? 'json'];
}

foreach ($formats as $format) {
$contentSchema = $this->getContentSchemaForType($requestBody, $format);
Util::modifyAnnotationValue($contentSchema, 'ref', $modelRef);

if ($argumentMetaData->isNullable()) {
$contentSchema->nullable = true;
}
}
}
}

private function getContentSchemaForType(OA\RequestBody $requestBody, string $type): OA\Schema
{
Util::modifyAnnotationValue($requestBody, 'content', []);
switch ($type) {
case 'json':
$contentType = 'application/json';

break;
case 'xml':
$contentType = 'application/xml';

break;
default:
throw new \InvalidArgumentException('Unsupported media type');
}

if (!isset($requestBody->content[$contentType])) {
$weakContext = Util::createWeakContext($requestBody->_context);
$requestBody->content[$contentType] = new OA\MediaType(
[
'mediaType' => $contentType,
'_context' => $weakContext,
]
);
}

return Util::getChild(
$requestBody->content[$contentType],
OA\Schema::class
);
}
}
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
Loading