Skip to content

Commit

Permalink
feat(symfony): describe MapUploadedFile property
Browse files Browse the repository at this point in the history
  • Loading branch information
jankal committed Jan 8, 2025
1 parent 13408ab commit fab548d
Show file tree
Hide file tree
Showing 7 changed files with 344 additions and 0 deletions.
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 @@ 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:: 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 @@ 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 @@ -147,6 +180,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 @@ -197,4 +250,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 @@ -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

0 comments on commit fab548d

Please sign in to comment.