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
@@ -1,6 +1,7 @@
# CHANGELOG

## 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 @@ -75,6 +75,35 @@
groups: ["create"],
)

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

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

.. versionadded:: 7.1

Check failure on line 83 in docs/symfony_attributes.rst

View workflow job for this annotation

GitHub Actions / Lint (DOCtor-RST)

You are not allowed to use version "7.1". Only major version "6" is allowed.

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 @@ -104,6 +133,10 @@
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 @@ -147,6 +180,26 @@
{
// ...
}

/**
* 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 @@ -197,4 +250,5 @@
.. _`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 @@ -28,6 +28,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 @@ -43,6 +44,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 @@ -223,6 +225,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 @@ -172,6 +172,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