Bonjour tout le monde,

Je me permets de poster ici car je suis novice avec Symfony , du ce fait je suis en train de suivre les tutoriels symfony Symfony 4 par l'exemple > Gestion des photos.
Et je rencontre un petit souci lorsque je cherche à uploader des images avec le bundle VichUploaderBundle avec Symfony 5.2.3.

Avant de poster ici, j'ai essayé sans succès de chercher les choses suivantes :

Symfony 5 : Column 'image_name' cannot be null

ImageFile is null after upload

Ce que je fais

Comme indiqué dans le tutoriel, j'ai un formulaire qui permet d'éditer les informations relatives à une propriété (un bien) :
https://i.ibb.co/0GZqNTT/Capture.png
Formulaire que j'ai un peu restylé mais qui comporte exactement le même source que celui du tutoriel :

{{ form_start(form) }}

    <div class="row align-items-center">
        <div class="col-4">{{ form_row(form.title) }}</div>
        <div class="col-4">{{ form_row(form.price) }}</div>
        <div class="col-2 bg-light align-middle pt-3">{{ form_row(form.sold) }}</div>
    </div>

    <div class="row">
        <div class="col-md-8">
            {{ include("pages/property/_property_carousel.html.twig",{property: property }) }}
        </div>
        <div class="col-md-4">
            <div class="row ">
                <div class="col">
                    {{ form_row(form.pictureFiles) }}
                </div>

            </div>

            <div class="row">
                {% if property.picture %} 

                    {% for picture in property.pictures %}

                        <div class="col-6   mb-2 ">
                            <img class="w-100 " src="{{ vich_uploader_asset(picture, 'imageFile') | imagine_filter('thumb') }}" alt="{{ picture.filename }}">
                            <a href="{{ path('admin.picture.delete',{id:picture.id}) }}"  
                                class="btn btn-sm btn-danger p-1 w-100 mt-1" data-delete data-token="{{ csrf_token('delete' ~ picture.id) }}">Supprimer</a>
                        </div>
                    {% endfor %}

                {% endif %}

            </div>

            <div class="row">
                <div class="col">
                {{ form_row(form.rooms) }}
                </div>
                <div class="col">
                {{ form_row(form.surface) }}
                </div>
            </div>

            <div class="row">
                <div class="col">
                {{ form_row(form.bedrooms) }}
                </div>
                <div class="col">
                {{ form_row(form.floor) }}
                </div>
            </div> 

            <div class="row">
                <div class="col">
                {{ form_row(form.heat) }}
                </div>
                <div class="col">
                {{ form_row(form.options) }}
                </div>
            </div>

            <div class="row ">
                <div class="col h-50">
                    {{ form_row(form.description) }}
                </div>

            </div>

        </div>  
    </div>

    <div class="row">
        <div class="col">
            {{ form_row(form.address) }}
        </div>
        <div class="col">
            {{ form_row(form.city) }}
        </div>
        <div class="col-3">
            {{ form_row(form.postal_code) }}
        </div>
    </div>

    <div class="row justify-content-center">

    <button class="btn btn-primary w-100">{{ button |default('Eregistrer') }}</button>

        {{ form_rest(form) }}

    </div>
 {{ form_end(form) }}

Géré avec le PropertyType :

<?php

namespace App\Form;

use App\Entity\Option;
use App\Entity\Property;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class PropertyType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('title')
            ->add('description')
            ->add('surface')
            ->add('rooms')
            ->add('bedrooms')
            ->add('heat', ChoiceType::class, [
                'choices' => $this->getChoices()
            ])
            ->add('options', EntityType::class,[
                'class' => Option::class,
                'choice_label' => 'name',
                'multiple' => 'true',
                'required' => false
            ])
            ->add('price')
            ->add('pictureFiles',FileType::class,[
                'required' => false,
                'multiple' => true
            ])
            ->add('city')
            ->add('address')
            ->add('postal_code')
            ->add('sold')
            ->add('floor')
        ;
    }

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

    private function getChoices(): Array
    {
        $choices = Property::HEAT;
        $output = [];
        foreach($choices as $k => $v){
            $output[$v] = $k ;
        }
        return $output;
    }
}

Et j'ai une entité Property

<?php

namespace App\Entity;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Cocur\Slugify\Slugify;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;

/**
 * @ORM\Entity(repositoryClass="App\Repository\PropertyRepository")
 * @UniqueEntity("title")
 */
class Property
{

    const HEAT = [
        0 => 'Electrique',
        1 => 'Gaz'
    ];

    /**
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @Assert\Length(min=5, max=255)
     * @ORM\Column(type="string", length=255)
     */
    private $title;

