From 1988244ef01542b248daa9b5545b9c545481a053 Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Sun, 24 Oct 2021 17:38:01 +0500 Subject: [PATCH 01/15] improve tests --- tests/MessageTest.php | 237 +++++++++++----------------------- tests/RequestFactoryTest.php | 4 +- tests/RequestTest.php | 39 +++++- tests/ResponseFactoryTest.php | 25 ++-- tests/ResponseTest.php | 13 +- 5 files changed, 132 insertions(+), 186 deletions(-) diff --git a/tests/MessageTest.php b/tests/MessageTest.php index ed96697..f6c22c3 100644 --- a/tests/MessageTest.php +++ b/tests/MessageTest.php @@ -1,4 +1,6 @@ -assertInstanceOf(MessageInterface::class, $mess); } + /** + * @return void + */ + public function testConstructor() : void + { + $headers = ['X-Foo' => ['bar', 'baz'], 'X-Bar' => ['baz']]; + $body = (new StreamFactory)->createStreamFromResource(\STDOUT); + $protocol = '2.0'; + + $mess = new Message($headers, $body, $protocol); + + $this->assertSame($headers, $mess->getHeaders()); + $this->assertSame($body, $mess->getBody()); + $this->assertSame($protocol, $mess->getProtocolVersion()); + } + /** * @return void */ @@ -63,22 +79,22 @@ public function testInvalidProtocolVersion($protocolVersion) : void public function testSetHeader() : void { $mess = new Message(); - $copy = $mess->withHeader('x-foo', 'bar'); + $copy = $mess->withHeader('X-Foo', 'bar'); $this->assertInstanceOf(MessageInterface::class, $copy); $this->assertNotEquals($mess, $copy); $this->assertEquals([], $mess->getHeaders()); - $this->assertEquals(['x-foo' => ['bar']], $copy->getHeaders()); + $this->assertEquals(['X-Foo' => ['bar']], $copy->getHeaders()); } /** * @return void */ - public function testSetHeaderWithValueArray() : void + public function testSetHeaderWithSeveralValues() : void { - $mess = (new Message)->withHeader('x-foo', ['bar', 'baz']); + $mess = (new Message)->withHeader('X-Foo', ['bar', 'baz']); - $this->assertEquals(['x-foo' => ['bar', 'baz']], $mess->getHeaders()); + $this->assertEquals(['X-Foo' => ['bar', 'baz']], $mess->getHeaders()); } /** @@ -87,12 +103,12 @@ public function testSetHeaderWithValueArray() : void public function testSetSeveralHeaders() : void { $mess = (new Message) - ->withHeader('x-foo', ['bar', 'baz']) - ->withHeader('x-quux', ['quuux', 'quuuux']); + ->withHeader('X-Foo', ['bar', 'baz']) + ->withHeader('X-Quux', ['quuux', 'quuuux']); $this->assertEquals([ - 'x-foo' => ['bar', 'baz'], - 'x-quux' => ['quuux', 'quuuux'], + 'X-Foo' => ['bar', 'baz'], + 'X-Quux' => ['quuux', 'quuuux'], ], $mess->getHeaders()); } @@ -101,9 +117,9 @@ public function testSetSeveralHeaders() : void */ public function testSetHeaderLowercase() : void { - $mess = (new Message)->withHeader('X-Foo', 'bar'); + $mess = (new Message)->withHeader('x-foo', 'bar'); - $this->assertEquals(['x-foo' => ['bar']], $mess->getHeaders()); + $this->assertEquals(['X-Foo' => ['bar']], $mess->getHeaders()); } /** @@ -127,7 +143,7 @@ public function testSetInvalidHeaderValue($headerValue) : void { $this->expectException(\InvalidArgumentException::class); - (new Message)->withHeader('x-foo', $headerValue); + (new Message)->withHeader('X-Foo', $headerValue); } /** @@ -135,11 +151,11 @@ public function testSetInvalidHeaderValue($headerValue) : void * * @return void */ - public function testSetInvalidHeaderValueInArray($headerValue) : void + public function testSetInvalidHeaderValueItem($headerValue) : void { $this->expectException(\InvalidArgumentException::class); - (new Message)->withHeader('x-foo', ['bar', $headerValue, 'baz']); + (new Message)->withHeader('X-Foo', ['bar', $headerValue, 'baz']); } /** @@ -147,25 +163,25 @@ public function testSetInvalidHeaderValueInArray($headerValue) : void */ public function testAddHeader() : void { - $mess = (new Message)->withHeader('x-foo', 'bar'); - $copy = $mess->withAddedHeader('x-foo', 'baz'); + $mess = (new Message)->withHeader('X-Foo', 'bar'); + $copy = $mess->withAddedHeader('X-Foo', 'baz'); $this->assertInstanceOf(MessageInterface::class, $copy); $this->assertNotEquals($mess, $copy); - $this->assertEquals(['x-foo' => ['bar']], $mess->getHeaders()); - $this->assertEquals(['x-foo' => ['bar', 'baz']], $copy->getHeaders()); + $this->assertEquals(['X-Foo' => ['bar']], $mess->getHeaders()); + $this->assertEquals(['X-Foo' => ['bar', 'baz']], $copy->getHeaders()); } /** * @return void */ - public function testAddHeaderWithValueArray() : void + public function testAddHeaderWithSeveralValues() : void { $mess = (new Message) - ->withHeader('x-foo', ['bar', 'baz']) - ->withAddedHeader('x-foo', ['quux', 'quuux']); + ->withHeader('X-Foo', ['bar', 'baz']) + ->withAddedHeader('X-Foo', ['quux', 'quuux']); - $this->assertEquals(['x-foo' => ['bar', 'baz', 'quux', 'quuux']], $mess->getHeaders()); + $this->assertEquals(['X-Foo' => ['bar', 'baz', 'quux', 'quuux']], $mess->getHeaders()); } /** @@ -174,14 +190,14 @@ public function testAddHeaderWithValueArray() : void public function testAddSeveralHeaders() : void { $mess = (new Message) - ->withHeader('x-foo', 'bar') - ->withHeader('x-baz', 'quux') - ->withAddedHeader('x-foo', 'quuux') - ->withAddedHeader('x-baz', 'quuuux'); + ->withHeader('X-Foo', 'bar') + ->withHeader('X-Baz', 'quux') + ->withAddedHeader('X-Foo', 'quuux') + ->withAddedHeader('X-Baz', 'quuuux'); $this->assertEquals([ - 'x-foo' => ['bar', 'quuux'], - 'x-baz' => ['quux', 'quuuux'], + 'X-Foo' => ['bar', 'quuux'], + 'X-Baz' => ['quux', 'quuuux'], ], $mess->getHeaders()); } @@ -192,9 +208,9 @@ public function testAddHeaderLowercase() : void { $mess = (new Message) ->withHeader('x-foo', 'bar') - ->withAddedHeader('X-Foo', 'baz'); + ->withAddedHeader('x-foo', 'baz'); - $this->assertEquals(['x-foo' => ['bar', 'baz']], $mess->getHeaders()); + $this->assertEquals(['X-Foo' => ['bar', 'baz']], $mess->getHeaders()); } /** @@ -218,7 +234,7 @@ public function testAddInvalidHeaderValue($headerValue) : void { $this->expectException(\InvalidArgumentException::class); - (new Message)->withAddedHeader('x-foo', $headerValue); + (new Message)->withAddedHeader('X-Foo', $headerValue); } /** @@ -226,108 +242,11 @@ public function testAddInvalidHeaderValue($headerValue) : void * * @return void */ - public function testAddInvalidHeaderValueInArray($headerValue) : void + public function testAddInvalidHeaderValueItem($headerValue) : void { $this->expectException(\InvalidArgumentException::class); - (new Message)->withAddedHeader('x-foo', ['bar', $headerValue, 'baz']); - } - - /** - * @return void - */ - 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()); - } - - /** - * @return void - */ - public function testWithHeaderObject() : void - { - $foo1 = $this->createMock(HeaderInterface::class); - $foo1->method('getFieldName')->willReturn('x-foo'); - $foo1->method('getFieldValue')->willReturn('1'); - - $foo2 = $this->createMock(HeaderInterface::class); - $foo2->method('getFieldName')->willReturn('x-foo'); - $foo2->method('getFieldValue')->willReturn('2'); - - $mess0 = new Message(); - - $copy1 = $mess0->withHeaderObject($foo1); - $this->assertNotSame($copy1, $mess0); - $this->assertEquals([], $mess0->getHeaders()); - $this->assertEquals(['x-foo' => ['1']], $copy1->getHeaders()); - - $copy2 = $copy1->withHeaderObject($foo2); - $this->assertNotSame($copy2, $copy1); - $this->assertEquals(['x-foo' => ['1']], $copy1->getHeaders()); - $this->assertEquals(['x-foo' => ['2']], $copy2->getHeaders()); - - $copy3 = $copy2->withHeaderObject($foo1, true); - $this->assertNotSame($copy3, $copy2); - $this->assertEquals(['x-foo' => ['2']], $copy2->getHeaders()); - $this->assertEquals(['x-foo' => ['2', '1']], $copy3->getHeaders()); - } - - /** - * @return void - */ - public function testWithHeaderCollection() : void - { - $foo1 = $this->createMock(HeaderInterface::class); - $foo1->method('getFieldName')->willReturn('x-foo'); - $foo1->method('getFieldValue')->willReturn('1'); - - $foo2 = $this->createMock(HeaderInterface::class); - $foo2->method('getFieldName')->willReturn('x-foo'); - $foo2->method('getFieldValue')->willReturn('2'); - - $bar1 = $this->createMock(HeaderInterface::class); - $bar1->method('getFieldName')->willReturn('x-bar'); - $bar1->method('getFieldValue')->willReturn('1'); - - $bar2 = $this->createMock(HeaderInterface::class); - $bar2->method('getFieldName')->willReturn('x-bar'); - $bar2->method('getFieldValue')->willReturn('2'); - - $coll1 = new HeaderCollection([$foo1, $bar1]); - $coll2 = new HeaderCollection([$foo2, $bar2]); - - $mess0 = new Message(); - - $copy1 = $mess0->withHeaderCollection($coll1); - $this->assertNotSame($copy1, $mess0); - $this->assertEquals([], $mess0->getHeaders()); - $this->assertEquals(['x-foo' => ['1'], 'x-bar' => ['1']], $copy1->getHeaders()); - - $copy2 = $copy1->withHeaderCollection($coll2); - $this->assertNotSame($copy2, $copy1); - $this->assertEquals(['x-foo' => ['1'], 'x-bar' => ['1']], $copy1->getHeaders()); - $this->assertEquals(['x-foo' => ['2'], 'x-bar' => ['2']], $copy2->getHeaders()); - - $copy3 = $copy2->withHeaderCollection($coll1, true); - $this->assertNotSame($copy3, $copy2); - $this->assertEquals(['x-foo' => ['2'], 'x-bar' => ['2']], $copy2->getHeaders()); - $this->assertEquals(['x-foo' => ['2', '1'], 'x-bar' => ['2', '1']], $copy3->getHeaders()); + (new Message)->withAddedHeader('X-Foo', ['bar', $headerValue, 'baz']); } /** @@ -335,12 +254,12 @@ public function testWithHeaderCollection() : void */ public function testDeleteHeader() : void { - $mess = (new Message)->withHeader('x-foo', 'bar'); - $copy = $mess->withoutHeader('x-foo'); + $mess = (new Message)->withHeader('X-Foo', 'bar'); + $copy = $mess->withoutHeader('X-Foo'); $this->assertInstanceOf(MessageInterface::class, $copy); $this->assertNotEquals($mess, $copy); - $this->assertEquals(['x-foo' => ['bar']], $mess->getHeaders()); + $this->assertEquals(['X-Foo' => ['bar']], $mess->getHeaders()); $this->assertEquals([], $copy->getHeaders()); } @@ -361,11 +280,11 @@ public function testDeleteHeaderCaseInsensitive() : void */ public function testReplaceHeader() : void { - $mess = (new Message)->withHeader('x-foo', 'bar'); - $copy = $mess->withHeader('x-foo', 'baz'); + $mess = (new Message)->withHeader('X-Foo', 'bar'); + $copy = $mess->withHeader('X-Foo', 'baz'); - $this->assertEquals(['x-foo' => ['bar']], $mess->getHeaders()); - $this->assertEquals(['x-foo' => ['baz']], $copy->getHeaders()); + $this->assertEquals(['X-Foo' => ['bar']], $mess->getHeaders()); + $this->assertEquals(['X-Foo' => ['baz']], $copy->getHeaders()); } /** @@ -377,7 +296,7 @@ public function testReplaceHeaderCaseInsensitive() : void ->withHeader('x-foo', 'bar') ->withHeader('X-Foo', 'baz'); - $this->assertEquals(['x-foo' => ['baz']], $mess->getHeaders()); + $this->assertEquals(['X-Foo' => ['baz']], $mess->getHeaders()); } /** @@ -385,10 +304,10 @@ public function testReplaceHeaderCaseInsensitive() : void */ public function testHasHeader() : void { - $mess = (new Message)->withHeader('x-foo', 'bar'); + $mess = (new Message)->withHeader('X-Foo', 'bar'); - $this->assertTrue($mess->hasHeader('x-foo')); - $this->assertFalse($mess->hasHeader('x-bar')); + $this->assertTrue($mess->hasHeader('X-Foo')); + $this->assertFalse($mess->hasHeader('X-Bar')); } /** @@ -408,10 +327,10 @@ public function testHasHeaderCaseInsensitive() : void */ public function testGetHeader() : void { - $mess = (new Message)->withHeader('x-foo', 'bar'); + $mess = (new Message)->withHeader('X-Foo', 'bar'); - $this->assertEquals(['bar'], $mess->getHeader('x-foo')); - $this->assertEquals([], $mess->getHeader('x-bar')); + $this->assertEquals(['bar'], $mess->getHeader('X-Foo')); + $this->assertEquals([], $mess->getHeader('X-Bar')); } /** @@ -429,11 +348,11 @@ public function testGetHeaderCaseInsensitive() : void /** * @return void */ - public function testGetHeaderWithMultipleValue() : void + public function testGetHeaderWithSeveralValues() : void { - $mess = (new Message)->withHeader('x-foo', ['bar', 'baz', 'quux']); + $mess = (new Message)->withHeader('X-Foo', ['bar', 'baz', 'quux']); - $this->assertEquals(['bar', 'baz', 'quux'], $mess->getHeader('x-foo')); + $this->assertEquals(['bar', 'baz', 'quux'], $mess->getHeader('X-Foo')); } /** @@ -441,10 +360,10 @@ public function testGetHeaderWithMultipleValue() : void */ public function testGetHeaderLine() : void { - $mess = (new Message)->withHeader('x-foo', 'bar'); + $mess = (new Message)->withHeader('X-Foo', 'bar'); - $this->assertEquals('bar', $mess->getHeaderLine('x-foo')); - $this->assertEquals('', $mess->getHeaderLine('x-bar')); + $this->assertEquals('bar', $mess->getHeaderLine('X-Foo')); + $this->assertEquals('', $mess->getHeaderLine('X-Bar')); } /** @@ -462,11 +381,11 @@ public function testGetHeaderLineCaseInsensitive() : void /** * @return void */ - public function testGetHeaderLineWithMultipleValue() : void + public function testGetHeaderLineWithSeveralValues() : void { - $mess = (new Message)->withHeader('x-foo', ['bar', 'baz', 'quux']); + $mess = (new Message)->withHeader('X-Foo', ['bar', 'baz', 'quux']); - $this->assertEquals('bar, baz, quux', $mess->getHeaderLine('x-foo')); + $this->assertEquals('bar, baz, quux', $mess->getHeaderLine('X-Foo')); } /** @@ -474,7 +393,7 @@ public function testGetHeaderLineWithMultipleValue() : void */ public function testBody() : void { - $body =(new StreamFactory)->createStreamFromResource(\STDOUT); + $body = (new StreamFactory)->createStreamFromResource(\STDOUT); $mess = new Message(); $copy = $mess->withBody($body); @@ -482,7 +401,7 @@ public function testBody() : void $this->assertNotEquals($mess, $copy); // default value - $this->assertEquals(null, $mess->getBody()); + $this->assertNotSame($body, $mess->getBody()); // assigned value $this->assertEquals($body, $copy->getBody()); } diff --git a/tests/RequestFactoryTest.php b/tests/RequestFactoryTest.php index af0125f..eca5de0 100644 --- a/tests/RequestFactoryTest.php +++ b/tests/RequestFactoryTest.php @@ -1,4 +1,6 @@ -assertInstanceOf(RequestInterface::class, $mess); } + /** + * @return void + */ + public function testConstructor() : void + { + $method = 'POST'; + $uri = '/foo?bar'; + $headers = ['X-Foo' => ['bar', 'baz'], 'X-Bar' => ['baz']]; + $body = (new StreamFactory)->createStreamFromResource(\STDOUT); + $target = '/bar?baz'; + $protocol = '2.0'; + + $mess = new Request( + $method, + $uri, + $headers, + $body, + $target, + $protocol + ); + + $this->assertSame($method, $mess->getMethod()); + $this->assertSame('/foo', $mess->getUri()->getPath()); + $this->assertSame('bar', $mess->getUri()->getQuery()); + $this->assertSame($headers, $mess->getHeaders()); + $this->assertSame($body, $mess->getBody()); + $this->assertSame($target, $mess->getRequestTarget()); + $this->assertSame($protocol, $mess->getProtocolVersion()); + } + /** * @return void */ @@ -193,7 +226,7 @@ public function testUri() : void $this->assertNotEquals($mess, $copy); // default value - $this->assertEquals(null, $mess->getUri()); + $this->assertNotSame($uri, $mess->getUri()); // assigned value $this->assertEquals($uri, $copy->getUri()); } diff --git a/tests/ResponseFactoryTest.php b/tests/ResponseFactoryTest.php index 2d170c9..b2ff956 100644 --- a/tests/ResponseFactoryTest.php +++ b/tests/ResponseFactoryTest.php @@ -1,4 +1,6 @@ -assertInstanceOf(ResponseInterface::class, $response); $this->assertSame(400, $response->getStatusCode()); - $this->assertSame('text/html; charset=utf-8', $response->getHeaderLine('Content-Type')); + $this->assertSame('text/html; charset=UTF-8', $response->getHeaderLine('Content-Type')); $this->assertSame($content, (string) $response->getBody()); } @@ -83,15 +74,15 @@ public function testCreateHtmlResponse() : void public function testCreateJsonResponse() : void { $payload = ['foo' => '']; - $options = JSON_HEX_TAG; + $options = \JSON_HEX_TAG; $response = (new ResponseFactory) ->createJsonResponse(400, $payload, $options); $this->assertInstanceOf(ResponseInterface::class, $response); $this->assertSame(400, $response->getStatusCode()); - $this->assertSame('application/json; charset=utf-8', $response->getHeaderLine('Content-Type')); - $this->assertSame(json_encode($payload, $options), (string) $response->getBody()); + $this->assertSame('application/json; charset=UTF-8', $response->getHeaderLine('Content-Type')); + $this->assertSame(\json_encode($payload, $options), (string) $response->getBody()); } /** @@ -99,7 +90,7 @@ public function testCreateJsonResponse() : void */ public function testCreateResponseWithInvalidJson() : void { - $this->expectException(JsonException::class); + $this->expectException(\JsonException::class); $this->expectExceptionMessage('Maximum stack depth exceeded'); $response = (new ResponseFactory) diff --git a/tests/ResponseTest.php b/tests/ResponseTest.php index 3dcd699..3b93f3c 100644 --- a/tests/ResponseTest.php +++ b/tests/ResponseTest.php @@ -1,11 +1,12 @@ -assertEquals(200, $mess->getStatusCode()); - $this->assertEquals(PHRASES[200], $mess->getReasonPhrase()); + $this->assertEquals(REASON_PHRASES[200], $mess->getReasonPhrase()); // assigned values $this->assertEquals(204, $copy->getStatusCode()); - $this->assertEquals(PHRASES[204], $copy->getReasonPhrase()); + $this->assertEquals(REASON_PHRASES[204], $copy->getReasonPhrase()); } /** @@ -119,7 +120,7 @@ public function testCustomReasonPhrase() : void public function figStatusProvider() : array { return [ - [200, PHRASES[200] ?? ''], + [200, REASON_PHRASES[200] ?? ''], ]; } From f4552fe968d6d6fa9eaa3ec5bd2c9a1f4e4fba40 Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Sun, 24 Oct 2021 17:38:27 +0500 Subject: [PATCH 02/15] improve code --- src/Exception/JsonException.php | 51 ----- src/Message.php | 326 ++++++++++++++++---------------- src/Request.php | 204 +++++++++++++++----- src/RequestFactory.php | 18 +- src/Response.php | 103 +++++++--- src/ResponseFactory.php | 50 ++--- 6 files changed, 419 insertions(+), 333 deletions(-) delete mode 100644 src/Exception/JsonException.php diff --git a/src/Exception/JsonException.php b/src/Exception/JsonException.php deleted file mode 100644 index 9dbb017..0000000 --- a/src/Exception/JsonException.php +++ /dev/null @@ -1,51 +0,0 @@ - - * @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 b7e0607..f69cc0f 100644 --- a/src/Message.php +++ b/src/Message.php @@ -16,8 +16,19 @@ */ use Psr\Http\Message\MessageInterface; use Psr\Http\Message\StreamInterface; -use Sunrise\Http\Header\HeaderCollectionInterface; use Sunrise\Http\Header\HeaderInterface; +use Sunrise\Stream\StreamFactory; +use InvalidArgumentException; + +/** + * Import functions + */ +use function is_string; +use function join; +use function preg_match; +use function sprintf; +use function strtolower; +use function ucwords; /** * Hypertext Transfer Protocol Message @@ -29,28 +40,57 @@ class Message implements MessageInterface { /** - * Protocol version for the message + * The message protocol version * * @var string */ protected $protocolVersion = '1.1'; /** - * Headers of the message + * The message headers * - * @var array + * @var array> */ protected $headers = []; /** - * Body of the message + * The message body * - * @var null|StreamInterface + * @var StreamInterface|null */ - protected $body; + protected $body = null; /** - * {@inheritDoc} + * Constructor of the class + * + * @param array>|null $headers + * @param StreamInterface|null $body + * @param string|null $protocolVersion + * + * @throws InvalidArgumentException + */ + public function __construct( + ?array $headers = null, + ?StreamInterface $body = null, + ?string $protocolVersion = null + ) { + if (isset($protocolVersion)) { + $this->setProtocolVersion($protocolVersion); + } + + if (isset($headers)) { + foreach ($headers as $name => $value) { + $this->addHeader($name, $value); + } + } + + if (isset($body)) { + $this->body = $body; + } + } + + /** + * {@inheritdoc} */ public function getProtocolVersion() : string { @@ -58,20 +98,20 @@ public function getProtocolVersion() : string } /** - * {@inheritDoc} + * {@inheritdoc} + * + * @throws InvalidArgumentException */ - public function withProtocolVersion($protocolVersion) : MessageInterface + public function withProtocolVersion($version) : MessageInterface { - $this->validateProtocolVersion($protocolVersion); - $clone = clone $this; - $clone->protocolVersion = $protocolVersion; + $clone->setProtocolVersion($version); return $clone; } /** - * {@inheritDoc} + * {@inheritdoc} */ public function getHeaders() : array { @@ -79,7 +119,7 @@ public function getHeaders() : array } /** - * {@inheritDoc} + * {@inheritdoc} */ public function hasHeader($name) : bool { @@ -89,248 +129,220 @@ public function hasHeader($name) : bool } /** - * {@inheritDoc} + * {@inheritdoc} */ public function getHeader($name) : array { $name = $this->normalizeHeaderName($name); - if (empty($this->headers[$name])) { - return []; - } - return $this->headers[$name]; + return $this->headers[$name] ?? []; } /** - * {@inheritDoc} + * {@inheritdoc} */ public function getHeaderLine($name) : string { $name = $this->normalizeHeaderName($name); + if (empty($this->headers[$name])) { return ''; } - return \implode(', ', $this->headers[$name]); + return join(', ', $this->headers[$name]); } /** - * {@inheritDoc} + * {@inheritdoc} + * + * @throws InvalidArgumentException */ - public function withHeader($name, $value, bool $append = false) : MessageInterface + public function withHeader($name, $value) : MessageInterface { - $this->validateHeaderName($name); - $this->validateHeaderValue($value); - - $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; + $clone->addHeader($name, $value); return $clone; } /** - * {@inheritDoc} - */ - public function withAddedHeader($name, $value) : MessageInterface - { - return $this->withHeader($name, $value, true); - } - - /** - * Returns a new instance with the given headers - * - * [!] This method is not associated with PSR-7. - * - * @param iterable $headers - * @param bool $append - * - * @return MessageInterface + * {@inheritdoc} * - * @since 1.3.0 + * @throws InvalidArgumentException */ - public function withMultipleHeaders(iterable $headers, bool $append = false) : MessageInterface + public function withAddedHeader($name, $value) : MessageInterface { - $result = clone $this; - - foreach ($headers as $name => $value) { - $result = $result->withHeader($name, $value, $append); - } + $clone = clone $this; + $clone->addHeader($name, $value, false); - return $result; + return $clone; } /** - * Returns a new instance with the given header - * - * [!] This method is not associated with PSR-7. - * - * @param HeaderInterface $header - * @param bool $append - * - * @return MessageInterface - * - * @since 1.4.0 + * {@inheritdoc} */ - public function withHeaderObject(HeaderInterface $header, bool $append = false) : MessageInterface + public function withoutHeader($name) : MessageInterface { - $name = $header->getFieldName(); - $value = $header->getFieldValue(); + $name = $this->normalizeHeaderName($name); - $result = clone $this; - $result = $result->withHeader($name, $value, $append); + $clone = clone $this; - return $result; + unset($clone->headers[$name]); + + return $clone; } /** - * Returns a new instance with the given headers - * - * [!] This method is not associated with PSR-7. - * - * @param HeaderCollectionInterface $headers - * @param bool $append - * - * @return MessageInterface - * - * @since 1.4.0 + * {@inheritdoc} */ - public function withHeaderCollection(HeaderCollectionInterface $headers, bool $append = false) : MessageInterface + public function getBody() : StreamInterface { - $result = clone $this; - - foreach ($headers as $header) { - $name = $header->getFieldName(); - $value = $header->getFieldValue(); - - $result = $result->withHeader($name, $value, $append); + if (null === $this->body) { + $this->body = (new StreamFactory)->createStream(); } - return $result; + return $this->body; } /** - * {@inheritDoc} + * {@inheritdoc} */ - public function withoutHeader($name) : MessageInterface + public function withBody(StreamInterface $body) : MessageInterface { - $name = $this->normalizeHeaderName($name); - $clone = clone $this; - - unset($clone->headers[$name]); + $clone->body = $body; return $clone; } /** - * {@inheritDoc} + * Sets the given protocol version to the message + * + * @param string $version + * + * @return void + * + * @throws InvalidArgumentException */ - public function getBody() : ?StreamInterface + protected function setProtocolVersion($version) : void { - return $this->body; + $this->validateProtocolVersion($version); + + $this->protocolVersion = $version; } /** - * {@inheritDoc} + * Adds the given header field to the message + * + * @param string $name + * @param string|array $value + * @param bool $replace + * + * @return void + * + * @throws InvalidArgumentException */ - public function withBody(StreamInterface $body) : MessageInterface + protected function addHeader($name, $value, bool $replace = true) : void { - $clone = clone $this; - $clone->body = $body; + $this->validateHeaderName($name); + $this->validateHeaderValue($value); - return $clone; + $name = $this->normalizeHeaderName($name); + $value = (array) $value; + + if ($replace) { + $this->headers[$name] = $value; + return; + } + + foreach ($value as $item) { + $this->headers[$name][] = $item; + } } /** * Validates the given protocol version * - * @param mixed $protocolVersion + * @param mixed $version * * @return void * - * @throws \InvalidArgumentException + * @throws InvalidArgumentException * * @link https://tools.ietf.org/html/rfc7230#section-2.6 * @link https://tools.ietf.org/html/rfc7540 */ - protected function validateProtocolVersion($protocolVersion) : void + protected function validateProtocolVersion($version) : void { - if (! \is_string($protocolVersion)) { - throw new \InvalidArgumentException('HTTP protocol version must be a string'); + // allowed protocol versions: + static $allowed = [ + '1.0' => 1, + '1.1' => 1, + '2.0' => 1, + '2' => 1, + ]; + + if (!is_string($version)) { + throw new InvalidArgumentException('Protocol version must be a string'); } - if (! \preg_match('/^\d(?:\.\d)?$/', $protocolVersion)) { - throw new \InvalidArgumentException( - \sprintf('The given protocol version "%s" is not valid', $protocolVersion) - ); + if (!isset($allowed[$version])) { + throw new InvalidArgumentException(sprintf( + 'The protocol version "%s" is not valid. ' . + 'Allowed only: 1.0, 1.1 or 2{.0}', + $version + )); } } /** * Validates the given header name * - * @param mixed $headerName + * @param mixed $name * * @return void * - * @throws \InvalidArgumentException + * @throws InvalidArgumentException * * @link https://tools.ietf.org/html/rfc7230#section-3.2 */ - protected function validateHeaderName($headerName) : void + protected function validateHeaderName($name) : void { - if (! \is_string($headerName)) { - throw new \InvalidArgumentException('Header name must be a string'); + if (!is_string($name)) { + throw new InvalidArgumentException('Header name must be a string'); } - 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, $name)) { + throw new InvalidArgumentException(sprintf('The header name "%s" is not valid', $name)); } } /** * Validates the given header value * - * @param mixed $headerValue + * @param mixed $value * * @return void * - * @throws \InvalidArgumentException + * @throws InvalidArgumentException * * @link https://tools.ietf.org/html/rfc7230#section-3.2 */ - protected function validateHeaderValue($headerValue) : void + protected function validateHeaderValue($value) : void { - if (\is_string($headerValue)) { - $headerValue = [$headerValue]; - } + $items = (array) $value; - if (! \is_array($headerValue) || [] === $headerValue) { - throw new \InvalidArgumentException( - 'Header value must be a string or not an empty array' - ); + if ([] === $items) { + throw new InvalidArgumentException('Header value must be a string or a non-empty array'); } - foreach ($headerValue as $oneOf) { - if (! \is_string($oneOf)) { - throw new \InvalidArgumentException( - 'Header value must be a string or an array containing only strings' - ); + foreach ($items as $item) { + if (!is_string($item)) { + throw new InvalidArgumentException('Header value must be a string or an array with strings only'); } - 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, $item)) { + throw new InvalidArgumentException(sprintf('The header value "%s" is not valid', $item)); } } } @@ -338,32 +350,14 @@ protected function validateHeaderValue($headerValue) : void /** * Normalizes the given header name * - * @param string $headerName + * @param string $name * * @return string * * @link https://tools.ietf.org/html/rfc7230#section-3.2 */ - protected function normalizeHeaderName($headerName) : string - { - // Each header field consists of a case-insensitive field name... - $headerName = \strtolower($headerName); - - return $headerName; - } - - /** - * Normalizes the given header value - * - * @param string|array $headerValue - * - * @return array - */ - protected function normalizeHeaderValue($headerValue) : array + protected function normalizeHeaderName($name) : string { - $headerValue = (array) $headerValue; - $headerValue = \array_values($headerValue); - - return $headerValue; + return ucwords(strtolower($name), '-'); } } diff --git a/src/Request.php b/src/Request.php index eed277a..d55bac4 100644 --- a/src/Request.php +++ b/src/Request.php @@ -14,9 +14,22 @@ /** * Import classes */ +use Fig\Http\Message\RequestMethodInterface; use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\StreamInterface; use Psr\Http\Message\UriInterface; use Sunrise\Http\Header\HeaderInterface; +use Sunrise\Uri\UriFactory; +use InvalidArgumentException; + +/** + * Import functions + */ +use function is_string; +use function preg_match; +use function sprintf; +use function strncmp; +use function strtoupper; /** * HTTP Request Message @@ -24,32 +37,71 @@ * @link https://tools.ietf.org/html/rfc7230 * @link https://www.php-fig.org/psr/psr-7/ */ -class Request extends Message implements RequestInterface +class Request extends Message implements RequestInterface, RequestMethodInterface { /** - * Method of the message + * The request method (aka verb) * * @var string */ - protected $method = 'GET'; + protected $method = self::METHOD_GET; + + /** + * The request target + * + * @var string|null + */ + protected $requestTarget = null; /** - * Request target of the message + * The request URI * - * @var null|string + * @var UriInterface|null */ - protected $requestTarget; + protected $uri = null; /** - * URI of the message + * Constructor of the class + * + * @param string|null $method + * @param string|UriInterface|null $uri + * @param array>|null $headers + * @param StreamInterface|null $body + * @param string|null $requestTarget + * @param string|null $protocolVersion * - * @var null|UriInterface + * @throws InvalidArgumentException */ - protected $uri; + public function __construct( + ?string $method = null, + $uri = null, + ?array $headers = null, + ?StreamInterface $body = null, + ?string $requestTarget = null, + ?string $protocolVersion = null + ) { + parent::__construct( + $headers, + $body, + $protocolVersion + ); + + if (isset($method)) { + $this->setMethod($method); + } + + if (isset($requestTarget)) { + $this->setRequestTarget($requestTarget); + } + + if (isset($uri)) { + $this->setUri($uri); + } + } /** - * {@inheritDoc} + * {@inheritdoc} */ public function getMethod() : string { @@ -57,88 +109,142 @@ public function getMethod() : string } /** - * {@inheritDoc} + * {@inheritdoc} + * + * @throws InvalidArgumentException */ public function withMethod($method) : RequestInterface { - $this->validateMethod($method); - $clone = clone $this; - $clone->method = \strtoupper($method); + $clone->setMethod($method); return $clone; } /** - * {@inheritDoc} + * {@inheritdoc} */ public function getRequestTarget() : string { - if (! (null === $this->requestTarget)) { + if (isset($this->requestTarget)) { return $this->requestTarget; } - if (! ($this->uri instanceof UriInterface)) { - return '/'; - } + $uri = $this->getUri(); // https://tools.ietf.org/html/rfc7230#section-5.3.1 // https://tools.ietf.org/html/rfc7230#section-2.7 // // origin-form = absolute-path [ "?" query ] // absolute-path = 1*( "/" segment ) - if (! (0 === \strncmp($this->uri->getPath(), '/', 1))) { + if (0 <> strncmp($uri->getPath(), '/', 1)) { return '/'; } - $origin = $this->uri->getPath(); - if (! ('' === $this->uri->getQuery())) { - $origin .= '?' . $this->uri->getQuery(); + $requestTarget = $uri->getPath(); + if ('' !== $uri->getQuery()) { + $requestTarget .= '?' . $uri->getQuery(); } - return $origin; + return $requestTarget; } /** - * {@inheritDoc} + * {@inheritdoc} + * + * @throws InvalidArgumentException */ public function withRequestTarget($requestTarget) : RequestInterface { - $this->validateRequestTarget($requestTarget); - $clone = clone $this; - $clone->requestTarget = $requestTarget; + $clone->setRequestTarget($requestTarget); return $clone; } /** - * {@inheritDoc} + * {@inheritdoc} */ - public function getUri() : ?UriInterface + public function getUri() : UriInterface { + if (null === $this->uri) { + $this->uri = (new UriFactory)->createUri(); + } + return $this->uri; } /** - * {@inheritDoc} + * {@inheritdoc} */ public function withUri(UriInterface $uri, $preserveHost = false) : RequestInterface { $clone = clone $this; - $clone->uri = $uri; + $clone->setUri($uri, $preserveHost); + + return $clone; + } + + /** + * Sets the given method to the request + * + * @param string $method + * + * @return void + * + * @throws InvalidArgumentException + */ + protected function setMethod($method) : void + { + $this->validateMethod($method); + + $this->method = strtoupper($method); + } + + /** + * Sets the given request-target to the request + * + * @param string $requestTarget + * + * @return void + * + * @throws InvalidArgumentException + */ + protected function setRequestTarget($requestTarget) : void + { + $this->validateRequestTarget($requestTarget); + + $this->requestTarget = $requestTarget; + } + + /** + * Sets the given URI to the request + * + * @param string|UriInterface $uri + * @param bool $preserveHost + * + * @return void + * + * @throws InvalidArgumentException + */ + protected function setUri($uri, $preserveHost = false) : void + { + if (! ($uri instanceof UriInterface)) { + $uri = (new UriFactory)->createUri($uri); + } + + $this->uri = $uri; - if ('' === $uri->getHost() || ($preserveHost && $clone->hasHeader('host'))) { - return $clone; + if ('' === $uri->getHost() || ($preserveHost && $this->hasHeader('Host'))) { + return; } - $newhost = $uri->getHost(); - if (! (null === $uri->getPort())) { - $newhost .= ':' . $uri->getPort(); + $host = $uri->getHost(); + if (null !== $uri->getPort()) { + $host .= ':' . $uri->getPort(); } - // Reassigning the "Host" header - return $clone->withHeader('host', $newhost); + $this->addHeader('Host', $host); } /** @@ -148,18 +254,18 @@ public function withUri(UriInterface $uri, $preserveHost = false) : RequestInter * * @return void * - * @throws \InvalidArgumentException + * @throws InvalidArgumentException * * @link https://tools.ietf.org/html/rfc7230#section-3.1.1 */ protected function validateMethod($method) : void { - if (! \is_string($method)) { - throw new \InvalidArgumentException('HTTP method must be a string'); + if (!is_string($method)) { + throw new InvalidArgumentException('HTTP method must be a string'); } - if (! \preg_match(HeaderInterface::RFC7230_TOKEN, $method)) { - throw new \InvalidArgumentException(\sprintf('The given method "%s" is not valid', $method)); + if (!preg_match(HeaderInterface::RFC7230_TOKEN, $method)) { + throw new InvalidArgumentException(sprintf('The method "%s" is not valid', $method)); } } @@ -170,18 +276,18 @@ protected function validateMethod($method) : void * * @return void * - * @throws \InvalidArgumentException + * @throws InvalidArgumentException * * @link https://tools.ietf.org/html/rfc7230#section-5.3 */ protected function validateRequestTarget($requestTarget) : void { - if (! \is_string($requestTarget)) { - throw new \InvalidArgumentException('HTTP request-target must be a string'); + if (!is_string($requestTarget)) { + throw new InvalidArgumentException('HTTP request-target must be a string'); } - if (! \preg_match('/^[\x21-\x7E\x80-\xFF]+$/', $requestTarget)) { - throw new \InvalidArgumentException(\sprintf('The given request-target "%s" is not valid', $requestTarget)); + if (!preg_match('/^[\x21-\x7E\x80-\xFF]+$/', $requestTarget)) { + throw new InvalidArgumentException(sprintf('The request-target "%s" is not valid', $requestTarget)); } } } diff --git a/src/RequestFactory.php b/src/RequestFactory.php index 507a271..c52f067 100644 --- a/src/RequestFactory.php +++ b/src/RequestFactory.php @@ -16,12 +16,9 @@ */ use Psr\Http\Message\RequestFactoryInterface; use Psr\Http\Message\RequestInterface; -use Psr\Http\Message\UriInterface; -use Sunrise\Stream\StreamFactory; -use Sunrise\Uri\UriFactory; /** - * RequestFactory + * HTTP Request Message Factory * * @link https://www.php-fig.org/psr/psr-17/ */ @@ -29,19 +26,10 @@ class RequestFactory implements RequestFactoryInterface { /** - * {@inheritDoc} + * {@inheritdoc} */ public function createRequest(string $method, $uri) : RequestInterface { - if (! ($uri instanceof UriInterface)) { - $uri = (new UriFactory)->createUri($uri); - } - - $body = (new StreamFactory)->createStream(); - - return (new Request) - ->withMethod($method) - ->withUri($uri) - ->withBody($body); + return new Request($method, $uri); } } diff --git a/src/Response.php b/src/Response.php index 19eebe9..3db3a64 100644 --- a/src/Response.php +++ b/src/Response.php @@ -14,8 +14,19 @@ /** * Import classes */ +use Fig\Http\Message\StatusCodeInterface; use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\StreamInterface; use Sunrise\Http\Header\HeaderInterface; +use InvalidArgumentException; + +/** + * Import functions + */ +use function is_int; +use function is_string; +use function preg_match; +use function sprintf; /** * HTTP Response Message @@ -23,25 +34,54 @@ * @link https://tools.ietf.org/html/rfc7230 * @link https://www.php-fig.org/psr/psr-7/ */ -class Response extends Message implements ResponseInterface +class Response extends Message implements ResponseInterface, StatusCodeInterface { /** - * Status code of the message + * The response status code * * @var int */ - protected $statusCode = 200; + protected $statusCode = self::STATUS_OK; /** - * Reason phrase of the message + * The response reason phrase * * @var string */ - protected $reasonPhrase = 'OK'; + protected $reasonPhrase = REASON_PHRASES[self::STATUS_OK]; /** - * {@inheritDoc} + * Constrictor of the class + * + * @param int|null $statusCode + * @param string|null $reasonPhrase + * @param array>|null $headers + * @param StreamInterface|null $body + * @param string|null $protocolVersion + * + * @throws InvalidArgumentException + */ + public function __construct( + ?int $statusCode = null, + ?string $reasonPhrase = null, + ?array $headers = null, + ?StreamInterface $body = null, + ?string $protocolVersion = null + ) { + parent::__construct( + $headers, + $body, + $protocolVersion + ); + + if (isset($statusCode)) { + $this->setStatus($statusCode, $reasonPhrase ?? ''); + } + } + + /** + * {@inheritdoc} */ public function getStatusCode() : int { @@ -49,7 +89,7 @@ public function getStatusCode() : int } /** - * {@inheritDoc} + * {@inheritdoc} */ public function getReasonPhrase() : string { @@ -57,22 +97,39 @@ public function getReasonPhrase() : string } /** - * {@inheritDoc} + * {@inheritdoc} + * + * @throws InvalidArgumentException */ public function withStatus($statusCode, $reasonPhrase = '') : ResponseInterface + { + $clone = clone $this; + $clone->setStatus($statusCode, $reasonPhrase); + + return $clone; + } + + /** + * Sets the given status to the response + * + * @param int $statusCode + * @param string $reasonPhrase + * + * @return void + * + * @throws InvalidArgumentException + */ + protected function setStatus($statusCode, $reasonPhrase) : void { $this->validateStatusCode($statusCode); $this->validateReasonPhrase($reasonPhrase); if ('' === $reasonPhrase) { - $reasonPhrase = PHRASES[$statusCode] ?? 'Unknown Status Code'; + $reasonPhrase = REASON_PHRASES[$statusCode] ?? 'Unknown Status Code'; } - $clone = clone $this; - $clone->statusCode = $statusCode; - $clone->reasonPhrase = $reasonPhrase; - - return $clone; + $this->statusCode = $statusCode; + $this->reasonPhrase = $reasonPhrase; } /** @@ -82,18 +139,18 @@ public function withStatus($statusCode, $reasonPhrase = '') : ResponseInterface * * @return void * - * @throws \InvalidArgumentException + * @throws InvalidArgumentException * * @link https://tools.ietf.org/html/rfc7230#section-3.1.2 */ protected function validateStatusCode($statusCode) : void { - if (! \is_int($statusCode)) { - throw new \InvalidArgumentException('HTTP status-code must be an integer'); + if (!is_int($statusCode)) { + throw new InvalidArgumentException('HTTP status-code must be an integer'); } if (! ($statusCode >= 100 && $statusCode <= 599)) { - throw new \InvalidArgumentException(\sprintf('The given status-code "%d" is not valid', $statusCode)); + throw new InvalidArgumentException(sprintf('The status-code "%d" is not valid', $statusCode)); } } @@ -104,18 +161,18 @@ protected function validateStatusCode($statusCode) : void * * @return void * - * @throws \InvalidArgumentException + * @throws InvalidArgumentException * * @link https://tools.ietf.org/html/rfc7230#section-3.1.2 */ protected function validateReasonPhrase($reasonPhrase) : void { - if (! \is_string($reasonPhrase)) { - throw new \InvalidArgumentException('HTTP reason-phrase must be a string'); + if (!is_string($reasonPhrase)) { + throw new InvalidArgumentException('HTTP reason-phrase must be a string'); } - if (! \preg_match(HeaderInterface::RFC7230_FIELD_VALUE, $reasonPhrase)) { - throw new \InvalidArgumentException(\sprintf('The given reason-phrase "%s" is not valid', $reasonPhrase)); + if (!preg_match(HeaderInterface::RFC7230_FIELD_VALUE, $reasonPhrase)) { + throw new InvalidArgumentException(sprintf('The reason-phrase "%s" is not valid', $reasonPhrase)); } } } diff --git a/src/ResponseFactory.php b/src/ResponseFactory.php index 3418083..5e34821 100644 --- a/src/ResponseFactory.php +++ b/src/ResponseFactory.php @@ -16,7 +16,7 @@ */ use Psr\Http\Message\ResponseFactoryInterface; use Psr\Http\Message\ResponseInterface; -use Sunrise\Stream\StreamFactory; +use JsonException; /** * Import functions @@ -24,7 +24,12 @@ use function json_encode; /** - * ResponseFactory + * Import constants + */ +use const JSON_THROW_ON_ERROR; + +/** + * HTTP Response Message Factory * * @link https://www.php-fig.org/psr/psr-17/ */ @@ -32,15 +37,11 @@ class ResponseFactory implements ResponseFactoryInterface { /** - * {@inheritDoc} + * {@inheritdoc} */ - public function createResponse(int $code = 200, string $reasonPhrase = '') : ResponseInterface + public function createResponse(int $statusCode = 200, string $reasonPhrase = '') : ResponseInterface { - $body = (new StreamFactory)->createStream(); - - return (new Response) - ->withStatus($code, $reasonPhrase) - ->withBody($body); + return new Response($statusCode, $reasonPhrase); } /** @@ -53,15 +54,13 @@ public function createResponse(int $code = 200, string $reasonPhrase = '') : Res */ public function createHtmlResponse(int $status, $content) : ResponseInterface { - $content = (string) $content; + $headers = ['Content-Type' => 'text/html; charset=UTF-8']; + $response = new Response($status, null, $headers); - $body = (new StreamFactory)->createStream(); - $body->write($content); + $content = (string) $content; + $response->getBody()->write($content); - return (new Response) - ->withStatus($status) - ->withHeader('Content-Type', 'text/html; charset=utf-8') - ->withBody($body); + return $response; } /** @@ -74,23 +73,16 @@ public function createHtmlResponse(int $status, $content) : ResponseInterface * * @return ResponseInterface * - * @throws Exception\JsonException + * @throws JsonException */ 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(); + $headers = ['Content-Type' => 'application/json; charset=UTF-8']; + $response = new Response($status, null, $headers); - $body = (new StreamFactory)->createStream(); - $body->write($content); + $content = json_encode($payload, $options | JSON_THROW_ON_ERROR, $depth); + $response->getBody()->write($content); - return (new Response) - ->withStatus($status) - ->withHeader('Content-Type', 'application/json; charset=utf-8') - ->withBody($body); + return $response; } } From 54014af86f1965c7df32426a2183c56e2bf390b0 Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Sun, 24 Oct 2021 17:38:42 +0500 Subject: [PATCH 03/15] integrate circleci --- .circleci/config.yml | 55 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 .circleci/config.yml diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000..0441507 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,55 @@ +# PHP CircleCI 2.0 configuration file +# +# Check https://circleci.com/docs/2.0/language-php/ for more details +# +version: 2 +jobs: + php71: + docker: + - image: circleci/php:7.1-cli-node-browsers + steps: + - checkout + - run: php -v + - run: composer install --no-interaction --no-suggest --prefer-source + - run: XDEBUG_MODE=coverage php vendor/bin/phpunit --coverage-text + php72: + docker: + - image: circleci/php:7.2-cli-node-browsers + steps: + - checkout + - run: php -v + - run: composer install --no-interaction --no-suggest --prefer-source + - run: XDEBUG_MODE=coverage php vendor/bin/phpunit --coverage-text + php73: + docker: + - image: circleci/php:7.3-cli-node-browsers + steps: + - checkout + - run: php -v + - run: composer install --no-interaction --no-suggest --prefer-source + - run: XDEBUG_MODE=coverage php vendor/bin/phpunit --coverage-text + php74: + docker: + - image: circleci/php:7.4-cli-node-browsers + steps: + - checkout + - run: php -v + - run: composer install --no-interaction --no-suggest --prefer-source + - run: XDEBUG_MODE=coverage php vendor/bin/phpunit --coverage-text + php80: + docker: + - image: circleci/php:8.0-cli-node-browsers + steps: + - checkout + - run: php -v + - run: composer install --no-interaction --no-suggest --prefer-source + - run: XDEBUG_MODE=coverage php vendor/bin/phpunit --coverage-text +workflows: + version: 2 + build: + jobs: + - php71 + - php72 + - php73 + - php74 + - php80 From 97a2205788873aa17c776c48336c275312234dbf Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Sun, 24 Oct 2021 17:38:57 +0500 Subject: [PATCH 04/15] update editorconfig --- .editorconfig | 3 --- 1 file changed, 3 deletions(-) diff --git a/.editorconfig b/.editorconfig index 27e2667..91aebb8 100644 --- a/.editorconfig +++ b/.editorconfig @@ -11,8 +11,5 @@ indent_size = 4 trim_trailing_whitespace = true insert_final_newline = true -[*.md] -trim_trailing_whitespace = false - [*.yml] indent_size = 2 From c57fda6cdf82dea86e0ac73519631039ff18371a Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Sun, 24 Oct 2021 17:39:08 +0500 Subject: [PATCH 05/15] update gitignore --- .gitignore | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/.gitignore b/.gitignore index 41cf726..ac89ebd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,8 @@ -.php_cs.cache -.phpunit.result.cache -composer.lock -coverage.xml -phpcs.xml -phpunit.xml -vendor/ +/.php_cs.cache +/.phpunit.result.cache +/composer.lock +/coverage.xml +/phpbench.json +/phpcs.xml +/phpunit.xml +/vendor/ From 9728bffc5b9ceaa0b3e585fb0b394bff17b6f6d2 Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Sun, 24 Oct 2021 17:39:17 +0500 Subject: [PATCH 06/15] update scrutinizer.yml --- .scrutinizer.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.scrutinizer.yml b/.scrutinizer.yml index d95425d..1e6a96f 100644 --- a/.scrutinizer.yml +++ b/.scrutinizer.yml @@ -1,4 +1,7 @@ build: + environment: + php: + version: '8.0' nodes: analysis: tests: @@ -7,7 +10,7 @@ build: coverage: tests: override: - - command: php vendor/bin/phpunit --coverage-clover coverage.xml + - command: XDEBUG_MODE=coverage php vendor/bin/phpunit --coverage-clover coverage.xml coverage: file: coverage.xml format: clover From c6eea2c59509be32a8309a23c352aca85f3c3dbe Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Sun, 24 Oct 2021 17:39:29 +0500 Subject: [PATCH 07/15] delete --- .travis.yml | 18 ------------------ 1 file changed, 18 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index b0e5e8d..0000000 --- a/.travis.yml +++ /dev/null @@ -1,18 +0,0 @@ -language: php - -matrix: - include: - - php: 7.1 - - php: 7.2 - - php: 7.3 - - php: 7.4 - - php: nightly - fast_finish: true - -before_install: - - travis_retry composer self-update - -install: - - travis_retry composer install --no-interaction --prefer-source --no-suggest - -script: php vendor/bin/phpunit --colors=always --coverage-text From a7dcf2bb7abe49df700ab3726a63af829e25cc18 Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Sun, 24 Oct 2021 17:39:39 +0500 Subject: [PATCH 08/15] update README.md --- README.md | 53 ++++++++++++----------------------------------------- 1 file changed, 12 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index 4e83055..169ad64 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,18 @@ -## HTTP message wrapper for PHP 7.1+ (incl. PHP 8) based on RFC-7230, PSR-7 & PSR-17 +# HTTP message wrapper for PHP 7.1+ (incl. PHP 8) based on RFC-7230, PSR-7 and PSR-17 -[![Gitter](https://badges.gitter.im/sunrise-php/support.png)](https://gitter.im/sunrise-php/support) -[![Build Status](https://scrutinizer-ci.com/g/sunrise-php/http-message/badges/build.png?b=master)](https://scrutinizer-ci.com/g/sunrise-php/http-message/build-status/master) -[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/sunrise-php/http-message/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/sunrise-php/http-message/?branch=master) +[![Build Status](https://circleci.com/gh/sunrise-php/http-message.svg?style=shield)](https://circleci.com/gh/sunrise-php/http-message) [![Code Coverage](https://scrutinizer-ci.com/g/sunrise-php/http-message/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/sunrise-php/http-message/?branch=master) -[![Latest Stable Version](https://poser.pugx.org/sunrise/http-message/v/stable)](https://packagist.org/packages/sunrise/http-message) -[![Total Downloads](https://poser.pugx.org/sunrise/http-message/downloads)](https://packagist.org/packages/sunrise/http-message) -[![License](https://poser.pugx.org/sunrise/http-message/license)](https://packagist.org/packages/sunrise/http-message) - -## Awards +[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/sunrise-php/http-message/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/sunrise-php/http-message/?branch=master) +[![Total Downloads](https://poser.pugx.org/sunrise/http-message/downloads?format=flat)](https://packagist.org/packages/sunrise/http-message) +[![Latest Stable Version](https://poser.pugx.org/sunrise/http-message/v/stable?format=flat)](https://packagist.org/packages/sunrise/http-message) +[![License](https://poser.pugx.org/sunrise/http-message/license?format=flat)](https://packagist.org/packages/sunrise/http-message) -[![SymfonyInsight](https://insight.symfony.com/projects/62934e27-3e71-439c-9569-4aa57cdb3f36/big.svg)](https://insight.symfony.com/projects/62934e27-3e71-439c-9569-4aa57cdb3f36) +--- ## Installation ```bash -composer require sunrise/http-message +composer require 'sunrise/http-message:^2.0' ``` ## How to use? @@ -40,35 +37,7 @@ $message = (new ResponseFactory)->createResponse(200, 'OK'); // just use PSR-7 methods... ``` - -#### Using headers as objects - -> Please note that this isn't related to the PSR-7... - -```bash -composer require sunrise/http-header-kit -``` - -```php -use Sunrise\Http\Header\HeaderLastModified; - -$header = new HeaderLastModified(new \DateTime('1 day ago')); - -$response = $response->withHeaderObject($header); -``` - -```php -use Sunrise\Http\Header\HeaderCollection; -use Sunrise\Http\Header\HeaderContentLength; -use Sunrise\Http\Header\HeaderContentType; - -$headers = new HeaderCollection([ - new HeaderContentLength(1024), - new HeaderContentType('application/jpeg'), -]); - -$response = $response->withHeaderCollection($headers); -``` +#### HTTP Headers as Objects * https://github.com/sunrise-php/http-header-kit @@ -78,10 +47,12 @@ $response = $response->withHeaderCollection($headers); * https://github.com/sunrise-php/stream * https://github.com/sunrise-php/uri +--- + ## Test run ```bash -php vendor/bin/phpunit +composer test ``` ## Useful links From 5b8592a10330b8837aa8258337624cc0fa934688 Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Sun, 24 Oct 2021 17:39:49 +0500 Subject: [PATCH 09/15] update composer.json --- composer.json | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/composer.json b/composer.json index e87a300..d0e4ff9 100644 --- a/composer.json +++ b/composer.json @@ -1,16 +1,20 @@ { "name": "sunrise/http-message", "homepage": "https://github.com/sunrise-php/http-message", - "description": "Sunrise HTTP message wrapper for PHP 7.1+ based on RFC-7230, PSR-7 & PSR-17", + "description": "Sunrise // HTTP message wrapper for PHP 7.1+ based on RFC-7230, PSR-7 and PSR-17", "license": "MIT", "keywords": [ "fenric", "sunrise", "http", "http-message", + "http-request", + "http-response", "rfc-7230", "psr-7", - "psr-17" + "psr-17", + "php-7", + "php-8" ], "authors": [ { @@ -21,10 +25,10 @@ ], "require": { "php": "^7.1|^8.0", + "fig/http-message-util": "^1.1", "psr/http-factory": "^1.0", "psr/http-message": "^1.0", - "sunrise/http-header": "^1.1", - "sunrise/http-header-collection": "^1.1", + "sunrise/http-header": "^2.0", "sunrise/stream": "^1.2", "sunrise/uri": "^1.2" }, @@ -37,7 +41,7 @@ }, "autoload": { "files": [ - "data/phrases.php" + "constants/REASON_PHRASES.php" ], "psr-4": { "Sunrise\\Http\\Message\\": "src/" @@ -45,8 +49,12 @@ }, "scripts": { "test": [ - "phpunit --colors=always --coverage-text", - "phpcs" + "phpcs", + "XDEBUG_MODE=coverage phpunit --coverage-text --colors=always" + ], + "build": [ + "phpdoc -d src/ -t phpdoc/", + "XDEBUG_MODE=coverage phpunit --coverage-html coverage/" ] } } From 43ee951a61c050d24556340d7c92322d74a83024 Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Sun, 24 Oct 2021 17:40:06 +0500 Subject: [PATCH 10/15] renamed --- data/phrases.php => constants/REASON_PHRASES.php | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) rename data/phrases.php => constants/REASON_PHRASES.php (94%) diff --git a/data/phrases.php b/constants/REASON_PHRASES.php similarity index 94% rename from data/phrases.php rename to constants/REASON_PHRASES.php index a11eea6..815f05e 100644 --- a/data/phrases.php +++ b/constants/REASON_PHRASES.php @@ -12,13 +12,11 @@ namespace Sunrise\Http\Message; /** - * List of Phrases - * - * MUST NOT be used outside of this package. + * List of Reason Phrases * * @link http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml */ -const PHRASES = [ +const REASON_PHRASES = [ // 1xx 100 => 'Continue', @@ -45,7 +43,6 @@ 303 => 'See Other', 304 => 'Not Modified', 305 => 'Use Proxy', - 306 => '(Unused)', 307 => 'Temporary Redirect', 308 => 'Permanent Redirect', @@ -68,7 +65,6 @@ 415 => 'Unsupported Media Type', 416 => 'Range Not Satisfiable', 417 => 'Expectation Failed', - 418 => 'I\'m a teapot', 421 => 'Misdirected Request', 422 => 'Unprocessable Entity', 423 => 'Locked', From ccff4b79c7a36bd0f65764908ed52eeaf4c9993c Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Sun, 24 Oct 2021 17:40:19 +0500 Subject: [PATCH 11/15] update phpcs.xml.dist --- phpcs.xml.dist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpcs.xml.dist b/phpcs.xml.dist index 9873884..b1d5376 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -2,7 +2,7 @@ - data + constants src tests From 460a3ceb5120c4468841804e9e848f9cc9b1669c Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Sun, 24 Oct 2021 17:40:27 +0500 Subject: [PATCH 12/15] update phpunit.xml.dist --- phpunit.xml.dist | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/phpunit.xml.dist b/phpunit.xml.dist index ee8ab5a..7478e63 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,19 +1,13 @@ - + + + + ./src + + - + ./tests/ - - - ./src - - - - - - - - From fb9de3fa5ce4d9d04d3d2d7108527d752434965f Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Sun, 24 Oct 2021 17:43:55 +0500 Subject: [PATCH 13/15] update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 169ad64..41d959b 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# HTTP message wrapper for PHP 7.1+ (incl. PHP 8) based on RFC-7230, PSR-7 and PSR-17 +# HTTP message wrapper for PHP 7.1+ based on RFC-7230, PSR-7 and PSR-17 [![Build Status](https://circleci.com/gh/sunrise-php/http-message.svg?style=shield)](https://circleci.com/gh/sunrise-php/http-message) [![Code Coverage](https://scrutinizer-ci.com/g/sunrise-php/http-message/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/sunrise-php/http-message/?branch=master) From 63f453043605dcb9949d1cd312923fc5020c259c Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Sun, 24 Oct 2021 17:56:11 +0500 Subject: [PATCH 14/15] improve code --- src/ResponseFactory.php | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/ResponseFactory.php b/src/ResponseFactory.php index 5e34821..39f4063 100644 --- a/src/ResponseFactory.php +++ b/src/ResponseFactory.php @@ -16,17 +16,19 @@ */ use Psr\Http\Message\ResponseFactoryInterface; use Psr\Http\Message\ResponseInterface; -use JsonException; +use InvalidArgumentException; /** * Import functions */ use function json_encode; +use function json_last_error; +use function json_last_error_msg; /** * Import constants */ -use const JSON_THROW_ON_ERROR; +use const JSON_ERROR_NONE; /** * HTTP Response Message Factory @@ -54,10 +56,12 @@ public function createResponse(int $statusCode = 200, string $reasonPhrase = '') */ public function createHtmlResponse(int $status, $content) : ResponseInterface { + $content = (string) $content; + $headers = ['Content-Type' => 'text/html; charset=UTF-8']; + $response = new Response($status, null, $headers); - $content = (string) $content; $response->getBody()->write($content); return $response; @@ -73,14 +77,20 @@ public function createHtmlResponse(int $status, $content) : ResponseInterface * * @return ResponseInterface * - * @throws JsonException + * @throws InvalidArgumentException */ public function createJsonResponse(int $status, $payload, int $options = 0, int $depth = 512) : ResponseInterface { + json_encode(''); // reset previous error... + $content = json_encode($payload, $options, $depth); + if (JSON_ERROR_NONE <> json_last_error()) { + throw new InvalidArgumentException(json_last_error_msg()); + } + $headers = ['Content-Type' => 'application/json; charset=UTF-8']; + $response = new Response($status, null, $headers); - $content = json_encode($payload, $options | JSON_THROW_ON_ERROR, $depth); $response->getBody()->write($content); return $response; From 884550c5517c0424d59a5776b46a89b32ce2d808 Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Sun, 24 Oct 2021 17:56:23 +0500 Subject: [PATCH 15/15] improve tests --- tests/ResponseFactoryTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ResponseFactoryTest.php b/tests/ResponseFactoryTest.php index b2ff956..5a3d095 100644 --- a/tests/ResponseFactoryTest.php +++ b/tests/ResponseFactoryTest.php @@ -90,7 +90,7 @@ public function testCreateJsonResponse() : void */ public function testCreateResponseWithInvalidJson() : void { - $this->expectException(\JsonException::class); + $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage('Maximum stack depth exceeded'); $response = (new ResponseFactory)