Article Image
Jeudi 9 janvier 2025
L'upload d'une seule image est une fonctionnalité commune dans les projets web, mais elle doit être bien pensée pour garantir robustesse et simplicité.
Cet article se concentre sur une approche pratique pour ajouter cette capacité à vos entités Symfony grâce à API Platform et au VichUploaderBundle.

Vous apprendrez à implémenter le décodeur multipart et à gérer efficacement la désérialisation des données pour garantir des échanges structurés et adaptés. 
Des exemples précis accompagneront chaque étape pour vous guider dans la mise en œuvre.

Cycle de Transformation et Validation des Données dans une API Symfony

  1. Frontend : Envoie une requête POST avec des données au format JSON ou autre.
  2. Decoder : Le composant Serializer transforme les données brutes de la requête en tableau PHP.
  3. Denormalizer : Le Serializer crée une entité PHP à partir de ce tableau en mappant les propriétés.
  4. Validation : Le Validator de Symfony vérifie les contraintes définies sur l'entité.
  5. Normalizer : Le Serializer reconvertit l'entité PHP validée en tableau PHP pour la réponse.
  6. Encoder : Le Serializer encode ce tableau au format JSON, JSON-LD, ou autre, selon la requête.
  7. Frontend : Reçoit et traite la réponse formatée.

Prérequis

Avant de commencer, assurez-vous que les éléments suivants sont déjà en place dans votre projet :

Si ces prérequis sont remplis, nous pouvons maintenant explorer la mise en place de la fonctionnalité d'upload.

Installation de VichUploaderBundle

Nous aurons besoin du bundle VichUploader :

composer require vich/uploader-bundle

Configurer VichUploaderBundle et API Platform

On configure le VichUploaderBundle pour notre entité Products.

# api/config/packages/vich_uploader.yaml
vich_uploader:
  db_driver: orm
  metadata:
    type: attribute    
  mappings:
    products:
      uri_prefix: /images/products
      upload_destination: '%kernel.project_dir%/public/images/products'
      namer: Vich\UploaderBundle\Naming\SmartUniqueNamer

Puis API Platform en activant globalement le format Multipart.

#config/packages/api_platform.yaml
api_platform:
  formats:
    multipart: ['multipart/form-data']
    jsonld: ['application/ld+json']

Entité Products

Nous allons prendre l'exemple de l'ajout d'une image pour un produit.
L'entité Products devra posséder au moins les champs suivants :

Commençons par lui ajouter un attribut PHP #[Vich\Uploadable] pour indiquer que la classe Products est uploadable dans le contexte du VichUploaderBundle

use Vich\UploaderBundle\Mapping\Annotation as Vich;

#[Vich\Uploadable]
class Products
{ 
   // ...
}

Ensuite, nous aurons besoin de lui ajouter une autre propriété indispensable au bon fonctionnement du bundle :

Une propriété updatedAt est indispensable pour notifier Doctrine des modifications de fichier, déclencher les événements nécessaires et assurer une gestion synchronisée entre la base de données et le système de fichiers uploadés.

# src/Entity/Products.php
	//...
    use Symfony\Component\HttpFoundation\File\File;
    //...
class Products
{
  	//...
    #[Assert\File(
        maxSize: '5M',
        maxSizeMessage: 'Seules les images dont la taille est inférieure à 5 Mo sont acceptées.',
        mimeTypes: ['image/jpeg','image/png','image/webp'],
        mimeTypesMessage: 'Seules les images aux formats JPEG, PNG ou WEBP sont autorisées.'
    )]
    #[Vich\UploadableField(mapping: 'products', fileNameProperty: 'imageName')]
    private ?File $imageFile = null;

    #[ORM\Column(nullable: true)]
    private ?\DateTimeImmutable $updatedAt = null;
    
    //...
    
    public function setImageFile(?File $imageFile = null): void
    {
        $this->imageFile = $imageFile;

        if (null !== $imageFile) {
            $this->updatedAt = new \DateTimeImmutable();
        }
    }

    public function getImageFile(): ?File
    {
        return $this->imageFile;
    }

