Upload multiple files to an entity with Symfony4 and VichUploaderBundle

Preparation

 

To begin with we need an entity Product as well as a ProductImage entity that will contain the names of the images.

To do this, we use the order in our terminal

php bin/console make:entity
namespace App-Entity;

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

/**
 @ORM-Entity (repositoryClass-"App-Repository-ProductRepository")
 */
class Product
{
    /**
     @ORM-Id()
     @ORM-GeneratedValue()
     @ORM-Column (type"integer")
     */
    private $id;

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

    /**
     @ORM-OneToMany (targetEntity-"App-Entity-ProductImage", mappedBy-"product")
     */
    private $productImages;

    public function __construct()
    {
        $this--productImages - new ArrayCollection(;
    }

    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;
    }

    /**
     '@return Collection' ProductImage[]
     */
    public function getProductImages(): Collection
    {
        return $this--productImages;
    }

    public function addProductImage (ProductImage $productImage): self
    {
        if (!$this--productImages------$productImage
            $this--productImages[] - $productImage;
            $productImage-setProduct ($this);
        }

        return $this;
    }

    public function removeProductImage (ProductImage $productImage): self
    {
        if ($this--productImages----contains($productImage))
            $this--productImages---removeElement($productImage;
            set the owning side to null (unless already changed)
            if ($productImage--getProduct() '$this) '
                $productImage-setProduct (null);
            }
        }

        return $this;
    }
}
namespace App-Entity;

use Doctrine-ORM-Mapping as ORM;

/**
 @ORM-Entity (repositoryClass-"App-Repository-ProductImageRepository")
 */
ProductImage class
{
    /**
     @ORM-Id()
     @ORM-GeneratedValue()
     @ORM-Column (type"integer")
     */
    private $id;

    /**
     "@ORM-ManyToOne (targetEntity-"App-Entity-Product", inversedBy-"productImages")
     */
    private $product;

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

    /**
     @ORM-Column (type"string", length-255, nullable-true)
     */
    private $image-size;

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

    public function getProduct(): ? Product
    {
        return $this-product;
    }

    public function setProduct(? Product $product): self
    {
        $this--product-$product;

        return $this;
    }

    public function getImageName(): ?string
    {
        return $this-image_name;
    }

    public function setImageName (string $image-name): self
    {
        $this-image_name-$image-name;

        return $this;
    }

    public function getImageSize(): ?string
    {
        return $this-image_size;
    }

    public function setImageSize (?string $image-size): self
    {
        $this-image_size-$image-size;

        return $this;
    }
}

BichUploaderBundle Setup

As said in the Symfony4 documentation we will use the VichUploaderBundle bundle

composer require vich/uploader-bundle

Then add the necessary “config/packages/vich_uploader.yaml” configuration file

config/packages/vich_uploader.yaml or app/config/config.yml
vich_uploader:
    db_driver: orm - or mongodb or propel or phpcr

Once this is done, the bundle is ready to be used. To start, you have to change the ProductImage entity and add “@Vich uploadable” before the class is declaration.

...

/**
 @ORM-Entity (repositoryClass-"App-Repository-ProductImageRepository")
 @Vich-Uploadable
 */
ProductImage class
{
...

Then add these properties.


    /**
     NOTE: This is not a mapped field of entity metadata, just a simple property.
     *
     @Vich-UploadableField (mapping product_image, fileNameProperty- "imageName," size-"imageSize")
     *
     @var File
     */
    private $imageFile;

    /**
     "@ORM-Column(type"datetime")
     *
     "@var'S DateTime
     */
    private $updatedAt;

Then these methods

/**
     If manually uploading a file (i.e. not using Symfony Form) ensure an instance
     Of 'UploadedFile' is injected into this setter to trigger the update. If this
     'bundle's configuration parameter 'inject_on_load' is set to 'true' this setter
     Must be able to accept an instance of 'File' as the bundle will inject one here
     During Doctrine hydration.
     *
     @param File-Symfony-Component-HttpFoundation-File-UploadedFile $imageFile
     */
    public function setImageFile(? File $imageFile - null): void
    {
        $this--imageFile - $imageFile;

        if (null! $imageFile)
            It is required that at least one field changes if you are using doctrine
            otherwise the event listeners won't be called and the file is lost
            $this-updatedAt - new 'DateTimeImmutable(;
        }
    }

    public function getImageFile(): ? File
    {
        return $this-imageFile;
    }

