How to integrate VichUploaderBundle with Symfony 6.*

How to integrate VichUploaderBundle with Symfony 6.*

Introduction

This guide serves as an introduction to a series of materials on 'How to link Symfony + VichUploaderBundle + LiipBundle + AWS S3'

Preparation

Full code on GitHub

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:

src/Entity/Banner.php

<?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):

config/bundles.php

<?php

return [
    ...
+   Vich\UploaderBundle\VichUploaderBundle::class => ['all' => true],
];

Add fields to entity

We add the required fields to the class manually:

src/Entity/Banner.php

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

config/packages/vich_uploader.yaml

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:

config/packages/vich_uploader.yaml

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:

src/Form/BannerType.php
    <?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.

Create

After the upload, we can see that our file has appeared in the public/images/banners folder:

Create

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

Create

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:

src/Entity/Banner.php

use Vich\UploaderBundle\Entity\File as EmbeddedFile;

+    public function __construct()
+    {
+        $this->image = new EmbeddedFile();
+    }