How to force file download from controller using BinaryFileResponse

Letting users download a file is a common task.

Sometimes a simple direct link is sufficient, but if you want to implement something a little more sophisticated you could serve the file without directly exposing it, or with another filename, or execute some logic before serving the file, or again log the action in a database and then serve the file.

Symfony >= 2.2 provides the BinayFileResponse() class for this purpose; this guide is a simple but working implementation.

This method consists in creating a single action in a Symfony Controller.

Return a BinaryFileResponse object

A new action will be created. It will receive the parameter filename and return a BinaryFileResponse response.

Uses the component FileSystem to check the existence of a file (thanks @flo_schield).

Here is the code:

<?php

use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\ResponseHeaderBag;

class FooController extends Controller
{
    /**
     * Serve a file by forcing the download
     *
     * @Route("/download/{filename}", name="download_file", requirements={"filename": ".+"})
     */
    public function downloadFileAction($filename)
    {
        /**
         * $basePath can be either exposed (typically inside web/)
         * or "internal"
         */
        $basePath = $this->container->getParameter('kernel.root_dir').'/Resources/my_custom_folder';

        $filePath = $basePath.'/'.$filename;

        // check if file exists
        $fs = new FileSystem();
        if (!$fs->exists($filePath)) {
            throw $this->createNotFoundException();
        }

        // prepare BinaryFileResponse
        $response = new BinaryFileResponse($filePath);
        $response->trustXSendfileTypeHeader();
        $response->setContentDisposition(
            ResponseHeaderBag::DISPOSITION_INLINE,
            $filename,
            iconv('UTF-8', 'ASCII//TRANSLIT', $filename)
        );

        return $response;
    }
}

Note: to use BinaryFileResponse you will need Symfony >= 2.2.0

Hints

  1. Be sure not to have RewriteCond %{REQUEST_FILENAME} !-f in .htaccess (thanks Eric Grivilers)

  2. Outside of Symfony, you need to call prepare(Request $request) to make the BinaryFileResponse work (thanks Julius Beckmann @h4cc):

    $response = new Symfony\Component\HttpFoundation\BinaryFileResponse($filePath);
    $response->prepare(Symfony\Component\HttpFoundation\Request::createFromGlobals());
    $response->send();
    

Notes

The download_file route accepts as parameter the filename of the file you will serve to the user. Obviously you are free to implement a smarter way to compose the url, as well as to serve a better filename.

Alternative solution

If for some reason this is not working, here is the raw PHP code to serve a file:

        [...]
        // check if file exists
        [...]

        header('Content-Description: File Transfer');
        header('Content-Type: application/pdf');
        header('Content-Disposition: inline; filename=' . basename($filename));
        header('Content-Transfer-Encoding: binary');
        header('Expires: 0');
        header('Cache-Control: must-revalidate');
        header('Pragma: public');
        header('Content-Length: ' . filesize($filePath));
        @ob_clean();
        flush();
        readfile($filePath);
    }
}

Resources

A useful resource about BinaryFileResponse (and nginx) is http://developmentwithart.com/2012/08/29/how-to-serve-protected-files-in-Symfony2-using-X-Sendfile/


  • 2017-06-27 Emanuele Gaspari Castelletti

    thanks, and fixed now!

  • 2017-06-11 Artemio Vegas

    Hi all. It is nice article. But you made a little typo in your code.
    Line 26: if (!$fs->exists($filepath)) {

    A Must be : if (!$fs->exists($filePath)) {

    Best regards, Artemio

  • 2016-11-10 Leonid Vilents

    Hi there,
    I utilized this code and also compared it to other suggestions in various forums, and looking at the Profiler, I am getting the right response; but no file view is triggered and no download is prompted.
    I checked my .htaccess, the critical code is not appearing.
    Anything I might have overseen?
    [...]
    $file = $this->getTargetFile($filename, $permission); //gets the full filepath

    $fs = new Filesystem();
    //Does the file exist?
    if(!$fs->exists($file)){
    throw new FileNotFoundException($file);
    }

    $response = new BinaryFileResponse($file);
    $response->headers->set('Content-Type', 'application/pdf');
    $response->setContentDisposition(
    ResponseHeaderBag::DISPOSITION_INLINE,
    $filename
    );

    return $response;

  • 2016-02-11 aguidis

    Thanks for the article. First, I didn't have any download, my browser displayed directly the file. In order to "force" the download of the file I changed the Content-disposition header value to ResponseHeaderBag::DISPOSITION_ATTACHMENT and it worked

  • 2015-08-19 Sir. Lie

    Thanks alot. u save my life.. wkwk
    Give u big hug {{}}

  • 2015-05-27 Florent SCHILDKNECHT

    There might be a debate about the utility to use an OOP approach to replace just one single PHP native method, but I personally prefer it :)

    Filesystem has been introduced in 2.1, so it should not be a problem, BinaryFileResponse is up to 2.2.

    Thanks again for your guide, much appreciated!

  • 2015-05-26 Emanuele Gaspari Castelletti

    Florent SCHILDKNECHT updated, thanks!

  • 2015-05-26 Emanuele Gaspari Castelletti

    the only reason is that when I wrote this guide I didn't know the existence of that function, but it's a good hint

    thanks, I'll update this soon

  • 2015-05-26 Florent SCHILDKNECHT

    Nice :)

    Do you have any special reason to use the native file_exists() method instead of the Symfony FileSystem ?

    use Symfony\Component\Filesystem\Filesystem;

    $fileSystem = new FileSystem();

    if (!$fileSystem->exists($filepath)) {
    throw $this->createNotFoundException();
    }

  • 2014-03-19 Emanuele Gaspari Castelletti

    Thanks for this, it's been included right now