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

feat(symfony): describe MapUploadedFile property #2418

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ nelmio_api_doc:
```

## 4.34.0
* Added support for the `#[MapUploadedFile]` symfony controller argument attribute
* Changed minimum Symfony version for 7.x from 7.0 to 7.1

## 4.33.6
Expand Down
54 changes: 54 additions & 0 deletions docs/symfony_attributes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,35 @@ Customizing the documentation of the request body can be done by adding the ``#[
groups: ["create"],
)

MapUploadedFile
-------------------------------

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

.. versionadded:: 4.37

The :class:`Symfony\\Component\\HttpKernel\\Attribute\\MapUploadedFile` attribute was introduced in Symfony 7.1.


Modify generated documentation
~~~~~~~

Customizing the documentation of the uploaded file can be done by adding the ``#[OA\RequestBody]`` attribute with the corresponding ``#[OA\MediaType]`` and ``#[OA\Schema]`` to your controller method.

.. code-block:: php-attributes

#[OA\RequestBody(
description: 'Describe the body',
content: [
new OA\MediaType('multipart/form-data', new OA\Schema(
properties: [new OA\Property(
property: 'file',
description: 'Describe the file'
)],
)),
],
)]

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

Expand Down Expand Up @@ -90,6 +119,10 @@ Complete example
use AppBundle\UserDTO;
use AppBundle\UserQuery;
use OpenApi\Attributes as OA;
use Symfony\Component\HttpKernel\Attribute\MapQueryParameter;
use Symfony\Component\HttpKernel\Attribute\MapQueryString;
use Symfony\Component\HttpKernel\Attribute\MapRequestPayload;
use Symfony\Component\HttpKernel\Attribute\MapUploadedFile;
use Symfony\Component\Routing\Annotation\Route;

class UserController
Expand Down Expand Up @@ -133,6 +166,26 @@ Complete example
{
// ...
}

/**
* Upload a profile picture
*/
#[Route('/api/users/picture', methods: ['POST'])]
#[OA\RequestBody(
description: 'Content of the profile picture upload request',
content: [
new OA\MediaType('multipart/form-data', new OA\Schema(
properties: [new OA\Property(
property: 'file',
description: 'File containing the profile picture',
)],
)),
],
)]
public function createUser(#[MapUploadedFile] UploadedFile $picture)
{
// ...
}
}

