Live Coding : Upload d'images multiples avec Symfony 4 et 5

25 avril 2020 - : Javascript PHP Tutoriel Symfony Live-Coding - : 3 commentaires - Tutoriel Javascript Ajax MySQL PHP Symfony Live-Coding

Dernière modification le 25 avril 2020

Dans ce Live Coding, nous traitons un sujet très demandé, comment gérer plusieurs images au sein de notre projet, sans bundle, lorsque ces images sont liées à la même entité ?

Nous prendrons l'exemple d'un site d'annonces dans lequel l'utilisateur a la possibilité d'attacher plusieurs images à son annonce.

ATTENTION : dans la vidéo, Bootstrap a été utilisé pour avoir une esthétique plus élaborée au niveau des formulaires, ce n'est pas décrit ci-dessous.

La base de données

Pour commencer, une base de données spécifique à cet exemple, qui ne contiendra "que" deux tables.

Bien sûr, il faudra adapter cet exemple à votre base.

Nous aurons donc une table "annonces" et une table "images" liées par une relation "un à plusieurs" comme ci-dessous

Nous allons donc créer les entités "Annonces" et "Images" sur ce modèle.

Pour ce faire, nous utiliserons la commande suivante à deux reprises

php bin/console make:entity

Nos entités seront les suivantes

<?php

namespace App\Entity;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity(repositoryClass="App\Repository\AnnoncesRepository")
 */
class Annonces
{
    /**
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\Column(type="string", length=255)
     */
    private $title;

    /**
     * @ORM\Column(type="text")
     */
    private $content;

    /**
     * @ORM\OneToMany(targetEntity="App\Entity\Images", mappedBy="annonces",cascade={"persist"})
     */
    private $images;

    public function __construct()
    {
        $this->images = new ArrayCollection();
    }

    public function getId(): ?int
    {
        return $this->id;
    }

    public function getTitle(): ?string
    {
        return $this->title;
    }

    public function setTitle(string $title): self
    {
        $this->title = $title;

        return $this;
    }

    public function getContent(): ?string
    {
        return $this->content;
    }

    public function setContent(string $content): self
    {
        $this->content = $content;

        return $this;
    }

    /**
     * @return Collection|Images[]
     */
    public function getImages(): Collection
    {
        return $this->images;
    }

    public function addImage(Images $image): self
    {
        if (!$this->images->contains($image)) {
            $this->images[] = $image;
            $image->setAnnonces($this);
        }

        return $this;
    }

    public function removeImage(Images $image): self
    {
        if ($this->images->contains($image)) {
            $this->images->removeElement($image);
            // set the owning side to null (unless already changed)
            if ($image->getAnnonces() === $this) {
                $image->setAnnonces(null);
            }
        }

        return $this;
    }
}

et

<?php

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity(repositoryClass="App\Repository\ImagesRepository")
 */
class Images
{
    /**
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\Column(type="string", length=255)
     */
    private $name;

    /**
     * @ORM\ManyToOne(targetEntity="App\Entity\Annonces", inversedBy="images")
     */
    private $annonces;

    public function getId(): ?int
    {
        return $this->id;
    }

    public function getName(): ?string
    {
        return $this->name;
    }

    public function setName(string $name): self
    {
        $this->name = $name;

        return $this;
    }

    public function getAnnonces(): ?Annonces
    {
        return $this->annonces;
    }

    public function setAnnonces(?Annonces $annonces): self
    {
        $this->annonces = $annonces;

        return $this;
    }
}

Une fois les entités créées, n'oublions pas d'exécuter les deux lignes suivantes pour créer les tables dans la base de données

php bin/console make:migration
php bin/console doctrine:migration:migrate

Le contrôleur

Nous allons créer un contrôleur "AnnoncesController" pour gérer les pages d'affichage, ajout et modification des annonces.

La commande suivante nous fera gagner du temps pour ce live coding mais vous pouvez le créer "à la main"

php bin/console make:crud

Cette commande a créé le contrôleur et toutes les méthodes nécessaires à la mise en place des fonctionnalités souhaitées.

Le formulaire

Dans les formulaires d'ajout et de modification, la commande "make:crud" ne met pas en place la gestion des images.

Nous allons donc devoir modifier le formulaire créé par défaut pour y ajouter les images.

Le formulaire d'ajout d'annonces est le fichier "src/Form/AnnoncesType.php"

Nous modifierons donc ce fichier comme suit

<?php

namespace App\Form;

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

class AnnoncesType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('title')
            ->add('content')
            // On ajoute le champ "images" dans le formulaire
            // Il n'est pas lié à la base de données (mapped à false)
            ->add('images', FileType::class,[
                'label' => false,
                'multiple' => true,
                'mapped' => false,
                'required' => false
            ])
        ;
    }

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

Les méthodes du contrôleur

Dans le contrôleur, nous allons modifier plusieurs choses :

  • Gestion des images ajoutées lors de la création d'une annonce
  • Gestion des images ajoutées lors de la modification d'une annonce
  • Suppression des images depuis le formulaire de modification

Avant toute chose nous allons devoir copier les images dans un dossier physique situé dans le dossier "public"

Nous allons modifier le fichier "services.yaml" et la section "parameters" en y inscrivant le code ci-dessous

parameters:
    images_directory: '%kernel.project_dir%/public/uploads'

