From c7c71bdbd7e2309ae47bd72a69d64d606c42c748 Mon Sep 17 00:00:00 2001 From: Gustavo Freze Date: Sun, 4 Feb 2024 12:35:19 -0300 Subject: [PATCH] Release/3.0.0 - (#22) --- composer.json | 6 +- src/HttpHeaders.php | 16 ++++-- src/HttpResponse.php | 26 +++++++++ src/Internal/Header.php | 10 ++++ src/Internal/Response.php | 15 +++-- src/Internal/Stream/StreamMetaData.php | 10 ++-- tests/HttpResponseTest.php | 79 ++++++++++++++++++++++---- tests/Internal/HeadersTest.php | 6 +- tests/Internal/ResponseTest.php | 11 ++-- tests/Mock/Xyz.php | 4 +- 10 files changed, 141 insertions(+), 42 deletions(-) diff --git a/composer.json b/composer.json index d6813b5..b07c413 100644 --- a/composer.json +++ b/composer.json @@ -41,16 +41,16 @@ } }, "require": { - "php": "^8.1||^8.2", + "php": "^8.2", "tiny-blocks/serializer": "^2.0", "psr/http-message": "^1.1", "ext-mbstring": "*" }, "require-dev": { "infection/infection": "^0.27", - "phpmd/phpmd": "^2.13", + "phpmd/phpmd": "^2.15", "phpunit/phpunit": "^9.6", - "squizlabs/php_codesniffer": "^3.7" + "squizlabs/php_codesniffer": "^3.8" }, "suggest": { "ext-mbstring": "Provides multibyte-specific string functions that help us deal with multibyte encodings in PHP." diff --git a/src/HttpHeaders.php b/src/HttpHeaders.php index 4d19178..6d2e40f 100644 --- a/src/HttpHeaders.php +++ b/src/HttpHeaders.php @@ -13,16 +13,20 @@ final class HttpHeaders { private array $values = []; - private function __construct() + public static function build(): HttpHeaders { + return new HttpHeaders(); } - public static function build(): HttpHeaders + public function addFromCode(HttpCode $code): HttpHeaders { - return new HttpHeaders(); + $template = 'HTTP/1.1 %s'; + $this->values['Status'][] = sprintf($template, $code->message()); + + return $this; } - public function add(Header $header): HttpHeaders + public function addFromContentType(Header $header): HttpHeaders { $this->values[$header->key()][] = $header->value(); @@ -34,9 +38,9 @@ public function getHeader(string $key): array return $this->values[$key] ?? []; } - public function hasHeaders(): bool + public function hasNoHeaders(): bool { - return !empty($this->values); + return empty($this->values); } public function hasHeader(string $key): bool diff --git a/src/HttpResponse.php b/src/HttpResponse.php index 92fd54d..4b4d157 100644 --- a/src/HttpResponse.php +++ b/src/HttpResponse.php @@ -12,6 +12,8 @@ */ final class HttpResponse { + # Successful (200 – 299) + public static function ok(mixed $data, ?HttpHeaders $headers = null): ResponseInterface { return Response::from(code: HttpCode::OK, data: $data, headers: $headers); @@ -31,4 +33,28 @@ public static function noContent(?HttpHeaders $headers = null): ResponseInterfac { return Response::from(code: HttpCode::NO_CONTENT, data: null, headers: $headers); } + + # Client error (400 – 499) + + public static function badRequest(mixed $data, ?HttpHeaders $headers = null): ResponseInterface + { + return Response::from(code: HttpCode::BAD_REQUEST, data: $data, headers: $headers); + } + + public static function notFound(mixed $data, ?HttpHeaders $headers = null): ResponseInterface + { + return Response::from(code: HttpCode::NOT_FOUND, data: $data, headers: $headers); + } + + public static function conflict(mixed $data, ?HttpHeaders $headers = null): ResponseInterface + { + return Response::from(code: HttpCode::CONFLICT, data: $data, headers: $headers); + } + + # Server error (500 – 599) + + public static function internalServerError(mixed $data, ?HttpHeaders $headers = null): ResponseInterface + { + return Response::from(code: HttpCode::INTERNAL_SERVER_ERROR, data: $data, headers: $headers); + } } diff --git a/src/Internal/Header.php b/src/Internal/Header.php index 6eb3807..8d2d590 100644 --- a/src/Internal/Header.php +++ b/src/Internal/Header.php @@ -4,7 +4,17 @@ interface Header { + /** + * Get the key of the header. + * + * @return string The key of the header. + */ public function key(): string; + /** + * Get the value of the header. + * + * @return string The value of the header. + */ public function value(): string; } diff --git a/src/Internal/Response.php b/src/Internal/Response.php index 0154b35..a43c3d0 100644 --- a/src/Internal/Response.php +++ b/src/Internal/Response.php @@ -11,19 +11,18 @@ use TinyBlocks\Http\Internal\Exceptions\BadMethodCall; use TinyBlocks\Http\Internal\Stream\StreamFactory; -final class Response implements ResponseInterface +final readonly class Response implements ResponseInterface { - private function __construct( - private readonly HttpCode $code, - private readonly StreamInterface $body, - private readonly HttpHeaders $headers - ) { + private function __construct(private HttpCode $code, private StreamInterface $body, private HttpHeaders $headers) + { } public static function from(HttpCode $code, mixed $data, ?HttpHeaders $headers): ResponseInterface { - if (is_null($headers) || !$headers->hasHeaders()) { - $headers = HttpHeaders::build()->add(header: HttpContentType::APPLICATION_JSON); + if (is_null($headers) || $headers->hasNoHeaders()) { + $headers = HttpHeaders::build() + ->addFromCode(code: $code) + ->addFromContentType(header: HttpContentType::APPLICATION_JSON); } return new Response(code: $code, body: StreamFactory::from(data: $data), headers: $headers); diff --git a/src/Internal/Stream/StreamMetaData.php b/src/Internal/Stream/StreamMetaData.php index fe4131a..c25bfff 100644 --- a/src/Internal/Stream/StreamMetaData.php +++ b/src/Internal/Stream/StreamMetaData.php @@ -2,13 +2,13 @@ namespace TinyBlocks\Http\Internal\Stream; -final class StreamMetaData +final readonly class StreamMetaData { public function __construct( - private readonly string $uri, - private readonly string $mode, - private readonly bool $seekable, - private readonly string $streamType + private string $uri, + private string $mode, + private bool $seekable, + private string $streamType ) { } diff --git a/tests/HttpResponseTest.php b/tests/HttpResponseTest.php index 10e23a2..b458f13 100644 --- a/tests/HttpResponseTest.php +++ b/tests/HttpResponseTest.php @@ -8,13 +8,6 @@ class HttpResponseTest extends TestCase { - private array $defaultHeader; - - protected function setUp(): void - { - $this->defaultHeader = ['Content-Type' => [HttpContentType::APPLICATION_JSON->value]]; - } - /** * @dataProvider providerData */ @@ -26,7 +19,7 @@ public function testResponseOk(mixed $data, mixed $expected): void self::assertEquals($expected, $response->getBody()->getContents()); self::assertEquals(HttpCode::OK->value, $response->getStatusCode()); self::assertEquals(HttpCode::OK->message(), $response->getReasonPhrase()); - self::assertEquals($this->defaultHeader, $response->getHeaders()); + self::assertEquals($this->defaultHeaderFrom(code: HttpCode::OK), $response->getHeaders()); } /** @@ -40,7 +33,7 @@ public function testResponseCreated(mixed $data, mixed $expected): void self::assertEquals($expected, $response->getBody()->getContents()); self::assertEquals(HttpCode::CREATED->value, $response->getStatusCode()); self::assertEquals(HttpCode::CREATED->message(), $response->getReasonPhrase()); - self::assertEquals($this->defaultHeader, $response->getHeaders()); + self::assertEquals($this->defaultHeaderFrom(code: HttpCode::CREATED), $response->getHeaders()); } /** @@ -54,7 +47,7 @@ public function testResponseAccepted(mixed $data, mixed $expected): void self::assertEquals($expected, $response->getBody()->getContents()); self::assertEquals(HttpCode::ACCEPTED->value, $response->getStatusCode()); self::assertEquals(HttpCode::ACCEPTED->message(), $response->getReasonPhrase()); - self::assertEquals($this->defaultHeader, $response->getHeaders()); + self::assertEquals($this->defaultHeaderFrom(code: HttpCode::ACCEPTED), $response->getHeaders()); } public function testResponseNoContent(): void @@ -65,7 +58,63 @@ public function testResponseNoContent(): void self::assertEquals('', $response->getBody()->getContents()); self::assertEquals(HttpCode::NO_CONTENT->value, $response->getStatusCode()); self::assertEquals(HttpCode::NO_CONTENT->message(), $response->getReasonPhrase()); - self::assertEquals($this->defaultHeader, $response->getHeaders()); + self::assertEquals($this->defaultHeaderFrom(code: HttpCode::NO_CONTENT), $response->getHeaders()); + } + + /** + * @dataProvider providerData + */ + public function testResponseBadRequest(mixed $data, mixed $expected): void + { + $response = HttpResponse::badRequest(data: $data); + + self::assertEquals($expected, $response->getBody()->__toString()); + self::assertEquals($expected, $response->getBody()->getContents()); + self::assertEquals(HttpCode::BAD_REQUEST->value, $response->getStatusCode()); + self::assertEquals(HttpCode::BAD_REQUEST->message(), $response->getReasonPhrase()); + self::assertEquals($this->defaultHeaderFrom(code: HttpCode::BAD_REQUEST), $response->getHeaders()); + } + + /** + * @dataProvider providerData + */ + public function testResponseNotFound(mixed $data, mixed $expected): void + { + $response = HttpResponse::notFound(data: $data); + + self::assertEquals($expected, $response->getBody()->__toString()); + self::assertEquals($expected, $response->getBody()->getContents()); + self::assertEquals(HttpCode::NOT_FOUND->value, $response->getStatusCode()); + self::assertEquals(HttpCode::NOT_FOUND->message(), $response->getReasonPhrase()); + self::assertEquals($this->defaultHeaderFrom(code: HttpCode::NOT_FOUND), $response->getHeaders()); + } + + /** + * @dataProvider providerData + */ + public function testResponseConflict(mixed $data, mixed $expected): void + { + $response = HttpResponse::conflict(data: $data); + + self::assertEquals($expected, $response->getBody()->__toString()); + self::assertEquals($expected, $response->getBody()->getContents()); + self::assertEquals(HttpCode::CONFLICT->value, $response->getStatusCode()); + self::assertEquals(HttpCode::CONFLICT->message(), $response->getReasonPhrase()); + self::assertEquals($this->defaultHeaderFrom(code: HttpCode::CONFLICT), $response->getHeaders()); + } + + /** + * @dataProvider providerData + */ + public function testResponseInternalServerError(mixed $data, mixed $expected): void + { + $response = HttpResponse::internalServerError(data: $data); + + self::assertEquals($expected, $response->getBody()->__toString()); + self::assertEquals($expected, $response->getBody()->getContents()); + self::assertEquals(HttpCode::INTERNAL_SERVER_ERROR->value, $response->getStatusCode()); + self::assertEquals(HttpCode::INTERNAL_SERVER_ERROR->message(), $response->getReasonPhrase()); + self::assertEquals($this->defaultHeaderFrom(code: HttpCode::INTERNAL_SERVER_ERROR), $response->getHeaders()); } public function providerData(): array @@ -97,4 +146,12 @@ public function providerData(): array ] ]; } + + private function defaultHeaderFrom(HttpCode $code): array + { + return [ + 'Status' => [sprintf('HTTP/1.1 %s', $code->message())], + 'Content-Type' => [HttpContentType::APPLICATION_JSON->value] + ]; + } } diff --git a/tests/Internal/HeadersTest.php b/tests/Internal/HeadersTest.php index dc34fba..2094cfd 100644 --- a/tests/Internal/HeadersTest.php +++ b/tests/Internal/HeadersTest.php @@ -10,7 +10,7 @@ class HeadersTest extends TestCase { public function testAddAndGetValues(): void { - $headers = HttpHeaders::build()->add(header: HttpContentType::APPLICATION_JSON); + $headers = HttpHeaders::build()->addFromContentType(header: HttpContentType::APPLICATION_JSON); $expected = ['Content-Type' => [HttpContentType::APPLICATION_JSON->value]]; self::assertEquals($expected, $headers->toArray()); @@ -19,8 +19,8 @@ public function testAddAndGetValues(): void public function testAddAndGetUniqueValues(): void { $headers = HttpHeaders::build() - ->add(header: HttpContentType::TEXT_HTML) - ->add(header: HttpContentType::APPLICATION_PDF); + ->addFromContentType(header: HttpContentType::TEXT_HTML) + ->addFromContentType(header: HttpContentType::APPLICATION_PDF); $expected = ['Content-Type' => [HttpContentType::APPLICATION_PDF->value]]; self::assertEquals($expected, $headers->toArray()); diff --git a/tests/Internal/ResponseTest.php b/tests/Internal/ResponseTest.php index 25d7f59..0b37aca 100644 --- a/tests/Internal/ResponseTest.php +++ b/tests/Internal/ResponseTest.php @@ -14,7 +14,10 @@ class ResponseTest extends TestCase public function testDefaultHeaders(): void { $response = Response::from(code: HttpCode::OK, data: [], headers: null); - $expected = ['Content-Type' => [HttpContentType::APPLICATION_JSON->value]]; + $expected = [ + 'Status' => [sprintf('HTTP/1.1 %s', HttpCode::OK->message())], + 'Content-Type' => [HttpContentType::APPLICATION_JSON->value] + ]; self::assertEquals($expected, $response->getHeaders()); } @@ -28,7 +31,7 @@ public function testGetProtocolVersion(): void public function testGetHeaders(): void { - $headers = HttpHeaders::build()->add(header: HttpContentType::APPLICATION_JSON); + $headers = HttpHeaders::build()->addFromContentType(header: HttpContentType::APPLICATION_JSON); $response = Response::from(code: HttpCode::OK, data: [], headers: $headers); $expected = [HttpContentType::APPLICATION_JSON->value]; @@ -38,7 +41,7 @@ public function testGetHeaders(): void public function testHasHeader(): void { - $headers = HttpHeaders::build()->add(header: HttpContentType::TEXT_PLAIN); + $headers = HttpHeaders::build()->addFromContentType(header: HttpContentType::TEXT_PLAIN); $response = Response::from(code: HttpCode::OK, data: [], headers: $headers); $expected = [HttpContentType::TEXT_PLAIN->value]; @@ -48,7 +51,7 @@ public function testHasHeader(): void public function testGetHeaderLine(): void { - $headers = HttpHeaders::build()->add(header: HttpContentType::APPLICATION_JSON); + $headers = HttpHeaders::build()->addFromContentType(header: HttpContentType::APPLICATION_JSON); $response = Response::from(code: HttpCode::OK, data: [], headers: $headers); self::assertEquals(HttpContentType::APPLICATION_JSON->value, $response->getHeaderLine(name: 'Content-Type')); diff --git a/tests/Mock/Xyz.php b/tests/Mock/Xyz.php index 673e17d..b6a5fb6 100644 --- a/tests/Mock/Xyz.php +++ b/tests/Mock/Xyz.php @@ -2,9 +2,9 @@ namespace TinyBlocks\Http\Mock; -final class Xyz +final readonly class Xyz { - public function __construct(public readonly int $value) + public function __construct(public int $value) { } }