Skip to content

Commit 9d86d13

Browse files
innocenzibrendt
andauthored
feat(router): support getting raw body from requests (#1093)
Co-authored-by: Brent Roose <[email protected]>
1 parent 78273cc commit 9d86d13

File tree

6 files changed

+72
-18
lines changed

6 files changed

+72
-18
lines changed

src/Tempest/Router/src/Input/StdinInputStream.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ public function parse(): array
1313
$input = file_get_contents('php://input');
1414

1515
if (json_validate($input)) {
16-
return json_decode($input, true);
16+
return json_decode($input, associative: true);
1717
}
1818

1919
$inputStreamData = str($input)

src/Tempest/Router/src/IsRequest.php

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
use Tempest\Validation\SkipValidation;
1212

1313
use function Tempest\get;
14+
use function Tempest\Support\Arr\get_by_key;
15+
use function Tempest\Support\Arr\has;
1416

1517
/** @phpstan-require-implements \Tempest\Router\Request */
1618
trait IsRequest
@@ -21,6 +23,9 @@ trait IsRequest
2123
#[SkipValidation]
2224
private(set) string $uri;
2325

26+
#[SkipValidation]
27+
private(set) ?string $raw = null;
28+
2429
#[SkipValidation]
2530
private(set) array $body = [];
2631

@@ -48,12 +53,14 @@ public function __construct(
4853
array $body = [],
4954
array $headers = [],
5055
array $files = [],
56+
?string $raw = null,
5157
) {
5258
$this->method = $method;
5359
$this->uri = $uri;
5460
$this->body = $body;
5561
$this->headers = RequestHeaders::normalizeFromArray($headers);
5662
$this->files = $files;
63+
$this->raw = $raw;
5764

5865
$this->path ??= $this->resolvePath();
5966
$this->query ??= $this->resolveQuery();
@@ -62,11 +69,11 @@ public function __construct(
6269
public function get(string $key, mixed $default = null): mixed
6370
{
6471
if (array_key_exists($key, $this->body)) {
65-
return $this->body[$key];
72+
return get_by_key($this->body, $key);
6673
}
6774

6875
if (array_key_exists($key, $this->query)) {
69-
return $this->query[$key];
76+
return get_by_key($this->query, $key);
7077
}
7178

7279
return $default;
@@ -116,13 +123,17 @@ public function has(string $key): bool
116123
return $this->hasQuery($key);
117124
}
118125

119-
public function hasBody(string $key): bool
126+
public function hasBody(?string $key = null): bool
120127
{
121-
return array_key_exists($key, $this->body);
128+
if ($key) {
129+
return has($this->body, $key);
130+
}
131+
132+
return count($this->body) || ((bool) $this->raw);
122133
}
123134

124135
public function hasQuery(string $key): bool
125136
{
126-
return array_key_exists($key, $this->query);
137+
return has($this->query, $key);
127138
}
128139
}

src/Tempest/Router/src/Mappers/PsrRequestToGenericRequestMapper.php

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,10 @@ public function map(mixed $from, mixed $to): GenericRequest
2626
{
2727
/** @var PsrRequest $from */
2828
$data = (array) $from->getParsedBody();
29+
$raw = $from->getBody()->getContents();
2930

30-
if (arr($from->getHeader('content-type'))->contains('application/json')) {
31-
$bodyContents = $from->getBody()->getContents();
32-
33-
if (json_validate($bodyContents)) {
34-
$data = [...$data, ...json_decode($bodyContents, true)];
35-
}
31+
if (arr($from->getHeader('content-type'))->contains('application/json') && json_validate($raw)) {
32+
$data = [...$data, ...json_decode($raw, associative: true)];
3633
}
3734

3835
$headersAsString = array_map(
@@ -50,6 +47,7 @@ public function map(mixed $from, mixed $to): GenericRequest
5047
return map([
5148
'method' => Method::from($from->getMethod()),
5249
'uri' => (string) $from->getUri(),
50+
'raw' => $raw,
5351
'body' => $data,
5452
'headers' => RequestHeaders::normalizeFromArray($headersAsString),
5553
'path' => $from->getUri()->getPath(),

src/Tempest/Router/src/Request.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ interface Request
1717
get;
1818
}
1919

20+
public ?string $raw {
21+
get;
22+
}
23+
2024
public array $body {
2125
get;
2226
}

tests/Integration/Mapper/PsrRequestToRequestMapperTest.php

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,16 @@
44

55
namespace Tests\Tempest\Integration\Mapper;
66

7+
use Laminas\Diactoros\ServerRequest;
8+
use Laminas\Diactoros\Stream;
79
use Laminas\Diactoros\UploadedFile;
8-
use Tempest\Mapper\Exceptions\MissingValuesException;
10+
use Laminas\Diactoros\Uri;
911
use Tempest\Router\GenericRequest;
1012
use Tempest\Router\Mappers\PsrRequestToGenericRequestMapper;
1113
use Tempest\Router\Request;
1214
use Tempest\Router\Upload;
13-
use Tests\Tempest\Fixtures\Modules\Books\Requests\CreateBookRequest;
14-
use Tests\Tempest\Fixtures\Modules\Posts\PostRequest;
1515
use Tests\Tempest\Integration\FrameworkIntegrationTestCase;
1616

17-
use function Tempest\map;
18-
use function Tempest\Support\arr;
19-
2017
/**
2118
* @internal
2219
*/
@@ -34,6 +31,25 @@ public function test_generic_request_is_used_when_interface_is_passed(): void
3431
$this->assertInstanceOf(GenericRequest::class, $request);
3532
}
3633

34+
public function test_raw(): void
35+
{
36+
$stream = new Stream(fopen('php://memory', 'r+'));
37+
$stream->write(json_encode(['foo' => 'bar']));
38+
$stream->rewind();
39+
40+
$request = new PsrRequestToGenericRequestMapper()->map(new ServerRequest(
41+
uri: new Uri('/json-endpoint'),
42+
method: 'POST',
43+
body: $stream,
44+
headers: [
45+
'Content-Type' => 'application/json',
46+
],
47+
), to: Request::class);
48+
49+
$this->assertEquals(json_encode(['foo' => 'bar']), $request->raw);
50+
$this->assertEquals(['foo' => 'bar'], $request->body);
51+
}
52+
3753
public function test_files(): void
3854
{
3955
$currentPath = __DIR__ . '/Fixtures/upload-current.txt';

tests/Integration/Route/RequestTest.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace Tests\Tempest\Integration\Route;
66

7+
use PHPUnit\Framework\Attributes\TestWith;
78
use Tempest\Database\Id;
89
use Tempest\Database\Migrations\CreateMigrationsTable;
910
use Tempest\Http\Method;
@@ -176,6 +177,9 @@ public function test_has(): void
176177
uri: '/?bar',
177178
body: [
178179
'foo' => false,
180+
'nested' => [
181+
'baz' => 'quux',
182+
],
179183
],
180184
);
181185

@@ -188,5 +192,26 @@ public function test_has(): void
188192
$this->assertFalse($request->hasBody('unknown'));
189193
$this->assertFalse($request->hasQuery('unknown'));
190194
$this->assertFalse($request->has('unknown'));
195+
$this->assertTrue($request->has('nested.baz'));
196+
$this->assertFalse($request->has('nested.bar'));
197+
$this->assertTrue($request->hasBody('nested.baz'));
198+
$this->assertFalse($request->hasBody('nested.bar'));
199+
}
200+
201+
#[TestWith([[], null, false])]
202+
#[TestWith([[], '', false])]
203+
#[TestWith([[], 'foo', true])]
204+
#[TestWith([['foo' => 'bar'], null, true])]
205+
#[TestWith([['foo' => 'bar'], 'foo', true])]
206+
public function test_body(array $body, ?string $raw, bool $expected): void
207+
{
208+
$request = new GenericRequest(
209+
method: Method::GET,
210+
uri: '/?bar',
211+
body: $body,
212+
raw: $raw,
213+
);
214+
215+
$this->assertSame($expected, $request->hasBody());
191216
}
192217
}

0 commit comments

Comments
 (0)