Completed
Push — master ( 097ea9...854a90 )
by Julián
02:02
created

Parser::flattenArray()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 10
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 5
nc 1
nop 1
1
<?php
2
3
/*
4
 * body-parser (https://github.com/juliangut/body-parser).
5
 * PSR7 body parser middleware.
6
 *
7
 * @license BSD-3-Clause
8
 * @link https://github.com/juliangut/body-parser
9
 * @author Julián Gutiérrez <[email protected]>
10
 */
11
12
namespace Jgut\BodyParser;
13
14
use Jgut\BodyParser\Decoder\Decoder;
15
use Negotiation\Negotiator;
16
use Psr\Http\Message\ResponseInterface;
17
use Psr\Http\Message\ServerRequestInterface;
18
19
/**
20
 * Body content parser.
21
 */
22
class Parser
23
{
24
    const METHOD_ALL = 'ALL';
25
26
    /**
27
     * Content negotiator.
28
     *
29
     * @var Negotiator
30
     */
31
    protected $negotiator;
32
33
    /**
34
     * Registered decoders.
35
     *
36
     * @var array
37
     */
38
    protected $decoders = [];
39
40
    /**
41
     * Parser constructor.
42
     *
43
     * @param Negotiator $negotiator
44
     */
45
    public function __construct(Negotiator $negotiator)
46
    {
47
        $this->negotiator = $negotiator;
48
    }
49
50
    /**
51
     * Add body decoder.
52
     *
53
     * @param Decoder      $decoder
54
     * @param string|array $methods
55
     *
56
     * @throws \InvalidArgumentException
57
     * @throws \RuntimeException
58
     */
59
    public function addDecoder(Decoder $decoder, $methods = self::METHOD_ALL)
60
    {
61
        if ($methods === self::METHOD_ALL) {
62
            $methods = [$methods];
63
        } elseif (!is_array($methods)) {
64
            throw new \InvalidArgumentException('Methods must be a string or an array');
65
        }
66
67
        array_walk(
68
            $methods,
69
            function ($method) use ($decoder) {
70
                $method = strtoupper(trim($method));
71
72
                if (!$this->methodCarriesBody($method)) {
73
                    throw new \RuntimeException(sprintf('%s HTTP requests can not carry body', $method));
74
                }
75
76
                if (!array_key_exists($method, $this->decoders)) {
77
                    $this->decoders[$method] = [];
78
                }
79
80
                $this->decoders[$method][] = $decoder;
81
            }
82
        );
83
    }
84
85
    /**
86
     * Add decoder for POST requests.
87
     *
88
     * @param Decoder $decoder
89
     */
90
    public function addPostDecoder(Decoder $decoder)
91
    {
92
        $this->addDecoder($decoder, ['POST']);
93
    }
94
95
    /**
96
     * Add decoder for PUT requests.
97
     *
98
     * @param Decoder $decoder
99
     */
100
    public function addPutDecoder(Decoder $decoder)
101
    {
102
        $this->addDecoder($decoder, ['PUT']);
103
    }
104
105
    /**
106
     * Add decoder for PATCH requests.
107
     *
108
     * @param Decoder $decoder
109
     */
110
    public function addPatchDecoder(Decoder $decoder)
111
    {
112
        $this->addDecoder($decoder, ['PATCH']);
113
    }
114
115
    /**
116
     * Get decoders for a method.
117
     *
118
     * @param string $method
119
     *
120
     * @return Decoder[]
121
     */
122
    public function getDecoders($method = self::METHOD_ALL)
123
    {
124
        $decoders = [];
125
126
        if (!$this->methodCarriesBody($method)) {
127
            return $decoders;
128
        }
129
130
        if (array_key_exists(static::METHOD_ALL, $this->decoders)) {
131
            if ($method === static::METHOD_ALL) {
132
                return $this->flattenArray($this->decoders);
133
            }
134
135
            $decoders = $this->decoders[static::METHOD_ALL];
136
        }
137
138
        if (!array_key_exists($method, $this->decoders)) {
139
            return $decoders;
140
        }
141
142
        return array_unique(array_merge($decoders, $this->decoders[$method]), SORT_REGULAR);
143
    }
144
145
    /**
146
     * Flatten array.
147
     *
148
     * @param array $array
149
     *
150
     * @return array
151
     */
152
    private function flattenArray(array $array)
153
    {
154
        return array_reduce(
155
            $array,
156
            function ($carry, $item) {
157
                return is_array($item) ? array_merge($carry, $this->flattenArray($item)) : array_merge($carry, [$item]);
158
            },
159
            []
160
        );
161
    }
162
163
    /**
164
     * Invoke middleware.
165
     *
166
     * @param ServerRequestInterface $request
167
     * @param ResponseInterface      $response
168
     * @param callable               $next
169
     *
170
     * @return ResponseInterface
171
     */
172
    public function __invoke(ServerRequestInterface $request, ResponseInterface $response, callable $next)
173
    {
174
        if (!$this->methodCarriesBody($request->getMethod())) {
175
            return $next($request, $response);
176
        }
177
178
        $contentType = $request->getHeaderLine('Content-Type');
179
180
        foreach ($this->getDecoders($request->getMethod()) as $decoder) {
181
            if ($this->negotiate($contentType, $decoder->getMimeTypes()) === '') {
182
                continue;
183
            }
184
185
            return $next($request->withParsedBody($decoder->decode((string) $request->getBody())), $response);
186
        }
187
188
        return $next($request, $response);
189
    }
190
191
    /**
192
     * Negotiate content type.
193
     *
194
     * @param string $contentType
195
     * @param array  $priorities
196
     *
197
     * @return string
198
     */
199
    protected function negotiate($contentType, array $priorities)
200
    {
201
        if (trim($contentType) !== '' && count($priorities)) {
202
            try {
203
                /* @var \Negotiation\BaseAccept $best */
204
                $best = $this->negotiator->getBest($contentType, $priorities);
205
206
                if ($best) {
207
                    return (string) $best->getValue();
208
                }
209
                // @codeCoverageIgnoreStart
210
            } catch (\Exception $exception) {
211
                // No action needed
212
            }
213
            // @codeCoverageIgnoreEnd
214
        }
215
216
        return '';
217
    }
218
219
    /**
220
     * Does HTTP method carry body content.
221
     *
222
     * @param string $method
223
     *
224
     * @return bool
225
     */
226
    private function methodCarriesBody($method)
227
    {
228
        return !in_array($method, ['GET', 'HEAD', 'OPTIONS', 'CONNECT', 'TRACE']);
229
    }
230
}
231