Upload de fichier en php avec le bundle Form

Durée :

Environnement de travail :

Pré-requis :

Pour s'assurer de la compréhension de cette unité pédagogique, il est nécessaire d'avoir acquis les notions suivantes :

  • Mise en place de l'environnement de développement avec Php8, Symfony, Apache ou Symfony serveur et MySql,

  • Concept de Composant de Symfony avec Composer,

  • Création d'un CRUD avec l'extension Form de Symfony,

  • Connaître PHP, Twig, HTML/CSS, Composer, Concept Route, FormBuilder, Controller et Doctrine.

Contexte

Dans ce cours, nous allons vous expliquer comment mettre en place un formulaire permettant de téléverser un fichier, le stocker sur le serveur et l'afficher.

Pour une meilleure compréhension, nous allons procéder de 2 manières différentes au sein du Framework Symfony.

Premièrement, nous verrons comment mettre en place un widget de téléchargement de fichier avec le composant « FileType » du formulaire de Symfony.

Dans la deuxième partie, nous ferons la même chose en utilisant le composant Vich Uploader.

Rappel

Pour présenter cette notion, nous travaillerons sur la gestion d'une liste de produits très simpliste comportant une image.

C'est pourquoi nous aurons besoin au minimum des 2 pages suivantes :

  • Un formulaire de création de produit avec comme propriétés : nom, prix et description,

  • Un listing des produits créés.

Objectifs

  • Apprendre à télécharger une image et de l'associer à une fiche produit,

  • Apprendre à utiliser le widget Form FileType,

  • Apprendre à sauvegarder le fichier sur le serveur,

  • Apprendre à afficher l'image associée à la fiche produit.

Contexte

Dans le contexte d'une pratique professionnelle, vous utiliserez la notion d'upload de fichier dans le cas où vous auriez besoin de donner la possibilité de présenter du contenu autre que du texte (image, pdf, etc.), de stocker les fichiers sur le serveur et en base de données.

Nous allons illustrer cette notion en rattachant une image à une fiche produit grâce au type de formulaire « FileType » du Bundle Form inclus dans Symfony, afin de donner un visuel plus attractif au produit. Le fichier sera stocké sur le serveur et son nom sera mis en base de données en tant que propriété du produit.

Pour commencer, nous allons donner la possibilité aux produits d'avoir comme nouvelle propriété le nom de la photo à téléverser. Nous mettrons également à jour le schéma de la base de données concernant la table Produit. Cette propriété doit être optionnelle.

Méthode

Dans la Classe entité « Produit », nous allons rajouter une propriété privée « nom_photo », ainsi que ses méthodes publiques accesseur/mutateur. Cette propriété sera mappée à l'ORM avec, comme type de champs, « string » et nullable en utilisant l'annotation : @ORM\Column(type="string", nullable=true).

ExempleCode

1
namespace App\Entity;
2
  use App\Repository\ProduitRepository;
3
  use Doctrine\ORM\Mapping as ORM;
4
  
5
  /**
6
   * @ORM\Entity(repositoryClass=ProduitRepository::class)
7
   */
8
  class Produit
9
  {
10
      // ..........................
11
      // Annotation de mapping ORM
12
      /**
13
       * @ORM\Column(type="string", nullable=true)
14
       */
15
      private $nom_photo;
16
  
17
      /**
18
       * @return string
19
       */
20
      public function getNomPhoto() : ?string
21
      {
22
          return $this->nom_photo;
23
      }
24
  
25
      /**
26
       * @param mixed $nom_photo
27
       */
28
      public function setNomPhoto(?string $nom_photo): void
29
      {
30
          $this->nom_photo = $nom_photo;
31
      }
32
  }

Méthode

L'ORM de doctrine nous donne la possibilité de mettre à jour le schéma de notre base de données grâce aux annotations de mapping et en exécutant la commande suivante :

1
> php bin/console doctrine:schema:update  --force

Maintenant que notre entité produit possède la propriété « nom de la photo », nous pouvons gérer l'affichage du formulaire de création de produit en y ajoutant le composant d'upload de fichier FileType.

Ce composant va donner la possibilité de téléverser un fichier depuis notre ordinateur dans le formulaire de création de produit. Il possède plusieurs options configurables telles que l'intitulé, les contraintes sur les types et la taille maximale des fichiers et si le champ est optionnel.

Complément

Méthode

