Skip to content

Commit f39e70c

Browse files
authored
Merge pull request #98 from sunrise-php/release/v2.15.0
v2.15.0
2 parents 43f5a92 + 7208cd1 commit f39e70c

File tree

8 files changed

+321
-5
lines changed

8 files changed

+321
-5
lines changed

CHANGELOG.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@
44

55
## v2.14.0
66

7-
* New method added: `Route::getHolder():Reflector`;
8-
* New method added: `Router::resolveHostname(string):?string`;
9-
* New method added: `Router::getRoutesByHostname(string):array`;
10-
* New method added: `RouterBuilder::setEventDispatcher(?EventDispatcherInterface):void`.
7+
* New method: `Route::getHolder():Reflector`;
8+
* New method: `Router::resolveHostname(string):?string`;
9+
* New method: `Router::getRoutesByHostname(string):array`;
10+
* New method: `RouterBuilder::setEventDispatcher(?EventDispatcherInterface):void`.
11+
12+
## v2.15.0
13+
14+
* New middleware: `Sunrise\Http\Router\Middleware\JsonPayloadDecodingMiddleware`.

README.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
## Installation
1313

1414
```bash
15-
composer require 'sunrise/http-router:^2.14'
15+
composer require 'sunrise/http-router:^2.15'
1616
```
1717

1818
## Support for OpenAPI (Swagger) Specification (optional)
@@ -470,6 +470,14 @@ final class SomeController
470470

471471
## Useful to know
472472

473+
### JSON-payload decoding
474+
475+
```php
476+
use Sunrise\Http\Router\Middleware\JsonPayloadDecodingMiddleware;
477+
478+
$router->addMiddleware(new JsonPayloadDecodingMiddleware());
479+
```
480+
473481
### Get a current route
474482

475483
#### Through Router
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+
}

src/Loader/ConfigLoader.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,9 @@ public function load() : RouteCollectionInterface
149149

150150
foreach ($this->resources as $resource) {
151151
(function () use ($resource) {
152+
/**
153+
* @psalm-suppress UnresolvableInclude
154+
*/
152155
require $resource;
153156
})->call($collector);
154157
}

src/Loader/DescriptorLoader.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,9 @@ private function scandir(string $directory) : array
451451

452452
foreach ($files as $file) {
453453
if ('php' === $file->getExtension()) {
454+
/**
455+
* @psalm-suppress UnresolvableInclude
456+
*/
454457
require_once $file->getPathname();
455458
}
456459
}
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
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+
39+
/**
40+
* JsonPayloadDecodingMiddleware
41+
*
42+
* @since 2.15.0
43+
*/
44+
class JsonPayloadDecodingMiddleware implements MiddlewareInterface
45+
{
46+
47+
/**
48+
* JSON Media Type
49+
*
50+
* @var string
51+
*
52+
* @link https://datatracker.ietf.org/doc/html/rfc4627
53+
*/
54+
private const JSON_MEDIA_TYPE = 'application/json';
55+
56+
/**
57+
* JSON decoding options
58+
*
59+
* @var int
60+
*
61+
* @link https://www.php.net/manual/ru/json.constants.php
62+
*/
63+
protected const JSON_DECODING_OPTIONS = JSON_BIGINT_AS_STRING;
64+
65+
/**
66+
* {@inheritdoc}
67+
*/
68+
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface
69+
{
70+
if (!$this->isSupportedRequest($request)) {
71+
return $handler->handle($request);
72+
}
73+
74+
$parsedBody = $this->decodeRequestJsonPayload($request);
75+
76+
return $handler->handle($request->withParsedBody($parsedBody));
77+
}
78+
79+
/**
80+
* Checks if the given request is supported
81+
*
82+
* @param ServerRequestInterface $request
83+
*
84+
* @return bool
85+
*/
86+
private function isSupportedRequest(ServerRequestInterface $request) : bool
87+
{
88+
return self::JSON_MEDIA_TYPE === $this->getRequestMediaType($request);
89+
}
90+
91+
/**
92+
* Gets Media Type from the given request
93+
*
94+
* @param ServerRequestInterface $request
95+
*
96+
* @return string|null
97+
*
98+
* @link https://tools.ietf.org/html/rfc7231#section-3.1.1.1
99+
*/
100+
private function getRequestMediaType(ServerRequestInterface $request) : ?string
101+
{
102+
if (!$request->hasHeader('Content-Type')) {
103+
return null;
104+
}
105+
106+
// type "/" subtype *( OWS ";" OWS parameter )
107+
$mediaType = $request->getHeaderLine('Content-Type');
108+
109+
$semicolonPosition = strpos($mediaType, ';');
110+
if (false === $semicolonPosition) {
111+
return $mediaType;
112+
}
113+
114+
return rtrim(substr($mediaType, 0, $semicolonPosition));
115+
}
116+
117+
/**
118+
* Tries to decode the given request's JSON payload
119+
*
120+
* @param ServerRequestInterface $request
121+
*
122+
* @return mixed
123+
*
124+
* @throws UndecodablePayloadException
125+
* If the request's payload cannot be decoded.
126+
*/
127+
private function decodeRequestJsonPayload(ServerRequestInterface $request)
128+
{
129+
json_decode('');
130+
$result = json_decode($request->getBody()->__toString(), true, 512, static::JSON_DECODING_OPTIONS);
131+
if (JSON_ERROR_NONE === json_last_error()) {
132+
return $result;
133+
}
134+
135+
throw new UndecodablePayloadException(sprintf('Invalid Payload: %s', json_last_error_msg()));
136+
}
137+
}

src/Router.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -480,7 +480,12 @@ public function run(ServerRequestInterface $request) : ResponseInterface
480480

481481
if (isset($this->eventDispatcher)) {
482482
$event = new RouteEvent($route, $request);
483+
484+
/**
485+
* @psalm-suppress TooManyArguments
486+
*/
483487
$this->eventDispatcher->dispatch($event, RouteEvent::NAME);
488+
484489
$request = $event->getRequest();
485490
}
486491

@@ -508,7 +513,12 @@ public function handle(ServerRequestInterface $request) : ResponseInterface
508513

509514
if (isset($this->eventDispatcher)) {
510515
$event = new RouteEvent($route, $request);
516+
517+
/**
518+
* @psalm-suppress TooManyArguments
519+
*/
511520
$this->eventDispatcher->dispatch($event, RouteEvent::NAME);
521+
512522
$request = $event->getRequest();
513523
}
514524

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)