Skip to content

Commit f41a273

Browse files
committed
feat(symfony): describe MapUploadedFile property
1 parent 8e56941 commit f41a273

File tree

7 files changed

+348
-0
lines changed

7 files changed

+348
-0
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# CHANGELOG
22

3+
## 4.34.0
4+
* Added support for the `#[MapUploadedFile]` symfony controller argument attribute
5+
36
## 4.33.6
47
* Fixed Symfony 7.2 deprecation of tagged arguments
58

docs/symfony_attributes.rst

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,35 @@ Customizing the documentation of the request body can be done by adding the ``#[
7575
groups: ["create"],
7676
)
7777
78+
MapUploadedFile
79+
-------------------------------
80+
81+
Using the `Symfony MapUploadedFile`_ attribute allows NelmioApiDocBundle to automatically generate your request body documentation for your endpoint.
82+
83+
.. versionadded:: 7.1
84+
85+
The :class:`Symfony\\Component\\HttpKernel\\Attribute\\MapUploadedFile` attribute was introduced in Symfony 7.1.
86+
87+
88+
Modify generated documentation
89+
~~~~~~~
90+
91+
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.
92+
93+
.. code-block:: php-attributes
94+
95+
#[OA\RequestBody(
96+
description: 'Describe the body',
97+
content: [
98+
new OA\MediaType('multipart/form-data', new OA\Schema(
99+
properties: [new OA\Property(
100+
property: 'file',
101+
description: 'Describe the file'
102+
)],
103+
)),
104+
],
105+
)]
106+
78107
Complete example
79108
----------------------
80109

