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/