Skip to content

Commit

Permalink
upload file api #3622
Browse files Browse the repository at this point in the history
  • Loading branch information
sfinx13 committed Feb 11, 2025
1 parent d924d57 commit 5bef770
Show file tree
Hide file tree
Showing 13 changed files with 666 additions and 9 deletions.
106 changes: 106 additions & 0 deletions src/Controller/Api/SignalementFileUpdateController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
<?php

namespace App\Controller\Api;

use App\Dto\Api\Request\FileRequest;
use App\Entity\Enum\DocumentType;
use App\Entity\File;
use App\Factory\Api\FileFactory;
use Doctrine\ORM\EntityManagerInterface;
use OpenApi\Attributes as OA;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\DependencyInjection\Attribute\When;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\MapRequestPayload;
use Symfony\Component\Routing\Attribute\Route;

#[When('dev')]
#[When('test')]
#[Route('/api')]
class SignalementFileUpdateController extends AbstractController
{
public function __construct(private readonly EntityManagerInterface $entityManager, private readonly FileFactory $fileFactory)
{
}

#[Route('/files/{uuid:file}', name: 'api_signalements_files_patch', methods: ['PATCH'])]
#[OA\Patch(
path: '/api/files/{uuid}',
description: 'Edite le type de document ainsi que la description pour un fichier.',
summary: 'Edition d\'un fichier',
security: [['Bearer' => []]],
requestBody: new OA\RequestBody(
description: 'Mettre à jour le type de document et la description.',
content: new OA\JsonContent(ref: '#/components/schemas/FileRequest')
),
tags: ['Fichiers'],
responses: [
new OA\Response(
response: 200,
description: 'Fichier édité avec succès',
content: new OA\JsonContent(
properties: [
new OA\Property(property: 'uuid', type: 'string', example: '123'),
new OA\Property(property: 'documentType', type: 'string', example: 'BAILLEUR_REPONSE_BAILLEUR'),
new OA\Property(property: 'description', type: 'string', example: 'lorem ipsum dolor sit amet'),
]
)
),
new OA\Response(
response: 400,
description: 'Erreur de validation ou autres erreurs liées à la requête.',
content: new OA\JsonContent(
properties: [
new OA\Property(property: 'status', type: 'integer', example: 400),
new OA\Property(property: 'message', type: 'string', example: 'Validation Failed'),
new OA\Property(property: 'errors', type: 'array', items: new OA\Items(
type: 'object'
)),
]
)
),
new OA\Response(
response: 404,
description: 'Fichier non trouvé.',
content: new OA\JsonContent(
properties: [
new OA\Property(property: 'status', type: 'integer', example: 404),
new OA\Property(property: 'message', type: 'string', example: 'Resource Not Found'),
]
)
),
new OA\Response(
response: 401,
description: 'Accès non autorisé.',
content: new OA\JsonContent(
properties: [
new OA\Property(property: 'status', type: 'integer', example: 401),
new OA\Property(property: 'message', type: 'string', example: 'Unauthorized'),
]
)
),
]
)]
public function __invoke(
#[MapRequestPayload]
FileRequest $fileRequest,
?File $file = null,
): JsonResponse {
if (null === $file) {
return $this->json(
['message' => 'Fichier introuvable', 'status' => Response::HTTP_NOT_FOUND],
Response::HTTP_NOT_FOUND
);
}
$this->denyAccessUnlessGranted('FILE_EDIT', $file);

$file->setDocumentType(DocumentType::tryFrom($fileRequest->documentType));
if (File::FILE_TYPE_PHOTO === $file->getFileType()) {
$file->setDescription($fileRequest->description);
}
$this->entityManager->flush();

return $this->json($this->fileFactory->createFrom($file));
}
}
192 changes: 192 additions & 0 deletions src/Controller/Api/SignalementFileUploadController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
<?php

namespace App\Controller\Api;

use App\Dto\Api\Model\File as FileResponse;
use App\Dto\Api\Request\FilesUploadRequest;
use App\Entity\File;
use App\Entity\Signalement;
use App\Entity\User;
use App\Event\FileUploadedEvent;
use App\Factory\Api\FileFactory;
use App\Service\Signalement\SignalementFileProcessor;
use Doctrine\ORM\EntityManagerInterface;
use Nelmio\ApiDocBundle\Annotation\Model;
use OpenApi\Attributes as OA;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\DependencyInjection\Attribute\When;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Serializer\Exception\ExceptionInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Validator\Exception\ValidationFailedException;
use Symfony\Component\Validator\Validator\ValidatorInterface;

