Skip to content
This repository was archived by the owner on Jan 29, 2020. It is now read-only.

Add DownloadResponse Class #361

Open
wants to merge 13 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"ext-dom": "*",
"ext-libxml": "*",
"http-interop/http-factory-tests": "^0.5.0",
"mikey179/vfsstream": "^1.6",
"php-http/psr7-integration-tests": "dev-master",
"phpunit/phpunit": "^7.5.18",
"zendframework/zend-coding-standard": "~1.0.0"
Expand Down
48 changes: 47 additions & 1 deletion composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

28 changes: 28 additions & 0 deletions docs/book/v2/custom-responses.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,34 @@ $response = new Zend\Diactoros\Response\JsonResponse(
);
```

## CSV Responses

`Zend\Diactoros\Response\CsvResponse` creates a plain text response. It sets the
`Content-Type` header to `text/csv` by default:

```php
$csvContent = <<<EOF
"first","last","email","dob",
"john","citizen","[email protected]","01/01/1970",
EOF;

$response = new Zend\Diactoros\Response\CsvResponse($csvContent);
```

The constructor accepts three additional arguments:

- A status code
- A filename, if the response is to be sent as a download
- An array of supplemental headers

```php
$response = new Zend\Diactoros\Response\TextResponse(
$text,
200,
'monthly-sports-report.csv',
['X-Generated-By' => ['zend-diactoros']]
);

## Empty Responses

Many API actions allow returning empty responses:
Expand Down
84 changes: 84 additions & 0 deletions src/Response/CsvResponse.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<?php
/**
* @see https://github.com/zendframework/zend-diactoros for the canonical source repository
* @copyright Copyright (c) 2019 Zend Technologies USA Inc. (https://www.zend.com)
* @license https://github.com/zendframework/zend-diactoros/blob/master/LICENSE.md New BSD License
*/

declare(strict_types=1);

namespace Zend\Diactoros\Response;

use Psr\Http\Message\StreamInterface;
use Zend\Diactoros\Exception;
use Zend\Diactoros\Response;
use Zend\Diactoros\Stream;

use function get_class;
use function gettype;
use function is_object;
use function is_string;
use function sprintf;

/**
* CSV response.
*
* Allows creating a CSV response by passing a string to the constructor;
* by default, sets a status code of 200 and sets the Content-Type header to
* text/csv.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wouldn't be better to accept also/only array with data and process it here?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Honestly, hadn't considered that. Thanks.

*/
class CsvResponse extends Response
{
use InjectContentTypeTrait;

/**
* Create a CSV response.
*
* Produces a CSV response with a Content-Type of text/csv and a default
* status of 200.
*
* @param string|StreamInterface $text String or stream for the message body.
* @param int $status Integer status code for the response; 200 by default.
* @param string $filename
* @param array $headers Array of headers to use at initialization.
*/
public function __construct($text, int $status = 200, string $filename = '', array $headers = [])
{
if (is_string($filename) && $filename !== '') {
$headers = $this->prepareDownloadHeaders($filename, $headers);
}

parent::__construct(
$this->createBody($text),
$status,
$this->injectContentType('text/csv; charset=utf-8', $headers)
);
}

/**
* Create the CSV message body.
*
* @param string|StreamInterface $text
* @return StreamInterface
* @throws Exception\InvalidArgumentException if $text is neither a string or stream.
*/
private function createBody($text) : StreamInterface
{
if ($text instanceof StreamInterface) {
return $text;
}

if (! is_string($text)) {
throw new Exception\InvalidArgumentException(sprintf(
'Invalid CSV content (%s) provided to %s',
(is_object($text) ? get_class($text) : gettype($text)),
__CLASS__
));
}

$body = new Stream('php://temp', 'wb+');
$body->write($text);
$body->rewind();
return $body;
}
}
79 changes: 79 additions & 0 deletions src/Response/DownloadResponse.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php
/**
* @see https://github.com/zendframework/zend-diactoros for the canonical source repository
* @copyright Copyright (c) 2019 Zend Technologies USA Inc. (https://www.zend.com)
* @license https://github.com/zendframework/zend-diactoros/blob/master/LICENSE.md New BSD License
*/

declare(strict_types=1);

namespace Zend\Diactoros\Response;

