How to integrate VichUploaderBundle with Symfony 6.*
Table of Contents
Introduction
This guide serves as an introduction to a series of materials on 'How to link Symfony + VichUploaderBundle + LiipBundle + AWS S3'
Preparation
I'll use Symfony 6.3 + Flex. Besides that, I already have the 'Banner' entity and CRUD operations (Controller, Form, twig templates).
Also, the code in this guide does not display the correct architecture. How it should be done - we will refactor it in another post.
My Banner
class:
<?php
namespace App\Entity;
use App\Repository\BannerRepository;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: BannerRepository::class)]
class Banner
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 55)]
private ?string $title = null;
public function getId(): ?int
{
return $this->id;
}
public function getTitle(): ?string
{
return $this->title;
}
public function setTitle(string $title): Banner
{
$this->title = $title;
return $this;
}
}
Install VichUploaderBundle
Lets install the bundle:
composer require vich/uploader-bundle
...
Symfony operations: 1 recipe (f9785b6d38e408056016bd1264ed27c0)
- WARNING vich/uploader-bundle (>=1.13): From github.com/symfony/recipes-contrib:main
The recipe for this package comes from the "contrib" repository, which is open to community contributions.
Review the recipe at https://github.com/symfony/recipes-contrib/tree/main/vich/uploader-bundle/1.13
Do you want to execute this recipe?
[y] Yes
[n] No
[a] Yes for all packages, only for the current installation session
[p] Yes permanently, never ask again for this project
(defaults to n):
Type Y - yes, we want execute this recipe
In bundles.php
you will see new line (I hope you use symfony/flex
):
<?php
return [
...
+ Vich\UploaderBundle\VichUploaderBundle::class => ['all' => true],
];
Add fields to entity
We add the required fields to the class manually:
<?php
namespace App\Entity;
use App\Repository\BannerRepository;
use Doctrine\ORM\Mapping as ORM;
+ use Vich\UploaderBundle\Mapping\Annotation as Vich;
+ use Vich\UploaderBundle\Entity\File as EmbeddedFile;
+ use Symfony\Component\HttpFoundation\File\File;
+ #[Vich\Uploadable]
#[ORM\Entity(repositoryClass: BannerRepository::class)]
class Banner
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 55)]
private ?string $title = null;
+ #[Vich\UploadableField(
+ mapping: 'banners', # We will remember this value. It will serve as the identifier for the section in the configuration.
+ fileNameProperty: 'image.name',
+ size: 'image.size'
+ )]
+ private ?File $imageFile = null;
+ #[ORM\Embedded(class: EmbeddedFile::class)]
+ private ?EmbeddedFile $image = null;
public function getId(): ?int
{
return $this->id;
}
public function getTitle(): ?string
{
return $this->title;
}
public function setTitle(string $title): Banner
{
$this->title = $title;
return $this;
}
+ public function getImageFile(): ?File
+ {
+ return $this->imageFile;
+ }
+
+ public function setImageFile(?File $imageFile): Banner
+ {
+ $this->imageFile = $imageFile;
+ return $this;
+ }
+
+ public function getImage(): ?EmbeddedFile
+ {
+ return $this->image;
+ }
+
+ public function setImage(?EmbeddedFile $image): Banner
+ {
+ $this->image = $image;
+ return $this;
+ }
}
Migration
Add new fields in database:
bin/console make:migration
created: migrations/Version20230811000024.php
Success!
Apply the migration:
php bin/console doctrine:migrations:migrate
WARNING! You are about to execute a migration in database "app" that could result in schema changes and data loss. Are you sure you wish to continue? (yes/no) [yes]:
>
[notice] Migrating up to DoctrineMigrations\Version20230811000024
[notice] finished in 40.6ms, used 22M memory, 1 migrations executed, 6 sql queries
[OK] Successfully migrated to version : DoctrineMigrations\Version20230811000024
Take look of how the table structure has been enhanced:
CREATE TABLE banner (
id INT NOT NULL,
title VARCHAR(55) NOT NULL,
-- These fields appeared because we added Vich\UploaderBundle\Entity\File
-- However, we could have defined them arbitrarily and
-- specified our own length or divided image_dimensions into different fields.
+ image_name VARCHAR(255) DEFAULT NULL,
+ image_original_name VARCHAR(255) DEFAULT NULL,
+ image_mime_type VARCHAR(255) DEFAULT NULL,
+ image_size INT DEFAULT NULL,
+ image_dimensions TEXT DEFAULT NULL,
PRIMARY KEY(id)
)
Configure bundle
Lets open bundle's configuration file:
vich_uploader:
db_driver: orm
#mappings:
# products:
# uri_prefix: /images/products
# upload_destination: '%kernel.project_dir%/public/images/products'
# namer: Vich\UploaderBundle\Naming\SmartUniqueNamer
We need to uncomment the content and make some modifications to it:
vich_uploader:
db_driver: orm
mappings:
banners: # This is the section identifier that we remembered from the entity.
uri_prefix: /images/banners # Prefix for the path of our images.
# For example, our image will be displayed with the following path.
# http://localhost:8008/images/banners/custom-image.png
upload_destination: '%kernel.project_dir%/public/images/products' # The bundle will upload images here.
namer:
service: vich_uploader.namer_hash # The service describes the method of naming images.
options: { algorithm: 'md5' } # Just a regular MD5. You can leave it as the default.
Configure entity`s form
Open src/Form/BannerType.php
and add new field:
<?php
namespace App\Form;
use App\Entity\Banner;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
+ use Vich\UploaderBundle\Form\Type\VichImageType;
class BannerType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('title')
+ ->add('imageFile', VichImageType::class, [
+ 'required' => false,
+ 'allow_delete' => false,
+ 'download_uri' => false
+ ])
;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Banner::class, # Please, use DTO instead of entities
]);
}
}
Check result
Lets open the page with the Banner
creation form. For me, it's /banner/new
.
Fill in the title, add an image, and click Save
.
After the upload, we can see that our file has appeared in the public/images/banners
folder:
Twig template
If you explicitly specify form fields in Twig, then don't forget to add:
{{ form_widget(form.imageFile) }} ## or skip if you use {{ form_widget(form) }}
That's all for now. Today we learned how to upload images using VichUploaderBundle for Symfony.
Bonus
To display an image in Twig, simply add this code:
<img src="{{ vich_uploader_asset(banner) }}" />
If banner
is not an object, but an array:
<img src="{{ vich_uploader_asset(banner, 'imageFile', 'App\\Entity\\Banner') }}" />
Possible issues
Issue:
PropertyAccessor requires a graph of objects or arrays to operate on,
but it found type "NULL" while trying to traverse path "image.name" at property "name".
Reason:
This happens because the fields were not explicitly specified but used with ORM\Embedded
. The following code will fix it:
use Vich\UploaderBundle\Entity\File as EmbeddedFile;
+ public function __construct()
+ {
+ $this->image = new EmbeddedFile();
+ }