Customization
Expand Down Expand Up @@ -183,4 +236,5 @@ Make sure to use at least php 8.1 (attribute support) to make use of this functi
.. _`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
.. _`Symfony MapUploadedFile`: https://symfony.com/doc/current/controller.html#mapping-uploaded-files
.. _`RouteArgumentDescriberInterface`: https://github.com/DjordyKoert/NelmioApiDocBundle/blob/master/src/RouteDescriber/RouteArgumentDescriber/RouteArgumentDescriberInterface.php
8 changes: 8 additions & 0 deletions src/DependencyInjection/NelmioApiDocExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
use Nelmio\ApiDocBundle\RouteDescriber\RouteArgumentDescriber\SymfonyMapQueryParameterDescriber;
use Nelmio\ApiDocBundle\RouteDescriber\RouteArgumentDescriber\SymfonyMapQueryStringDescriber;
use Nelmio\ApiDocBundle\RouteDescriber\RouteArgumentDescriber\SymfonyMapRequestPayloadDescriber;
use Nelmio\ApiDocBundle\RouteDescriber\RouteArgumentDescriber\SymfonyMapUploadedFileDescriber;
use Nelmio\ApiDocBundle\Routing\FilteredRouteCollectionBuilder;
use OpenApi\Generator;
use Symfony\Component\Config\FileLocator;
Expand All @@ -44,6 +45,7 @@
use Symfony\Component\HttpKernel\Attribute\MapQueryParameter;
use Symfony\Component\HttpKernel\Attribute\MapQueryString;
use Symfony\Component\HttpKernel\Attribute\MapRequestPayload;
use Symfony\Component\HttpKernel\Attribute\MapUploadedFile;
use Symfony\Component\Routing\RouteCollection;

final class NelmioApiDocExtension extends Extension implements PrependExtensionInterface
Expand Down Expand Up @@ -229,6 +231,12 @@ public function load(array $configs, ContainerBuilder $container): void
->setPublic(false)
->addTag('nelmio_api_doc.route_argument_describer', ['priority' => 0]);
}

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

$bundles = $container->getParameter('kernel.bundles');
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

declare(strict_types=1);

/*
* 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\RouteDescriber\RouteArgumentDescriber;

use Nelmio\ApiDocBundle\OpenApiPhp\Util;
use OpenApi\Annotations as OA;
use Symfony\Component\HttpKernel\Attribute\MapUploadedFile;
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;

final class SymfonyMapUploadedFileDescriber implements RouteArgumentDescriberInterface
{
public const CONTEXT_ARGUMENT_METADATA = 'nelmio_api_doc_bundle.argument_metadata.'.self::class;
public const CONTEXT_MODEL_REF = 'nelmio_api_doc_bundle.model_ref.'.self::class;

public function describe(ArgumentMetadata $argumentMetadata, OA\Operation $operation): void
{
if (!$attribute = $argumentMetadata->getAttributes(MapUploadedFile::class, ArgumentMetadata::IS_INSTANCEOF)[0] ?? null) {
return;
}

$name = $attribute->name ?? $argumentMetadata->getName();
$body = Util::getChild($operation, OA\RequestBody::class);

$mediaType = Util::getCollectionItem($body, OA\MediaType::class, [
'mediaType' => 'multipart/form-data'
]);

/** @var OA\Schema $schema */
$schema = Util::getChild($mediaType, OA\Schema::class, [
'type' => 'object'
]);

$property = Util::getCollectionItem($schema, OA\Property::class, ['property' => $name]);
Util::modifyAnnotationValue($property, 'type', 'string');
Util::modifyAnnotationValue($property, 'format', 'binary');
}
}
80 changes: 80 additions & 0 deletions tests/Functional/Controller/MapUploadedFileController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<?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\Controller;

use OpenApi\Attributes as OA;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpKernel\Attribute\MapUploadedFile;
use Symfony\Component\Routing\Annotation\Route;

class MapUploadedFileController
{
#[Route('/article_map_uploaded_file', methods: ['POST'])]
#[OA\Response(response: '200', description: '')]
public function createUploadFromMapUploadedFilePayload(
#[MapUploadedFile]
UploadedFile $upload,
) {
}

#[Route('/article_map_uploaded_file_nullable', methods: ['POST'])]
#[OA\Response(response: '200', description: '')]
public function createUploadFromMapUploadedFilePayloadNullable(
#[MapUploadedFile]
?UploadedFile $upload,
) {
}

#[Route('/article_map_uploaded_file_multiple', methods: ['POST'])]
#[OA\Response(response: '200', description: '')]
public function createUploadFromMapUploadedFilePayloadMultiple(
#[MapUploadedFile]
UploadedFile $firstUpload,
#[MapUploadedFile]
UploadedFile $secondUpload,
) {
}

#[Route('/article_map_uploaded_file_add_to_existing', methods: ['POST'])]
#[OA\RequestBody(content: [
new OA\MediaType('multipart/form-data', new OA\Schema(
properties: [new OA\Property(property: 'existing', type: 'string', format: 'binary')],
type: 'object',
)),
])]
#[OA\Response(response: '200', description: '')]
public function createUploadFromMapUploadedFileAddToExisting(
#[MapUploadedFile]
?UploadedFile $upload,
) {
}

#[Route('/article_map_uploaded_file_overwrite', methods: ['POST'])]
#[OA\RequestBody(
description: 'Body if file upload request',
content: [
new OA\MediaType('multipart/form-data', new OA\Schema(
properties: [new OA\Property(
property: 'upload',
description: 'A file',
)],
type: 'object',
)),
],
)]
#[OA\Response(response: '200', description: '')]
public function createUploadFromMapUploadedFileOverwrite(
#[MapUploadedFile]
?UploadedFile $upload,
) {
}
}
9 changes: 9 additions & 0 deletions tests/Functional/ControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,15 @@ public static function provideAttributeTestCases(): \Generator
];
}
}

if (version_compare(Kernel::VERSION, '7.1.0', '>=')) {
yield 'Symfony 7.1 MapUploadedFile attribute' => [
[
'name' => 'MapUploadedFileController',
'type' => $type,
],
];
}
}

public static function provideAnnotationTestCases(): \Generator
Expand Down
Loading
Loading