3

I had a perfectly working multi files upload. Upload one file at a time with one "browse" button. It's basically a Places entity that can have Many images.

I am trying to modify it to upload all files at once through only one "browse" window. Select multiple files with Ctrl / shift.

So the first inside that I got is that VichUploader (VichFileType::class) doesn't support multiple upload so only one option that I find so far is to change VichFileType::class to FileType::class in my AttachmentType.php and add in options ['multiple' => true] so now I have in my admin panel field with possibility to choose many files at once.This is exactly what i need. But after I chose all needed files and clicked Create to create a new place I got error: Return value of Vich\UploaderBundle\Mapping\PropertyMapping::getFile() must be an instance of Symfony\Component\HttpFoundation\File\File or null, array returned. It seems VichUploader waits only one file not array, so I modified my Images entity.

Before:

   /**
    * @param mixed $imageFile
    */
   public function setImageFile($imageFile): void {
       $this->imageFile = $imageFile;

       if ($imageFile) {
           $this->updatedAt = new \DateTime();
       }
   }

After:

   /**
     * @param mixed $imageFile
     */
    public function setImageFile($imageFile): void {
        foreach ($imageFile as $file) {
            $this->imageFile = $file;
            if ($imageFile) {
                $this->updatedAt = new \DateTime();
            }
        }
    }

After that, the error disappeared, but the problem is that if I add more than one picture, then only the last one from the array is added.

Full code: Places.php

   /**
     * @ORM\OneToMany(targetEntity=Images::class, mappedBy="place", cascade={"persist", "remove"})
     */
    private $images;
    
     public function __construct()
    {
        $this->images = new ArrayCollection();
    }
    
     /**
     * @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->setPlace($this);
        }

        return $this;
    }

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

        return $this;
    }

Images.php

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

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

    /**
     * @Vich\UploadableField(mapping="attachments", fileNameProperty="title")
     */
    private $imageFile;

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

    /**
     * @ORM\ManyToOne(targetEntity=Places::class, inversedBy="images")
     */
    private $place;

    /**
     * @ORM\ManyToOne(targetEntity=Regions::class, inversedBy="image")
     */
    private $region;




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

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

        return $this;
    }

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

        return $this;
    }

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

    /**
     * @param mixed $imageFile
     */
    public function setImageFile($imageFile): void {
        foreach ($imageFile as $file) {
            $this->imageFile = $file;
            if ($imageFile) {
                $this->updatedAt = new \DateTime();
            }
        }
    }

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

    public function getPlace(): ?Places
    {
        return $this->place;
    }

    public function setPlace(?Places $place): self
    {
        $this->place = $place;

        return $this;
    }

    public function getRegion(): ?Regions
    {
        return $this->region;
    }

    public function setRegion(?Regions $region): self
    {
        $this->region = $region;

        return $this;
    }
}
    

AttachmentType.php

class AttachmentType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('imageFile', FileType::class, [
                'multiple' => true
            ])
            ->add('updatedAt')
            ->add('place')
        ;
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'data_class' => Images::class,
        ]);
    }
}
laneboyandrew
  • 290
  • 6
  • 17
  • Hi, @laneboyandrew! I recommend you to start a bounty for 50 reputation points. It might attract knowledgeable users. – Rikijs Aug 25 '21 at 13:22

2 Answers2

2

I was able to get multiple images for a Gallery to upload with EasyAdmin3 by creating my own Field class.

Here is an example of how I achieved this...

<?php

namespace App\Controller\Admin\Fields;

use EasyCorp\Bundle\EasyAdminBundle\Contracts\Field\FieldInterface;
use EasyCorp\Bundle\EasyAdminBundle\Field\FieldTrait;
use Symfony\Component\Form\Extension\Core\Type\FileType;

class MultipleImageField implements FieldInterface
{
    use FieldTrait;

    public static function new(string $propertyName, ?string $label = null): self
    {
        return (new self())
            ->setProperty($propertyName)
            ->setFormType(FileType::class)
            ->setFormTypeOptions([
                'multiple' => true,
                'data_class' => null,
            ]);
    }
}

In my Gallery entity I have 2 fields to handle the imageFile and image (which just stores the filename) and an upload() function to handle the upload itself.

    const UPLOAD_IMAGE_DIRECTORY = 'uploads/images';

    /**
     * @ORM\Column(type="string", length=255)
     * @Assert\Regex(pattern="/[\/.](gif|jpg|jpeg|tiff|png)$/i",  message="Please upload a valid image")
     */
    private string $image;


    /**
     * Unmapped property to handle file uploads
     */
    private $imageFile;
    
    public function getImage(): ?string
    {
        return $this->image;
    }

    public function setImage(string $image): self
    {
        $this->image = $image;

        return $this;
    }

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

    /**
     * @param mixed $imageFile
     */
    public function setImageFile($imageFile): void
    {
        $this->imageFile = $imageFile;
    }

    public function upload($file)
    {
        if(null === $file){
            return;
        }

        $file->move(
            self::UPLOAD_IMAGE_DIRECTORY,
            $file->getClientOriginalName()
        );

        $this->setImage($file->getClientOriginalName());

        $this->setImageFile(null);
    }

