Serve a file stream in Symfony
Tags: symfony, php, flysystem, aws
Published: 2023-09-07 08:02:52

## Introduction

As stated in [The Twelve-Factor App](https://12factor.net/backing-services), it is a good practice to treat all backing services as attached resources. That includes binary assets too, i.e., the files our users upload to our application. Following this rule would allow our application, among other benefits, to scale horizontally.

PHP possesses a rich ecosystem, so there are libraries that allow us to work with file storages transparently. The most popular one is [Flysystem](https://flysystem.thephpleague.com/docs/), which integrates with many different storage systems, such as Amazon S3, Google Cloud Storage, Azure Blob Storage or SFTP.

However, we may face a problem when we are working with private assets and need to serve them to privileged users. As our storage is private, we can not serve the assets publicly, nor can we give them access to our storage system. We would need to serve the assets differently.

One option is to serve the users [presigned URLs](https://docs.aws.amazon.com/AmazonS3/latest/userguide/ShareObjectPreSignedURL.html). These URLs are accessible only for a specific period of time, after which they stop working. [Flysystem allows you to generate these URLs for some adapters](https://flysystem.thephpleague.com/docs/usage/temporary-urls/).

However, not all adapters support them, so in some cases we need an alternative. One option would be to temporarily download the remote file locally, then serve it as a `BinaryFileReponse`, giving it the full path within the server. Nonetheless, this option is neither optimal in time nor in memory, as the file has to be written twice, one to our server and another to the client.

As an alternative, we could use Symfony's [`StreamedResponse`](https://symfony.com/doc/current/components/http_foundation.html#streaming-a-response).

## `StreamedResponse`

Symfony offers a special kind of `Response`, `StreamedResponse`, to serve chunks of data to clients. It requires a function within that iterates over a flow of data and serves it.

For our current case, we can obtain a stream of the file within the file storage and serve it directly with a `StreamedResponse`, so our application becomes a proxy in this situation.

A stream is a `resource` type in PHP. [Flysystem has the `readStream` function](https://flysystem.thephpleague.com/docs/usage/filesystem-api/) in its API that gives you the stream for a concrete file.

Once we have the stream of a file, we can proceed to serve it using a `StreamedResponse`. There are several options to do so, all similar in terms of memory consumption and execution time.

### Option 1: `fpasstrhu`

The easiest method uses `fpasstrhu`, a language function that outputs all the remaining data in a file pointer (a stream).

An implementation could be:

```php
$stream = getStreamSomehow(); // For example, with Flysystem::readStream();

return new StreamedResponse(
    function () use ($stream) {
        fpassthru($stream);
    },
    Response::HTTP_OK,
    [
        'Content-Transfer-Encoding', 'binary',
        'Content-Type' => 'image/jpeg',
        'Content-Disposition' => 'attachment; filename="attachment.jpg"',
        'Content-Length' => fstat($stream)['size'],
    ]
);

```

### Option 2: `stream_copy_to_stream`

The second option consists of using the `stream_copy_to_stream` function, which does what its name says: copy the content of one stream into another.

In this case, we need a stream to copy to. As we are serving a response, we can use the `php://output` I/O stream.

An implementation, then, could look like this:

```php
$stream = getStreamSomehow(); // For example, with Flysystem::readStream();

return new StreamedResponse(
    function () use ($stream) {
        $outputStream = fopen('php://output', 'wb');
        
        stream_copy_to_stream($stream, $outputStream);
    },
    Response::HTTP_OK,
    [
        'Content-Transfer-Encoding', 'binary',
        'Content-Type' => 'image/jpeg',
        'Content-Disposition' => 'attachment; filename="attachment.jpg"',
        'Content-Length' => fstat($stream)['size'],
    ]
);

```

### Option 3: `fread`

The latest option uses the language function `fread` to print chunks of data from the file stream.

An implementation could be as follows, reading chunks of an arbitrary size of 1024 bytes:

```php
$stream = getStreamSomehow(); // For example, with Flysystem::readStream();

return new StreamedResponse(
    function () use ($stream) {
        while (! feof($stream)) {
            echo fread($stream, 1024);
        }
        fclose($stream);
    },
    Response::HTTP_OK,
    [
        'Content-Transfer-Encoding', 'binary',
        'Content-Type' => 'image/jpeg',
        'Content-Disposition' => 'attachment; filename="attachment.jpg"',
        'Content-Length' => fstat($stream)['size'],
    ]
);

```

## Summary

- We listed the reasons and benefits of treating our file storage as a resource.
- We saw the problem of serving files privately from a file storage service.
- We explained the alternative of generating temporary URLs, which does not work in all cases.
- Finally, we implemented three alternatives to serve files from a file storage system, taking advantage of Symfony's `StreamedResponse`.