Please wait...

Symfony 5 - Upload a file

Hello,

I would like to show you how to upload very simply a file with the framework Symfony 5, without any Bundle.

To properly structure our actions, we will create and configure:

  1. A service allowing you to upload any type of file to a specific directory,
  2. A form allowing to apply restrictions on the file to upload,
  3. A controller allowing to manage all the actions,
  4. A view allowing to define the web interface.

Service creation and configuration

Let's create the code of a service that will allow the upload of any type of file, without restriction:

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

use Symfony\Component\HttpFoundation\File\Exception\FileException;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\String\Slugger\SluggerInterface;

class FileUploader
{
    private $targetDirectory;
    private $slugger;

    public function __construct($targetDirectory, SluggerInterface $slugger)
    {
        $this->targetDirectory = $targetDirectory;
        $this->slugger = $slugger;
    }

    public function upload(UploadedFile $file)
    {
        $originalFilename = pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME);
        $safeFilename = $this->slugger->slug($originalFilename);
        $fileName = $safeFilename.'-'.uniqid().'.'.$file->guessExtension();

        try {
            $file->move($this->getTargetDirectory(), $fileName);
        } catch (FileException $e) {
            // ... handle exception if something happens during file upload
            return null; // for example
        }

        return $fileName;
    }

    public function getTargetDirectory()
    {
        return $this->targetDirectory;
    }
}

You must then configure this service, in particular by passing it the directory where all uploaded files will be stored.

# config/services.yaml

parameters:
    upload_directory: '%kernel.project_dir%/public/uploads'
#...

services:
    # ...
    App\Service\FileUploader:
        arguments:
            $targetDirectory: '%upload_directory%'

Snippets issued from Framework documentation.

Form creation and configuration

This is where we will decide which types of file mimes we want to upload:

<?php
// src/Form/FileUploadType.php
namespace App\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\File;

class FileUploadType extends AbstractType
{
  public function buildForm(FormBuilderInterface $builder, array $options)
  {
      $builder
        ->add('upload_file', FileType::class, [
          'label' => false,
          'mapped' => false, // Tell that there is no Entity to link
          'required' => true,
          'constraints' => [
            new File([ 
              'mimeTypes' => [ // We want to let upload only txt, csv or Excel files
                'text/x-comma-separated-values', 
                'text/comma-separated-values', 
                'text/x-csv', 
                'text/csv', 
                'text/plain',
                'application/octet-stream', 
                'application/vnd.ms-excel', 
                'application/x-csv', 
                'application/csv', 
                'application/excel', 
                'application/vnd.msexcel', 
                'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
              ],
              'mimeTypesMessage' => "This document isn't valid.",
            ])
          ],
        ])
        ->add('send', SubmitType::class); // We could have added it in the view, as stated in the framework recommendations
  }
}

You will find on Mozilla developper documentation tous les types mimes possibles.

Controller creation

This controller allows to validate the file mime type and to upload with the service that we have created. We could for example explore the content of the file or parse it, but this might be another Blog post!


namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;

use App\Service\FileUploader;
use App\Form\FileUploadType;

class UploadController extends BaseController
{
  // ...

  /**
   * @Route("/test-upload", name="app_test_upload")
   */
  public function excelCommunesAction(Request $request, FileUploader $file_uploader)
  {
    $form = $this->createForm(FileUploadType::class);
    $form->handleRequest($request);

    if ($form->isSubmitted() && $form->isValid()) 
    {
      $file = $form['upload_file']->getData();
      if ($file) 
      {
        $file_name = $file_uploader->upload($file);
        if (null !== $file_name) // for example
        {
          $directory = $file_uploader->getTargetDirectory();
          $full_path = $directory.'/'.$file_name;
          // Do what you want with the full path file...
          // Why not read the content or parse it !!!
        }
        else
        {
          // Oups, an error occured !!!
        }
      }
    }

    return $this->render('app/test-upload.html.twig', [
      'form' => $form->createView(),
    ]);
  }

  // ...
}

View creation

This view will display our recently created form:

{# templates/app/test-upload.html.twig #}
{% extends 'app/layout.html.twig' %}

{% block title %}Upload test{% endblock %}
{% block description %}This page will render a simple form to upload a file.{% endblock %}

{% block content %}
  <div class="container container-fluid">
    <div class="row">
      <div class="col-lg-8">
      {{ form_start(form, { attr: { 'accept-charset' : 'utf-8' }}) }}
        <div class="form-group">
          {{ form_label(form.upload_file) }}
          {{ form_widget(form.upload_file) }}
          {{ form_errors(form.upload_file) }}
        </div>
        <div class="form-group">
          {{ form_widget(form.send, {'label': "Upload this file", 'attr' : { 'class': 'btn btn-primary' }}) }}
        </div>
        {{ form_rest(form) }}
      {{ form_end(form) }}
      </div>
    </div>
  </div>
{% endblock %}

There you go, you now know how to upload a file without any third party bundle !!!

See you soon,

Mathieu