So the productImage Entity looks like this

<?php?>

namespace App-Entity;

dateTimeImmutable use;
use Doctrine-ORM-Mapping as ORM;
use Symfony-Component-HttpFoundation-File-File;
use Vich-UploaderBundle-Mapping-Annotation as Vich;

/**
 @ORM-Entity (repositoryClass-"App-Repository-ProductImageRepository")
 @Vich-Uploadable
 */
ProductImage class
{
    /**
     @ORM-Id()
     @ORM-GeneratedValue()
     @ORM-Column (type"integer")
     */
    private $id;

    /**
     "@ORM-ManyToOne (targetEntity-"App-Entity-Product", inversedBy-"productImages")
     */
    private $product;

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

    /**
     @ORM-Column (type"string", length-255, nullable-true)
     */
    private $image-size;

    /**
     NOTE: This is not a mapped field of entity metadata, just a simple property.
     *
     @Vich-UploadableField (mapping product_image, fileNameProperty- "imageName," size-"imageSize")
     *
     @var File
     */
    private $imageFile;

    /**
     "@ORM-Column(type"datetime")
     *
     "@var'S DateTime
     */
    private $updatedAt;

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

    public function getProduct(): ? Product
    {
        return $this-product;
    }

    public function setProduct(? Product $product): self
    {
        $this--product-$product;

        return $this;
    }

    public function getImageName(): ?string
    {
        return $this-image_name;
    }

    public function setImageName (string $image-name): self
    {
        $this-image_name-$image-name;

        return $this;
    }

    public function getImageSize(): ?string
    {
        return $this-image_size;
    }

    public function setImageSize (?string $image-size): self
    {
        $this-image_size-$image-size;

        return $this;
    }

    /**
     If manually uploading a file (i.e. not using Symfony Form) ensure an instance
     Of 'UploadedFile' is injected into this setter to trigger the update. If this
     'bundle's configuration parameter 'inject_on_load' is set to 'true' this setter
     Must be able to accept an instance of 'File' as the bundle will inject one here
     During Doctrine hydration.
     *
     @param File-Symfony-Component-HttpFoundation-File-UploadedFile $imageFile
     @throws 'Exception'
     */
    public function setImageFile(? File $imageFile - null): void
    {
        $this--imageFile - $imageFile;

        if (null! $imageFile)
            It is required that at least one field changes if you are using doctrine
            otherwise the event listeners won't be called and the file is lost
            $this-updatedAt - new DateTimeImmutable();
        }
    }

    public function getImageFile(): ? File
    {
        return $this-imageFile;
    }
}

To make it all easier, the make:crud function will be used, so you don’t have to create the form of any piece

php bin/console make:crud Product

Once this is done, it is still necessary to have a ProductImageType

<?php?>
namespace App-Form;

use App-Entity-ProductImage;
use Symfony-Component-Form-AbstractType;
use Symfony-Component-Form-FormBuilderInterface;
use Symfony-Component-OptionsResolver-OptionsResolver;
vich-UploaderBundle-Form-Type-VichImageType;

class ProductImageType extends AbstractType
{
    public function buildForm (FormBuilderInterface $builder, array $options)
    {
        $builder
            -add ('imageFile',VichImageType::class, [
                'required' false,
                'download_uri' - true,
                'image_uri' - true
            ]);
        ;
    }

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

Then in the ProductType it is necessary to add the collection of productImages

...
    public function buildForm (FormBuilderInterface $builder, array $options)
    {
        $builder
            -add ('name')
            -add ('product_images', CollectionType::class, [
                'entry_type' ProductImageType::class,
                'allow_add' true,
                'allow_delete' true,
                'prototype' - true
            ])
        ;
    }
...

Templating

Once this is done, the form has the images, but it is not possible to add an image

So it’s time to add this button. To do this, you have to create a form template: “_form_theme.html.twig”

%block _product_product_images_widget%
    <ul id="product-images-fields-list" id="product-images-fields-list"></ul id="product-images-fields-list">
        data-prototype"form_widget (prototype)
        data-widget-tags<li></li>'E's'
        data-widget-counter
    >
        %for key,productImageForm in form.children %
            'form_widget (productImageForm)

