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

Commit

Permalink
Merge 56ddc75 into c0ebb95
Browse files Browse the repository at this point in the history
  • Loading branch information
settermjd committed Dec 27, 2019
2 parents c0ebb95 + 56ddc75 commit 04235e0
Show file tree
Hide file tree
Showing 7 changed files with 631 additions and 1 deletion.
1 change: 1 addition & 0 deletions composer.json
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
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","john.citizen@afakeemailaddress.com","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
104 changes: 104 additions & 0 deletions src/Response/CsvResponse.php
@@ -0,0 +1,104 @@
<?php
/**
* @see https://github.com/zendframework/zend-diactoros for the canonical source repository
* @copyright Copyright (c) 2015-2018 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.
*/
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;
}

/**
* Get download headers
*
* @param string $filename
* @return array
*/
private function getDownloadHeaders(string $filename): array
{
$headers = [];
$headers['cache-control'] = ['must-revalidate'];
$headers['content-description'] = ['File Transfer'];
$headers['content-disposition'] = [sprintf('attachment; filename=%s', $filename)];
$headers['content-transfer-encoding'] = ['Binary'];
$headers['content-type'] = ['text/csv; charset=utf-8'];
$headers['expires'] = ['0'];
$headers['pragma'] = ['Public'];

return $headers;
}
}
180 changes: 180 additions & 0 deletions src/Response/DownloadResponse.php
@@ -0,0 +1,180 @@
<?php
/**
* @see https://github.com/zendframework/zend-diactoros for the canonical source repository
* @copyright Copyright (c) 2015-2018 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 array_keys;
use function array_merge;
use function implode;
use function in_array;
use function sprintf;

/**
* Class DownloadResponse
* @package Zend\Diactoros\Response
*/
class DownloadResponse extends Response
{
const DEFAULT_CONTENT_TYPE = 'application/octet-stream';
const DEFAULT_DOWNLOAD_FILENAME = 'download';

/**
* A list of header keys required to be sent with a download response
*
* @var array
*/
private $downloadResponseHeaders = [
'cache-control',
'content-description',
'content-disposition',
'content-transfer-encoding',
'expires',
'pragma'
];

/**
* @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;

/**
* 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)
);
}

/**
* 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,
]
);
}

/**
* @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;
}
}

0 comments on commit 04235e0

Please sign in to comment.