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
57 changes: 51 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,19 +59,64 @@ to access route parameters and JSON body fields consistently.
$id = $decoded->uri()->route()->get(key: 'id')->toInteger();
```

- **Access the HTTP method**: Use `method()` directly on the `Request` to retrieve the HTTP verb as a typed `Method`
enum.

```php
use Psr\Http\Message\ServerRequestInterface;
use TinyBlocks\Http\Request;

/** @var ServerRequestInterface $psrRequest */
$request = Request::from(request: $psrRequest);

$method = $request->method(); # Method::POST
$method->value; # "POST"
```

- **Access the full URI**: Use `toString()` on the decoded `uri()` to retrieve the complete request URI as a string.

```php
use TinyBlocks\Http\Request;

$decoded = Request::from(request: $psrRequest)->decode();

$fullUri = $decoded->uri()->toString(); # "https://api.example.com/v1/dragons?sort=name"
```

- **Access query parameters**: Use `queryParameters()` on the decoded `uri()` to retrieve typed access to query string
values. Each value is returned as an `Attribute`, providing the same safe conversions and defaults as body fields.

```php
use TinyBlocks\Http\Request;

$decoded = Request::from(request: $psrRequest)->decode();

$queryParams = $decoded->uri()->queryParameters()->toArray(); # ['sort' => 'name', 'limit' => '50']
$sort = $decoded->uri()->queryParameters()->get(key: 'sort')->toString(); # "name"
$limit = $decoded->uri()->queryParameters()->get(key: 'limit')->toInteger(); # 50
$active = $decoded->uri()->queryParameters()->get(key: 'active')->toBoolean(); # default: false
```

- **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\Request;

$decoded = Request::from(request: $psrRequest)->decode();
$request = Request::from(request: $psrRequest);
$decoded = $request->decode();

$method = $request->method(); # default: Method enum

$id = $decoded->uri()->route()->get(key: 'id')->toInteger(); # default: 0
$uri = $decoded->uri()->toString(); # default: ""
$sort = $decoded->uri()->queryParameters()->get(key: 'sort')->toString(); # default: ""
$limit = $decoded->uri()->queryParameters()->get(key: 'limit')->toInteger(); # default: 0

$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
$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
```

- **Custom route attribute name**: If your framework stores route params in a different request attribute, you can
Expand Down
12 changes: 9 additions & 3 deletions src/Internal/Request/Body.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,17 @@ public static function from(ServerRequestInterface $request): Body
$body = $request->getBody();
$streamFactory = StreamFactory::fromStream(stream: $body);

if ($streamFactory->isEmptyContent()) {
return new Body(data: []);
if (!$streamFactory->isEmptyContent()) {
return new Body(data: json_decode($streamFactory->content(), true));
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The json_decode on line 22 can return null if the JSON is invalid or a non-array value if the JSON doesn't represent an array. This would cause a type error when constructing Body, which expects an array parameter. Consider adding a check to ensure json_decode returns an array before passing it to the constructor, falling through to the empty array case otherwise.

Suggested change
return new Body(data: json_decode($streamFactory->content(), true));
$decoded = json_decode($streamFactory->content(), true);
if (is_array($decoded)) {
return new Body(data: $decoded);
}

Copilot uses AI. Check for mistakes.
}

return new Body(data: json_decode($streamFactory->content(), true));
$parsedBody = $request->getParsedBody();

if (is_array($parsedBody)) {
return new Body(data: $parsedBody);
}

return new Body(data: []);
}

public function get(string $key): Attribute
Expand Down
5 changes: 4 additions & 1 deletion src/Internal/Request/Decoder.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ 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));
return new Decoder(
uri: Uri::from(request: $request),
body: Body::from(request: $request)
);
}

public function decode(): DecodedRequest
Expand Down
31 changes: 31 additions & 0 deletions src/Internal/Request/QueryParameters.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

namespace TinyBlocks\Http\Internal\Request;

use Psr\Http\Message\ServerRequestInterface;

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

public static function from(ServerRequestInterface $request): QueryParameters
{
return new QueryParameters(data: $request->getQueryParams());
}

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

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

public function toArray(): array
{
return $this->data;
}
}
25 changes: 24 additions & 1 deletion src/Internal/Request/Uri.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
use Psr\Http\Message\ServerRequestInterface;

/**
* Provides access to route parameters extracted from a PSR-7 ServerRequestInterface.
* Provides access to URI components and route parameters extracted from a PSR-7 ServerRequestInterface.
*
* The route parameters are resolved in the following priority:
* 1. The explicitly specified attribute name (default: `__route__`).
Expand All @@ -34,6 +34,29 @@ public static function from(ServerRequestInterface $request): Uri
);
}

/**
* Returns the full URI of the request as a string.
*
* Delegates to the PSR-7 UriInterface's string representation,
* which includes scheme, host, path, query string, and fragment.
*
* @return string The complete URI string (e.g., "https://api.example.com/v1/dragons?sort=name").
*/
public function toString(): string
{
return $this->request->getUri()->__toString();
}

/**
* Returns a typed wrapper around the query string parameters.
*
* @return QueryParameters Provides typed access to individual query parameters via get().
*/
public function queryParameters(): QueryParameters
{
return QueryParameters::from(request: $this->request);
}

/**
* Returns a new Uri instance configured to read route parameters from the given attribute name.
*
Expand Down
5 changes: 5 additions & 0 deletions src/Request.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,9 @@ public function decode(): DecodedRequest
{
return Decoder::from(request: $this->request)->decode();
}

public function method(): Method
{
return Method::from(value: $this->request->getMethod());
}
Comment on lines +27 to +30
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Method::from() call will throw a ValueError if the HTTP method string returned by getMethod() is not one of the standard HTTP methods defined in the Method enum (GET, PUT, POST, HEAD, PATCH, TRACE, DELETE, OPTIONS, CONNECT). While this is acceptable for most cases, some applications may need to handle custom HTTP methods like PROPFIND, MKCOL, or other WebDAV methods. Consider documenting this behavior or adding a test that validates this throws an exception for non-standard methods to make the behavior explicit.

Copilot uses AI. Check for mistakes.
}
Loading