Aller au contenu principal

L'upload de fichiers dans un formulaire Symfony

Comment permettre à un utilisateur d'envoyer un fichier (image, document) via un formulaire Symfony

Notions théoriques

Le champ FileType

Vous avez déjà appris à créer des formulaires Symfony avec AbstractType et des champs comme TextType. Pour permettre l'envoi d'un fichier (une photo de profil, un document joint...), Symfony fournit un champ dédié : FileType.

use Symfony\Component\Form\Extension\Core\Type\FileType;

$builder->add('imageFile', FileType::class, [
'label' => 'Image du personnage',
'mapped' => false,
'required' => false,
]);
info

'mapped' => false signifie que ce champ n'est pas directement lié à une propriété de l'entité. C'est normal : un formulaire Symfony ne peut pas mapper automatiquement un fichier envoyé (un objet UploadedFile) vers une propriété censée contenir un simple nom de fichier en base de données. Le fichier sera traité manuellement dans le contrôleur.

Le formulaire HTML : enctype="multipart/form-data"

En PHP procédural, vous avez déjà vu qu'un formulaire HTML capable d'envoyer un fichier doit obligatoirement porter l'attribut :

<form method="post" enctype="multipart/form-data">

Sans cet attribut, le fichier n'est jamais transmis au serveur. Avec Symfony, si votre formulaire contient un champ FileType, le composant Form ajoute automatiquement cet attribut lors du rendu avec form_start(form). Vous n'avez donc rien à faire de spécial côté Twig : c'est la présence du champ FileType dans la classe FormType qui déclenche ce comportement.

Récupérer le fichier envoyé : UploadedFile

Dans le contrôleur, après avoir géré la soumission du formulaire, on récupère le fichier envoyé sous la forme d'un objet UploadedFile :

use Symfony\Component\HttpFoundation\File\UploadedFile;

/** @var UploadedFile $imageFile */
$imageFile = $form->get('imageFile')->getData();

Cet objet donne accès à des informations utiles : getClientOriginalName() (le nom du fichier sur l'ordinateur de l'utilisateur), getSize(), guessExtension(), etc.

Déplacer le fichier et générer un nom unique

Par défaut, un fichier envoyé est stocké dans un dossier temporaire du serveur et sera supprimé à la fin de la requête. Il faut donc le déplacer explicitement vers un dossier de destination, avec la méthode move() :

$newFilename = uniqid() . '.' . $imageFile->guessExtension();

$imageFile->move(
$this->getParameter('characters_directory'),
$newFilename
);
attention

Ne jamais réutiliser le nom original du fichier (getClientOriginalName()) comme nom de destination. Deux utilisateurs peuvent envoyer un fichier portant le même nom (par exemple photo.jpg), ce qui écraserait un fichier existant. Le nom original peut aussi contenir des caractères spéciaux dangereux ou permettre à un attaquant de deviner l'emplacement d'un autre fichier. On génère toujours un nom unique et imprévisible, par exemple avec uniqid().

Stocker le nom du fichier en base de données

Une fois le fichier déplacé, seul le nom du fichier (pas le fichier lui-même) est enregistré dans une propriété de l'entité, via le setter habituel :

$character->setImage($newFilename);

Cette propriété image peut ensuite être utilisée dans une vue Twig pour afficher l'image, en la combinant avec le chemin public du dossier de stockage.

Valider le fichier avec la contrainte File

Comme pour n'importe quelle propriété d'entité, il est possible de valider le fichier envoyé, par exemple pour n'accepter que des images de moins de 2 Mo :

use Symfony\Component\Validator\Constraints\File;

$builder->add('imageFile', FileType::class, [
'mapped' => false,
'required' => false,
'constraints' => [
new File([
'maxSize' => '2M',
'mimeTypes' => [
'image/jpeg',
'image/png',
],
'mimeTypesMessage' => 'Veuillez envoyer une image valide (JPEG ou PNG).',
])
],
]);
info

La contrainte File vérifie le type MIME réel du fichier (en inspectant son contenu), pas seulement son extension. Un fichier renommé .jpg mais contenant en réalité un script ne passera pas la validation mimeTypes.

Exemple de mise en application