Dans la méthode où vous avez construit votre formulaire, ajoutez à celui-ci un nouveau composant de type « FileType » possédant les propriétés suivantes :

  • Nom du composant : « photo »,

  • Type : Symfony\Component\Form\Extension\Core\Type\FileType,

  • Mapped : false (pour que Symfony n'essaye pas d'obtenir et/ou de définir sa valeur à partir de l'entité associée),

  • Required : false (car l'image est optionnelle).

Lorsque le composant n'est pas mappé, les contraintes doivent être spécifiées par un validateur spécifique « Symfony\Component\Validator\Constraints\File ». Ajoutons-lui les contraintes suivantes :

  • Taille maximum : 1 024 k,

  • mimeTypes : 'application/jpeg', 'application/png', 'image/jpeg',

  • mimeTypesMessage : veuillez télécharger un fichier image valide !

Remarque

Il arrive que la construction du formulaire soit faite dans l'action du « controller ». Cependant, il est préconisé d'utiliser une classe spécifique qui permette de définir le type de formulaire. Celle-ci doit étendre de la classe « AbstractType ».

Complément

ExempleCode

Cet exemple utilise la classe ProduitType, qui permet de construire le formulaire de type produit :

1
namespace App\Form\Type;
2
use App\Entity\Produit;
3
use Symfony\Component\Form\AbstractType;
4
// ...
5
use Symfony\Component\Form\FormBuilderInterface;
6
use Symfony\Component\OptionsResolver\OptionsResolver;
7
use Symfony\Component\Validator\Constraints\File;
8
use Symfony\Component\Form\Extension\Core\Type\FileType;    
9
10
class ProduitType extends AbstractType
11
{
12
    public function buildForm(FormBuilderInterface $builder, array $options): void
13
    {
14
        $builder
15
            // .............................
16
            ->add('photo', FileType::class, [
17
                'label' => 'Photo',
18
                'mapped' => false,
19
                // propriété permettant de rendre obligatoire ou non le poho
20
                'required' => false,
21
                // Lorsque le champ n'est pas mappé, les contraintes doivent être spécifiées par un validateur spécifique au type. Ici : use Symfony\Component\Validator\Constraints\File
22
                'constraints' => [
23
                    new File([
24
                        'maxSize' => '1024k',
25
                        'mimeTypes' => [
26
                            image/jpeg',
27
                            image/png',
28
                            'image/jpeg'
29
                        ],
30
                        'mimeTypesMessage' => 'Veuillez télécharger un fichier image valide!',
31
                    ])
32
                ],
33
            ])
34
        ;
35
}
36
}

Complément

Vous pouvez personnaliser l'affichage du composant d'upload de fichier depuis la vue du formulaire de création de produit dans le fichier twig.

Remarque

Chaque fichier possède un type MIME spécifique. Ici, nous avons spécifié des fichiers de type jpeg et png. Si nous voulions ajouter comme contrainte les fichiers de type PDF, il nous suffirait d'ajouter les types MIME suivants : 'application/pdf', 'application/x-pdf'.

Maintenant que le composant de téléversement de fichier est en place depuis notre formulaire de création de produit, nous pouvons nous attaquer à la validation des informations du produit, à l'enregistrement du fichier dans un dossier spécifique du serveur et à l'ajout du nom de la photo dans la table produit.

Méthode

Nous allons enregistrer le chemin dossier de stockage des fichiers téléversés dans les paramètres de l'application.

Dans le fichier config/services.yaml, ajoutons le paramètre suivant :

repertoire_photos_produits : '%kernel.project_dir%/public/uploads/photoproduit'

Remarque

Nous avons spécifié un sous-répertoire du dossier « public » pour donner l'accessibilité des fichiers depuis le site.

Conseil

Si le document est confidentiel, il serait préférable de le stocker dans un répertoire avec une gestion de droits spécifique.

ExempleCode

1
parameters:
2
        repertoire_photos_produits: '%kernel.project_dir%/public/uploads/photoproduit'

Méthode

Dans l'action du « controller », où nous avons implémenté la validation du formulaire, nous allons récupérer la donnée de la photo en récupérant le composant du « FileType » avec le nom qu'on lui avait spécifié à la construction (dans l'exemple « photo »).

Ce composant possède la méthode getData() qui retourne un objet de type « Symfony\Component\HttpFoundation\File\UploadedFile » si un fichier a été téléversé. Sinon, il retournera « null ».

Si le retour est un objet, nous pouvons enregistrer le fichier avec sa méthode « move() » qui prend en paramètre le répertoire de destination et le nom du fichier.

Enfin, s'il n'y a pas d'erreur, nous allons muter le nom de la photo du produit avec le nom du fichier.

N'oublions pas de persister l'objet produit et de le « flush » afin que les informations du produit soient enregistrées en base de données.

Conseil

Assurez-vous que le nom est unique pour éviter qu'un fichier existant avec le même nom soit écrasé et qu'il soit sans caractères spécifiques.

Les fonctions suivantes peuvent servir : « uniqid() » et « slug ».

ExempleCode

1
class ProduitController extends AbstractController
2
{  
3
   #[Route('/produit/nouveau', name: 'produit_nouveau')]
4
   public function new(Request $request, SluggerInterface $slugger): Response {
5
6
       $produit = new Produit();
7
       // Creation de la form
8
       $form = $this->createForm(ProduitType::class, $produit);
9
10
       $form->handleRequest($request);
11
       if ($form->isSubmitted() && $form->isValid()) {
12
13
 // Nous récupérons le fichier téléversé
14
           /** @var UploadedFile $fichierPhoto */
15
           $fichierPhoto = $form->get('photo')->getData();
16
17
           // On vérifie si la photo a été envoyée et est valide
18
           if ($fichierPhoto) {
19
               $nomPhotoOriginale = pathinfo($fichierPhoto->getClientOriginalName(), PATHINFO_FILENAME);
20
21
               // On va reformater le nom de la photo pour avoir un nom sans caractères spécifiques pour être conforme à une url lorsqu'on va vouloir récupérer le fichier depuis le site
22
               // On utilise l'interface de slugger par injection dans l'action
23
            	$nomPhotoReformate = $slugger->slug($nomPhotoOriginale);
24
25
               // On va créer un nom unique à cette photo avec le nom formaté et un identifiant unique généré par uniqid()
26
               $nomPhoto = $nomPhotoReformate . '-' . uniqid() . '.' . $fichierPhoto->getExtension();
27
28
               // On va déplacer la photo dans un répertoire spécifique sur le serveur
29
               try {
30
                   // La méthode move prend en compte le dossier de destination et le nom du fichier (d'où l'unicité pour ne pas gérer l'existante du nom)
31
                   // Nous allons mettre en paramètre le dossier de destination dans les configs
32
                   $fichierPhoto->move(
33
                       $this->getParameter('repertoire_photos_produits'),
34
                       $nomPhoto
35
                   );
36
               } catch (FileException $e) {
37
                   // C'est pour gérer l'exception d'une éventuelle erreur lors de l'enregistrement de la photo
38
                   throw $e;   
39
               }
40
41
               // Et on enregistre uniquement le nom de la photo car on a choisi que toutes les photos soient dans un même répertoire
42
               $produit->setNomPhoto($nomPhoto);
43
           }
44
45
           $produit = $form->getData();
46
47
           $entityManager = $this->getDoctrine()->getManager();
48
           $entityManager->persist($produit);
49
           $entityManager->flush();
50
51
           return $this->redirectToRoute('produit_edit', [
52
               'id' => $produit->getId()
53
           ]);
54
       }
55
56
       return $this->render('produit/new.html.twig', [
57
           'form' => $form->createView(),
58
       ]);
59
   }
60
   
61
   // ...............
62
}

Remarque

Remarques importantes à considérer dans le code du contrôleur ci-dessus :

Dans les applications Symfony, les fichiers téléchargés sont des objets de la classe Symfony\Component\HttpFoundation\File\UploadedFile. Cette classe fournit des méthodes pour les opérations les plus courantes lors du traitement des fichiers téléchargés. Une bonne pratique de sécurité bien connue consiste à ne jamais faire confiance aux informations fournies par les utilisateurs. Cela s'applique également aux fichiers téléchargés par vos visiteurs.

La classe UploadedFile fournit des méthodes pour obtenir l'extension de fichier d'origine (getExtension()), la taille de fichier d'origine (getSize()) et le nom de fichier d'origine (getClientOriginalName()). Cependant, ils ne sont pas considérés comme sûrs, car un utilisateur malveillant pourrait altérer ces informations. C'est pourquoi il est toujours préférable de générer un nom unique et d'utiliser la méthode guessExtension() pour laisser Symfony deviner la bonne extension en fonction du type de fichier MIME.

Complément

En plus de la classe d'exception Symfony\Component\HttpFoundation\File\Exception\FileException, il en existe d'autres :

Symfony\Component\HttpFoundation\File\Exception\CannotWriteFileException,

Symfony\Component\HttpFoundation\File\Exception\ExtensionFileException,

Symfony\Component\HttpFoundation\File\Exception\FormSizeFileException,

Symfony\Component\HttpFoundation\File\Exception\IniSizeFileException,

Symfony\Component\HttpFoundation\File\Exception\NoFileException,

Symfony\Component\HttpFoundation\File\Exception\NoTmpDirFileException, and

Symfony\Component\HttpFoundation\File\Exception\PartialFileException.

Notre fichier est stocké dans un dossier que nous avons spécifié et son nom est sauvegardé dans la base de données.

Il ne nous reste plus qu'à afficher la photo.

Méthode

Pour afficher la photo depuis la vue twig, nous pouvons utiliser la fonction asset() avec, comme paramètre, l'url relative au fichier depuis le répertoire « public ».

ExempleCode

1
<td>{% if produit.getNomPhoto() %}<img src="{{ asset('uploads/photoproduit/' ~ produit.getNomPhoto()) }}">{% endif %} {{ produit.getNomPhoto() }}</td>

Remarque

Tous les fichiers du dossier « public » de votre application sont accessibles depuis le site internet.

Complément