#[When('dev')]
#[When('test')]
#[Route('/api')]
class SignalementFileUploadController extends AbstractController
{
public function __construct(
private readonly DenormalizerInterface $normalizer,
private readonly ValidatorInterface $validator,
private readonly SignalementFileProcessor $signalementFileProcessor,
private readonly EntityManagerInterface $entityManager,
private readonly EventDispatcherInterface $eventDispatcher,
private readonly FileFactory $fileFactory,
) {
}

/**
* @throws ExceptionInterface
*/
#[Route('/signalements/{uuid:signalement}/files', name: 'api_signalements_files_post', methods: ['POST'])]
#[OA\Post(
path: '/api/signalements/{uuid}/files',
description: 'Retourne les informations du fichier téléversé pour un signalement.',
summary: 'Téléversement d\'un fichier',
security: [['Bearer' => []]],
requestBody: new OA\RequestBody(
description: 'Données de téléversement du fichier',
required: true,
content: new OA\MediaType(
mediaType: 'multipart/form-data',
schema: new OA\Schema(
properties: [
'files' => new OA\Property(
description: 'Liste des fichiers téléversés',
type: 'array',
items: new OA\Items(type: 'string', format: 'binary'),
),
],
type: 'object'
),
examples: [
new OA\Examples(
example: "Exemple d'envoi d'un fichier",
summary: "Exemple d'envoi d'un fichier",
value: [
'files' => ['file1.jpg', 'file2.pdf'],
]
),
]
)
),
tags: ['Fichiers'],
)]
#[OA\Response(
response: Response::HTTP_OK,
description: 'Un document',
content: new OA\JsonContent(
type: 'array',
items: new OA\Items(ref: new Model(type: FileResponse::class))
)
)]
#[OA\Response(
response: 400,
description: 'Erreur liée à la validation de la requête.',
content: new OA\JsonContent(
properties: [
new OA\Property(property: 'message', type: 'string', example: 'Valeurs invalides pour les champs suivants :'),
new OA\Property(property: 'status', type: 'integer', example: 400),
new OA\Property(
property: 'errors',
type: 'array',
items: new OA\Items(
properties: [
new OA\Property(property: 'property', type: 'string', example: 'files'),
new OA\Property(property: 'message', type: 'string', example: 'Vous devez téléverser au moins un fichier.'),
new OA\Property(property: 'invalidValue', type: 'array', items: new OA\Items(type: 'string'), example: []),
]
)
),
]
)
)]
#[OA\Response(
response: 401,
description: 'Accès non autorisé à la ressource.',
content: new OA\JsonContent(
properties: [
new OA\Property(property: 'status', type: 'integer', example: 401),
new OA\Property(property: 'message', type: 'string', example: 'Unauthorized'),
]
)
)]
#[OA\Response(
response: 404,
description: 'Signalement non trouvé.',
content: new OA\JsonContent(
properties: [
new OA\Property(property: 'status', type: 'integer', example: 404),
new OA\Property(property: 'message', type: 'string', example: 'Resource Not Found'),
]
)
)]
public function __invoke(
Request $request,
?Signalement $signalement = null,
): JsonResponse {
if (null === $signalement) {
return $this->json(
['message' => 'Signalement introuvable', 'status' => Response::HTTP_NOT_FOUND],
Response::HTTP_NOT_FOUND
);
}
$this->denyAccessUnlessGranted('SIGN_EDIT', $signalement);
$data['files'] = $request->files->all();
$fileRequest = $this->normalizer->denormalize($data['files'], FilesUploadRequest::class, 'json');
$errors = $this->validator->validate($fileRequest);
if (count($errors) > 0) {
throw new ValidationFailedException($fileRequest, $errors);
}

/** @var User $user */
$user = $this->getUser();
$fileList = $this->processFiles($fileRequest);
$this->signalementFileProcessor->addFilesToSignalement(
fileList: $fileList,
signalement: $signalement,
user: $user
);

$this->entityManager->persist($signalement);
$this->entityManager->flush();

$fileUploadedEvent = $this->eventDispatcher->dispatch(
new FileUploadedEvent($signalement, $user, $fileList),
FileUploadedEvent::NAME
);

$response = $this->fileFactory->createFromArray($fileUploadedEvent->getFilesPushed());

return $this->json($response, Response::HTTP_CREATED);
}

