Skip to content

Commit bce4e88

Browse files
authored
Release/4.5.0 (#48)
* feat: Refactor namespaces and add request handling classes for improved HTTP processing.
1 parent 57a753e commit bce4e88

33 files changed

+658
-110
lines changed

README.md

Lines changed: 87 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,19 @@
55
* [Overview](#overview)
66
* [Installation](#installation)
77
* [How to use](#how-to-use)
8-
* [Using the status code](#status_code)
9-
* [Creating a response](#response)
8+
* [Request](#request)
9+
* [Response](#response)
1010
* [License](#license)
1111
* [Contributing](#contributing)
1212

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

1515
## Overview
1616

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

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

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

3232
## How to use
3333

34-
The library exposes interfaces like `Headers` and concrete implementations like `Response`, `ContentType`, and others,
35-
which facilitate construction.
34+
The library exposes interfaces like `Headers` and concrete implementations like `Request`, `Response`, `ContentType`,
35+
and others, which facilitate construction.
3636

37-
<div id='status_code'></div>
37+
<div id='request'></div>
3838

39-
### Using the status code
39+
### Request
4040

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

44-
- **Get message**: Returns the [HTTP status message](https://developer.mozilla.org/en-US/docs/Web/HTTP/Messages)
45-
associated with the enum's code.
43+
The library provides a small public API to decode a PSR-7 `ServerRequestInterface` into a typed structure, allowing you
44+
to access route parameters and JSON body fields consistently.
45+
46+
- **Decode a request**: Use `Request::from(...)` to wrap the PSR-7 request and call `decode()`. The decoded object
47+
exposes `uri` and `body`.
4648

4749
```php
48-
use TinyBlocks\Http\Code;
49-
50-
Code::OK->value; # 200
51-
Code::OK->message(); # OK
52-
Code::IM_A_TEAPOT->message(); # I'm a teapot
53-
Code::INTERNAL_SERVER_ERROR->message(); # Internal Server Error
54-
```
50+
use Psr\Http\Message\ServerRequestInterface;
51+
use TinyBlocks\Http\Request;
5552

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

58-
```php
59-
use TinyBlocks\Http\Code;
60-
61-
Code::isValidCode(code: 200); # true
62-
Code::isValidCode(code: 999); # false
56+
$name = $decoded->body->get(key: 'name')->toString();
57+
$payload = $decoded->body->toArray();
58+
59+
$id = $decoded->uri->route()->get(key: 'id')->toInteger();
6360
```
6461

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

6765
```php
68-
use TinyBlocks\Http\Code;
66+
use TinyBlocks\Http\Request;
6967

70-
Code::isErrorCode(code: 500); # true
71-
Code::isErrorCode(code: 200); # false
68+
$decoded = Request::from(request: $psrRequest)->decode();
69+
70+
$id = $decoded->uri->route()->get(key: 'id')->toInteger(); # default: 0
71+
$note = $decoded->body->get(key: 'note')->toString(); # default: ""
72+
$tags = $decoded->body->get(key: 'tags')->toArray(); # default: []
73+
$price = $decoded->body->get(key: 'price')->toFloat(); # default: 0.00
74+
$active = $decoded->body->get(key: 'active')->toBoolean(); # default: false
7275
```
7376

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

7680
```php
77-
use TinyBlocks\Http\Code;
81+
use TinyBlocks\Http\Request;
7882

79-
Code::isSuccessCode(code: 500); # false
80-
Code::isSuccessCode(code: 200); # true
83+
$decoded = Request::from(request: $psrRequest)->decode();
84+
85+
$id = $decoded->uri->route(name: '_route_params')->get(key: 'id')->toInteger();
8186
```
8287

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

85-
### Creating a response
90+
### Response
91+
92+
#### Creating a response
8693

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

132+
#### Using the status code
133+
134+
The library exposes a concrete implementation through the `Code` enum. You can retrieve the status codes, their
135+
corresponding messages, and check for various status code ranges using the methods provided.
136+
137+
- **Get message**: Returns the [HTTP status message](https://developer.mozilla.org/en-US/docs/Web/HTTP/Messages)
138+
associated with the enum's code.
139+
140+
```php
141+
use TinyBlocks\Http\Code;
142+
143+
Code::OK->value; # 200
144+
Code::OK->message(); # OK
145+
Code::IM_A_TEAPOT->message(); # I'm a teapot
146+
Code::INTERNAL_SERVER_ERROR->message(); # Internal Server Error
147+
```
148+
149+
- **Check if the code is valid**: Determines if the given code is a valid HTTP status code represented by the enum.
150+
151+
```php
152+
use TinyBlocks\Http\Code;
153+
154+
Code::isValidCode(code: 200); # true
155+
Code::isValidCode(code: 999); # false
156+
```
157+
158+
- **Check if the code is an error**: Determines if the given code is in the error range (**4xx** or **5xx**).
159+
160+
```php
161+
use TinyBlocks\Http\Code;
162+
163+
Code::isErrorCode(code: 500); # true
164+
Code::isErrorCode(code: 200); # false
165+
```
166+
167+
- **Check if the code is a success**: Determines if the given code is in the success range (**2xx**).
168+
169+
```php
170+
use TinyBlocks\Http\Code;
171+
172+
Code::isSuccessCode(code: 500); # false
173+
Code::isSuccessCode(code: 200); # true
174+
```
175+
125176
<div id='license'></div>
126177

127178
## License

composer.json

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,11 @@
4141
},
4242
"autoload-dev": {
4343
"psr-4": {
44-
"TinyBlocks\\Http\\": "tests/"
44+
"Test\\TinyBlocks\\Http\\": "tests/"
4545
}
4646
},
4747
"require": {
4848
"php": "^8.5",
49-
"ext-mbstring": "*",
5049
"psr/http-message": "^2.0",
5150
"tiny-blocks/mapper": "^2.0"
5251
},
@@ -59,9 +58,6 @@
5958
"squizlabs/php_codesniffer": "^4.0",
6059
"laminas/laminas-httphandlerrunner": "^2.13"
6160
},
62-
"suggest": {
63-
"ext-mbstring": "Provides multibyte-specific string functions that help us deal with multibyte encodings in PHP."
64-
},
6561
"scripts": {
6662
"test": "php -d memory_limit=2G ./vendor/bin/phpunit --configuration phpunit.xml tests",
6763
"phpcs": "php ./vendor/bin/phpcs --standard=PSR12 --extensions=php ./src",

phpstan.neon.dist

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ parameters:
66
ignoreErrors:
77
- '#expects#'
88
- '#should return#'
9+
- '#mixed to string#'
910
- '#does not accept#'
1011
- '#type specified in iterable type#'
1112
reportUnmatchedIgnoredErrors: false

src/Internal/Request/Attribute.php

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TinyBlocks\Http\Internal\Request;
6+
7+
final readonly class Attribute
8+
{
9+
private function __construct(private mixed $value)
10+
{
11+
}
12+
13+
public static function from(mixed $value): Attribute
14+
{
15+
return new Attribute(value: $value);
16+
}
17+
18+
public function toArray(): array
19+
{
20+
return match (true) {
21+
is_array($this->value) => $this->value,
22+
default => []
23+
};
24+
}
25+
26+
public function toFloat(): float
27+
{
28+
return match (true) {
29+
is_scalar($this->value) => (float)$this->value,
30+
default => 0.00
31+
};
32+
}
33+
34+
public function toString(): string
35+
{
36+
return match (true) {
37+
is_scalar($this->value) => (string)$this->value,
38+
default => ''
39+
};
40+
}
41+
42+
public function toInteger(): int
43+
{
44+
return match (true) {
45+
is_scalar($this->value) => (int)$this->value,
46+
default => 0
47+
};
48+
}
49+
50+
public function toBoolean(): bool
51+
{
52+
return match (true) {
53+
is_scalar($this->value) => (bool)$this->value,
54+
default => false
55+
};
56+
}
57+
}

src/Internal/Request/Body.php

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TinyBlocks\Http\Internal\Request;
6+
7+
use Psr\Http\Message\ServerRequestInterface;
8+
use TinyBlocks\Http\Internal\Stream\StreamFactory;
9+
10+
final readonly class Body
11+
{
12+
private function __construct(private array $data)
13+
{
14+
}
15+
16+
public static function from(ServerRequestInterface $request): Body
17+
{
18+
$body = $request->getBody();
19+
$streamFactory = StreamFactory::fromStream(stream: $body);
20+
21+
if ($streamFactory->isEmptyContent()) {
22+
return new Body(data: []);
23+
}
24+
25+
return new Body(data: json_decode($streamFactory->content(), true));
26+
}
27+
28+
public function get(string $key): Attribute
29+
{
30+
$value = ($this->data[$key] ?? null);
31+
32+
return Attribute::from(value: $value);
33+
}
34+
35+
public function toArray(): array
36+
{
37+
return $this->data;
38+
}
39+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TinyBlocks\Http\Internal\Request;
6+
7+
final readonly class DecodedRequest
8+
{
9+
private function __construct(public Uri $uri, public Body $body)
10+
{
11+
}
12+
13+
public static function from(Uri $uri, Body $body): DecodedRequest
14+
{
15+
return new DecodedRequest(uri: $uri, body: $body);
16+
}
17+
}

src/Internal/Request/Decoder.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TinyBlocks\Http\Internal\Request;
6+
7+
use Psr\Http\Message\ServerRequestInterface;
8+
9+
final readonly class Decoder
10+
{
11+
private function __construct(private Uri $uri, private Body $body)
12+
{
13+
}
14+
15+
public static function from(ServerRequestInterface $request): Decoder
16+
{
17+
return new Decoder(uri: Uri::from(request: $request), body: Body::from(request: $request));
18+
}
19+
20+
public function decode(): DecodedRequest
21+
{
22+
return DecodedRequest::from(uri: $this->uri, body: $this->body);
23+
}
24+
}

src/Internal/Request/Uri.php

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TinyBlocks\Http\Internal\Request;
6+
7+
use Psr\Http\Message\ServerRequestInterface;
8+
9+
final readonly class Uri
10+
{
11+
private const string ROUTE = '__route__';
12+
13+
private function __construct(private ServerRequestInterface $request, private string $routeAttributeName)
14+
{
15+
}
16+
17+
public static function from(ServerRequestInterface $request): Uri
18+
{
19+
return new Uri(request: $request, routeAttributeName: self::ROUTE);
20+
}
21+
22+
public function route(string $name = self::ROUTE): Uri
23+
{
24+
return new Uri(request: $this->request, routeAttributeName: $name);
25+
}
26+
27+
public function get(string $key): Attribute
28+
{
29+
$attribute = $this->request->getAttribute($this->routeAttributeName);
30+
31+
if (is_array($attribute)) {
32+
return Attribute::from(value: $attribute[$key] ?? null);
33+
}
34+
35+
return Attribute::from(value: $attribute);
36+
}
37+
}

src/Internal/Response/InternalResponse.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
use TinyBlocks\Http\Code;
1111
use TinyBlocks\Http\Headers;
1212
use TinyBlocks\Http\Internal\Exceptions\BadMethodCall;
13-
use TinyBlocks\Http\Internal\Response\Stream\StreamFactory;
13+
use TinyBlocks\Http\Internal\Stream\StreamFactory;
1414

1515
final readonly class InternalResponse implements ResponseInterface
1616
{

0 commit comments

Comments
 (0)