    /**
     * @ORM\Column(type="text", nullable=true)
     */
    private $description;

    /**
     * @ORM\Column(type="integer")
     * @Assert\Range(min=10, max=400)
     */
    private $surface;

    /**
     * @ORM\Column(type="integer")
     */
    private $rooms;

    /**
     * @ORM\Column(type="integer")
     */
    private $bedrooms;

    /**
     * @ORM\Column(type="integer")
     */
    private $floor;

    /**
     * @ORM\Column(type="integer")
     */
    private $price;

    /**
     * @ORM\Column(type="integer")
     */
    private $heat;

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

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

    /**
     * @Assert\Regex("/^[0-9]{5}$/")
     * @ORM\Column(type="string", length=255)
     */
    private $postal_code;

    /**
     * @ORM\Column(type="boolean", options={"default": false})
     */
    private $sold = false;

    /**
     * @ORM\Column(type="datetime")
     */
    private $created_at;

    /**
     * @ORM\ManyToMany(targetEntity="App\Entity\Option", inversedBy="properties")
     */
    private $options;

    /**
     * @ORM\Column(type="datetime")
     */
    private $updated_at;

    /**
     * @ORM\OneToMany(targetEntity="App\Entity\Picture", mappedBy="property", orphanRemoval=true, cascade={"persist"})
     */
    private $pictures;

    /**
     * @Assert\All({
     *   @Assert\Image(mimeTypes="image/jpeg")
     * })
     */
    private $pictureFiles;

    public function __construct()
    {
        $this->created_at = new \DateTime();
        $this->options = new ArrayCollection();
        $this->pictures = 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 getSlug(): string
    {
        return (new Slugify())->slugify($this->title);
    }

    public function getDescription(): ?string
    {
        return $this->description;
    }

    public function setDescription(?string $description): self
    {
        $this->description = $description;

        return $this;
    }

    public function getSurface(): ?int
    {
        return $this->surface;
    }

    public function setSurface(int $surface): self
    {
        $this->surface = $surface;

        return $this;
    }

    public function getRooms(): ?int
    {
        return $this->rooms;
    }

    public function setRooms(int $rooms): self
    {
        $this->rooms = $rooms;

        return $this;
    }

    public function getBedrooms(): ?int
    {
        return $this->bedrooms;
    }

    public function setBedrooms(int $bedrooms): self
    {
        $this->bedrooms = $bedrooms;

        return $this;
    }

    public function getFloor(): ?int
    {
        return $this->floor;
    }

    public function setFloor(int $floor): self
    {
        $this->floor = $floor;

        return $this;
    }

    public function getPrice(): ?int
    {
        return $this->price;
    }

    public function setPrice(int $price): self
    {
        $this->price = $price;

        return $this;
    }

    public function getFormattedPrice(): string
    {
        return number_format($this->price, 0, '', ' ');
    }

    public function getHeat(): ?int
    {
        return $this->heat;
    }

    public function setHeat(int $heat): self
    {
        $this->heat = $heat;

        return $this;
    }

    public function getHeatType(): string
    {
        return self::HEAT[$this->heat];
    }

    public function getCity(): ?string
    {
        return $this->city;
    }

    public function setCity(string $city): self
    {
        $this->city = $city;

        return $this;
    }

    public function getAddress(): ?string
    {
        return $this->address;
    }

    public function setAddress(string $address): self
    {
        $this->address = $address;

        return $this;
    }

    public function getPostalCode(): ?string
    {
        return $this->postal_code;
    }

    public function setPostalCode(string $postal_code): self
    {
        $this->postal_code = $postal_code;

        return $this;
    }

    public function getSold(): ?bool
    {
        return $this->sold;
    }

    public function setSold(bool $sold): self
    {
        $this->sold = $sold;

        return $this;
    }

    public function getCreatedAt(): ?\DateTimeInterface
    {
        return $this->created_at;
    }

    public function setCreatedAt(\DateTimeInterface $created_at): self
    {
        $this->created_at = $created_at;

        return $this;
    }

    /**
     * @return Collection|Option[]
     */
    public function getOptions(): Collection
    {
        return $this->options;
    }

    public function addOption(Option $option): self
    {
        if (!$this->options->contains($option)) {
            $this->options[] = $option;
            $option->addProperty($this);
        }

        return $this;
    }

    public function removeOption(Option $option): self
    {
        if ($this->options->contains($option)) {
            $this->options->removeElement($option);
            $option->removeProperty($this);
        }

        return $this;
    }

    public function getUpdatedAt(): ?\DateTimeInterface
    {
        return $this->updated_at;
    }

    public function setUpdatedAt(\DateTimeInterface $updated_at): self
    {
        $this->updated_at = $updated_at;

        return $this;
    }

    /**
     * @return Collection|Picture[]
     */
    public function getPictures(): Collection
    {
        return $this->pictures;
    }

    public function getPicture(): ?Picture
    {
        if ($this->pictures->isEmpty()) {
            return null;
        }
        return $this->pictures->first();
    }

    public function addPicture(Picture $picture): self
    {
        if (!$this->pictures->contains($picture)) {
            $this->pictures[] = $picture;
            $picture->setProperty($this);
        }

        return $this;
    }

    public function removePicture(Picture $picture): self
    {
        if ($this->pictures->contains($picture)) {
            $this->pictures->removeElement($picture);
            // set the owning side to null (unless already changed)
            if ($picture->getProperty() === $this) {
                $picture->setProperty(null);
            }
        }

        return $this;
    }

    /**
     * @return mixed
     */
    public function getPictureFiles()
    {
        return $this->pictureFiles;
    }

    /**
     * @param mixed $pictureFiles
     * @return Property
     */
    public function setPictureFiles($pictureFiles): self
    {
        foreach($pictureFiles as $pictureFile) {
            $picture = new Picture();
            $picture->setImageFile($pictureFile);
            $this->addPicture($picture);
        }
        $this->pictureFiles = $pictureFiles;
        return $this;
    }

}

Ainsi qu'une entité Image qui se présente :

<?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(repositoryClass="App\Repository\PictureRepository")
 * @Vich\Uploadable()
 */
class Picture
{
    /**
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @var File|null
     * @Assert\Image(
     *     mimeTypes="image/jpeg"
     * )
     * @Vich\UploadableField(mapping="property_image", fileNameProperty="filename")
     */
    private $imageFile;

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

