Skip to content

Commit 98f4627

Browse files
committed
feat(symfony): describe MapUploadedFile property
1 parent 8b4803f commit 98f4627

File tree

7 files changed

+344
-0
lines changed

7 files changed

+344
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ nelmio_api_doc:
2929
```
3030
3131
## 4.34.0
32+
* Added support for the `#[MapUploadedFile]` symfony controller argument attribute
3233
* Changed minimum Symfony version for 7.x from 7.0 to 7.1
3334

3435
## 4.33.6

docs/symfony_attributes.rst

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,35 @@ Customizing the documentation of the request body can be done by adding the ``#[
6161
groups: ["create"],
6262
)
6363
64+
MapUploadedFile
65+
-------------------------------
66+
67+
Using the `Symfony MapUploadedFile`_ attribute allows NelmioApiDocBundle to automatically generate your request body documentation for your endpoint.
68+
69+
.. versionadded:: 4.37
70+
71+
The :class:`Symfony\\Component\\HttpKernel\\Attribute\\MapUploadedFile` attribute was introduced in Symfony 7.1.
72+
73+
74+
Modify generated documentation
75+
~~~~~~~
76+
77+
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.
78+
79+
.. code-block:: php-attributes
80+
81+
#[OA\RequestBody(
82+
description: 'Describe the body',
83+
content: [
84+
new OA\MediaType('multipart/form-data', new OA\Schema(
85+
properties: [new OA\Property(
86+
property: 'file',
87+
description: 'Describe the file'
88+
)],
89+
)),
90+
],
91+
)]
92+
6493
Complete example
6594
----------------------
6695

@@ -90,6 +119,10 @@ Complete example
90119
use AppBundle\UserDTO;
91120
use AppBundle\UserQuery;
92121
use OpenApi\Attributes as OA;
122+
use Symfony\Component\HttpKernel\Attribute\MapQueryParameter;
123+
use Symfony\Component\HttpKernel\Attribute\MapQueryString;
124+
use Symfony\Component\HttpKernel\Attribute\MapRequestPayload;
125+
use Symfony\Component\HttpKernel\Attribute\MapUploadedFile;
93126
use Symfony\Component\Routing\Annotation\Route;
94127
95128
class UserController
@@ -133,6 +166,26 @@ Complete example
133166
{
134167
// ...
135168
}
169+
170+
/**
171+
* Upload a profile picture
172+
*/
173+
#[Route('/api/users/picture', methods: ['POST'])]
174+
#[OA\RequestBody(
175+
description: 'Content of the profile picture upload request',
176+
content: [
177+
new OA\MediaType('multipart/form-data', new OA\Schema(
178+
properties: [new OA\Property(
179+
property: 'file',
180+
description: 'File containing the profile picture',
181+
)],
182+
)),
183+
],
184+
)]
185+
public function createUser(#[MapUploadedFile] UploadedFile $picture)
186+
{
187+
// ...
188+
}
136189
}
137190
138191
Customization
@@ -183,4 +236,5 @@ Make sure to use at least php 8.1 (attribute support) to make use of this functi
183236
.. _`Symfony MapQueryString`: https://symfony.com/doc/current/controller.html#mapping-the-whole-query-string
184237
.. _`Symfony MapQueryParameter`: https://symfony.com/doc/current/controller.html#mapping-query-parameters-individually
185238
.. _`Symfony MapRequestPayload`: https://symfony.com/doc/current/controller.html#mapping-request-payload
239+
.. _`Symfony MapUploadedFile`: https://symfony.com/doc/current/controller.html#mapping-uploaded-files
186240
.. _`RouteArgumentDescriberInterface`: https://github.com/DjordyKoert/NelmioApiDocBundle/blob/master/src/RouteDescriber/RouteArgumentDescriber/RouteArgumentDescriberInterface.php

src/DependencyInjection/NelmioApiDocExtension.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
use Nelmio\ApiDocBundle\RouteDescriber\RouteArgumentDescriber\SymfonyMapQueryParameterDescriber;
3030
use Nelmio\ApiDocBundle\RouteDescriber\RouteArgumentDescriber\SymfonyMapQueryStringDescriber;
3131
use Nelmio\ApiDocBundle\RouteDescriber\RouteArgumentDescriber\SymfonyMapRequestPayloadDescriber;
32+
use Nelmio\ApiDocBundle\RouteDescriber\RouteArgumentDescriber\SymfonyMapUploadedFileDescriber;
3233
use Nelmio\ApiDocBundle\Routing\FilteredRouteCollectionBuilder;
3334
use OpenApi\Generator;
3435
use Symfony\Component\Config\FileLocator;
@@ -44,6 +45,7 @@
4445
use Symfony\Component\HttpKernel\Attribute\MapQueryParameter;
4546
use Symfony\Component\HttpKernel\Attribute\MapQueryString;
4647
use Symfony\Component\HttpKernel\Attribute\MapRequestPayload;
48+
use Symfony\Component\HttpKernel\Attribute\MapUploadedFile;
4749
use Symfony\Component\Routing\RouteCollection;
4850

4951
final class NelmioApiDocExtension extends Extension implements PrependExtensionInterface
@@ -229,6 +231,12 @@ public function load(array $configs, ContainerBuilder $container): void
229231
->setPublic(false)
230232
->addTag('nelmio_api_doc.route_argument_describer', ['priority' => 0]);
231233
}
234+
235+
if (class_exists(MapUploadedFile::class)) {
236+
$container->register('nelmio_api_doc.route_argument_describer.map_uploaded_file', SymfonyMapUploadedFileDescriber::class)
237+
->setPublic(false)
238+
->addTag('nelmio_api_doc.route_argument_describer', ['priority' => 0]);
239+
}
232240
}
233241

