Passed
Push — master ( 9013bb...125cf1 )
by Kirill
03:56
created

InputManager::isJsonExpected()   B

Complexity

Conditions 7
Paths 7

Size

Total Lines 19
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 10
c 1
b 0
f 0
dl 0
loc 19
rs 8.8333
cc 7
nc 7
nop 1
1
<?php
2
3
/**
4
 * Spiral Framework.
5
 *
6
 * @license   MIT
7
 * @author    Anton Titov (Wolfy-J)
8
 */
9
10
declare(strict_types=1);
11
12
namespace Spiral\Http\Request;
13
14
use Psr\Container\ContainerExceptionInterface;
15
use Psr\Container\ContainerInterface;
16
use Psr\Http\Message\ServerRequestInterface as Request;
17
use Psr\Http\Message\UploadedFileInterface;
18
use Psr\Http\Message\UriInterface;
19
use Spiral\Core\Container\SingletonInterface;
20
use Spiral\Core\Exception\ScopeException;
21
use Spiral\Http\Exception\InputException;
22
use Spiral\Http\Header\AcceptHeader;
23
24
/**
25
 * Provides simplistic way to access request input data in controllers and can also be used to
26
 * populate RequestFilters.
27
 *
28
 * Attention, this class is singleton based, it reads request from current active container scope!
29
 *
30
 * Technically this class can be made as middleware, but due spiral provides container scoping
31
 * such functionality may be replaces with simple container request routing.
32
 *
33
 * @property-read HeadersBag $headers
34
 * @property-read InputBag   $data
35
 * @property-read InputBag   $query
36
 * @property-read InputBag   $cookies
37
 * @property-read FilesBag   $files
38
 * @property-read ServerBag  $server
39
 * @property-read InputBag   $attributes
40
 */
