diff --git a/src/Exception/JsonException.php b/src/Exception/JsonException.php new file mode 100644 index 0000000..b9765ba --- /dev/null +++ b/src/Exception/JsonException.php @@ -0,0 +1,51 @@ + + * @copyright Copyright (c) 2018, Anatoly Fenric + * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-message + */ + +namespace Sunrise\Http\Message\Exception; + +/** + * Import classes + */ +use RuntimeException; + +/** + * Import functions + */ +use function json_last_error; +use function json_last_error_msg; + +/** + * Import constants + */ +use const JSON_ERROR_NONE; + +/** + * JsonException + */ +class JsonException extends RuntimeException +{ + + /** + * @return void + * + * @throws self + */ + public static function assert() : void + { + $code = json_last_error(); + + if (JSON_ERROR_NONE === $code) { + return; + } + + throw new self(json_last_error_msg(), $code); + } +} diff --git a/src/Message.php b/src/Message.php index 6921b26..7939b80 100644 --- a/src/Message.php +++ b/src/Message.php @@ -95,8 +95,7 @@ public function getHeader($name) : array { $name = $this->normalizeHeaderName($name); - if (empty($this->headers[$name])) - { + if (empty($this->headers[$name])) { return []; } @@ -110,8 +109,7 @@ public function getHeaderLine($name) : string { $name = $this->normalizeHeaderName($name); - if (empty($this->headers[$name])) - { + if (empty($this->headers[$name])) { return ''; } @@ -121,7 +119,7 @@ public function getHeaderLine($name) : string /** * {@inheritDoc} */ - public function withHeader($name, $value) : MessageInterface + public function withHeader($name, $value, bool $append = false) : MessageInterface { $this->validateHeaderName($name); $this->validateHeaderValue($value); @@ -129,6 +127,10 @@ public function withHeader($name, $value) : MessageInterface $name = $this->normalizeHeaderName($name); $value = $this->normalizeHeaderValue($value); + if (isset($this->headers[$name]) && $append) { + $value = \array_merge($this->headers[$name], $value); + } + $clone = clone $this; $clone->headers[$name] = $value; @@ -141,22 +143,28 @@ public function withHeader($name, $value) : MessageInterface */ public function withAddedHeader($name, $value) : MessageInterface { - $this->validateHeaderName($name); - $this->validateHeaderValue($value); + return $this->withHeader($name, $value, true); + } - $name = $this->normalizeHeaderName($name); - $value = $this->normalizeHeaderValue($value); + /** + * Returns a new instance with the given headers + * + * @param iterable $headers + * @param bool $append + * + * @return MessageInterface + * + * @since 1.3.0 + */ + public function withMultipleHeaders(iterable $headers, bool $append = false) : MessageInterface + { + $result = clone $this; - if (! empty($this->headers[$name])) - { - $value = \array_merge($this->headers[$name], $value); + foreach ($headers as $name => $value) { + $result = $result->withHeader($name, $value, $append); } - $clone = clone $this; - - $clone->headers[$name] = $value; - - return $clone; + return $result; } /** @@ -207,13 +215,14 @@ public function withBody(StreamInterface $body) : MessageInterface */ protected function validateProtocolVersion($protocolVersion) : void { - if (! \is_string($protocolVersion)) - { + if (! \is_string($protocolVersion)) { throw new \InvalidArgumentException('HTTP protocol version must be a string'); } - else if (! \preg_match('/^\d(?:\.\d)?$/', $protocolVersion)) - { - throw new \InvalidArgumentException(\sprintf('The given protocol version "%s" is not valid', $protocolVersion)); + + if (! \preg_match('/^\d(?:\.\d)?$/', $protocolVersion)) { + throw new \InvalidArgumentException( + \sprintf('The given protocol version "%s" is not valid', $protocolVersion) + ); } } @@ -230,13 +239,14 @@ protected function validateProtocolVersion($protocolVersion) : void */ protected function validateHeaderName($headerName) : void { - if (! \is_string($headerName)) - { + if (! \is_string($headerName)) { throw new \InvalidArgumentException('Header name must be a string'); } - else if (! \preg_match(HeaderInterface::RFC7230_TOKEN, $headerName)) - { - throw new \InvalidArgumentException(\sprintf('The given header name "%s" is not valid', $headerName)); + + if (! \preg_match(HeaderInterface::RFC7230_TOKEN, $headerName)) { + throw new \InvalidArgumentException( + \sprintf('The given header name "%s" is not valid', $headerName) + ); } } @@ -253,25 +263,23 @@ protected function validateHeaderName($headerName) : void */ protected function validateHeaderValue($headerValue) : void { - if (\is_string($headerValue)) - { + if (\is_string($headerValue)) { $headerValue = [$headerValue]; } - if (! \is_array($headerValue) || [] === $headerValue) - { + if (! \is_array($headerValue) || [] === $headerValue) { throw new \InvalidArgumentException('Header value must be a string or not an empty array'); } - foreach ($headerValue as $oneOf) - { - if (! \is_string($oneOf)) - { + foreach ($headerValue as $oneOf) { + if (! \is_string($oneOf)) { throw new \InvalidArgumentException('Header value must be a string or an array containing only strings'); } - else if (! \preg_match(HeaderInterface::RFC7230_FIELD_VALUE, $oneOf)) - { - throw new \InvalidArgumentException(\sprintf('The given header value "%s" is not valid', $oneOf)); + + if (! \preg_match(HeaderInterface::RFC7230_FIELD_VALUE, $oneOf)) { + throw new \InvalidArgumentException( + \sprintf('The given header value "%s" is not valid', $oneOf) + ); } } } @@ -303,7 +311,6 @@ protected function normalizeHeaderName($headerName) : string protected function normalizeHeaderValue($headerValue) : array { $headerValue = (array) $headerValue; - $headerValue = \array_values($headerValue); return $headerValue; diff --git a/src/ResponseFactory.php b/src/ResponseFactory.php index 177d37d..8206231 100644 --- a/src/ResponseFactory.php +++ b/src/ResponseFactory.php @@ -53,8 +53,10 @@ public function createResponse(int $code = 200, string $reasonPhrase = '') : Res */ public function createHtmlResponse(int $status, $content) : ResponseInterface { + $content = (string) $content; + $body = (new StreamFactory)->createStream(); - $body->write((string) $content); + $body->write($content); return (new Response) ->withStatus($status) @@ -63,22 +65,32 @@ public function createHtmlResponse(int $status, $content) : ResponseInterface } /** - * Creates a JSON response object + * Creates a JSON response instance * * @param int $status * @param mixed $payload * @param int $options + * @param int $depth * * @return ResponseInterface + * + * @throws Exception\JsonException */ - public function createJsonResponse(int $status, $payload, int $options = 0) : ResponseInterface + public function createJsonResponse(int $status, $payload, int $options = 0, int $depth = 512) : ResponseInterface { + // clears a previous error... + json_encode(null); + + $content = json_encode($payload, $options, $depth); + + Exception\JsonException::assert(); + $body = (new StreamFactory)->createStream(); - $body->write(json_encode($payload, $options)); + $body->write($content); return (new Response) ->withStatus($status) - ->withHeader('Content-Type', 'application/json') + ->withHeader('Content-Type', 'application/json; charset=utf-8') ->withBody($body); } } diff --git a/tests/MessageTest.php b/tests/MessageTest.php index 90e7634..d1d5af9 100644 --- a/tests/MessageTest.php +++ b/tests/MessageTest.php @@ -180,6 +180,27 @@ public function testAddInvalidHeaderValueInArray($headerValue) (new Message)->withAddedHeader('x-foo', ['bar', $headerValue, 'baz']); } + public function testWithMultipleHeaders() : void + { + $message = new Message(); + + $subject = $message + ->withMultipleHeaders(['x-foo' => 'bar']) + ->withMultipleHeaders(['x-foo' => 'baz']); + + $this->assertNotSame($subject, $message); + $this->assertCount(0, $message->getHeaders()); + $this->assertSame(['x-foo' => ['baz']], $subject->getHeaders()); + + $subject = $message + ->withMultipleHeaders(['x-foo' => 'bar'], true) + ->withMultipleHeaders(['x-foo' => 'baz'], true); + + $this->assertNotSame($subject, $message); + $this->assertCount(0, $message->getHeaders()); + $this->assertSame(['x-foo' => ['bar', 'baz']], $subject->getHeaders()); + } + public function testDeleteHeader() { $mess = (new Message)->withHeader('x-foo', 'bar'); diff --git a/tests/ResponseFactoryTest.php b/tests/ResponseFactoryTest.php index 0b68470..8ba4f01 100644 --- a/tests/ResponseFactoryTest.php +++ b/tests/ResponseFactoryTest.php @@ -9,6 +9,7 @@ use Psr\Http\Message\ResponseFactoryInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\StreamInterface; +use Sunrise\Http\Message\Exception\JsonException; use Sunrise\Http\Message\ResponseFactory; /** @@ -89,7 +90,19 @@ public function testCreateJsonResponse() : void $this->assertInstanceOf(ResponseInterface::class, $response); $this->assertSame(400, $response->getStatusCode()); - $this->assertSame('application/json', $response->getHeaderLine('Content-Type')); + $this->assertSame('application/json; charset=utf-8', $response->getHeaderLine('Content-Type')); $this->assertSame(json_encode($payload, $options), (string) $response->getBody()); } + + /** + * @return void + */ + public function testCreateResponseWithInvalidJson() : void + { + $this->expectException(JsonException::class); + $this->expectExceptionMessage('Maximum stack depth exceeded'); + + $response = (new ResponseFactory) + ->createJsonResponse(200, [[]], 0, 1); + } }