Nous pouvons effectuer la migration

symfony console make:migration
symfony console d:m:m

Création du Decoder et du Denormalizer

Le rôle de classe MultipartDecode.

La classe MultipartDecoder agit comme un pont pour extraire et combiner les paramètres et fichiers d'une requête multipart, en les renvoyant sous forme de tableau prêt à être utilisé dans le processus de désérialisation.

// src/Encoder/MultipartDecoder.php
namespace App\Encoder;

use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Serializer\Encoder\DecoderInterface;
use Symfony\Component\HttpFoundation\File\UploadedFile;

final class MultipartDecoder implements DecoderInterface
{
    public const FORMAT = 'multipart';

    public function __construct(private readonly RequestStack $requestStack)
    {
    }
    public function decode(string $data, string $format, array $context = []): ?array
    {
        $request = $this->requestStack->getCurrentRequest();

        if (!$request) return null;

        return $request->request->all() + $request->files->all();
    }

    public function supportsDecoding(string $format): bool
    {
        return self::FORMAT === $format;
    }
}

(Source doc API Platform)

Le rôle de la classe UploadedFileDenormalizer

La classe UploadedFileDenormalizer vérifie si les données à dénormaliser sont des fichiers, autrement dit des instances de la classe File. Si c'est le cas, elle les retourne telles quelles.

// src/Serializer/UploadedFileDenormalizer.php
namespace App\Serializer;

use Symfony\Component\HttpFoundation\File\File;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;

final class UploadedFileDenormalizer implements DenormalizerInterface
{
    public function denormalize($data, string $type, string $format = null, array $context = []): File
    {
        return $data;
    }

    public function supportsDenormalization($data, $type, $format = null, array $context = []): bool
    {
        return $data instanceof File;
    }

    public function getSupportedTypes(?string $format): array
    {
        return [File::class => true,];
    }
}

(Source doc API Platform)

Avant de lancer le serveur et d'envoyer notre requête, on va simplement ajouter un groupe de sérialisation aux propriétés pour définir quelles données seront incluses dans la réponse et surtout, ajouter le format multipart :

use Symfony\Component\Serializer\Attribute\Groups;
//...
#[Post(
    normalizationContext: ['groups' => ['products:read']],
)]
#[Vich\Uploadable]
class Products
{
    //...

    #[ORM\Column(length: 255)]
    #[Groups(['products:read'])]
    private ?string $name = null;

    #[ORM\Column(length: 255, nullable: true)]
    #[Groups(['products:read'])]
    private ?string $imageName = null;
//...

Lançons le serveur

symfony serve -d

Envoie d'une requête POST

On utilise un outil comme Postman ou Insomnia pour envoyer une requête POST à cette adresse :
http://localhost:8000/api/products

Et avec les paramètres suivants dans l'onglet body/form-data(multipart)

Key Value Type
name monProduit Text
imageFile [Votre fichier] File

Sans oublier de préciser le format de réponse attendu dans le Header

Key Value Type
Accept application/ld+json Text

Et voilà ! Nous avons bien reçu une réponse 201 du serveur, confirmant le succès de l'opération, avec le nom de l'image uploadée retourné en réponse.

Conclusion

Mettre en place un système d’upload d’une seule image avec Symfony, API Platform et le VichUploaderBundle est à la fois simple et efficace. Cette approche permet d’ajouter rapidement une fonctionnalité essentielle tout en garantissant une gestion performante et sécurisée des fichiers. En suivant les étapes décrites, il devient facile de répondre à des besoins courants sans complexité superflue, offrant ainsi une base solide pour évoluer vers des scénarios plus complexes à l’avenir.

Documentation officielle d’API Platform, de Symfony et du VichUploaderBundle.


Technologies :

PHP Symfony API Platform