Symfony11 min

Upload de fichiers en Symfony : VichUploaderBundle vs natif

Par Pierre-Arthur Demengel
SymfonyUploadVichUploaderFichiers

L'upload de fichiers est une fonctionnalite presente dans presque toute application web : photos de profil, documents PDF, pieces jointes, images produits. Symfony offre deux approches principales - l'upload natif avec FileType et un service dedie, ou VichUploaderBundle qui automatise une grande partie du travail. Voyons quand utiliser chaque approche, avec des exemples concrets en Symfony 7.2 et PHP 8.3.

L'upload natif en Symfony

Symfony fournit tout le necessaire pour gerer l'upload de fichiers sans bundle externe. Le composant Form inclut un type FileType et le composant HttpFoundation fournit la classe UploadedFile pour manipuler le fichier recu.

Configuration de l'entite

Commencons par une entite Document qui stocke le nom du fichier en base de donnees :

// src/Entity/Document.php
namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;

#[ORM\Entity(repositoryClass: DocumentRepository::class)]
class Document
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\Column(length: 255)]
    private ?string $title = null;

    #[ORM\Column(length: 255)]
    private ?string $filename = null;

    #[ORM\Column]
    private ?\DateTimeImmutable $uploadedAt = null;

    // Pas de mapping Doctrine - propriete temporaire
    #[Assert\File(
        maxSize: '5M',
        mimeTypes: ['application/pdf', 'image/jpeg', 'image/png'],
        mimeTypesMessage: 'Veuillez uploader un PDF, JPEG ou PNG.'
    )]
    private ?\Symfony\Component\HttpFoundation\File\UploadedFile $file = null;

    // Getters et setters...
    public function getFile(): ?\Symfony\Component\HttpFoundation\File\UploadedFile
    {
        return $this->file;
    }

    public function setFile(?\Symfony\Component\HttpFoundation\File\UploadedFile $file): static
    {
        $this->file = $file;
        return $this;
    }
}

Le formulaire avec FileType

// src/Form/DocumentType.php
namespace App\Form;

use App\Entity\Document;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class DocumentType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('title', TextType::class, [
                'label' => 'Titre du document',
            ])
            ->add('file', FileType::class, [
                'label' => 'Fichier (PDF, JPEG, PNG - max 5 Mo)',
                'mapped' => false, // pas lie directement a l'entite
                'required' => true,
            ]);
    }

    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            'data_class' => Document::class,
        ]);
    }
}

Le service d'upload

La bonne pratique est d'isoler la logique d'upload dans un service dedie plutot que de la mettre dans le controleur :

// src/Service/FileUploader.php
namespace App\Service;

use Symfony\Component\HttpFoundation\File\Exception\FileException;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\String\Slugger\SluggerInterface;

class FileUploader
{
    public function __construct(
        private readonly string $targetDirectory,
        private readonly SluggerInterface $slugger,
    ) {}

    public function upload(UploadedFile $file): string
    {
        $originalFilename = pathinfo(
            $file->getClientOriginalName(),
            PATHINFO_FILENAME
        );
        $safeFilename = $this->slugger->slug($originalFilename);
        $fileName = $safeFilename . '-' . uniqid() . '.' . $file->guessExtension();

        try {
            $file->move($this->targetDirectory, $fileName);
        } catch (FileException $e) {
            throw new \RuntimeException(
                'Erreur lors de l\'upload : ' . $e->getMessage()
            );
        }

        return $fileName;
    }
}

Enregistrez le service avec le parametre target_directory dans services.yaml :

# config/services.yaml
parameters:
    upload_directory: '%kernel.project_dir%/public/uploads'

services:
    App\Service\FileUploader:
        arguments:
            $targetDirectory: '%upload_directory%'

Le controleur

// src/Controller/DocumentController.php
#[Route('/document/new', name: 'document_new')]
public function new(
    Request $request,
    FileUploader $fileUploader,
    EntityManagerInterface $em,
): Response {
    $document = new Document();
    $form = $this->createForm(DocumentType::class, $document);
    $form->handleRequest($request);

    if ($form->isSubmitted() && $form->isValid()) {
        $file = $form->get('file')->getData();
        if ($file) {
            $filename = $fileUploader->upload($file);
            $document->setFilename($filename);
        }
        $document->setUploadedAt(new \DateTimeImmutable());
        $em->persist($document);
        $em->flush();

        return $this->redirectToRoute('document_show', [
            'id' => $document->getId(),
        ]);
    }

    return $this->render('document/new.html.twig', [
        'form' => $form,
    ]);
}

VichUploaderBundle : automatiser l'upload

L'approche native fonctionne bien, mais elle genere du code repetitif des qu'on a plusieurs entites avec des fichiers. VichUploaderBundle automatise le nommage, le stockage et la suppression des fichiers anciens lors d'un re-upload.

Installation et configuration

# Installation
composer require vich/uploader-bundle

# config/packages/vich_uploader.yaml
vich_uploader:
    db_driver: orm
    metadata:
        type: attribute
    mappings:
        documents:
            uri_prefix: /uploads/documents
            upload_destination: '%kernel.project_dir%/public/uploads/documents'
            namer: Vich\UploaderBundle\Naming\UniqidNamer
            inject_on_load: false
            delete_on_update: true
            delete_on_remove: true

Configuration de l'entite avec VichUploader

// src/Entity/Document.php
namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\HttpFoundation\File\File;
use Symfony\Component\Validator\Constraints as Assert;
use Vich\UploaderBundle\Mapping\Annotation as Vich;