    /**
     * @ORM\ManyToOne(targetEntity="App\Entity\Property", inversedBy="pictures")
     * @ORM\JoinColumn(nullable=false)
     */
    private $property;

    public function __construct()
    {
        $this->created_at = new \DateTime();
    }

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

    public function getFilename(): ?string
    {
        return $this->filename;
    }

    public function setFilename(?string $filename): self
    {
        $this->filename = $filename;

        return $this;
    }

    public function getProperty(): ?Property
    {
        return $this->property;
    }

    public function setProperty(?Property $property): self
    {
        $this->property = $property;

        return $this;
    }

    /**
     * @return null|File
     */
    public function getImageFile(): ?File
    {
        return $this->imageFile;
    }

    /**
     * @param null|File $imageFile
     * @return self
     */
    public function setImageFile(?File $imageFile): self
    {
        $this->imageFile = $imageFile;
        return $this;
    }
}

Ce que je veux

Lorsque que je clique sur le bouton Editer / Enregistrer, je souhaiterai que la ou les Images soient rattachées à la Property, tout en ayant bien été enregistrées dans la base de données (avec un id(autogénéré), un filename, un property_id)

Ce que j'obtiens

Or lorsque je clique sur le bouton Editer/Enregistrer j'ai une erreur :

An exception occurred while executing 'INSERT INTO picture (filename, property_id) VALUES (?, ?)' with params [null, 5]:

Alors que quand je regarde les éléments envoyés :
https://i.ibb.co/GVTPPsn/Capture2.png

Je vois bien qu'un filename est présent dans l'Image (UploadedFile).

Pourriez vous m'aider ?

1 réponse


Salut,

J'aurai tendances à dire que tu devrais dans ton twig mettre
{{ picture.pictureFiles }}
pour enregistrer ton fichier vu que dans ton formulaire tu le nomme pictureFiles.

Mais là je ne vois pas ton controller donc je ne sais pas exactement comment tu enregistre.

Dans la base de donné tu n'enregistre que le nom du fichier.
Dans ton projet, dans un dossier "images" tu enregistre ton image, le type File.

Là je ne comprend pas pourquoi il y a un supprimer si tu essaie d'ajouter.

Pour ajouter, il te faut :
ENTITY
entité property one to many picture
enity picture many to one property
FORM
Ton formulaire propety, que le champs picture soit de type collection
crée un formulaire relié à ton entity pictures qui contiennent le type File.

Puis utiliser data-prototype de symfony https://symfony.com/doc/current/form/form_collections.html
Permet en gros d'ajouter dans ton formulaire property d'ajouter autant de picture que l'utiliateur souhaite, c'est fait en js mais c'est du copier coller car tout est dans la doc.

Je ne connais pas le bundle que tu as utilier mais c'est le meme principe.