1e783bd753
Auto Tag Develop / tag (push) Successful in 8s
Infra d'upload de fichiers générique et réutilisable dans `Shared` (spec M4 § 2.7). Ne touche pas au module Transport.
## Livré
- **Table `uploaded_document`** (migration racine `DoctrineMigrations`) : fichier téléversé immuable (PDF / images) — `original_filename`, `stored_path`, `mime_type`, `size_bytes`, `checksum` (sha256), `created_at`, `created_by`. COMMENT ON COLUMN sur toutes les colonnes + bloc dans `ColumnCommentsCatalog`.
- **Service `Shared\Infrastructure\Upload\FileUploader`** : validation MIME server-side via `getMimeType()` (jamais `getClientMimeType()`), whitelist explicite (PDF + images), bornage taille (10 Mo), checksum sha256, écriture disque `var/uploads/{yyyy}/{mm}/`.
- **Endpoint `POST /api/uploaded_documents`** (multipart, `deserialize:false`) + `UploadedDocumentProcessor` -> renvoie l'IRI ; MIME hors whitelist -> 422.
- Wiring : mapping Doctrine `Shared` + path API Platform `Shared`.
## Tests
- `FileUploaderTest` (unitaire) + `UploadedDocumentApiTest` (fonctionnel : 201/IRI/checksum, 422 MIME interdit, 422 sans fichier, 401 anonyme).
`make test` vert (701 tests), `php-cs-fixer` propre.
## Hors scope
Pas d'antivirus / S3 / purge (§ 9). Pas de `carrier.discharge_document_id` (ticket consommateur M4).
Ticket ERP-154.
---------
Co-authored-by: Matthieu <contact@malio.fr>
Reviewed-on: #108
82 lines
3.2 KiB
PHP
82 lines
3.2 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Shared\Infrastructure\ApiPlatform\State;
|
|
|
|
use ApiPlatform\Metadata\Operation;
|
|
use ApiPlatform\State\ProcessorInterface;
|
|
use App\Shared\Domain\Exception\FileUploadException;
|
|
use App\Shared\Infrastructure\Upload\FileUploader;
|
|
use Symfony\Bundle\SecurityBundle\Security;
|
|
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
|
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
|
use Symfony\Component\HttpFoundation\RequestStack;
|
|
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
|
|
use Symfony\Component\Security\Core\User\UserInterface;
|
|
use Throwable;
|
|
|
|
/**
|
|
* Processor d'ecriture de l'upload generique (POST /api/uploaded_documents).
|
|
*
|
|
* L'operation Post est en `deserialize: false` : le binaire n'est pas mappe sur
|
|
* l'entite. Ce processor lit le fichier multipart de la requete (champ « file »),
|
|
* delegue au FileUploader (validation MIME server-side, bornage taille, checksum,
|
|
* ecriture disque), positionne l'auteur (created_by) puis persiste via le
|
|
* processor Doctrine standard. Le retour est l'entite, qu'API Platform serialise
|
|
* en JSON-LD (avec son @id / IRI).
|
|
*
|
|
* Mapping des erreurs :
|
|
* - fichier absent -> 422 ;
|
|
* - MIME hors whitelist / fichier trop volumineux (FileUploadException) -> 422.
|
|
*
|
|
* Si la persistance Doctrine echoue APRES l'ecriture disque, le fichier physique
|
|
* deja deplace est supprime (compensation) pour ne pas laisser de binaire orphelin.
|
|
*
|
|
* @implements ProcessorInterface<mixed, mixed>
|
|
*/
|
|
final class UploadedDocumentProcessor implements ProcessorInterface
|
|
{
|
|
public function __construct(
|
|
#[Autowire(service: 'api_platform.doctrine.orm.state.persist_processor')]
|
|
private readonly ProcessorInterface $persistProcessor,
|
|
private readonly FileUploader $fileUploader,
|
|
private readonly RequestStack $requestStack,
|
|
private readonly Security $security,
|
|
) {}
|
|
|
|
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): mixed
|
|
{
|
|
$request = $this->requestStack->getCurrentRequest();
|
|
$file = $request?->files->get('file');
|
|
|
|
if (!$file instanceof UploadedFile) {
|
|
throw new UnprocessableEntityHttpException('Aucun fichier fourni (champ « file » attendu).');
|
|
}
|
|
|
|
try {
|
|
$document = $this->fileUploader->upload($file);
|
|
} catch (FileUploadException $e) {
|
|
// MIME hors whitelist ou fichier trop volumineux -> 422 avec le
|
|
// message metier explicite porte par l'exception.
|
|
throw new UnprocessableEntityHttpException($e->getMessage(), $e);
|
|
}
|
|
|
|
$user = $this->security->getUser();
|
|
if ($user instanceof UserInterface) {
|
|
$document->setCreatedBy($user);
|
|
}
|
|
|
|
try {
|
|
return $this->persistProcessor->process($document, $operation, $uriVariables, $context);
|
|
} catch (Throwable $e) {
|
|
// La persistance a echoue APRES l'ecriture disque (erreur DB, FK...) :
|
|
// on supprime le fichier orphelin pour ne pas le laisser sans ligne
|
|
// uploaded_document correspondante, puis on relaie l'erreur.
|
|
$this->fileUploader->remove($document);
|
|
|
|
throw $e;
|
|
}
|
|
}
|
|
}
|