Passed
Push — master ( 197709...9b4e33 )
by Alexander
02:36
created

RequestBodyParser::getContentType()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3.4746

Importance

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