234242
$bundles = $container->getParameter('kernel.bundles');
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of the NelmioApiDocBundle package.
7+
*
8+
* (c) Nelmio
9+
*
10+
* For the full copyright and license information, please view the LICENSE
11+
* file that was distributed with this source code.
12+
*/
13+
14+
namespace Nelmio\ApiDocBundle\RouteDescriber\RouteArgumentDescriber;
15+
16+
use Nelmio\ApiDocBundle\OpenApiPhp\Util;
17+
use OpenApi\Annotations as OA;
18+
use Symfony\Component\HttpKernel\Attribute\MapUploadedFile;
19+
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
20+
21+
final class SymfonyMapUploadedFileDescriber implements RouteArgumentDescriberInterface
22+
{
23+
public const CONTEXT_ARGUMENT_METADATA = 'nelmio_api_doc_bundle.argument_metadata.'.self::class;
24+
public const CONTEXT_MODEL_REF = 'nelmio_api_doc_bundle.model_ref.'.self::class;
25+
26+
public function describe(ArgumentMetadata $argumentMetadata, OA\Operation $operation): void
27+
{
28+
if (!$attribute = $argumentMetadata->getAttributes(MapUploadedFile::class, ArgumentMetadata::IS_INSTANCEOF)[0] ?? null) {
29+
return;
30+
}
31+
32+
$name = $attribute->name ?? $argumentMetadata->getName();
33+
$body = Util::getChild($operation, OA\RequestBody::class);
34+
35+
$mediaType = Util::getCollectionItem($body, OA\MediaType::class, [
36+
'mediaType' => 'multipart/form-data'
37+
]);
38+
39+
/** @var OA\Schema $schema */
40+
$schema = Util::getChild($mediaType, OA\Schema::class, [
41+
'type' => 'object'
42+
]);
43+
44+
$property = Util::getCollectionItem($schema, OA\Property::class, ['property' => $name]);
45+
Util::modifyAnnotationValue($property, 'type', 'string');
46+
Util::modifyAnnotationValue($property, 'format', 'binary');
47+
}
48+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the NelmioApiDocBundle package.
5+
*
6+
* (c) Nelmio
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Nelmio\ApiDocBundle\Tests\Functional\Controller;
13+
14+
use OpenApi\Attributes as OA;
15+
use Symfony\Component\HttpFoundation\File\UploadedFile;
16+
use Symfony\Component\HttpKernel\Attribute\MapUploadedFile;
17+
use Symfony\Component\Routing\Annotation\Route;
18+
19+
class MapUploadedFileController
20+
{
21+
#[Route('/article_map_uploaded_file', methods: ['POST'])]
22+
#[OA\Response(response: '200', description: '')]
23+
public function createUploadFromMapUploadedFilePayload(
24+
#[MapUploadedFile]
25+
UploadedFile $upload,
26+
) {
27+
}
28+
29+
#[Route('/article_map_uploaded_file_nullable', methods: ['POST'])]
30+
#[OA\Response(response: '200', description: '')]
31+
public function createUploadFromMapUploadedFilePayloadNullable(
32+
#[MapUploadedFile]
33+
?UploadedFile $upload,
34+
) {
35+
}
36+
37+
#[Route('/article_map_uploaded_file_multiple', methods: ['POST'])]
38+
#[OA\Response(response: '200', description: '')]
39+
public function createUploadFromMapUploadedFilePayloadMultiple(
40+
#[MapUploadedFile]
41+
UploadedFile $firstUpload,
42+
#[MapUploadedFile]
43+
UploadedFile $secondUpload,
44+
) {
45+
}
46+
47+
#[Route('/article_map_uploaded_file_add_to_existing', methods: ['POST'])]
48+
#[OA\RequestBody(content: [
49+
new OA\MediaType('multipart/form-data', new OA\Schema(
50+
properties: [new OA\Property(property: 'existing', type: 'string', format: 'binary')],
51+
type: 'object',
52+
)),
53+
])]
54+
#[OA\Response(response: '200', description: '')]
55+
public function createUploadFromMapUploadedFileAddToExisting(
56+
#[MapUploadedFile]
57+
?UploadedFile $upload,
58+
) {
59+
}
60+
61+
#[Route('/article_map_uploaded_file_overwrite', methods: ['POST'])]
62+
#[OA\RequestBody(
63+
description: 'Body if file upload request',
64+
content: [
65+
new OA\MediaType('multipart/form-data', new OA\Schema(
66+
properties: [new OA\Property(
67+
property: 'upload',
68+
description: 'A file',
69+
)],
70+
type: 'object',
71+
)),
72+
],
73+
)]
74+
#[OA\Response(response: '200', description: '')]
75+
public function createUploadFromMapUploadedFileOverwrite(
76+
#[MapUploadedFile]
77+
?UploadedFile $upload,
78+
) {
79+
}
80+
}

tests/Functional/ControllerTest.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,15 @@ public static function provideAttributeTestCases(): \Generator
178178
];
179179
}
180180
}
181+
182+
if (version_compare(Kernel::VERSION, '7.1.0', '>=')) {
183+
yield 'Symfony 7.1 MapUploadedFile attribute' => [
184+
[
185+
'name' => 'MapUploadedFileController',
186+
'type' => $type,
187+
],
188+
];
189+
}
181190
}
182191

183192
public static function provideAnnotationTestCases(): \Generator

0 commit comments

Comments
 (0)