private function processFiles(FilesUploadRequest $fileRequest): array
{
$files = $fileList = [];
foreach ($fileRequest->files as $file) {
/** @var UploadedFile $file */
if (in_array($file->getMimeType(), File::IMAGE_MIME_TYPES)) {
$files['photos'][] = $file;
} else {
$files['documents'][] = $file;
}
}
if (isset($files['documents'])) {
$documentList = $this->signalementFileProcessor->process($files, 'documents');
$fileList = [...$fileList, ...$documentList];
}
if (isset($files['photos'])) {
$imageList = $this->signalementFileProcessor->process($files, 'photos');
$fileList = [...$fileList, ...$imageList];
}

return $fileList;
}
}
6 changes: 6 additions & 0 deletions src/Dto/Api/Model/File.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@
)]
class File
{
#[OA\Property(
description: 'Uuid du fichier',
example: '123e4567-e89b-12d3-a456-426614174000',
)]
public string $uuid;

#[OA\Property(
description: 'Titre du fichier',
example: 'sample.png',
Expand Down
62 changes: 62 additions & 0 deletions src/Dto/Api/Request/FileRequest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?php

namespace App\Dto\Api\Request;

use OpenApi\Attributes as OA;
use Symfony\Component\Validator\Constraints as Assert;

class FileRequest implements RequestInterface
{
#[OA\Property(
description: 'Type de document<br>
<ul>
<li>`AUTRE_PROCEDURE`</li>
<li>`AUTRE`</li>
<li>`SITUATION_FOYER_DPE`</li>
<li>`SITUATION_FOYER_ETAT_DES_LIEUX`</li>
<li>`PROCEDURE_RAPPORT_DE_VISITE`</li>
<li>`SITUATION_FOYER_BAIL`</li>
<li>`PHOTO_SITUATION`</li>
<li>`PHOTO_VISITE`</li>
<li>`SITUATION_DIAGNOSTIC_PLOMB_AMIANTE`</li>
<li>`PROCEDURE_MISE_EN_DEMEURE`</li>
<li>`PROCEDURE_ARRETE_PREFECTORAL`</li>
<li>`BAILLEUR_REPONSE_BAILLEUR`</li>
<li>`PROCEDURE_ARRETE_MUNICIPAL`</li>
<li>`PROCEDURE_SAISINE`</li>
<li>`BAILLEUR_DEVIS_POUR_TRAVAUX`</li>
<li>`EXPORT`</li>
</ul>
',
example: 'BAILLEUR_REPONSE_BAILLEUR'
)]
#[Assert\NotBlank()]
#[Assert\Choice(
choices: [
'AUTRE_PROCEDURE',
'AUTRE',
'SITUATION_FOYER_DPE',
'SITUATION_FOYER_ETAT_DES_LIEUX',
'PROCEDURE_RAPPORT_DE_VISITE',
'SITUATION_FOYER_BAIL',
'PHOTO_SITUATION',
'PHOTO_VISITE',
'SITUATION_DIAGNOSTIC_PLOMB_AMIANTE',
'PROCEDURE_MISE_EN_DEMEURE',
'PROCEDURE_ARRETE_PREFECTORAL',
'BAILLEUR_REPONSE_BAILLEUR',
'PROCEDURE_ARRETE_MUNICIPAL',
'PROCEDURE_SAISINE',
'BAILLEUR_DEVIS_POUR_TRAVAUX',
'EXPORT',
],
message: 'Veuillez choisir une valeur valide pour le type de document. {{ choices }}'
)]
public ?string $documentType = null;
#[OA\Property(
description: 'La description d\'une photo, elle sera ignoré pour un document',
example: 'lorem ipsum dolor sit amet'
)]
#[Assert\Length(max: 255)]
public ?string $description = null;
}
Loading

0 comments on commit 5bef770

Please sign in to comment.