This repository was archived by the owner on Jan 29, 2020. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 150
Add DownloadResponse Class #361
Open
settermjd
wants to merge
13
commits into
zendframework:develop
Choose a base branch
from
settermjd:add-csv-response-class
base: develop
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
a872d18
Merge branch 'qa/383'
michalbundyra 2c9cbe9
Create a CSV Response class
settermjd b05ec0c
Add a trait for managing download responses
settermjd 62b2605
Refactor the CsvResponse to support sending a downloadable response
settermjd 20b01d9
Update documentation to add document new CsvResponse class
settermjd 411aed2
Rename the DownloadResponse trait to a class
settermjd 2088793
Add vfsstream as a development dependency
settermjd aecca69
Test that a DownloadResponse can be created from a valid CSV string
settermjd d55873a
Refactor to be able to use stream or filename for response body
settermjd 5854462
Set the content-type and -disposition headers from constructor arguments
settermjd 56ddc75
Minor docblock cleanup
settermjd 071b3bd
Split core of DownloadResponse back into a DownloadResponseTrait
settermjd 3ef6022
Remove download functionality from CsvResponse
settermjd File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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: | ||
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. | ||
*/ | ||
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 !== '') { | ||
settermjd marked this conversation as resolved.
Show resolved
Hide resolved
|
||
$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; | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we can have better: |
||
*/ | ||
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)) { | ||
settermjd marked this conversation as resolved.
Show resolved
Hide resolved
|
||
$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, | ||
] | ||
); | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.