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

Commit

Permalink
Merge branch 'feature/61' into develop
Browse files Browse the repository at this point in the history
Close #61
  • Loading branch information
weierophinney committed Jun 24, 2015
2 parents 12117d8 + 57c9caa commit bfd0e19
Show file tree
Hide file tree
Showing 10 changed files with 403 additions and 162 deletions.
29 changes: 18 additions & 11 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,24 @@ All notable changes to this project will be documented in this file, in reverse

### Added

- [#52](https://github.com/zendframework/zend-diactoros/pull/52) adds
`Zend\Diactoros\Response\StringResponse`, a factory class for generating
HTML or JSON responses. It contains the static methods:
- `html($html, $status = 200, array $headers = [])`
- `json($data, $status = 200, array $headers = [])`
- [#58](https://github.com/zendframework/zend-diactoros/pull/58) adds
`Zend\Diactoros\Response\EmptyResponse`, a `Zend\Diactoros\Response` extension
for quickly creating empty, read-only responses.
- [#59](https://github.com/zendframework/zend-diactoros/pull/59) adds
`Zend\Diactoros\Response\RedirectResponse`, a `Zend\Diactoros\Response` extension
for quickly creating redirect responses.
- [#52](https://github.com/zendframework/zend-diactoros/pull/52),
[#58](https://github.com/zendframework/zend-diactoros/pull/58),
[#59](https://github.com/zendframework/zend-diactoros/pull/59), and
[#61](https://github.com/zendframework/zend-diactoros/pull/61) create several
custom response types for simplifying response creation:

- `Zend\Diactoros\Response\HtmlResponse` accepts HTML content via its
constructor, and sets the `Content-Type` to `text/html`.
- `Zend\Diactoros\Response\JsonResponse` accepts data to serialize to JSON via
its constructor, and sets the `Content-Type` to `application/json`.
- `Zend\Diactoros\Response\EmptyResponse` allows creating empty, read-only
responses, with a default status code of 204.
- `Zend\Diactoros\Response\RedirectResponse` allows specifying a URI for the
`Location` header in the constructor, with a default status code of 302.

Each also accepts an optional status code, and optional headers (which can
also be used to provide an alternate `Content-Type` in the case of the HTML
and JSON responses).

### Deprecated

Expand Down
74 changes: 50 additions & 24 deletions doc/book/custom-responses.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,44 +20,63 @@ Some standard use cases, however, make this un-wieldy:
- Returning a redirect response; in this case, you likely just want to specify the target for the
`Location` header, and optionally the status code.

Starting with version 1.1, Diactoros offers several custom response types and factories for
simplifying these common tasks.
Starting with version 1.1, Diactoros offers several custom response types for simplifying these
common tasks.

## String responses
## HTML Responses

`Zend\Diactoros\Response\StringResponse` provides factory methods for two standard string response
types: HTML and JSON.

### HTML

The `html()` factory will create a response with the provided HTML as a payload, setting the
`Zend\Diactoros\Response\HtmlResponse` allows specifying HTML as a payload, and sets the
`Content-Type` header to `text/html` by default:

```php
$response = StringResponse::html($htmlContent);
$response = new HtmlResponse($htmlContent);
```

The factory allows passing two additional arguments: a status code, and an array of headers. These
allow you to further seed the initial state of the response.
The constructor allows passing two additional arguments: a status code, and an array of headers.
These allow you to further seed the initial state of the response, as well as to override the
`Content-Type` header if desired:

```php
$response = new HtmlResponse($htmlContent, 200, [ 'Content-Type' => ['application/xhtml+xml']]);
```

Headers must be in the same format as you would provide to the
[Response constructor][api.md#response-message].

### JSON
The `json()` factory accepts a data structure to convert to JSON, and returns a response with the
JSON content and the `Content-Type` header set to `application/json`:
## JSON Responses

`Zend\Diactoros\Response\JsonResponse` accepts a data structure to convert to JSON, and sets
the `Content-Type` header to `application/json`:

```php
$response = StringResponse::json($data);
$response = new JsonResponse($data);
```

If a null value is provide, an empty JSON object is used for the content. Scalar data is cast to an
array before serialization. If providing an object, we recommend implementing
[JsonSerializable](http://php.net/JsonSerializable) to ensure your object is correctly serialized.

Just like the `html()` factory, the `json()` factory allows passing two additional arguments — a
Just like the `HtmlResponse`, the `JsonResponse` allows passing two additional arguments — a
status code, and an array of headers — to allow you to further seed the initial state of the
response.
response:

```php
$response = new JsonResponse($data, 200, [ 'Content-Type' => ['application/hal+json']]);
```

Finally, `JsonResponse` allows a fourth optional argument, the flags to provide to `json_encode()`.
By default, these are set to `JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT` (integer
15), providing [RFC 4627](http://tools.ietf.org/html/rfc4627) compliant JSON capable of embedding in
HTML. If you want to specify a different set of flags, use the fourth constructor argument:

```php
$response = new JsonResponse(
$data,
200,
[],
JSON_PRETTY_PRINT | JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT
);
```

## Empty Responses

Expand Down Expand Up @@ -154,13 +173,20 @@ extensions; this is done to protect encapsulation and ensure consistency of oper
instances.

If you don't want to go the extension route (perhaps you don't want another `ResponseInterface`
implementation within your object graph) you can instead create a factory.
[StringResponse](https://github.com/zendframework/zend-diactoros/tree/master/src/Response/StringResponse.php)
provides one such example. We recommend the following semantics:
implementation within your object graph) you can instead create a factory. As an example:

```php
function ($dataOrMessage, $status = 200, array $headers = []);
$plainTextResponse = function ($text, $status = 200, array $headers = []) {
$response = new Response('php://temp', $status, $headers);
$response->getBody()->write($text);
if (! $response->hasHeader('Content-Type')) {
$response = $response->withHeader('Content-Type', 'text/plain');
}
return $response;
};

$response = $plainTextResponse('Hello, world!');
```

These ensure consistency of factories, and allow consumers to provide the status and
instance-specific headers on creation. (Obviously, specify different defaults as necessary.)
We recommend following the semantic of providing the status and headers as the final two arguments
for any factory or custom response extensions.
6 changes: 6 additions & 0 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,10 @@
<directory>./test</directory>
</testsuite>
</testsuites>

<filter>
<whitelist processUncoveredFilesFromWhitelist="true">
<directory suffix=".php">src</directory>
</whitelist>
</filter>
</phpunit>
73 changes: 73 additions & 0 deletions src/Response/HtmlResponse.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<?php
/**
* Zend Framework (http://framework.zend.com/)
*
* @see http://github.com/zendframework/zend-diactoros for the canonical source repository
* @copyright Copyright (c) 2015 Zend Technologies USA Inc. (http://www.zend.com)
* @license https://github.com/zendframework/zend-diactoros/blob/master/LICENSE.md New BSD License
*/

namespace Zend\Diactoros\Response;

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

/**
* HTML response.
*
* Allows creating a response by passing an HTML string to the constructor;
* by default, sets a status code of 200 and sets the Content-Type header to
* text/html.
*/
class HtmlResponse extends Response
{
use InjectContentTypeTrait;

/**
* Create an HTML response.
*
* Produces an HTML response with a Content-Type of text/html and a default
* status of 200.
*
* @param string|StreamInterface $html HTML or stream for the message body.
* @param int $status Integer status code for the response; 200 by default.
* @param array $headers Array of headers to use at initialization.
* @throws InvalidArgumentException if $html is neither a string or stream.
*/
public function __construct($html, $status = 200, array $headers = [])
{
parent::__construct(
$this->createBody($html),
$status,
$this->injectContentType('text/html', $headers)
);
}

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

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

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

namespace Zend\Diactoros\Response;

trait InjectContentTypeTrait
{
/**
* Inject the provided Content-Type, if none is already present.
*
* @param string $contentType
* @param array $headers
* @return array Headers with injected Content-Type
*/
private function injectContentType($contentType, array $headers)
{
$hasContentType = array_reduce(array_keys($headers), function ($carry, $item) {
return $carry ?: (strtolower($item) === 'content-type');
}, false);

if (! $hasContentType) {
$headers['content-type'] = [$contentType];
}

return $headers;
}
}
96 changes: 96 additions & 0 deletions src/Response/JsonResponse.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
<?php
/**
* Zend Framework (http://framework.zend.com/)
*
* @see http://github.com/zendframework/zend-diactoros for the canonical source repository
* @copyright Copyright (c) 2015 Zend Technologies USA Inc. (http://www.zend.com)
* @license https://github.com/zendframework/zend-diactoros/blob/master/LICENSE.md New BSD License
*/

namespace Zend\Diactoros\Response;

use ArrayObject;
use InvalidArgumentException;
use Zend\Diactoros\Response;
use Zend\Diactoros\Stream;

/**
* HTML response.
*
* Allows creating a response by passing an HTML string to the constructor;
* by default, sets a status code of 200 and sets the Content-Type header to
* text/html.
*/
class JsonResponse extends Response
{
use InjectContentTypeTrait;

/**
* Create a JSON response with the given array of data.
*
* If the data provided is null, an empty ArrayObject is used; if the data
* is scalar, it is cast to an array prior to serialization.
*
* Default JSON encoding is performed with the following options, which
* produces RFC4627-compliant JSON, capable of embedding into HTML.
*
* - JSON_HEX_TAG
* - JSON_HEX_APOS
* - JSON_HEX_AMP
* - JSON_HEX_QUOT
*
* @param string $data Data to convert to JSON.
* @param int $status Integer status code for the response; 200 by default.
* @param array $headers Array of headers to use at initialization.
* @param int $encodingOptions JSON encoding options to use.
* @throws InvalidArgumentException if unable to encode the $data to JSON.
*/
public function __construct($data, $status = 200, array $headers = [], $encodingOptions = 15)
{
$body = new Stream('php://temp', 'wb+');
$body->write($this->jsonEncode($data, $encodingOptions));

$headers = $this->injectContentType('application/json', $headers);

parent::__construct($body, $status, $headers);
}

/**
* Encode the provided data to JSON.
*
* @param mixed $data
* @param int $encodingOptions
* @return string
* @throws InvalidArgumentException if unable to encode the $data to JSON.
*/
private function jsonEncode($data, $encodingOptions)
{
if (is_resource($data)) {
throw new InvalidArgumentException('Cannot JSON encode resources');
}

if ($data === null) {
// Use an ArrayObject to force an empty JSON object.
$data = new ArrayObject();
}

if (is_scalar($data)) {
$data = (array) $data;
}

// Clear json_last_error()
json_encode(null);

$json = json_encode($data, $encodingOptions);

if (JSON_ERROR_NONE !== json_last_error()) {
throw new InvalidArgumentException(sprintf(
'Unable to encode data to JSON in %s: %s',
__CLASS__,
json_last_error_msg()
));
}

return $json;
}
}
2 changes: 1 addition & 1 deletion src/Response/RedirectResponse.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public function __construct($uri, $status = 302, array $headers = [])
));
}

$headers['location'] = [$uri];
$headers['location'] = [(string) $uri];
parent::__construct('php://temp', $status, $headers);
}
}
Loading

0 comments on commit bfd0e19

Please sign in to comment.