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 ?