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

Temps de lecture : 25 minutes environ.

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

Live Coding : Upload d'images multiples avec Symfony 4 et 5
Article publié le - Modifié le

Catégories : Javascript PHP Tutoriel Symfony Live-Coding

Mots-clés : Tutoriel Javascript Ajax MySQL PHP Symfony Live-Coding images upload

Partager : Partager sur Facebook Partager sur Twitter Partager sur LinkedIn