Dans les méthodes "new" et "edit" du contrôleur, nous allons récupérer les données d'images, boucler dessus (ajout multiple possible) et les enregistrer sur le disque ainsi qu'en base de données

if ($form->isSubmitted() && $form->isValid()) {
    // On récupère les images transmises
    $images = $form->get('images')->getData();
    
    // On boucle sur les images
    foreach($images as $image){
        // On génère un nouveau nom de fichier
        $fichier = md5(uniqid()).'.'.$image->guessExtension();
        
        // On copie le fichier dans le dossier uploads
        $image->move(
            $this->getParameter('images_directory'),
            $fichier
        );
        
        // On crée l'image dans la base de données
        $img = new Images();
        $img->setName($fichier);
        $annonce->addImage($img);
    }

    $entityManager = $this->getDoctrine()->getManager();
    $entityManager->persist($annonce);
    $entityManager->flush();

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

A ce stade, nous pouvons ajouter des images lors de la création et la modification d'annonces mais nous ne pouvons pas les supprimer.

Nous allons donc créer une méthode "deleteImage" qui nous permettra de supprimer une image.

Cette méthode sera appelée en Ajax en utilisant la méthode "DELETE" comme ceci

/**
 * @Route("/supprime/image/{id}", name="annonces_delete_image", methods={"DELETE"})
 */
public function deleteImage(Images $image, Request $request){
    $data = json_decode($request->getContent(), true);

    // On vérifie si le token est valide
    if($this->isCsrfTokenValid('delete'.$image->getId(), $data['_token'])){
        // On récupère le nom de l'image
        $nom = $image->getName();
        // On supprime le fichier
        unlink($this->getParameter('images_directory').'/'.$nom);

        // On supprime l'entrée de la base
        $em = $this->getDoctrine()->getManager();
        $em->remove($image);
        $em->flush();

        // On répond en json
        return new JsonResponse(['success' => 1]);
    }else{
        return new JsonResponse(['error' => 'Token Invalide'], 400);
    }
}

Les fichiers Twig

Les fichiers Twig qui gèrent les formulaires devront être légèrement modifiés pour ajouter la gestion des images, principalement lors de la modification des annonces.

Les fichiers sur lequel nous interviendrons sont "_form.html.twig" et "edit.html.twig" du dossier "templates/annonces"

Dans le fichier "_form.html.twig" nous allons afficher les images existantes si nous sommes sur la route "annonces_edit" comme ceci

{{ form_start(form) }}
    {{ form_widget(form) }}

    {# Si la route est "annonces_edit on affiche les images #}
	{% if app.request.attributes.get('_route') == 'annonces_edit' %}
        <h2>Images</h2>
        {% for image in annonce.images %}
            <div>
                <img src="{{ asset('/uploads/'~image.name) }}" alt="" width="150">

                {# On ajoute un lien permettant de supprimer une image (sera géré en Ajax) #}
                <a href="{{ path('annonces_delete_image', {id: image.id})}}" data-delete data-token="{{ csrf_token('delete' ~ image.id )}}">Supprimer</a>
            </div>
        {% endfor %}
    {% endif %}
    <button class="btn">{{ button_label|default('Save') }}</button>
{{ form_end(form) }}

Dans le fichier "edit", nous allons ajouter une balise "script" qui chargera le fichier "images.js" qui sera créé à la partie suivante

{% block javascripts %}
    <script src="{{ asset('js/images.js') }}"></script>
{% endblock %}

Le Javascript

Nous allons ajouter du javascript permettant de gérer la suppression des images lors du clic sur le lien "supprimer" à côté des images.

Ce fichier contiendra le code suivant

window.onload = () => {
    // Gestion des boutons "Supprimer"
    let links = document.querySelectorAll("[data-delete]")
    
    // On boucle sur links
    for(link of links){
        // On écoute le clic
        link.addEventListener("click", function(e){
            // On empêche la navigation
            e.preventDefault()

            // On demande confirmation
            if(confirm("Voulez-vous supprimer cette image ?")){
                // On envoie une requête Ajax vers le href du lien avec la méthode DELETE
                fetch(this.getAttribute("href"), {
                    method: "DELETE",
                    headers: {
                        "X-Requested-With": "XMLHttpRequest",
                        "Content-Type": "application/json"
                    },
                    body: JSON.stringify({"_token": this.dataset.token})
                }).then(
                    // On récupère la réponse en JSON
                    response => response.json()
                ).then(data => {
                    if(data.success)
                        this.parentElement.remove()
                    else
                        alert(data.error)
                }).catch(e => alert(e))
            }
        })
    }
}

Obtenir de l'aide

Pour obtenir de l'aide, vous pouvez accéder au serveur Discord pour une entraide par chat

Partager

Partager sur Facebook Partager sur Twitter Partager sur LinkedIn

Commentaires

Ecrire un commentaire

superhit a écrit le 7 juillet 2020 à 12:53

bonjour je travaille avec api platform j'aimerais voir un tutoriel sur l'upload multiple avec api platform

Répondre

Nouvelle-Techno.fr a répondu le 8 juillet 2020 à 09:27

Bonjour,

Malheureusement je n'utilise pas API Platform.

Répondre

khaledbou a écrit le 2 juin 2020 à 15:50

pouvez vous voir l'upload multiple avec api platform ? 

Répondre

Ecrire un commentaire