Skip to content

Commit c37249e

Browse files
author
Анатолий Нехай
committed
json payload decoding
1 parent 43f5a92 commit c37249e

File tree

3 files changed

+289
-0
lines changed

3 files changed

+289
-0
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php declare(strict_types=1);
2+
3+
/**
4+
* It's free open-source software released under the MIT License.
5+
*
6+
* @author Anatoly Fenric <[email protected]>
7+
* @copyright Copyright (c) 2018, Anatoly Fenric
8+
* @license https://github.com/sunrise-php/http-router/blob/master/LICENSE
9+
* @link https://github.com/sunrise-php/http-router
10+
*/
11+
12+
namespace Sunrise\Http\Router\Exception;
13+
14+
/**
15+
* UndecodablePayloadException
16+
*
17+
* @since 2.15.0
18+
*/
19+
class UndecodablePayloadException extends BadRequestException
20+
{
21+
}
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
<?php declare(strict_types=1);
2+
3+
/**
4+
* It's free open-source software released under the MIT License.
5+
*
6+
* @author Anatoly Fenric <[email protected]>
7+
* @copyright Copyright (c) 2018, Anatoly Fenric
8+
* @license https://github.com/sunrise-php/http-router/blob/master/LICENSE
9+
* @link https://github.com/sunrise-php/http-router
10+
*/
11+
12+
namespace Sunrise\Http\Router\Middleware;
13+
14+
/**
15+
* Import classes
16+
*/
17+
use Psr\Http\Message\ResponseInterface;
18+
use Psr\Http\Message\ServerRequestInterface;
19+
use Psr\Http\Server\MiddlewareInterface;
20+
use Psr\Http\Server\RequestHandlerInterface;
21+
use Sunrise\Http\Router\Exception\UndecodablePayloadException;
22+
23+
/**
24+
* Import functions
25+
*/
26+
use function json_decode;
27+
use function json_last_error;
28+
use function json_last_error_msg;
29+
use function rtrim;
30+
use function strpos;
31+
use function substr;
32+
33+
/**
34+
* Import constants
35+
*/
36+
use const JSON_BIGINT_AS_STRING;
37+
use const JSON_ERROR_NONE;
38+
use const JSON_OBJECT_AS_ARRAY;
39+
40+
/**
41+
* JsonPayloadDecodingMiddleware
42+
*
43+
* @since 2.15.0
44+
*/
45+
class JsonPayloadDecodingMiddleware implements MiddlewareInterface
46+
{
47+
48+
/**
49+
* JSON Media Type
50+
*
51+
* @var string
52+
*
53+
* @link https://datatracker.ietf.org/doc/html/rfc4627
54+
*/
55+
private const JSON_MEDIA_TYPE = 'application/json';
56+
57+
/**
58+
* JSON decoding options
59+
*
60+
* @var int
61+
*
62+
* @link https://www.php.net/manual/ru/json.constants.php
63+
*/
64+
protected const JSON_DECODING_OPTIONS = JSON_BIGINT_AS_STRING|JSON_OBJECT_AS_ARRAY;
65+
66+
/**
67+
* {@inheritdoc}
68+
*/
69+
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface
70+
{
71+
if (!$this->isSupportedRequest($request)) {
72+
return $handler->handle($request);
73+
}
74+
75+
$parsedBody = $this->decodeRequestJsonPayload($request);
76+
77+
return $handler->handle($request->withParsedBody($parsedBody));
78+
}
79+
80+
/**
81+
* Checks if the given request is supported
82+
*
83+
* @param ServerRequestInterface $request
84+
*
85+
* @return bool
86+
*/
87+
private function isSupportedRequest(ServerRequestInterface $request) : bool
88+
{
89+
return self::JSON_MEDIA_TYPE === $this->getRequestMediaType($request);
90+
}
91+
92+
/**
93+
* Gets Media Type from the given request
94+
*
95+
* @param ServerRequestInterface $request
96+
*
97+
* @return string|null
98+
*
99+
* @link https://tools.ietf.org/html/rfc7231#section-3.1.1.1
100+
*/
101+
private function getRequestMediaType(ServerRequestInterface $request) : ?string
102+
{
103+
if (!$request->hasHeader('Content-Type')) {
104+
return null;
105+
}
106+
107+
// type "/" subtype *( OWS ";" OWS parameter )
108+
$mediaType = $request->getHeaderLine('Content-Type');
109+
110+
$semicolonPosition = strpos($mediaType, ';');
111+
if (false === $semicolonPosition) {
112+
return $mediaType;
113+
}
114+
115+
return rtrim(substr($mediaType, 0, $semicolonPosition));
116+
}
117+
118+
/**
119+
* Tries to decode the given request's JSON payload
120+
*
121+
* @param ServerRequestInterface $request
122+
*
123+
* @return mixed
124+
*
125+
* @throws UndecodablePayloadException
126+
* If the request's payload cannot be decoded.
127+
*/
128+
private function decodeRequestJsonPayload(ServerRequestInterface $request)
129+
{
130+
json_decode('');
131+
$result = json_decode($request->getBody()->__toString(), null, 512, static::JSON_DECODING_OPTIONS);
132+
if (JSON_ERROR_NONE === json_last_error()) {
133+
return $result;
134+
}
135+
136+
throw new UndecodablePayloadException(sprintf('Invalid Payload: %s', json_last_error_msg()));
137+
}
138+
}
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Sunrise\Http\Router\Tests\Middleware;
4+
5+
/**
6+
* Import classes
7+
*/
8+
use PHPUnit\Framework\TestCase;
9+
use Psr\Http\Server\MiddlewareInterface;
10+
use Sunrise\Http\Router\Exception\UndecodablePayloadException;
11+
use Sunrise\Http\Router\Middleware\JsonPayloadDecodingMiddleware;
12+
use Sunrise\Http\Router\Tests\Fixtures;
13+
use Sunrise\Http\ServerRequest\ServerRequestFactory;
14+
15+
/**
16+
* JsonPayloadDecodingMiddlewareTest
17+
*/
18+
class JsonPayloadDecodingMiddlewareTest extends TestCase
19+
{
20+
21+
/**
22+
* @return void
23+
*/
24+
public function testContract() : void
25+
{
26+
$this->assertInstanceOf(MiddlewareInterface::class, new JsonPayloadDecodingMiddleware());
27+
}
28+
29+
/**
30+
* @param string $mediaType
31+
*
32+
* @return void
33+
*
34+
* @dataProvider supportedMediaTypeProvider
35+
*/
36+
public function testProcessWithSupportedMediaType(string $mediaType) : void
37+
{
38+
$request = (new ServerRequestFactory)->createServerRequest('GET', '/')
39+
->withHeader('Content-Type', $mediaType);
40+
41+
$request->getBody()->write('{"foo":"bar"}');
42+
43+
$handler = new Fixtures\Controllers\BlankController();
44+
45+
(new JsonPayloadDecodingMiddleware)->process($request, $handler);
46+
47+
$this->assertSame(['foo' => 'bar'], $handler->getRequest()->getParsedBody());
48+
}
49+
50+
/**
51+
* @param string $mediaType
52+
*
53+
* @return void
54+
*
55+
* @dataProvider unsupportedMediaTypeProvider
56+
*/
57+
public function testProcessWithUnsupportedMediaType(string $mediaType) : void
58+
{
59+
$request = (new ServerRequestFactory)->createServerRequest('GET', '/')
60+
->withHeader('Content-Type', $mediaType);
61+
62+
$request->getBody()->write('{"foo":"bar"}');
63+
64+
$handler = new Fixtures\Controllers\BlankController();
65+
66+
(new JsonPayloadDecodingMiddleware)->process($request, $handler);
67+
68+
$this->assertNull($handler->getRequest()->getParsedBody());
69+
}
70+
71+
/**
72+
* @return void
73+
*/
74+
public function testProcessWithoutMediaType() : void
75+
{
76+
$request = (new ServerRequestFactory)->createServerRequest('GET', '/');
77+
$request->getBody()->write('{"foo":"bar"}');
78+
79+
$handler = new Fixtures\Controllers\BlankController();
80+
81+
(new JsonPayloadDecodingMiddleware)->process($request, $handler);
82+
83+
$this->assertNull($handler->getRequest()->getParsedBody());
84+
}
85+
86+
/**
87+
* @return void
88+
*/
89+
public function testProcessWithInvalidPayload() : void
90+
{
91+
$request = (new ServerRequestFactory)->createServerRequest('GET', '/')
92+
->withHeader('Content-Type', 'application/json');
93+
94+
$request->getBody()->write('!');
95+
96+
$handler = new Fixtures\Controllers\BlankController();
97+
98+
$this->expectException(UndecodablePayloadException::class);
99+
$this->expectExceptionMessage('Invalid Payload: Syntax error');
100+
101+
(new JsonPayloadDecodingMiddleware)->process($request, $handler);
102+
}
103+
104+
/**
105+
* @return list<array{0: string}>
106+
*/
107+
public function supportedMediaTypeProvider() : array
108+
{
109+
return [
110+
['application/json'],
111+
['application/json; foo=bar'],
112+
['application/json ; foo=bar'],
113+
];
114+
}
115+
116+
/**
117+
* @return list<array{0: string}>
118+
*/
119+
public function unsupportedMediaTypeProvider() : array
120+
{
121+
return [
122+
['application/jsonx'],
123+
['application/json+x'],
124+
['application/jsonx; foo=bar'],
125+
['application/json+x; foo=bar'],
126+
['application/jsonx ; foo=bar'],
127+
['application/json+x ; foo=bar'],
128+
];
129+
}
130+
}

0 commit comments

Comments
 (0)