        %endfor %
    

    <button type="button" type="button"></button type="button">
            "add-another-collection-widget"
            data-list-selector"#product-images-fields-list" -Add another Product Image
%endblock %

then let’s modify the _form.html.twig of the product to add this one

%form_theme form 'product/_form_theme.html.twig' %
'form_start(form'
    'form_widget (form)
    <button class="btn btn-info btn-sm">"button_label-default('Save')</button>
'form_end(form'

Now it’s time to create the javascript that add the images

$('.add-another-collection-widget').click (function)
        var list - $($(this).attr ('data-list-selector'));
        Try to find the counter of the list or use the length of the list
        var counter -list.data ('widget-counter') list.children().length;

        grab the prototype template
        var newWidget - list.attr ('data-prototype');
        replace the "__name__" used in the id and name of the prototype
        with a number that's unique to your emails
        end name attribute looks like name"contact[emails][2]"
        newWidget - newWidget.replace (/__name__/g, counter);
        Increase the counter
        counter-terrorism;
        And store it, the length cannot be used if deleting widgets is allowed
        list.data ('widget-counter', counter);

        create a new list element and add it to the list
        var newElem - $(list.attr ('data-widget-tags')).html (newWidget);
        newElem.appendTo (list);
    });

Once this is done, by clicking the button, a new field appears with the ability to add an image

Publicités

Once this is done, you still need to set up the bundle to indicate where the images will be registered in the file “config/packages/vich_uploader.yaml”

mappings:
        product_image:
            uri_prefix: /images/products
            upload_destination: '%kernel.project_dir%/public/images/products'
            inject_on_load: false
            delete_on_update: true
            delete_on_remove: true
            namer: Vich-UploaderBundle-Naming UniqidNamer

Unfortunately a bug currently exists and if we stay that way, the productImage will be recorded in the product reference, which is why the controller needs to be modified for the “new” and “edit” method.

/**
     "@Route("/new," name "product_new," methods,"GET," "POST"
     */
    public function new (Request $request): Response
    {
        $product - New Product();
        $form - $this--createForm (ProductType::class, $product);
        $form-handleRequest ($request);

        if ($form-isSubmitted() $form-isValid()
            $productImages - $product--getProductImages(;
            foreach ($productImages a $key $productImage)
                $productImage-setProduct ($product);
                $productImages-set ($key,$productImage);
            }

            $entityManager - $this--getDoctrine()--getManager(;
            $entityManager--persist ($product;
            $entityManager-flush(;

            return $this-redirectToRoute ('product_index');
        }

        return $this-render ('product/new.html.twig', [
            'product' $product,
            'form' $form-createView(),
        ]);
    }

    /**
     @Route ("id"/edit," name "product_edit," methods"GET," "POST")
     */
    public function edit (Request $request, Product $product): Response
    {
        $form - $this--createForm (ProductType::class, $product);
        $form-handleRequest ($request);

        if ($form-isSubmitted() $form-isValid()
            $productImages - $product--getProductImages(;
            foreach ($productImages a $key $productImage)
                $productImage-setProduct ($product);
                $productImages-set ($key,$productImage);
            }

            $entityManager - $this--getDoctrine()--getManager(;
            $entityManager--persist ($product;
            $entityManager-flush(;

            return $this-redirectToRoute ('product_index', [
                'id' $product-getId(),
            ]);
        }

        return $this-render ('product/edit.html.twig', [
            'product' $product,
            'form' $form-createView(),
        ]);
    }
And here, the images are recorded correctly
Publicités

Leave a Reply