diff --git a/CHANGELOG.md b/CHANGELOG.md
index 14c0e60a..64626ed7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
diff --git a/doc/book/custom-responses.md b/doc/book/custom-responses.md
index a69414d1..b2bd8584 100644
--- a/doc/book/custom-responses.md
+++ b/doc/book/custom-responses.md
@@ -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
@@ -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.
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index e27fce61..6de09f62 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -4,4 +4,10 @@
./test
+
+
+
+ src
+
+
diff --git a/src/Response/HtmlResponse.php b/src/Response/HtmlResponse.php
new file mode 100644
index 00000000..d298ee01
--- /dev/null
+++ b/src/Response/HtmlResponse.php
@@ -0,0 +1,73 @@
+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;
+ }
+}
diff --git a/src/Response/InjectContentTypeTrait.php b/src/Response/InjectContentTypeTrait.php
new file mode 100644
index 00000000..00fd2171
--- /dev/null
+++ b/src/Response/InjectContentTypeTrait.php
@@ -0,0 +1,33 @@
+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;
+ }
+}
diff --git a/src/Response/RedirectResponse.php b/src/Response/RedirectResponse.php
index 621736f9..26911f0a 100644
--- a/src/Response/RedirectResponse.php
+++ b/src/Response/RedirectResponse.php
@@ -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);
}
}
diff --git a/src/Response/StringResponse.php b/src/Response/StringResponse.php
deleted file mode 100644
index db79eb44..00000000
--- a/src/Response/StringResponse.php
+++ /dev/null
@@ -1,94 +0,0 @@
-getBody()->write($body);
-
- if (empty($contentType) || $response->hasHeader('content-type')) {
- return $response;
- }
-
- return $response->withHeader('content-type', $contentType);
- }
-}
diff --git a/test/Response/HtmlResponseTest.php b/test/Response/HtmlResponseTest.php
new file mode 100644
index 00000000..d2e43e15
--- /dev/null
+++ b/test/Response/HtmlResponseTest.php
@@ -0,0 +1,82 @@
+Uh oh not found