Skip to content
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
123 changes: 87 additions & 36 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,19 @@
* [Overview](#overview)
* [Installation](#installation)
* [How to use](#how-to-use)
* [Using the status code](#status_code)
* [Creating a response](#response)
* [Request](#request)
* [Response](#response)
* [License](#license)
* [Contributing](#contributing)

<div id='overview'></div>

## Overview

Common implementations for HTTP protocol. The library exposes concrete implementations that follow the PSR standards,
specifically designed to operate with [PSR-7](https://www.php-fig.org/psr/psr-7)
and [PSR-15](https://www.php-fig.org/psr/psr-15), providing solutions for building HTTP responses, requests, and other
HTTP-related components.
Common implementations for the HTTP protocol. The library exposes concrete implementations that follow the PSR standards
and are **framework-agnostic**, designed to work consistently across any ecosystem that supports
[PSR-7](https://www.php-fig.org/psr/psr-7) and [PSR-15](https://www.php-fig.org/psr/psr-15), providing solutions for
building HTTP responses, requests, and other HTTP-related components.

<div id='installation'></div>

Expand All @@ -31,58 +31,65 @@ composer require tiny-blocks/http

## How to use

The library exposes interfaces like `Headers` and concrete implementations like `Response`, `ContentType`, and others,
which facilitate construction.
The library exposes interfaces like `Headers` and concrete implementations like `Request`, `Response`, `ContentType`,
and others, which facilitate construction.

<div id='status_code'></div>
<div id='request'></div>

### Using the status code
### Request

The library exposes a concrete implementation through the `Code` enum. You can retrieve the status codes, their
corresponding messages, and check for various status code ranges using the methods provided.
#### Decoding a request

- **Get message**: Returns the [HTTP status message](https://developer.mozilla.org/en-US/docs/Web/HTTP/Messages)
associated with the enum's code.
The library provides a small public API to decode a PSR-7 `ServerRequestInterface` into a typed structure, allowing you
to access route parameters and JSON body fields consistently.

- **Decode a request**: Use `Request::from(...)` to wrap the PSR-7 request and call `decode()`. The decoded object
exposes `uri` and `body`.

```php
use TinyBlocks\Http\Code;

Code::OK->value; # 200
Code::OK->message(); # OK
Code::IM_A_TEAPOT->message(); # I'm a teapot
Code::INTERNAL_SERVER_ERROR->message(); # Internal Server Error
```
use Psr\Http\Message\ServerRequestInterface;
use TinyBlocks\Http\Request;

- **Check if the code is valid**: Determines if the given code is a valid HTTP status code represented by the enum.
/** @var ServerRequestInterface $psrRequest */
$decoded = Request::from(request: $psrRequest)->decode();

```php
use TinyBlocks\Http\Code;

Code::isValidCode(code: 200); # true
Code::isValidCode(code: 999); # false
$name = $decoded->body->get(key: 'name')->toString();
$payload = $decoded->body->toArray();

$id = $decoded->uri->route()->get(key: 'id')->toInteger();
```

- **Check if the code is an error**: Determines if the given code is in the error range (**4xx** or **5xx**).
- **Typed access with defaults**: Each value is returned as an Attribute, which provides safe conversions and default
values when the underlying value is missing or not compatible.

```php
use TinyBlocks\Http\Code;
use TinyBlocks\Http\Request;

Code::isErrorCode(code: 500); # true
Code::isErrorCode(code: 200); # false
$decoded = Request::from(request: $psrRequest)->decode();

$id = $decoded->uri->route()->get(key: 'id')->toInteger(); # default: 0
$note = $decoded->body->get(key: 'note')->toString(); # default: ""
$tags = $decoded->body->get(key: 'tags')->toArray(); # default: []
$price = $decoded->body->get(key: 'price')->toFloat(); # default: 0.00
$active = $decoded->body->get(key: 'active')->toBoolean(); # default: false
```

- **Check if the code is a success**: Determines if the given code is in the success range (**2xx**).
- **Custom route attribute name**: If your framework stores route params in a different request attribute, you can
specify it via route().

```php
use TinyBlocks\Http\Code;
use TinyBlocks\Http\Request;

Code::isSuccessCode(code: 500); # false
Code::isSuccessCode(code: 200); # true
$decoded = Request::from(request: $psrRequest)->decode();

$id = $decoded->uri->route(name: '_route_params')->get(key: 'id')->toInteger();
```

<div id='response'></div>

### Creating a response
### Response

#### Creating a response

The library provides an easy and flexible way to create HTTP responses, allowing you to specify the status code,
headers, and body. You can use the `Response` class to generate responses, and the result will always be a
Expand Down Expand Up @@ -122,6 +129,50 @@ to the [PSR-7](https://www.php-fig.org/psr/psr-7) standard.
->withHeader(name: 'X-NAME', value: 'Xpto');
```

#### Using the status code

The library exposes a concrete implementation through the `Code` enum. You can retrieve the status codes, their
corresponding messages, and check for various status code ranges using the methods provided.

- **Get message**: Returns the [HTTP status message](https://developer.mozilla.org/en-US/docs/Web/HTTP/Messages)
associated with the enum's code.

```php
use TinyBlocks\Http\Code;

Code::OK->value; # 200
Code::OK->message(); # OK
Code::IM_A_TEAPOT->message(); # I'm a teapot
Code::INTERNAL_SERVER_ERROR->message(); # Internal Server Error
```

- **Check if the code is valid**: Determines if the given code is a valid HTTP status code represented by the enum.

```php
use TinyBlocks\Http\Code;

Code::isValidCode(code: 200); # true
Code::isValidCode(code: 999); # false
```

- **Check if the code is an error**: Determines if the given code is in the error range (**4xx** or **5xx**).

```php
use TinyBlocks\Http\Code;

Code::isErrorCode(code: 500); # true
Code::isErrorCode(code: 200); # false
```

- **Check if the code is a success**: Determines if the given code is in the success range (**2xx**).

```php
use TinyBlocks\Http\Code;

Code::isSuccessCode(code: 500); # false
Code::isSuccessCode(code: 200); # true
```

<div id='license'></div>

## License
Expand Down
6 changes: 1 addition & 5 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,11 @@
},
"autoload-dev": {
"psr-4": {
"TinyBlocks\\Http\\": "tests/"
"Test\\TinyBlocks\\Http\\": "tests/"
}
},
"require": {
"php": "^8.5",
"ext-mbstring": "*",
"psr/http-message": "^2.0",
"tiny-blocks/mapper": "^2.0"
},
Expand All @@ -59,9 +58,6 @@
"squizlabs/php_codesniffer": "^4.0",
"laminas/laminas-httphandlerrunner": "^2.13"
},
"suggest": {
"ext-mbstring": "Provides multibyte-specific string functions that help us deal with multibyte encodings in PHP."
},
"scripts": {
"test": "php -d memory_limit=2G ./vendor/bin/phpunit --configuration phpunit.xml tests",
"phpcs": "php ./vendor/bin/phpcs --standard=PSR12 --extensions=php ./src",
Expand Down
1 change: 1 addition & 0 deletions phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ parameters:
ignoreErrors:
- '#expects#'
- '#should return#'
- '#mixed to string#'
- '#does not accept#'
- '#type specified in iterable type#'
reportUnmatchedIgnoredErrors: false
57 changes: 57 additions & 0 deletions src/Internal/Request/Attribute.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php

declare(strict_types=1);

namespace TinyBlocks\Http\Internal\Request;

final readonly class Attribute
{
private function __construct(private mixed $value)
{
}

public static function from(mixed $value): Attribute
{
return new Attribute(value: $value);
}

public function toArray(): array
{
return match (true) {
is_array($this->value) => $this->value,
default => []
};
}

public function toFloat(): float
{
return match (true) {
is_scalar($this->value) => (float)$this->value,
default => 0.00
};
}

public function toString(): string
{
return match (true) {
is_scalar($this->value) => (string)$this->value,
default => ''
};
}

public function toInteger(): int
{
return match (true) {
is_scalar($this->value) => (int)$this->value,
default => 0
};
}

public function toBoolean(): bool
{
return match (true) {
is_scalar($this->value) => (bool)$this->value,
default => false
};
}
}
39 changes: 39 additions & 0 deletions src/Internal/Request/Body.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

declare(strict_types=1);

namespace TinyBlocks\Http\Internal\Request;

use Psr\Http\Message\ServerRequestInterface;
use TinyBlocks\Http\Internal\Stream\StreamFactory;

final readonly class Body
{
private function __construct(private array $data)
{
}

public static function from(ServerRequestInterface $request): Body
{
$body = $request->getBody();
$streamFactory = StreamFactory::fromStream(stream: $body);

if ($streamFactory->isEmptyContent()) {
return new Body(data: []);
}

return new Body(data: json_decode($streamFactory->content(), true));
}

public function get(string $key): Attribute
{
$value = ($this->data[$key] ?? null);

return Attribute::from(value: $value);
}

public function toArray(): array
{
return $this->data;
}
}
17 changes: 17 additions & 0 deletions src/Internal/Request/DecodedRequest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

declare(strict_types=1);

namespace TinyBlocks\Http\Internal\Request;

final readonly class DecodedRequest
{
private function __construct(public Uri $uri, public Body $body)
{
}

public static function from(Uri $uri, Body $body): DecodedRequest
{
return new DecodedRequest(uri: $uri, body: $body);
}
}
24 changes: 24 additions & 0 deletions src/Internal/Request/Decoder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

declare(strict_types=1);

namespace TinyBlocks\Http\Internal\Request;

use Psr\Http\Message\ServerRequestInterface;

final readonly class Decoder
{
private function __construct(private Uri $uri, private Body $body)
{
}

public static function from(ServerRequestInterface $request): Decoder
{
return new Decoder(uri: Uri::from(request: $request), body: Body::from(request: $request));
}

public function decode(): DecodedRequest
{
return DecodedRequest::from(uri: $this->uri, body: $this->body);
}
}
37 changes: 37 additions & 0 deletions src/Internal/Request/Uri.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

declare(strict_types=1);

namespace TinyBlocks\Http\Internal\Request;

use Psr\Http\Message\ServerRequestInterface;

final readonly class Uri
{
private const string ROUTE = '__route__';

private function __construct(private ServerRequestInterface $request, private string $routeAttributeName)
{
}

public static function from(ServerRequestInterface $request): Uri
{
return new Uri(request: $request, routeAttributeName: self::ROUTE);
}

public function route(string $name = self::ROUTE): Uri
{
return new Uri(request: $this->request, routeAttributeName: $name);
}

public function get(string $key): Attribute
{
$attribute = $this->request->getAttribute($this->routeAttributeName);

if (is_array($attribute)) {
return Attribute::from(value: $attribute[$key] ?? null);
}

return Attribute::from(value: $attribute);
}
}
2 changes: 1 addition & 1 deletion src/Internal/Response/InternalResponse.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
use TinyBlocks\Http\Code;
use TinyBlocks\Http\Headers;
use TinyBlocks\Http\Internal\Exceptions\BadMethodCall;
use TinyBlocks\Http\Internal\Response\Stream\StreamFactory;
use TinyBlocks\Http\Internal\Stream\StreamFactory;

final readonly class InternalResponse implements ResponseInterface
{
Expand Down
Loading