Ebooks

Symfony upload file

Symfony upload file tutorial shows how to upload a file in a Symfony application. In the example, we use a normal form to send the file; we don't use a form builder.

Symfony

Symfony is a set of reusable PHP components and a PHP framework for web projects. Symfony was published as free software in 2005. The original author of Symfony is Fabien Potencier. Symfony was heavily inspired by the Spring Framework and by Ruby on Rails.

Uploading file

In order to upload a file, the form must have the enctype set to multipart/form-data and the type of the input set to file.

Also, in the PHP's php.ini, the file uploads are controlled with the file_uploads option.

Symfony file upload example

In the example, we have a simple form with one input field: file to upload. After the form is submitted, we validate the CSRF token and load the image, retrieve its name, and store the file inside the var directory.

Creating a Symfony project and installing packages

The composer tool is used to generage a Symfony skeleton project and install the necessary packages.

$ composer create-project symfony/skeleton upload
$ cd upload

We create a new Symfony project and go to the project directory.

$ composer require maker annotations twig

We install three basic Symfony packages for web development: annotations, maker, and twig. These are needed for generating routes, controllers and templates.

$ composer require symfony/security-csrf
$ composer require symfony/monolog-bundle

The security-csrf package is needed against cross-site request forgeries and monolog-bundle for logging.

$ composer require server --dev 
$ composer require symfony/profiler-pack --dev

For development stage, we also install a built-in server and profiler.

Building Symfony application

We define the directory where the images will be uploaded.

config/services.yaml
parameters:
    upload_dir: '../var/uploads'

services:
    # default configuration for services in *this* file
    _defaults:
        autowire: true      
        autoconfigure: true 
        public: false       
                                                            
        bind: 
            $uploadDir: '%upload_dir%'

We define a parameter which contains the name of the directory where the images should be uploaded. The upload_dir parameter is binded to the $uploadDir variable, which can be injected.

$ php bin/console make:controller HomeController

We create a HomeController. The controller sends a form to the client.

src/Controller/HomeController.php
<?php

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;

class HomeController extends AbstractController
{
    /**
     * @Route("/", name="home")
     */
    public function index()
    {
        return $this->render('home/index.html.twig');
    }
}

This is a simple controller that sends a view containing the web form to the user.

templates/home/index.html.twig
{% extends 'base.html.twig' %}

{% block title %}Home page{% endblock %}

{% block body %}

<form action="doUpload" method="post" enctype="multipart/form-data">

    <input type="hidden" name="token" value="{{ csrf_token('upload') }}" />

    <div>
        <label>File to upload:</label>
        <input type="file" name="myfile">
    </div>

    <button type="submit">Send</button>

</form>

{% endblock %}

This view creates a form. It defines the multipart/form-data enctype and the file input. In addition, it has a CSRF hidden input token.

$ php bin/console make:controller UploadController    

We create a UploadController that responds to the form submission. We don't need the generated twig template for this controller; therefore, we delete it.

src/Controller/UploadController.php
<?php

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use App\Service\FileUploader;
use Psr\Log\LoggerInterface;

class UploadController extends AbstractController
{
    /**
     * @Route("/doUpload", name="upload")
     */
     public function index(Request $request, string $uploadDir, 
             FileUploader $uploader, LoggerInterface $logger)
    {
        $token = $request->get("token");

        if (!$this->isCsrfTokenValid('upload', $token)) 
        {
            $logger->info("CSRF failure");

            return new Response("Operation not allowed",  Response::HTTP_BAD_REQUEST,
                ['content-type' => 'text/plain']);
        }        

        $file = $request->files->get('myfile');

        if (empty($file)) 
        {
            return new Response("No file specified",  
               Response::HTTP_UNPROCESSABLE_ENTITY, ['content-type' => 'text/plain']);
        }        

        $filename = $file->getClientOriginalName();
        $uploader->upload($uploadDir, $file, $filename);

        return new Response("File uploaded",  Response::HTTP_OK, 
            ['content-type' => 'text/plain']);         
    }
}

In the UploadController, we check the CSRF token, get the file from the request, and call the uploader service upload() method.

public function index(Request $request, string $uploadDir, 
        FileUploader $uploader, LoggerInterface $logger)
{

We inject the request object, the upload directory parameter, the FileUploader service, and the logger.

$token = $request->get("token");

if (!$this->isCsrfTokenValid('upload', $token)) 
{
    $logger->info("CSRF failure");

    return new Response("Operation not allowed",  Response::HTTP_BAD_REQUEST,
        ['content-type' => 'text/plain']);
}

We retrieve the token and validate it with isCsrfTokenValid() method. If the validation fails, we log the incident and sent a plain response "Operation not allowed" and Response::HTTP_BAD_REQUEST response code.

$file = $request->files->get('myfile');

if (empty($file)) 
{
    return new Response("No file specified",  Response::HTTP_UNPROCESSABLE_ENTITY, 
        ['content-type' => 'text/plain']);  
}     

We check if the user has specified any file in the form with the empty() method. If the input field is empty, we send a plain text "No file specified" back to the client with Response::HTTP_UNPROCESSABLE_ENTITY response code.

$filename = $file->getClientOriginalName();

We get the file name with getClientOriginalName().

$uploader->upload($uploadDir, $file, $filename);

We call the uploader service upload() method, which moves the file to the chosen directory. We pass the method the directory name, the file data, and the file name.

return new Response("File uploaded",  Response::HTTP_OK, 
    ['content-type' => 'text/plain']);  

If everything goes OK, we send a plain message "File uploaded" back to the client with Response::HTTP_OK response code.

FileUploader.php
<?php

namespace App\Service;

use Symfony\Component\HttpFoundation\File\Exception\FileException;
use Psr\Log\LoggerInterface;

class FileUploader 
{
    private $logger;

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

    public function upload($uploadDir, $file, $filename) 
    {
        try {

            $file->move($uploadDir, $filename);
        } catch (FileException $e){

            $this->logger->error('failed to upload image: ' . $e->getMessage());
            throw new FileException('Failed to upload file');
        }
    }
} 

The FileUploader service moves the file to the upload directory with move(). When the operation fails, we throw a FileException. This will lead to an error page being produced.

templates/bundles/TwigBundle/Exception/error.html.twig
{% extends "base.html.twig" %}
{% block title %}
    Problem detected
{% endblock %}
{% block body %}
    <div>
        <p>
            There was a problem: {{ exception.message }}
        </p>
    </div>
{% endblock %}

We override the Twig's error.html.twig template. We need to create this exact directory path: bundles/TwigBundle/Exception/ inside the templates directory. This error view will be generated for the user when a FileException occurs.

templates/base.html.twig
<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>{% block title %}Welcome!{% endblock %}</title>
        {% block stylesheets %}{% endblock %}
    </head>
    <body>
        {% block body %}{% endblock %}
        {% block javascripts %}{% endblock %}
    </body>
</html>

This is the base Twig template.

In this tutorial we have shown how to upload a file in a Symfony application.

You might also be interested in the following related tutorials: Introduction to Symfony, Symfony DBAL tutorial, Symfony form tutorial, Symfony service tutorial, Symfony validation tutorial, PHP tutorial.