Learn how to allow the upload of a file within a form created from a Doctrine Entity easily without moving the file manually in the controller on every action.

Normally, many tutorials indicate you that you need to retrieve the uploaded file in the controller and execute some logic there either to save, update or delete, however this with Symfony 3, ain't necessary if your form is based on a Doctrine Entity. But wait, the official tutorials of Symfony already have an article about how to upload a file automatically without modifying the controller with a doctrine entity, why should I read this article instead? Well, the official tutorial in Symfony will work as described, the problem is that the uploader will upload a new file everytime you edit (update) your entity, which leads to an extense collection of files that you dont need anymore, because your entity only stores the filename of a single one. If you want that everytime you update the form, the old file is removed (only if a new file was uploaded) or the old file remains (if nothing new was uploaded), then the following implementation will lead you through this.

We will make the tutorial with the same example of the Brochure field and Product entity in the official article of Symfony.

1. Configure FormType with FileType

The first thing you need to do, is to define the field of the form that should be used to upload the file using the FileType:

Note

Is important to set the data_class property of the field with null, otherwise you will face an exception namely:

The form's view data is expected to be an instance of class Symfony\Component\HttpFoundation\File\File, but is a(n) string.

when you try to update your entity with the form.

<?php

// src/AppBundle/Form/ProductType.php
namespace AppBundle\Form;

use AppBundle\Entity\Product;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\FileType;

class ProductType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            // ...
            ->add('brochure', FileType::class, array(
                'label' => 'Brochure (PDF file)',
                'data_class' => null,
                'required' => false
            ))
            // ...
        ;
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => Product::class,
        ));
    }
}

2. Create and configure the FileUploader service

As first step to configure the FileUploader class, is to define a global parameter that specifies where the brochure files will be uploaded. In this case, we will create 2 folders in web namely uploads and inside uploads, the brochures folder, where the uploaded files will be stored. You can define this parameter in the config.yml file of your application:

# app/config/config.yml
parameters:
    brochure_files_directory: '%kernel.project_dir%/web/uploads/brochures'

We will need the brochure_files_directory parameter to inject it into the FileUploader service that we'll create as next. Create the class FileUploader with the following code inside:

<?php

// src/AppBundle/Service/FileUploader.php
namespace AppBundle\Service;

use Symfony\Component\HttpFoundation\File\UploadedFile;

class FileUploader
{
    private $targetDir;

    public function __construct($targetDir)
    {
        $this->targetDir = $targetDir;
    }

    public function upload(UploadedFile $file)
    {
        $fileName = md5(uniqid()).'.'.$file->guessExtension();

        $file->move($this->getTargetDir(), $fileName);

        return $fileName;
    }

    public function getTargetDir()
    {
        return $this->targetDir;
    }
}

As a standard, we have created the Service folder inside our bundle, so the namespacing is easier to understand, besides it is accesible for our application. The code doesn't need to be modified as it implements a very basic file uploader that moves the providen file into the desired path with a random name generated in a single line of code. Once this file exists, you need to register it in the services.yml file of your Symfony project:

# app/config/services.yml
services:
    AppBundle\Service\FileUploader:
        arguments:
            $targetDir: '%brochure_files_directory%' 

Note that we are inyecting the created parameter brochure_files_directory as value for the $targetDir argument. This FileUploader class will be used by the doctrine listener to manipulate easily the files.

3. Create and configure Doctrine Listener

As mentioned in the official tutorial, in order to prevent extra code in the controllers to handle file uploads, you can create a Doctrine listener to automatically upload the file when persisting the entity. In this example, we have created the EventListener folder inside the AppBundle where our upload listener class will be placed. The code of the doctrine listener follows the next logic:

Note

This code must be modified according to the field of your entity. That means, change getters and setters of Brochure by yours.


<?php

// src/AppBundle/EventListener/BrochureUploadListener.php
namespace AppBundle\EventListener;

use Symfony\Component\HttpFoundation\File\UploadedFile;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Doctrine\ORM\Event\PreUpdateEventArgs;

// Include Product class and our file uploader
use AppBundle\Entity\Product;
use AppBundle\Service\FileUploader;

class BrochureUploadListener
{
    private $uploader;
    private $fileName;

    public function __construct(FileUploader $uploader)
    {
        $this->uploader = $uploader;
    }

    public function prePersist(LifecycleEventArgs $args)
    {
        $entity = $args->getEntity();

        $this->uploadFile($entity);
    }
    
    public function preUpdate(PreUpdateEventArgs $args)
    {
        // Retrieve Form as Entity
        $entity = $args->getEntity();
        
        // This logic only works for Product entities
        if (!$entity instanceof Product) {
            return;
        }

        // Check which fields were changes
        $changes = $args->getEntityChangeSet();
        
        // Declare a variable that will contain the name of the previous file, if exists.
        $previousFilename = null;
        
        // Verify if the brochure field was changed
        if(array_key_exists("brochure", $changes)){
            // Update previous file name
            $previousFilename = $changes["brochure"][0];
        }
        
        // If no new brochure file was uploaded
        if(is_null($entity->getBrochure())){
            // Let original filename in the entity
            $entity->setBrochure($previousFilename);

        // If a new brochure was uploaded in the form
        }else{
            // If some previous file exist
            if(!is_null($previousFilename)){
                $pathPreviousFile = $this->uploader->getTargetDir(). "/". $previousFilename;

                // Remove it
                if(file_exists($pathPreviousFile)){
                    unlink($pathPreviousFile);
                }
            }
            
            // Upload new file
            $this->uploadFile($entity);
        }
    }

    private function uploadFile($entity)
    {
        // upload only works for Product entities
        if (!$entity instanceof Product) {
            return;
        }

        $file = $entity->getBrochure();

        // only upload new files
        if ($file instanceof UploadedFile) {
            $fileName = $this->uploader->upload($file);
            
            $entity->setBrochure($fileName);
        }
    }
}

When a user creates a new register for Product (access the form to create a new product), the form allows him to upload a file in the brochure field. This, if providen, will be automatically saved in the /uploads/brochures directory with a random filename (name that is stored in the database in the brochure field), if not any file is uploaded, then the field will be null. When the user edits the product, if someone has already uploaded a brochure file and the user updates the product without uploading a new file, then the old file will be kept and nothing will happen, however if the user uploads a new file, then the old one is deleted and the new one is stored (updating the brochure field as well).

Finally, you need to register this doctrine listener in the services.yml file of your project:

# app/config/services.yml
services:
 
    AppBundle\EventListener\BrochureUploadListener:
        tags:
            - { name: doctrine.event_listener, event: prePersist }
            - { name: doctrine.event_listener, event: preUpdate }

Save changes, clear cache and test your form.

Happy coding !


Senior Software Engineer at Software Medico. Interested in programming since he was 14 years old, Carlos is a self-taught programmer and founder and author of most of the articles at Our Code World.

Sponsors