41
final class InputManager implements SingletonInterface
42
{
43
    /**
44
     * Associations between bags and representing class/request method.
45
     *
46
     * @invisible
47
     * @var array
48
     */
49
    protected $bagAssociations = [
50
        'headers'    => [
51
            'class'  => HeadersBag::class,
52
            'source' => 'getHeaders'
53
        ],
54
        'data'       => [
55
            'class'  => InputBag::class,
56
            'source' => 'getParsedBody'
57
        ],
58
        'query'      => [
59
            'class'  => InputBag::class,
60
            'source' => 'getQueryParams'
61
        ],
62
        'cookies'    => [
63
            'class'  => InputBag::class,
64
            'source' => 'getCookieParams'
65
        ],
66
        'files'      => [
67
            'class'  => FilesBag::class,
68
            'source' => 'getUploadedFiles'
69
        ],
70
        'server'     => [
71
            'class'  => ServerBag::class,
72
            'source' => 'getServerParams'
73
        ],
74
        'attributes' => [
75
            'class'  => InputBag::class,
76
            'source' => 'getAttributes'
77
        ]
78
    ];
79
    /**
80
     * @invisible
81
     * @var Request
82
     */
83
    protected $request;
84
85
    /**
86
     * @invisible
87
     * @var ContainerInterface
88
     */
89
    protected $container;
90
91
    /** @var InputBag[] */
92
    private $bags = [];
93
94
    /**
95
     * Prefix to add for each input request.
96
     *
97
     * @see self::withPrefix();
98
     * @var string
99
     */
100
    private $prefix = '';
101
102
    /**
103
     * List of content types that must be considered as JSON.
104
     * @var array
105
     */
106
    private $jsonTypes = [
107
        'application/json'
108
    ];
109
110
    /**
111
     * @param ContainerInterface $container
112
     */
113
    public function __construct(ContainerInterface $container)
114
    {
115
        $this->container = $container;
116
    }
117
118
    /**
119
     * @param string $name
120
     * @return InputBag
121
     */
122
    public function __get(string $name): InputBag
123
    {
124
        return $this->bag($name);
125
    }
126
127
    /**
128
     * Flushing bag instances when cloned.
129
     */
130
    public function __clone()
131
    {
132
        $this->bags = [];
133
    }
134
135
    /**
136
     * Creates new input slice associated with request sub-tree.
137
     *
138
     * @param string $prefix
139
     * @param bool   $add
140
     * @return InputManager
141
     */
142
    public function withPrefix(string $prefix, bool $add = true): self
143
    {
144
        $input = clone $this;
145
146
        if ($add) {
147
            $input->prefix .= '.' . $prefix;
148
            $input->prefix = trim($input->prefix, '.');
149
        } else {
150
            $input->prefix = $prefix;
151
        }
152
153
        return $input;
154
    }
155
156
    /**
157
     * Get page path (including leading slash) associated with active request.
158
     *
159
     * @return string
160
     */
161
    public function path(): string
162
    {
163
        $path = $this->uri()->getPath();
164
165
        if (empty($path)) {
166
            return '/';
167
        }
168
169
        if ($path[0] !== '/') {
170
            return '/' . $path;
171
        }
172
173
        return $path;
174
    }
175
176
    /**
177
     * Get UriInterface associated with active request.
178
     *
179
     * @return UriInterface
180
     */
181
    public function uri(): UriInterface
182
    {
183
        return $this->request()->getUri();
184
    }
185
186
    /**
187
     * Get active instance of ServerRequestInterface and reset all bags if instance changed.
188
     *
189
     * @return Request
190
     *
191
     * @throws ScopeException
192
     */
193
    public function request(): Request
194
    {
195
        try {
196
            $request = $this->container->get(Request::class);
197
        } catch (ContainerExceptionInterface $e) {
198
            throw new ScopeException(
199
                'Unable to get `ServerRequestInterface` in active container scope',
200
                $e->getCode(),
0 ignored issues
show
Bug introduced by
The method getCode() does not exist on Psr\Container\ContainerExceptionInterface. It seems like you code against a sub-type of said class. However, the method does not exist in Psr\Container\NotFoundExceptionInterface. Are you sure you never get one of those? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

200
                $e->/** @scrutinizer ignore-call */ 
201
                    getCode(),
Loading history...
201
                $e
202
            );
203
        }
204
205
        //Flushing input state
206
        if ($this->request !== $request) {
207
            $this->bags = [];
208
            $this->request = $request;
209
        }
210
211
        return $this->request;
212
    }
213
214
    /**
215
     * Http method. Always uppercase.
216
     *
217
     * @return string
218
     */
219
    public function method(): string
220
    {
221
        return strtoupper($this->request()->getMethod());
222
    }
223
224
    /**
225
     * Check if request was made over http protocol.
226
     *
227
     * @return bool
228
     */
229
    public function isSecure(): bool
230
    {
231
        //Double check though attributes?
232
        return $this->request()->getUri()->getScheme() === 'https';
233
    }
234
235
    /**
236
     * Check if request was via AJAX.
237
     * Legacy-support alias for isXmlHttpRequest()
238
     * @see isXmlHttpRequest()
239
     *
240
     * @return bool
241
     */
242
    public function isAjax(): bool
243
    {
244
        return $this->isXmlHttpRequest();
245
    }
246
247
    /**
248
     * Check if request was made using XmlHttpRequest.
249
     *
250
     * @return bool
251
     */
252
    public function isXmlHttpRequest(): bool
253
    {
254
        return mb_strtolower(
255
            $this->request()->getHeaderLine('X-Requested-With')
256
        ) === 'xmlhttprequest';
257
    }
258
259
    /**
260
     * Client requesting json response by Accept header.
261
     *
262
     * @param bool $softMatch
263
     * @return bool
264
     */
265
    public function isJsonExpected(bool $softMatch = false): bool
266
    {
267
        $acceptHeader = AcceptHeader::fromString($this->request()->getHeaderLine('Accept'));
268
        foreach ($this->jsonTypes as $jsonType) {
269
            if ($acceptHeader->has($jsonType)) {
270
                return true;
271
            }
272
        }
273
274
        if ($softMatch) {
275
            foreach ($acceptHeader->getAll() as $item) {
276
                $itemValue = strtolower($item->getValue());
277
                if (str_ends_with($itemValue, '/json') || str_ends_with($itemValue, '+json')) {
278
                    return true;
279
                }
280
            }
281
        }
282
283
        return false;
284
    }
285
286
    /**
287
     * Add new content type that will be considered as JSON.
288
     *
289
     * @param string $type
290
     * @return $this
291
     */
292
    public function withJsonType(string $type): self
293
    {
294
        $input = clone $this;
295
        $input->jsonTypes[] = $type;
296
297
        return $input;
298
    }
299
300
    /**
301
     * Get remove addr resolved from $_SERVER['REMOTE_ADDR']. Will return null if nothing if key not
302
     * exists. Consider using psr-15 middlewares to customize configuration.
303
     *
304
     * @return string|null
305
     */
306
    public function remoteAddress(): ?string
307
    {
308
        $serverParams = $this->request()->getServerParams();
309
310
        return $serverParams['REMOTE_ADDR'] ?? null;
311
    }
312
313
    /**
314
     * Get bag instance or create new one on demand.
315
     *
316
     * @param string $name
317
     * @return InputBag
318
     */
319
    public function bag(string $name): InputBag
320
    {
321
        // ensure proper request association
322
        $this->request();
323
324
        if (isset($this->bags[$name])) {
325
            return $this->bags[$name];
326
        }
327
328
        if (!isset($this->bagAssociations[$name])) {
329
            throw new InputException("Undefined input bag '{$name}'");
330
        }
331
332
        $class = $this->bagAssociations[$name]['class'];
333
        $data = call_user_func([$this->request(), $this->bagAssociations[$name]['source']]);
334
335
        if (!is_array($data)) {
336
            $data = (array)$data;
337
        }
338
339
        return $this->bags[$name] = new $class($data, $this->prefix);
340
    }
341
342
    /**
343
     * @param string      $name
344
     * @param mixed       $default
345
     * @param bool|string $implode Implode header lines, false to return header as array.
346
     * @return mixed
347
     */
348
    public function header(string $name, $default = null, $implode = ',')
349
    {
350
        return $this->headers->get($name, $default, $implode);
351
    }
352
353
    /**
354
     * @param string $name
355
     * @param mixed  $default
356
     * @return mixed
357
     *
358
     * @see data()
359
     */
360
    public function post(string $name, $default = null)
361
    {
362
        return $this->data($name, $default);
363
    }
364
365
    /**
366
     * @param string $name
367
     * @param mixed  $default
368
     *
369
     * @return mixed
370
     */
371
    public function data(string $name, $default = null)
372
    {
373
        return $this->data->get($name, $default);
374
    }
375
376
    /**
377
     * Reads data from data array, if not found query array will be used as fallback.
378
     *
379
     * @param string $name
380
     * @param mixed  $default
381
     * @return mixed
382
     */
383
    public function input(string $name, $default = null)
384
    {
385
        return $this->data($name, $this->query($name, $default));
386
    }
387
388
    /**
389
     * @param string $name
390
     * @param mixed  $default
391
392
     * @return mixed
393
     */
394
    public function query(string $name, $default = null)
395
    {
396
        return $this->query->get($name, $default);
397
    }
398
399
    /**
400
     * @param string $name
401
     * @param mixed  $default
402
403
     * @return mixed
404
     */
405
    public function cookie(string $name, $default = null)
406
    {
407
        return $this->cookies->get($name, $default);
408
    }
409
410
    /**
411
     * @param string $name
412
     * @param mixed  $default
413
     *
414
     * @return UploadedFileInterface|null
415
     */
416
    public function file(string $name, $default = null): ?UploadedFileInterface
417
    {
418
        return $this->files->get($name, $default);
419
    }
420
421
    /**
422
     * @param string $name
423
     * @param mixed  $default
424
     *
425
     * @return mixed
426
     */
427
    public function server(string $name, $default = null)
428
    {
429
        return $this->server->get($name, $default);
430
    }
431
432
    /**
433
     * @param string $name
434
     * @param mixed  $default
435
     *
436
     * @return mixed
437
     */
438
    public function attribute(string $name, $default = null)
439
    {
440
        return $this->attributes->get($name, $default);
441
    }
442
}
443