Response Objects for File Downloads

Update September 5: I’ve released a basic package based on the subject of this post.

It is not an uncommon requirement in a PHP web application to send files to the browser instead of web pages. You’ve likely, in the past, messed around with multiple header statements until you wrangled the browser into coughing up a file.

I recently came across this requirement once again and opted to develop some classes that would neatly handle the situation. The idea was to create something that would be reusable across the project in case of future file download requirements. The code could also easily be refactored out into a standalone package if it ever became necessary.

The first step is to use a proper HTTP response object, instead of messing about with calls to header and echo. The ubiquitous Symfony HttpFoundation component is great for this, even if you’re not using it already for your more regular responses. You can install that via Composer with the following composer.json:

{
    "require": {
        "symfony/http-foundation": "2.6.*"
    }
}

Check Packagist for the latest release of the package.

Setting up the Symfony response object to force a file to download is straight-forward enough. It does involve a lot of boilerplate to get going though, so I decided to encapsulate this setup in a class. The new response object, FileResponse, has the Symfony response as a component rather than extending it.

use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\ResponseHeaderBag;

class FileResponse
{
    protected $response;

    public function __construct($filename, $contents)
    {
        $this->response = new Response(
            $contents,
            Response::HTTP_OK,
            array(
                'Content-Type' => 'text/plain',
                'Pragma' => 'no-cache',
                'Expires' => 0,
            )
        );

        $disposition = $this->response->headers->makeDisposition(
            ResponseHeaderBag::DISPOSITION_ATTACHMENT,
            $filename
        );

        $this->response->headers->set('Content-Disposition', $disposition);
    }

    public function send()
    {
        $this->response->send();
    }

    public function getResponse()
    {
        return $this->response;
    }
}

The constructor takes a filename and the content of the file as strings. The send method delegates to the Symfony response object’s method, as this is most common use case for our object once it’s constructed. The Symfony object is available via a getter in case any further manipulation is required.

You’ll notice that the Content-Type header is set for plain text. This is fine for sending simple text files to the browser, but we will likely need to send other file types at other times. Let’s refactor the constructor to allow any MIME type.

public function __construct($contentType, $filename, $contents)
{
    $this->response = new Response(
        $contents,
        Response::HTTP_OK,
        array(
            'Content-Type' => $contentType,
            'Pragma' => 'no-cache',
            'Expires' => 0,
        )
    );
    
    $disposition = $this->response->headers->makeDisposition(
        ResponseHeaderBag::DISPOSITION_ATTACHMENT,
        $filename
    );

    $this->response->headers->set('Content-Disposition', $disposition);
}

It’s a relatively straight-forward change. The constructor now takes another parameter that will set the Content-Type of the response.

This is flexible, but I personally don’t like that any client code using this interface will be required to explicitly include the actual MIME type string. That may be fine for text/plain or image/jpeg, but it looks a bit messy with application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.

I decided to make the FileResponse class abstract and use inheritance to create subclasses that encapsulate the MIME type string and any additional headers.

use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\ResponseHeaderBag;

abstract class FileResponse
{
    protected $response;

    public function __construct($filename, $contents, array $additionalHeaders)
    {
        $this->response = new Response(
            $contents,
            Response::HTTP_OK,
            array_merge(array(
                'Pragma' => 'no-cache',
                'Expires' => 0,
            ), $additionalHeaders)
        );

        $disposition = $this->response->headers->makeDisposition(
            ResponseHeaderBag::DISPOSITION_ATTACHMENT,
            $filename
        );

        $this->response->headers->set('Content-Disposition', $disposition);
    }

    public function send()
    {
        $this->response->send();
    }

    public function getResponse()
    {
        return $this->response;
    }
}

The default headers can be overridden in a subclass, thanks to the behaviour of array_merge().

I had two scenarios to deal with in the project. One required text files to be downloaded and the other zip files. The two subclasses for the response objects look like this:

class TextFileResponse extends FileResponse
{
    public function __construct($filename, $contents)
    {
        parent::__construct($filename, $contents, array(
            'Content-Type' => 'text/plain'
        ));
    }
}

class ZipFileResponse extends FileResponse
{
    public function __construct($filename, $contents)
    {
        parent::__construct($filename, $contents, array(
            'Content-Type' => 'application/zip'
        ));
    }
}

The client code now only has to create a ZipFileResponse object, passing it a filename and contents. If a requirement for downloading gifs arose, a new class with less than a dozen lines would be all that’s needed to achieve this.

These classes are by no means finished of course. For instance, the FileResponse class could ensure that the Content-Type header is set or provide a default value for it. There’s also scope for automatically appending the correct file extension to the given filename.