Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Adds default status header based on HTTP code. Upgrade php version to 8.2. #22

Merged
merged 1 commit into from
Feb 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
Expand Down
16 changes: 10 additions & 6 deletions src/HttpHeaders.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -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
Expand Down
26 changes: 26 additions & 0 deletions src/HttpResponse.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
}
}
10 changes: 10 additions & 0 deletions src/Internal/Header.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
15 changes: 7 additions & 8 deletions src/Internal/Response.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
10 changes: 5 additions & 5 deletions src/Internal/Stream/StreamMetaData.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
) {
}

Expand Down
79 changes: 68 additions & 11 deletions tests/HttpResponseTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand All @@ -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());
}

/**
Expand All @@ -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());
}

/**
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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]
];
}
}
6 changes: 3 additions & 3 deletions tests/Internal/HeadersTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand All @@ -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());
Expand Down
11 changes: 7 additions & 4 deletions tests/Internal/ResponseTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
Expand All @@ -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];

Expand All @@ -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];

Expand All @@ -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'));
Expand Down
4 changes: 2 additions & 2 deletions tests/Mock/Xyz.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
}
}
Loading