Ajoutons la possibilité d'uploader une image pour un personnage. D'abord, ajoutons une propriété image à l'entité Character :

// src/Entity/Character.php
#[ORM\Column(type: "string", length: 255, nullable: true)]
private ?string $image = null;

public function getImage(): ?string
{
return $this->image;
}

public function setImage(?string $image): self
{
$this->image = $image;
return $this;
}

Ajoutons ensuite le champ imageFile non mappé au formulaire CharacterType :

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

use App\Entity\Character;
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;
use Symfony\Component\Validator\Constraints\File;

class CharacterType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('name', TextType::class)
->add('imageFile', FileType::class, [
'label' => 'Image du personnage',
'mapped' => false,
'required' => false,
'constraints' => [
new File([
'maxSize' => '2M',
'mimeTypes' => ['image/jpeg', 'image/png'],
'mimeTypesMessage' => 'Veuillez envoyer une image valide (JPEG ou PNG).',
])
],
]);
}

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

Enfin, traitons l'upload dans le GameController, après validation du formulaire :

use Symfony\Component\HttpFoundation\File\UploadedFile;

#[Route('/game/character/create', name: 'create_character')]
public function createCharacter(Request $request): Response
{
$character = new Character();
$form = $this->formFactory->create(CharacterType::class, $character);

$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
/** @var UploadedFile $imageFile */
$imageFile = $form->get('imageFile')->getData();

if ($imageFile) {
$newFilename = uniqid() . '.' . $imageFile->guessExtension();
$imageFile->move(
$this->getParameter('characters_directory'),
$newFilename
);
$character->setImage($newFilename);
}

$this->characterRepository->save($character, true);

return $this->redirectToRoute('game');
}

return $this->render('game/create_character.html.twig', ['form' => $form->createView()]);
}
astuce

Le paramètre characters_directory est défini dans config/services.yaml et pointe généralement vers un dossier public comme public/uploads/characters. Il est ensuite injecté automatiquement partout où $this->getParameter('characters_directory') est appelé.

Test de mémorisation/compréhension


Quel champ de formulaire Symfony permet d'envoyer un fichier ?


Quel attribut HTML doit obligatoirement porter un formulaire capable d'envoyer un fichier ?


Pourquoi met-on 'mapped' => false sur un champ FileType ?


Quel objet représente le fichier envoyé côté contrôleur, une fois récupéré depuis le formulaire ?


Quelle méthode permet de déplacer un fichier envoyé vers son dossier de destination ?


Pourquoi ne faut-il jamais utiliser le nom original du fichier comme nom de destination ?


Qu'enregistre-t-on dans la base de données après un upload de fichier ?


Quelle contrainte de validation Symfony permet de limiter la taille et le type MIME d'un fichier envoyé ?


TP pour réfléchir et résoudre des problèmes

Votre défi pour aujourd'hui consiste à ajouter l'upload d'une image de profil sur l'entité Player, en suivant le même principe que pour Character dans l'exemple ci-dessus.

Étape 1 — Ajouter le champ imageFile au formulaire PlayerType


Bonne pratique - Toujours rendre le champ imageFile non obligatoire

Ajouter 'required' => false sur un champ FileType utilisé pour une modification permet à l'utilisateur de modifier les autres champs du formulaire sans être obligé de renvoyer une nouvelle image à chaque fois.

Étape 2 — Récupérer le fichier envoyé dans le contrôleur

Étape 3 — Générer un nom unique, déplacer le fichier et enregistrer le nom en base


Bonne pratique - Ne déplacer le fichier que s'il a été envoyé

Le champ imageFile étant facultatif ('required' => false), il peut être null lorsque l'utilisateur ne souhaite pas changer son image. Toujours vérifier if ($imageFile) avant d'appeler move(), sinon une erreur sera levée sur un objet null.

Bonne pratique - Toujours valider le type et la taille du fichier

Sans contrainte File sur le champ imageFile, n'importe quel visiteur pourrait envoyer un fichier de plusieurs centaines de mégaoctets ou un fichier qui n'est pas réellement une image. Toujours limiter maxSize et mimeTypes sur un champ d'upload accessible depuis un formulaire public.

📌 Une solution