Passed
Push — master ( eac0c5...eb09ec )
by Sergei
10:25 queued 08:11
created

RequestBodyParser::process()   B

Complexity

Conditions 7
Paths 12

Size

Total Lines 24
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 7.7656

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 15
c 1
b 0
f 0
dl 0
loc 24
ccs 12
cts 16
cp 0.75
rs 8.8333
cc 7
nc 12
nop 2
crap 7.7656
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Request\Body;
6
7
use InvalidArgumentException;
8
use Psr\Container\ContainerInterface;
9
use Psr\Http\Message\ResponseFactoryInterface;
10
use Psr\Http\Message\ResponseInterface;
11
use Psr\Http\Message\ServerRequestInterface;
12
use Psr\Http\Server\MiddlewareInterface;
13
use Psr\Http\Server\RequestHandlerInterface;
14
use RuntimeException;
15
use Yiisoft\Http\Header;
16
use Yiisoft\Request\Body\Parser\JsonParser;
17
18
use function array_key_exists;
19
use function get_class;
20
use function is_array;
21
use function is_object;
22
23
/**
24
 * The package is a PSR-15 middleware that allows parsing PSR-7 server request body selecting the parser according
25
 * to the server request mime type.
26
 *
27
 * @see https://www.php-fig.org/psr/psr-7/
28
 * @see https://www.php-fig.org/psr/psr-15/
29
 */
30
final class RequestBodyParser implements MiddlewareInterface
31
{
32
    private ContainerInterface $container;
33
    private BadRequestHandlerInterface $badRequestHandler;
34
35
    /**
36
     * @var string[]
37
     * @psalm-var array<string, string>
38
     */
39
    private array $parsers = [
40
        'application/json' => JsonParser::class,
41
    ];
42
    private bool $ignoreBadRequestBody = false;
43
44 9
    public function __construct(
45
        ResponseFactoryInterface $responseFactory,
46
        ContainerInterface $container,
47
        BadRequestHandlerInterface $badRequestHandler = null
48
    ) {
49 9
        $this->container = $container;
50 9
        $this->badRequestHandler = $badRequestHandler ?? new BadRequestHandler($responseFactory);
51
    }
52
53
    /**
54
     * Registers a request parser for a mime type specified.
55
     *
56
     * @param string $mimeType Mime type to register parser for.
57
     * @param string $parserClass Parser fully qualified name.
58
     *
59
     * @return self
60
     */
61 5
    public function withParser(string $mimeType, string $parserClass): self
62
    {
63 5
        $this->validateMimeType($mimeType);
64 4
        if ($parserClass === '') {
65 1
            throw new InvalidArgumentException('The parser class cannot be an empty string.');
66
        }
67
68 3
        if ($this->container->has($parserClass) === false) {
69 1
            throw new InvalidArgumentException("The parser \"$parserClass\" cannot be found.");
70
        }
71
72 2
        $new = clone $this;
73 2
        $new->parsers[$this->normalizeMimeType($mimeType)] = $parserClass;
74 2
        return $new;
75
    }
76
77
    /**
78
     * Returns new instance with parsers un-registered for mime types specified.
79
     *
80
     * @param string ...$mimeTypes Mime types to unregister parsers for.
81
     *
82
     * @return self
83
     */
84 2
    public function withoutParsers(string ...$mimeTypes): self
85
    {
86 2
        $new = clone $this;
87 2
        if (count($mimeTypes) === 0) {
88 1
            $new->parsers = [];
89 1
            return $new;
90
        }
91 1
        foreach ($mimeTypes as $mimeType) {
92 1
            $this->validateMimeType($mimeType);
93 1
            unset($new->parsers[$this->normalizeMimeType($mimeType)]);
94
        }
95 1
        return $new;
96
    }
97
98
    /**
99
     * Makes the middleware to simple skip requests it cannot parse.
100
     *
101
     * @return self
102
     */
103 1
    public function ignoreBadRequestBody(): self
104
    {
105 1
        $new = clone $this;
106 1
        $new->ignoreBadRequestBody = true;
107 1
        return $new;
108
    }
109
110 6
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
111
    {
112 6
        $parser = $this->getParser($this->getContentType($request));
113 6
        if ($parser !== null) {
114
            try {
115
                /** @var mixed $parsed */
116 4
                $parsed = $parser->parse((string)$request->getBody());
117 1
                if ($parsed !== null && !is_object($parsed) && !is_array($parsed)) {
118
                    $parserClass = get_class($parser);
119
                    throw new RuntimeException(
120
                        "$parserClass::parse() return value must be an array, an object, or null."
121
                    );
122
                }
123 1
                $request = $request->withParsedBody($parsed);
124 3
            } catch (ParserException $e) {
125 3
                if (!$this->ignoreBadRequestBody) {
126 2
                    return $this->badRequestHandler
127 2
                        ->withParserException($e)
128 2
                        ->handle($request);
129
                }
130
            }
131
        }
132
133 4
        return $handler->handle($request);
134
    }
135
136
    /**
137
     * @psalm-suppress MixedInferredReturnType, MixedReturnStatement
138
     */
139 6
    private function getParser(?string $contentType): ?ParserInterface
140
    {
141 6
        if ($contentType !== null && array_key_exists($contentType, $this->parsers)) {
142 4
            return $this->container->get($this->parsers[$contentType]);
143
        }
144 2
        return null;
145
    }
146
147 6
    private function getContentType(ServerRequestInterface $request): ?string
148
    {
149 6
        $contentType = $request->getHeaderLine(Header::CONTENT_TYPE);
150 6
        if (trim($contentType) !== '') {
151 6
            if (str_contains($contentType, ';')) {
152
                $contentTypeParts = explode(';', $contentType, 2);
153
                return strtolower(trim($contentTypeParts[0]));
154
            }
155 6
            return strtolower(trim($contentType));
156
        }
157
        return null;
158
    }
159
160
    /**
161
     * @throws InvalidArgumentException
162
     */
163 6
    private function validateMimeType(string $mimeType): void
164
    {
165 6
        if (strpos($mimeType, '/') === false) {
166 1
            throw new InvalidArgumentException('Invalid mime type.');
167
        }
168
    }
169
170 3
    private function normalizeMimeType(string $mimeType): string
171
    {
172 3
        return strtolower(trim($mimeType));
173
    }
174
}
175