#[ORM\Entity]
#[Vich\Uploadable]
class Document
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\Column(length: 255)]
    private ?string $title = null;

    #[Vich\UploadableField(
        mapping: 'documents',
        fileNameProperty: 'filename',
        size: 'fileSize'
    )]
    #[Assert\File(maxSize: '5M', mimeTypes: ['application/pdf', 'image/jpeg', 'image/png'])]
    private ?File $documentFile = null;

    #[ORM\Column(length: 255, nullable: true)]
    private ?string $filename = null;

    #[ORM\Column(nullable: true)]
    private ?int $fileSize = null;

    #[ORM\Column(nullable: true)]
    private ?\DateTimeImmutable $updatedAt = null;

    public function setDocumentFile(?File $file = null): void
    {
        $this->documentFile = $file;
        if ($file !== null) {
            // Doctrine detecte le changement grace a updatedAt
            $this->updatedAt = new \DateTimeImmutable();
        }
    }

    public function getDocumentFile(): ?File
    {
        return $this->documentFile;
    }

    // Getters/setters pour filename, fileSize, etc.
}

Le formulaire devient plus simple - il suffit d'utiliser VichFileType :

use Vich\UploaderBundle\Form\Type\VichFileType;

$builder->add('documentFile', VichFileType::class, [
    'label' => 'Fichier',
    'required' => false,
    'allow_delete' => true,
    'download_uri' => true,
]);

Stockage sur S3 avec Flysystem

Pour les projets en production, stocker les fichiers sur le serveur local est rarement une bonne idee. Les conteneurs Docker sont ephemeres, les serveurs peuvent tomber, et le scaling horizontal necessite un stockage partage.

Flysystem (via league/flysystem-bundle) abstrait le systeme de fichiers. VichUploaderBundle s'integre nativement avec :

# composer require league/flysystem-bundle league/flysystem-aws-s3-v3

# config/packages/flysystem.yaml
flysystem:
    storages:
        default.storage:
            adapter: 'aws'
            options:
                client: 's3_client'
                bucket: '%env(AWS_S3_BUCKET)%'
                prefix: 'uploads'

# config/packages/vich_uploader.yaml
vich_uploader:
    mappings:
        documents:
            uri_prefix: /uploads/documents
            upload_destination: default.storage
            namer: Vich\UploaderBundle\Naming\UniqidNamer
            storage: flysystem

Le changement est transparent : votre code applicatif ne change pas d'une ligne. Seule la configuration evolue.

Validation et securite

L'upload de fichiers est un vecteur d'attaque classique. Voici les points critiques a verifier :

  • Validation MIME type serveur : ne faites jamais confiance au type MIME envoye par le navigateur. Utilisez $file->guessExtension() qui analyse les magic bytes du fichier.
  • Renommage systematique : ne conservez jamais le nom original du fichier. Un nom comme ../../etc/passwd peut exploiter une faille de traversee de chemin.
  • Taille maximale : validez la taille dans Symfony ET dans php.ini (upload_max_filesize, post_max_size).
  • Stockage hors webroot : si les fichiers sont sensibles, stockez-les hors de public/ et servez-les via un controleur avec controle d'acces.
  • Scan antivirus : pour les fichiers critiques, utilisez ClamAV ou un service tiers pour scanner les uploads.
// Servir un fichier prive avec controle d'acces
#[Route('/document/{id}/download', name: 'document_download')]
#[IsGranted('ROLE_USER')]
public function download(
    Document $document,
    string $privateUploadDir,
): BinaryFileResponse {
    $filePath = $privateUploadDir . '/' . $document->getFilename();

    if (!file_exists($filePath)) {
        throw $this->createNotFoundException('Fichier introuvable.');
    }

    return $this->file($filePath, $document->getTitle() . '.pdf');
}

Miniatures avec LiipImagineBundle

Pour les images, vous aurez souvent besoin de miniatures. LiipImagineBundle genere des versions redimensionnees a la volee :

# config/packages/liip_imagine.yaml
liip_imagine:
    resolvers:
        default:
            web_path: ~
    filter_sets:
        thumbnail:
            quality: 80
            filters:
                thumbnail:
                    size: [300, 300]
                    mode: outbound
        banner:
            quality: 85
            filters:
                relative_resize:
                    widen: 1200

En Twig, appliquez le filtre sur l'URL de l'image :

{# Miniature 300x300 #}
{{ document.title }}

{# Banniere 1200px de large #}
{{ document.title }}

Natif vs VichUploader : quand choisir quoi ?

Utilisez l'upload natif si vous avez une seule entite avec un seul champ fichier, ou si vous voulez un controle total sur la logique de stockage. Le code est simple, sans dependance externe.

Utilisez VichUploaderBundle des que vous avez plusieurs entites avec des fichiers, ou si vous devez gerer la suppression automatique, le re-upload, et le nommage unique. Le gain de productivite est significatif sur un projet moyen a grand.

Pour approfondir la creation de formulaires Symfony, consultez le guide ultime des FormType. Pour les contraintes de validation, reportez-vous a l'article sur le Validator. Et si la securisation de l'upload touche a l'authentification, le guide Security couvre le sujet.

Besoin d'aide pour implementer un systeme d'upload robuste dans votre application Symfony ? Contactez-moi pour un accompagnement technique, ou consultez mes tarifs et mes prestations.

Questions fréquentes

13 projets livrésGrand-Est & BelgiqueLighthouse >90Disponible immédiatement

Un projet en tête ?

Discutons de votre site web. Réponse garantie sous 24h.

Ou appelez directement :06 95 41 30 25

WhatsApp
Appeler