Skip to content

Commit 9d1b42f

Browse files
committed
Add BodyParamsMiddleware
1 parent e6d6526 commit 9d1b42f

File tree

2 files changed

+273
-0
lines changed

2 files changed

+273
-0
lines changed
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace HttpSoft\Basis\Middleware;
6+
7+
use JsonException;
8+
use Psr\Http\Message\ResponseInterface;
9+
use Psr\Http\Message\ServerRequestInterface;
10+
use Psr\Http\Server\MiddlewareInterface;
11+
use Psr\Http\Server\RequestHandlerInterface;
12+
13+
use function json_decode;
14+
use function in_array;
15+
use function parse_str;
16+
use function preg_match;
17+
18+
final class BodyParamsMiddleware implements MiddlewareInterface
19+
{
20+
/**
21+
* {@inheritDoc}
22+
*
23+
* @throws JsonException
24+
* @link https://tools.ietf.org/html/rfc7231
25+
* @psalm-suppress MixedAssignment
26+
* @psalm-suppress MixedArgument
27+
*/
28+
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
29+
{
30+
if ($request->getParsedBody() || in_array($request->getMethod(), ['GET', 'HEAD', 'OPTIONS'], true)) {
31+
return $handler->handle($request);
32+
}
33+
34+
$contentType = $request->getHeaderLine('content-type');
35+
36+
if (preg_match('#^application/(|[\S]+\+)json($|[ ;])#', $contentType)) {
37+
$parsedBody = json_decode((string) $request->getBody(), true, 512, JSON_THROW_ON_ERROR);
38+
} elseif (preg_match('#^application/x-www-form-urlencoded($|[ ;])#', $contentType)) {
39+
parse_str((string) $request->getBody(), $parsedBody);
40+
}
41+
42+
return $handler->handle(empty($parsedBody) ? $request : $request->withParsedBody($parsedBody));
43+
}
44+
}
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace HttpSoft\Tests\Basis\Middleware;
6+
7+
use HttpSoft\Basis\Middleware\BodyParamsMiddleware;
8+
use HttpSoft\Message\Response;
9+
use HttpSoft\Message\ServerRequest;
10+
use JsonException;
11+
use PHPUnit\Framework\TestCase;
12+
use Psr\Http\Message\ResponseInterface;
13+
use Psr\Http\Message\ServerRequestInterface;
14+
use Psr\Http\Server\RequestHandlerInterface;
15+
16+
use function rtrim;
17+
18+
class BodyParamsMiddlewareTest extends TestCase
19+
{
20+
/**
21+
* @var BodyParamsMiddleware
22+
*/
23+
private BodyParamsMiddleware $middleware;
24+
25+
public function setUp(): void
26+
{
27+
$this->middleware = new BodyParamsMiddleware();
28+
}
29+
30+
public function testProcessWithNotEmptyParsedBody(): void
31+
{
32+
$parsedBody = ['name' => 'value'];
33+
34+
$request = $this->createServerRequest('POST', 'application/json', $parsedBody);
35+
$request->getBody()->write('{"body":"content"}');
36+
$response = $this->middleware->process($request, $this->createRequestHandler());
37+
$this->assertSame(($this->parsedBodyToString())($parsedBody), (string) $response->getBody());
38+
39+
$request = $this->createServerRequest('PATCH', 'application/*+json', $parsedBody);
40+
$request->getBody()->write('{"body":"content"}');
41+
$response = $this->middleware->process($request, $this->createRequestHandler());
42+
$this->assertSame(($this->parsedBodyToString())($parsedBody), (string) $response->getBody());
43+
44+
$request = $this->createServerRequest('PUT', 'application/x-www-form-urlencoded', $parsedBody);
45+
$request->getBody()->write('body=content');
46+
$response = $this->middleware->process($request, $this->createRequestHandler());
47+
$this->assertSame(($this->parsedBodyToString())($parsedBody), (string) $response->getBody());
48+
}
49+
50+
public function testProcessWithMethodsForNonBodyRequest(): void
51+
{
52+
$request = $this->createServerRequest('GET', 'application/json');
53+
$response = $this->middleware->process($request, $this->createRequestHandler());
54+
$this->assertSame('', (string) $response->getBody());
55+
56+
$request = $this->createServerRequest('HEAD', 'application/*+json');
57+
$response = $this->middleware->process($request, $this->createRequestHandler());
58+
$this->assertSame('', (string) $response->getBody());
59+
60+
$request = $this->createServerRequest('OPTIONS', 'application/x-www-form-urlencoded');
61+
$response = $this->middleware->process($request, $this->createRequestHandler());
62+
$this->assertSame('', (string) $response->getBody());
63+
}
64+
65+
/**
66+
* @return array[]
67+
*/
68+
public function matchDataProvider(): array
69+
{
70+
return [
71+
'form' => [
72+
'application/x-www-form-urlencoded',
73+
'name=value',
74+
'name:value',
75+
],
76+
'form-with-charset' => [
77+
'application/x-www-form-urlencoded ; charset=UTF-8',
78+
'name1=value1&name2=value2',
79+
'name1:value1,name2:value2',
80+
],
81+
'json' => [
82+
"application/json ",
83+
'{"name":"value"}',
84+
'name:value',
85+
],
86+
'json-with-charset' => [
87+
"application/json; charset=UTF-8 ",
88+
'{"name":"value"}',
89+
'name:value',
90+
],
91+
'json-suffix' => [
92+
'application/vnd.api+json;charset=UTF-8',
93+
'{"name":"value"}',
94+
'name:value',
95+
],
96+
];
97+
}
98+
99+
/**
100+
* @dataProvider matchDataProvider
101+
* @param string $contentType
102+
* @param string $requestBody
103+
* @param string $expectedBody
104+
* @throws JsonException
105+
*/
106+
public function testProcessWithMatchData(string $contentType, string $requestBody, string $expectedBody)
107+
{
108+
$request = $this->createServerRequest('POST', $contentType);
109+
$request->getBody()->write($requestBody);
110+
$response = $this->middleware->process($request, $this->createRequestHandler());
111+
$this->assertSame($expectedBody, (string) $response->getBody());
112+
}
113+
114+
/**
115+
* @return array[]
116+
*/
117+
public function mismatchDataProvider(): array
118+
{
119+
return [
120+
'html-content-type' => [
121+
'text/html',
122+
'<h1>name</h1>',
123+
],
124+
'text-content-type' => [
125+
'text/plain',
126+
'name:value',
127+
],
128+
'xml-content-type' => [
129+
'application/xml',
130+
'<name>name</name>',
131+
],
132+
'empty-content-type' => [
133+
'',
134+
'{"name":"value"}',
135+
],
136+
'unknown-content-type' => [
137+
'application/name+value',
138+
'{"name":"value"}',
139+
],
140+
'invalid-content-type' => [
141+
'name',
142+
'{"name":"value"}',
143+
],
144+
'invalid-json-content-type' => [
145+
"application/ json",
146+
'{"name":"value"}',
147+
],
148+
'invalid-form-content-type' => [
149+
'application/ x-www-form-urlencoded',
150+
'name=value',
151+
],
152+
];
153+
}
154+
155+
/**
156+
* @dataProvider mismatchDataProvider
157+
* @param string $contentType
158+
* @param string $requestBody
159+
* @throws JsonException
160+
*/
161+
public function testProcessWithMismatchData(string $contentType, string $requestBody)
162+
{
163+
$request = $this->createServerRequest('POST', $contentType);
164+
$request->getBody()->write($requestBody);
165+
$response = $this->middleware->process($request, $this->createRequestHandler());
166+
$this->assertSame('', (string) $response->getBody());
167+
}
168+
169+
public function testProcessThrowJsonExceptionForInvalidJsonBody(): void
170+
{
171+
$request = $this->createServerRequest('POST', 'application/json');
172+
$request->getBody()->write('{"name"}/value');
173+
$this->expectException(JsonException::class);
174+
$this->middleware->process($request, $this->createRequestHandler());
175+
}
176+
177+
/**
178+
* @param string $method
179+
* @param string|null $contentType
180+
* @param null|array|object $parsedBody
181+
* @return ServerRequestInterface
182+
*/
183+
private function createServerRequest(
184+
string $method,
185+
string $contentType = null,
186+
$parsedBody = null
187+
): ServerRequestInterface {
188+
$headers = ($contentType === null) ? [] : ['Content-Type' => $contentType];
189+
return new ServerRequest([], [], [], [], $parsedBody, $method, 'https://example.com', $headers);
190+
}
191+
192+
/**
193+
* @return RequestHandlerInterface
194+
*/
195+
private function createRequestHandler(): RequestHandlerInterface
196+
{
197+
return new class ($this->parsedBodyToString()) implements RequestHandlerInterface {
198+
public $callback;
199+
200+
public function __construct(callable $callback)
201+
{
202+
$this->callback = $callback;
203+
}
204+
205+
public function handle(ServerRequestInterface $request): ResponseInterface
206+
{
207+
$response = new Response();
208+
$response->getBody()->write(($this->callback)($request->getParsedBody()));
209+
return $response;
210+
}
211+
};
212+
}
213+
214+
/**
215+
* @return callable
216+
*/
217+
private function parsedBodyToString(): callable
218+
{
219+
return static function (?array $parsedBody): string {
220+
$content = '';
221+
222+
foreach ((array) $parsedBody as $name => $value) {
223+
$content .= "{$name}:{$value},";
224+
}
225+
226+
return rtrim($content, ',');
227+
};
228+
}
229+
}

0 commit comments

Comments
 (0)