You can probably do a better job at asserting the file type here..

I then added both fields to my EasyAdmin CRUDController, one to show the actual images on index/detail and one for the form, as well as updateEntity and persistEntity functions.

<?php

namespace App\Controller\Admin;

use App\Controller\Admin\Fields\MultipleImageField;
use App\Entity\Gallery;
use Doctrine\ORM\EntityManagerInterface;
use EasyCorp\Bundle\EasyAdminBundle\Config\Action;
use EasyCorp\Bundle\EasyAdminBundle\Config\Actions;
use EasyCorp\Bundle\EasyAdminBundle\Config\Crud;
use EasyCorp\Bundle\EasyAdminBundle\Config\Filters;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
use EasyCorp\Bundle\EasyAdminBundle\Field\AssociationField;
use EasyCorp\Bundle\EasyAdminBundle\Field\ImageField;

class GalleryCrudController extends AbstractCrudController
{
    public static function getEntityFqcn(): string
    {
        return Gallery::class;
    }

    public function configureFilters(Filters $filters): Filters
    {
        return $filters
            ->add('category')
        ;
    }

    public function configureActions(Actions $actions): Actions
    {
        return $actions
            ->add(Crud::PAGE_INDEX, Action::DETAIL)
            ->add(Crud::PAGE_EDIT, Action::SAVE_AND_ADD_ANOTHER)
            ;
    }

    public function configureFields(string $pageName): iterable
    {
        return [
            AssociationField::new('category'),
            ImageField::new('image')
                ->setUploadDir('public/uploads/images')
                ->setBasePath('uploads/images')
                ->setRequired(false)
                ->hideOnForm(),
            MultipleImageField::new('imageFile')
                ->setRequired(false)
                ->onlyOnForms(),
        ];
    }

    /**
     * @param EntityManagerInterface $entityManager
     * @param Gallery $entityInstance
     */
    public function persistEntity(EntityManagerInterface $entityManager, $entityInstance): void
    {
        foreach($entityInstance->getImageFile() as $file)
        {
            $gallery = (new Gallery())
                ->setCategory($entityInstance->getCategory())
                ->setImage($file->getClientOriginalName())
            ;
            $entityInstance->upload($file);
            $entityManager->persist($gallery);
        }
        $entityManager->flush();
    }

    /**
     * @param EntityManagerInterface $entityManager
     * @param Gallery $entityInstance
     */
    public function updateEntity(EntityManagerInterface $entityManager, $entityInstance): void
    {
        foreach($entityInstance->getImageFile() as $file)
        {
            $gallery = (new Gallery())
                ->setCategory($entityInstance->getCategory())
                ->setImage($file->getClientOriginalName())
            ;
            $entityInstance->upload($file);
            $entityManager->persist($gallery);
        }
        $entityManager->flush();
    }
}

I hope this helps anyone who is trying to do something similar with EasyAdmin3

HoppyB
  • 175
  • 1
  • 12
  • 1
    Thank you for the answer. After some time I have returned to this question. My problem is I need to save multiple images in `PlacesCrudController` and my `Places` entity has OneToMany association with `Images`, so I can't do like this ` foreach($entityInstance->getImageFile() as $file)` because my file is in another entity. – laneboyandrew Nov 18 '21 at 18:34
  • @laneboyandrew In this case each file does get uploaded as different entities. I'm uploading many images at once for a single category. Let's say I pick my Category, and Upload 10 files and click save. This will create 10 Gallery entities all related to the same Category. The `getImageFile` is unmapped so its only stored in the entity temporarily so you can then loop through it and actually create a unique entity for each file. – HoppyB Nov 18 '21 at 22:00
0

VichUploader only supports to upload single files, so the only solution is to have a bridge entity with a OneToMany relation with the entity that contains the file, like you can see in this answer

A viable alternative would be to create many different Image entities instead of trying to put multiple attachments to the same Image.

So your form builder for the Places would be something like this

public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('images', CollectionType::class, [
                'entry_type' => Image::class,
                'multiple' => true,
                'by_reference' => false
            ])
        ;
    }

Make sure to pass by_reference => false when updating linked entities in a form to be sure that the setter method is called. In this case you can revert your changes as before and it would work with VichUploader.

Instead if you want to use FileType::class directly the answer from HobbyB has a good point, you will have to manually manage persistEntity and updateEntity events in the controller but without using VichUploader classes at all. And remember that when dealing with images the Entity is the only data persisted to the database, while the attached media file is stored in a (probably the public one) directory. So if your purpose is to have an Image entity with multiple Attachments where an Attachment is a list of Paths you will have to put a string array field to the image entity and manage the actual storage of the pictures accordingly.

Not related to the question, but in Symfony you can use Timestampable annotation to automatically manage an updatedAt field like this, using builtin features is a best practice.

/**
     * @var \DateTime $updated
     *
     * @Gedmo\Timestampable(on="update")
     * @ORM\Column(type="datetime")
     */
    private $updated;
David
  • 369
  • 3
  • 14