Upload de fichiers en Symfony : VichUploaderBundle vs natif
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/passwdpeut 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 #}
{# Banniere 1200px de large #}
 | imagine_filter('banner') }})
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.
