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