use Psr\Http\Message\StreamInterface;
use Zend\Diactoros\Exception\InvalidArgumentException;
use Zend\Diactoros\Response;
use Zend\Diactoros\Stream;

use function sprintf;

/**
* Class DownloadResponse
* @package Zend\Diactoros\Response
*/
class DownloadResponse extends Response
{
use DownloadResponseTrait;

const DEFAULT_CONTENT_TYPE = 'application/octet-stream';
const DEFAULT_DOWNLOAD_FILENAME = 'download';

/**
* DownloadResponse constructor.
*
* @param string|StreamInterface $body String or stream for the message body.
* @param int $status Integer status code for the response; 200 by default.
* @param string $filename The file name to be sent with the response
* @param string $contentType The content type to be sent with the response
* @param array $headers An array of optional headers. These cannot override those set in getDownloadHeaders */
public function __construct(
$body,
int $status = 200,
string $filename = self::DEFAULT_DOWNLOAD_FILENAME,
string $contentType = self::DEFAULT_CONTENT_TYPE,
array $headers = []
) {
$this->filename = $filename;
$this->contentType = $contentType;

parent::__construct(
$this->createBody($body),
$status,
$this->prepareDownloadHeaders($headers)
);
}

/**
* @param string|StreamInterface $content
* @return StreamInterface
* @throws InvalidArgumentException if $body is neither a string nor a Stream
*/
private function createBody($content): StreamInterface
{
if ($content instanceof StreamInterface) {
return $content;
}

if (!is_string($content)) {
throw new InvalidArgumentException(sprintf(
'Invalid content (%s) provided to %s',
(is_object($content) ? get_class($content) : gettype($content)),
__CLASS__
));
}

$body = new Stream('php://temp', 'wb+');
$body->write($content);
$body->rewind();
return $body;
}
}
113 changes: 113 additions & 0 deletions src/Response/DownloadResponseTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
<?php


namespace Zend\Diactoros\Response;

use InvalidArgumentException;
use function array_keys;
use function array_merge;
use function implode;
use function in_array;

trait DownloadResponseTrait
{

/**
* @var string The filename to be sent with the response
*/
private $filename;

/**
* @var string The content type to be sent with the response
*/
private $contentType;

/**
* A list of header keys required to be sent with a download response
*
* @var array
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can have better: string[] here

*/
private $downloadResponseHeaders = [
'cache-control',
'content-description',
'content-disposition',
'content-transfer-encoding',
'expires',
'pragma'
];

/**
* Get download headers
*
* @return array
*/
private function getDownloadHeaders(): array
{
$headers = [];
$headers['cache-control'] = 'must-revalidate';
$headers['content-description'] = 'File Transfer';
$headers['content-disposition'] = sprintf('attachment; filename=%s', self::DEFAULT_DOWNLOAD_FILENAME);
$headers['content-transfer-encoding'] = 'Binary';
$headers['content-type'] = 'application/octet-stream';
$headers['expires'] = '0';
$headers['pragma'] = 'Public';

return $headers;
}

/**
* Check if the extra headers contain any of the download headers
*
* The download headers cannot be overridden.
*
* @param array $downloadHeaders
* @param array $headers
* @return bool
*/
public function overridesDownloadHeaders(array $downloadHeaders, array $headers = []) : bool
{
$overridesDownloadHeaders = false;

foreach (array_keys($headers) as $header) {
if (in_array($header, $downloadHeaders)) {
$overridesDownloadHeaders = true;
break;
}
}

return $overridesDownloadHeaders;
}

/**
* Prepare download response headers
*
* This function prepares the download response headers. It does so by:
* - Merging the optional with over the default ones (the default ones cannot be overridden)
* - Set the content-type and content-disposition headers from $filename and $contentType passed
* to the constructor.
*
* @param array $headers
* @return array
* @throws InvalidArgumentException if an attempt is made to override a default header
*/
private function prepareDownloadHeaders(array $headers = []) : array
{
if ($this->overridesDownloadHeaders($this->downloadResponseHeaders, $headers)) {
throw new InvalidArgumentException(
sprintf(
'Cannot override download headers (%s) when download response is being sent',
implode(', ', $this->downloadResponseHeaders)
)
);
}

return array_merge(
$headers,
$this->getDownloadHeaders(),
[
'content-disposition' => sprintf('attachment; filename=%s', $this->filename),
'content-type' => $this->contentType,
]
);
}
}
Loading