@@ -104,6 +133,10 @@ Complete example
104133
use AppBundle\UserDTO;
105134
use AppBundle\UserQuery;
106135
use OpenApi\Attributes as OA;
136+
use Symfony\Component\HttpKernel\Attribute\MapQueryParameter;
137+
use Symfony\Component\HttpKernel\Attribute\MapQueryString;
138+
use Symfony\Component\HttpKernel\Attribute\MapRequestPayload;
139+
use Symfony\Component\HttpKernel\Attribute\MapUploadedFile;
107140
use Symfony\Component\Routing\Annotation\Route;
108141
109142
class UserController
@@ -147,6 +180,26 @@ Complete example
147180
{
148181
// ...
149182
}
183+
184+
/**
185+
* Upload a profile picture
186+
*/
187+
#[Route('/api/users/picture', methods: ['POST'])]
188+
#[OA\RequestBody(
189+
description: 'Content of the profile picture upload request',
190+
content: [
191+
new OA\MediaType('multipart/form-data', new OA\Schema(
192+
properties: [new OA\Property(
193+
property: 'file',
194+
description: 'File containing the profile picture',
195+
)],
196+
)),
197+
],
198+
)]
199+
public function createUser(#[MapUploadedFile] UploadedFile $picture)
200+
{
201+
// ...
202+
}
150203
}
151204
152205
Customization
@@ -197,4 +250,5 @@ Make sure to use at least php 8.1 (attribute support) to make use of this functi
197250
.. _`Symfony MapQueryString`: https://symfony.com/doc/current/controller.html#mapping-the-whole-query-string
198251
.. _`Symfony MapQueryParameter`: https://symfony.com/doc/current/controller.html#mapping-query-parameters-individually
199252
.. _`Symfony MapRequestPayload`: https://symfony.com/doc/current/controller.html#mapping-request-payload
253+
.. _`Symfony MapUploadedFile`: https://symfony.com/doc/current/controller.html#mapping-uploaded-files
200254
.. _`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
@@ -28,6 +28,7 @@
2828
use Nelmio\ApiDocBundle\RouteDescriber\RouteArgumentDescriber\SymfonyMapQueryParameterDescriber;
2929
use Nelmio\ApiDocBundle\RouteDescriber\RouteArgumentDescriber\SymfonyMapQueryStringDescriber;
3030
use Nelmio\ApiDocBundle\RouteDescriber\RouteArgumentDescriber\SymfonyMapRequestPayloadDescriber;
31+
use Nelmio\ApiDocBundle\RouteDescriber\RouteArgumentDescriber\SymfonyMapUploadedFileDescriber;
3132
use Nelmio\ApiDocBundle\Routing\FilteredRouteCollectionBuilder;
3233
use OpenApi\Generator;
3334
use Symfony\Component\Config\FileLocator;
@@ -43,6 +44,7 @@
4344
use Symfony\Component\HttpKernel\Attribute\MapQueryParameter;
4445
use Symfony\Component\HttpKernel\Attribute\MapQueryString;
4546
use Symfony\Component\HttpKernel\Attribute\MapRequestPayload;
47+
use Symfony\Component\HttpKernel\Attribute\MapUploadedFile;
4648
use Symfony\Component\Routing\RouteCollection;
4749

4850
final class NelmioApiDocExtension extends Extension implements PrependExtensionInterface
@@ -223,6 +225,12 @@ public function load(array $configs, ContainerBuilder $container): void
223225
->setPublic(false)
224226
->addTag('nelmio_api_doc.route_argument_describer', ['priority' => 0]);
225227
}
228+
229+
if (class_exists(MapUploadedFile::class)) {
230+
$container->register('nelmio_api_doc.route_argument_describer.map_uploaded_file', SymfonyMapUploadedFileDescriber::class)
231+
->setPublic(false)
232+
->addTag('nelmio_api_doc.route_argument_describer', ['priority' => 0]);
233+
}
226234
}
227235

228236
$bundles = $container->getParameter('kernel.bundles');
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
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 OpenApi\Generator;
19+
use Symfony\Component\HttpKernel\Attribute\MapUploadedFile;
20+
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
21+
22+
final class SymfonyMapUploadedFileDescriber implements RouteArgumentDescriberInterface
23+
{
24+
public const CONTEXT_ARGUMENT_METADATA = 'nelmio_api_doc_bundle.argument_metadata.'.self::class;
25+
public const CONTEXT_MODEL_REF = 'nelmio_api_doc_bundle.model_ref.'.self::class;
26+
27+
public function describe(ArgumentMetadata $argumentMetadata, OA\Operation $operation): void
28+
{
29+
if (!$attribute = $argumentMetadata->getAttributes(MapUploadedFile::class, ArgumentMetadata::IS_INSTANCEOF)[0] ?? null) {
30+
return;
31+
}
32+
33+
$name = $attribute->name ?? $argumentMetadata->getName();
34+
$body = Util::getChild($operation, OA\RequestBody::class);
35+
36+
$mediaType = Util::getCollectionItem($body, OA\MediaType::class, [
37+
'mediaType' => 'multipart/form-data'
38+
]);
39+
40+
/** @var OA\Schema $schema */
41+
$schema = Util::getChild($mediaType, OA\Schema::class, [
42+
'type' => 'object'
43+
]);
44+
45+
$property = Util::getCollectionItem($schema, OA\Property::class, ['property' => $name]);
46+
Util::modifyAnnotationValue($property, 'type', 'string');
47+
Util::modifyAnnotationValue($property, 'format', 'binary');
48+
}
49+
}
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: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use JMS\SerializerBundle\JMSSerializerBundle;
1515
use OpenApi\Annotations as OA;
1616
use Symfony\Component\HttpKernel\Attribute\MapRequestPayload;
17+
use Symfony\Component\HttpKernel\Attribute\MapUploadedFile;
1718
use Symfony\Component\HttpKernel\Bundle\Bundle;
1819
use Symfony\Component\HttpKernel\Kernel;
1920
use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator;
@@ -172,6 +173,15 @@ public static function provideAttributeTestCases(): \Generator
172173
];
173174
}
174175
}
176+
177+
if (version_compare(Kernel::VERSION, '7.1.0', '>=')) {
178+
yield 'Symfony 7.1 MapUploadedFile attribute' => [
179+
[
180+
'name' => 'MapUploadedFileController',
181+
'type' => $type,
182+
],
183+
];
184+
}
175185
}
176186

177187
public static function provideAnnotationTestCases(): \Generator

0 commit comments

Comments
 (0)