Passed
Push — main ( 89bfd0...bcc78b )
by Dimitri
04:50
created

ServerRequest::clearDetectorCache()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 1
c 0
b 0
f 0
nc 1
nop 0
dl 0
loc 3
ccs 0
cts 1
cp 0
crap 2
rs 10
1
<?php
2
3
/**
4
 * This file is part of Blitz PHP framework.
5
 *
6
 * (c) 2022 Dimitri Sitchet Tomkeu <[email protected]>
7
 *
8
 * For the full copyright and license information, please view
9
 * the LICENSE file that was distributed with this source code.
10
 */
11
12
namespace BlitzPHP\Http;
13
14
use BadMethodCallException;
15
use BlitzPHP\Container\Services;
16
use BlitzPHP\Exceptions\FrameworkException;
17
use BlitzPHP\Exceptions\HttpException;
18
use BlitzPHP\Filesystem\Files\UploadedFile;
19
use BlitzPHP\Session\Cookie\CookieCollection;
20
use BlitzPHP\Session\Store;
21
use BlitzPHP\Utilities\Helpers;
22
use BlitzPHP\Utilities\Iterable\Arr;
23
use Closure;
24
use GuzzleHttp\Psr7\ServerRequest as Psr7ServerRequest;
25
use GuzzleHttp\Psr7\Stream;
26
use InvalidArgumentException;
27
use Psr\Http\Message\ServerRequestInterface;
28
use Psr\Http\Message\StreamInterface;
29
use Psr\Http\Message\UploadedFileInterface;
30
use Psr\Http\Message\UriInterface;
31
32
/**
33
 * Une classe qui aide à envelopper les informations de la requête et les détails d'une seule requête.
34
 * Fournit des méthodes couramment utilisées pour effectuer une introspection sur les en-têtes et le corps de la requête.
35
 */
36
class ServerRequest implements ServerRequestInterface
37
{
38
    /**
39
     * Tableau de paramètres analysés à partir de l'URL.
40
     */
41
    protected array $params = [
42
        'plugin'     => null,
43
        'controller' => null,
44
        'action'     => null,
45
        '_ext'       => null,
46
        'pass'       => [],
47
    ];
48
49
    /**
50
     * Tableau de données POST. Contiendra des données de formulaire ainsi que des fichiers téléchargés.
51
     * Dans les requêtes PUT/PATCH/DELETE, cette propriété contiendra les données encodées du formulaire.
52
     */
53
    protected null|array|object $data = [];
54
55
    /**
56
     * Tableau d'arguments de chaîne de requête
57
     */
58
    protected array $query = [];
59
60
    /**
61
     * Tableau de données de cookie.
62
     *
63
     * @var array<string, mixed>
64
     */
65
    protected array $cookies = [];
66
67
    /**
68
     * Tableau de données d'environnement.
69
     *
70
     * @var array<string, mixed>
71
     */
72
    protected array $_environment = [];
73
74
    /**
75
     * Chemin de l'URL de base.
76
     */
77
    protected string $base;
78
79
    /**
80
     * segment de chemin webroot pour la demande.
81
     */
82
    protected string $webroot = '/';
83
84
    /**
85
     * S'il faut faire confiance aux en-têtes HTTP_X définis par la plupart des équilibreurs de charge.
86
     * Défini sur vrai uniquement si votre application s'exécute derrière des équilibreurs de charge/proxies que vous contrôlez.
87
     */
88
    public bool $trustProxy = false;
89
90
    /**
91
     * Liste des proxys de confiance
92
     *
93
     * @var string[]
94
     */
95
    protected array $trustedProxies = [];
96
97
    /**
98
     * Les détecteurs intégrés utilisés avec `is()` peuvent être modifiés avec `addDetector()`.
99
     *
100
     * Il existe plusieurs façons de spécifier un détecteur, voir `addDetector()` pour
101
     * les différents formats et façons de définir des détecteurs.
102
     *
103
     * @var array<array|Closure>
104
     */
105
    protected static array $_detectors = [
106
        'get'     => ['env' => 'REQUEST_METHOD', 'value' => 'GET'],
107
        'post'    => ['env' => 'REQUEST_METHOD', 'value' => 'POST'],
108
        'put'     => ['env' => 'REQUEST_METHOD', 'value' => 'PUT'],
109
        'patch'   => ['env' => 'REQUEST_METHOD', 'value' => 'PATCH'],
110
        'delete'  => ['env' => 'REQUEST_METHOD', 'value' => 'DELETE'],
111
        'head'    => ['env' => 'REQUEST_METHOD', 'value' => 'HEAD'],
112
        'options' => ['env' => 'REQUEST_METHOD', 'value' => 'OPTIONS'],
113
        'https'   => ['env' => 'HTTPS', 'options' => [1, 'on']],
114
        'ssl'     => ['env' => 'HTTPS', 'options' => [1, 'on']],
115
        'ajax'    => ['env' => 'HTTP_X_REQUESTED_WITH', 'value' => 'XMLHttpRequest'],
116
        'json'    => ['accept' => ['application/json'], 'param' => '_ext', 'value' => 'json'],
117
        'xml'     => ['accept' => ['application/xml', 'text/xml'], 'param' => '_ext', 'value' => 'xml'],
118
    ];
119
120
    /**
121
     * Cache d'instance pour les résultats des appels is(something)
122
     *
123
     * @var array<string, bool>
124
     */
125
    protected array $_detectorCache = [];
126
127
    /**
128
     * Flux du corps de la requête. Contient php://input sauf si l'option constructeur `input` est utilisée.
129
     */
130
    protected StreamInterface $stream;
131
132
    /**
133
     * instance Uri
134
     */
135
    protected UriInterface $uri;
136
137
    /**
138
     * Instance d'un objet Session relative à cette requête
139
     */
140
    protected Store $session;
141
142
    /**
143
     * Stockez les attributs supplémentaires attachés à la requête.
144
     *
145
     * @var array<string, mixed>
146
     */
147
    protected array $attributes = [];
148
149
    /**
150
     * Une liste de propriétés émulées par les méthodes d'attribut PSR7.
151
     *
152
     * @var array<string>
153
     */
154
    protected array $emulatedAttributes = ['session', 'flash', 'webroot', 'base', 'params', 'here'];
155
156
    /**
157
     * Tableau de fichiers.
158
     *
159
     * @var UploadedFile[]
160
     */
161
    protected array $uploadedFiles = [];
162
163
    /**
164
     * La version du protocole HTTP utilisée.
165
     */
166
    protected ?string $protocol = null;
167
168
    /**
169
     * La cible de la requête si elle est remplacée
170
     */
171
    protected ?string $requestTarget = null;
172
173
    /**
174
     * Negotiator
175
     */
176
    protected ?Negotiator $negotiator = null;
177
178
    /**
179
     * Créer un nouvel objet de requête.
180
     *
181
     * Vous pouvez fournir les données sous forme de tableau ou de chaîne. Si tu utilises
182
     * une chaîne, vous ne pouvez fournir que l'URL de la demande. L'utilisation d'un tableau
183
     * vous permettent de fournir les clés suivantes :
184
     *
185
     * - `post` Données POST ou données de chaîne sans requête
186
     * - `query` Données supplémentaires de la chaîne de requête.
187
     * - `files` Fichiers téléchargés dans une structure normalisée, avec chaque feuille une instance de UploadedFileInterface.
188
     * - `cookies` Cookies pour cette demande.
189
     * - `environment` $_SERVER et $_ENV données.
190
     * - `url` L'URL sans le chemin de base de la requête.
191
     * - `uri` L'objet PSR7 UriInterface. Si nul, un sera créé à partir de `url` ou `environment`.
192
     * - `base` L'URL de base de la requête.
193
     * - `webroot` Le répertoire webroot pour la requête.
194
     * - `input` Les données qui proviendraient de php://input ceci est utile pour simuler
195
     * requêtes avec mise, patch ou suppression de données.
196
     * - `session` Une instance d'un objet Session
197
     *
198
     * @param array<string, mixed> $config Un tableau de données de requête avec lequel créer une requête.
199
     */
200
    public function __construct(array $config = [])
201
    {
202
        $config += [
203
            'params'      => $this->params,
204
            'query'       => $_GET,
205
            'post'        => $_POST,
206
            'files'       => $_FILES,
207
            'cookies'     => $_COOKIE,
208
            'environment' => [],
209
            'url'         => '',
210
            'uri'         => null,
211
            'base'        => '',
212
            'webroot'     => '',
213
            'input'       => null,
214
        ];
215
216
        $this->_setConfig($config);
217
    }
218
219
    /**
220
     * Traitez les données de configuration/paramètres dans les propriétés.
221
     *
222
     * @param array<string, mixed> $config
223
     */
224
    protected function _setConfig(array $config): void
225
    {
226
        if (empty($config['session'])) {
227
            $config['session'] = Services::session(false);
228
        }
229
230
        if (empty($config['environment']['REQUEST_METHOD'])) {
231
            $config['environment']['REQUEST_METHOD'] = $_SERVER['REQUEST_METHOD'] ?? 'GET';
232
        }
233
234
        $this->cookies = $config['cookies'];
235
236
        if (isset($config['uri'])) {
237
            if (! $config['uri'] instanceof UriInterface) {
238
                throw new FrameworkException('The `uri` key must be an instance of ' . UriInterface::class);
239
            }
240
            $uri = $config['uri'];
241
        } else {
242
            if ($config['url'] !== '') {
243
                $config = $this->processUrlOption($config);
244
            }
245
            $uri = Psr7ServerRequest::getUriFromGlobals();
246
        }
247
248
        $this->_environment = $config['environment'];
249
250
        $this->uri     = $uri;
251
        $this->base    = $config['base'];
252
        $this->webroot = $config['webroot'];
253
254
        if (isset($config['input'])) {
255
            $stream = new Stream(\GuzzleHttp\Psr7\Utils::tryFopen('php://memory', 'rw'));
256
            $stream->write($config['input']);
257
            $stream->rewind();
258
        } else {
259
            $stream = new Stream(\GuzzleHttp\Psr7\Utils::tryFopen('php://input', 'r'));
260
        }
261
        $this->stream = $stream;
262
263
        $config['post'] = $this->_processPost($config['post']);
264
        $this->data     = $this->_processFiles($config['post'], $config['files']);
265
        $this->query    = $config['query'];
266
        $this->params   = $config['params'];
267
        $this->session  = $config['session'];
268
    }
269
270
    /**
271
     * Définissez les variables d'environnement en fonction de l'option `url` pour faciliter la génération d'instance UriInterface.
272
     *
273
     * L'option `query` est également mise à jour en fonction de la chaîne de requête de l'URL.
274
     */
275
    protected function processUrlOption(array $config): array
276
    {
277
        if ($config['url'][0] !== '/') {
278
            $config['url'] = '/' . $config['url'];
0 ignored issues
show
Bug introduced by
Are you sure $config['url'] of type array can be used in concatenation? ( Ignorable by Annotation )

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

278
            $config['url'] = '/' . /** @scrutinizer ignore-type */ $config['url'];
Loading history...
279
        }
280
281
        if (str_contains($config['url'], '?')) {
282
            [$config['url'], $config['environment']['QUERY_STRING']] = explode('?', $config['url']);
283
284
            parse_str($config['environment']['QUERY_STRING'], $queryArgs);
285
            $config['query'] += $queryArgs;
286
        }
287
288
        $config['environment']['REQUEST_URI'] = $config['url'];
289
290
        return $config;
291
    }
292
293
    /**
294
     * Obtenez le type de contenu utilisé dans cette requête.
295
     */
296
    public function contentType(): ?string
297
    {
298
        return $this->getEnv('CONTENT_TYPE') ?: $this->getEnv('HTTP_CONTENT_TYPE');
299
    }
300
301
    /**
302
     * Renvoie l'instance de l'objet Session pour cette requête
303
     */
304
    public function session(): Store
305
    {
306
        return $this->session;
307
    }
308
309
    /**
310
     * Obtenez l'adresse IP que le client utilise ou dit qu'il utilise.
311
     */
312
    public function clientIp(): string
313
    {
314
        if ($this->trustProxy && $this->getEnv('HTTP_X_FORWARDED_FOR')) {
315
            $addresses = array_map('trim', explode(',', (string) $this->getEnv('HTTP_X_FORWARDED_FOR')));
316
            $trusted   = (count($this->trustedProxies) > 0);
317
            $n         = count($addresses);
318
319
            if ($trusted) {
320
                $trusted = array_diff($addresses, $this->trustedProxies);
321
                $trusted = (count($trusted) === 1);
322
            }
323
324
            if ($trusted) {
325
                return $addresses[0];
326
            }
327
328
            return $addresses[$n - 1];
329
        }
330
331
        if ($this->trustProxy && $this->getEnv('HTTP_X_REAL_IP')) {
332
            $ipaddr = $this->getEnv('HTTP_X_REAL_IP');
333
        } elseif ($this->trustProxy && $this->getEnv('HTTP_CLIENT_IP')) {
334
            $ipaddr = $this->getEnv('HTTP_CLIENT_IP');
335
        } else {
336
            $ipaddr = $this->getEnv('REMOTE_ADDR');
337
        }
338
339
        return trim((string) $ipaddr);
340
    }
341
342
    /**
343
     * Enregistrer des proxys de confiance
344
     *
345
     * @param string[] $proxies ips liste des proxys de confiance
346
     */
347
    public function setTrustedProxies(array $proxies): void
348
    {
349
        $this->trustedProxies = $proxies;
350
        $this->trustProxy     = true;
351
        $this->uri            = $this->uri->withScheme($this->scheme());
0 ignored issues
show
Bug introduced by
It seems like $this->scheme() can also be of type null; however, parameter $scheme of Psr\Http\Message\UriInterface::withScheme() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

351
        $this->uri            = $this->uri->withScheme(/** @scrutinizer ignore-type */ $this->scheme());
Loading history...
352
    }
353
354
    /**
355
     * Obtenez les proxys de confiance
356
     */
357
    public function getTrustedProxies(): array
358
    {
359
        return $this->trustedProxies;
360
    }
361
362
    /**
363
     * Renvoie le référent qui a référé cette requête.
364
     *
365
     * @param bool $local Tentative de renvoi d'une adresse locale.
366
     *                    Les adresses locales ne contiennent pas de noms d'hôtes..
367
     */
368
    public function referer(bool $local = true): ?string
369
    {
370
        $ref = $this->getEnv('HTTP_REFERER');
371
372
        $base = config('app.base_url') . $this->webroot;
0 ignored issues
show
Bug introduced by
Are you sure config('app.base_url') of type BlitzPHP\Config\Config|null can be used in concatenation? ( Ignorable by Annotation )

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

372
        $base = /** @scrutinizer ignore-type */ config('app.base_url') . $this->webroot;
Loading history...
373
        if (empty($base) || empty($ref)) {
374
            return null;
375
        }
376
377
        if ($local && str_starts_with($ref, $base)) {
378
            $ref = substr($ref, strlen($base));
379
            if ($ref === '' || str_starts_with($ref, '//')) {
380
                $ref = '/';
381
            }
382
            if ($ref[0] !== '/') {
383
                $ref = '/' . $ref;
384
            }
385
386
            return $ref;
387
        }
388
389
        if ($local) {
390
            return null;
391
        }
392
393
        return $ref;
394
    }
395
396
    /**
397
     * Gestionnaire de méthodes manquant, les poignées enveloppent les anciennes méthodes de type isAjax()
398
     *
399
     * @return bool
400
     *
401
     * @throws BadMethodCallException lorsqu'une méthode invalide est appelée.
402
     */
403
    public function __call(string $name, array $params)
404
    {
405
        if (str_starts_with($name, 'is')) {
406
            $type = strtolower(substr($name, 2));
407
408
            array_unshift($params, $type);
409
410
            return $this->is(...$params);
411
        }
412
413
        throw new BadMethodCallException(sprintf('La méthode "%s()" n\'existe pas', $name));
414
    }
415
416
    /**
417
     * Vérifiez si une demande est d'un certain type.
418
     *
419
     * Utilise les règles de détection intégrées ainsi que des règles supplémentaires
420
     * défini avec {@link \BlitzPHP\Http\ServerRequest::addDetector()}. Tout détecteur peut être appelé
421
     * comme `is($type)` ou `is$Type()`.
422
     *
423
     * @param string|string[] $type Le type de requête que vous souhaitez vérifier. S'il s'agit d'un tableau, cette méthode renverra true si la requête correspond à n'importe quel type.
424
     *
425
     * @return bool Si la demande est du type que vous vérifiez.
426
     */
427
    public function is($type, ...$args): bool
428
    {
429
        if (is_array($type)) {
430
            foreach ($type as $_type) {
431
                if ($this->is($_type)) {
432
                    return true;
433
                }
434
            }
435
436
            return false;
437
        }
438
439
        $type = strtolower($type);
440
        if (! isset(static::$_detectors[$type])) {
441
            return false;
442
        }
443
        if ($args) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $args of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
444
            return $this->_is($type, $args);
445
        }
446
447
        return $this->_detectorCache[$type] ??= $this->_is($type, $args);
448
    }
449
450
    /**
451
     * Efface le cache du détecteur d'instance, utilisé par la fonction is()
452
     */
453
    public function clearDetectorCache(): void
454
    {
455
        $this->_detectorCache = [];
456
    }
457
458
    /**
459
     * Worker pour la fonction publique is()
460
     *
461
     * @param string $type Le type de requête que vous souhaitez vérifier.
462
     * @param array  $args Tableau d'arguments de détecteur personnalisés.
463
     *
464
     * @return bool Si la demande est du type que vous vérifiez.
465
     */
466
    protected function _is(string $type, array $args): bool
467
    {
468
        $detect = static::$_detectors[$type];
469
        if ($detect instanceof Closure) {
470
            array_unshift($args, $this);
471
472
            return $detect(...$args);
473
        }
474
        if (isset($detect['env']) && $this->_environmentDetector($detect)) {
475
            return true;
476
        }
477
        if (isset($detect['header']) && $this->_headerDetector($detect)) {
478
            return true;
479
        }
480
        if (isset($detect['accept']) && $this->_acceptHeaderDetector($detect)) {
481
            return true;
482
        }
483
484
        return (bool) (isset($detect['param']) && $this->_paramDetector($detect));
485
    }
486
487
    /**
488
     * Détecte si un en-tête d'acceptation spécifique est présent.
489
     *
490
     * @param array $detect Tableau d'options du détecteur.
491
     *
492
     * @return bool Si la demande est du type que vous vérifiez.
493
     */
494
    protected function _acceptHeaderDetector(array $detect): bool
495
    {
496
        $acceptHeaders = explode(',', (string) $this->getEnv('HTTP_ACCEPT'));
497
498
        foreach ($detect['accept'] as $header) {
499
            if (in_array($header, $acceptHeaders, true)) {
500
                return true;
501
            }
502
        }
503
504
        return false;
505
    }
506
507
    /**
508
     * Détecte si un en-tête spécifique est présent.
509
     *
510
     * @param array $detect Tableau d'options du détecteur.
511
     *
512
     * @return bool Si la demande est du type que vous vérifiez.
513
     */
514
    protected function _headerDetector(array $detect): bool
515
    {
516
        foreach ($detect['header'] as $header => $value) {
517
            $header = $this->getEnv('http_' . $header);
518
            if ($header !== null) {
519
                if ($value instanceof Closure) {
520
                    return $value($header);
521
                }
522
523
                return $header === $value;
524
            }
525
        }
526
527
        return false;
528
    }
529
530
    /**
531
     * Détecte si un paramètre de requête spécifique est présent.
532
     *
533
     * @param array $detect Tableau d'options du détecteur.
534
     *
535
     * @return bool Si la demande est du type que vous vérifiez.
536
     */
537
    protected function _paramDetector(array $detect): bool
538
    {
539
        $key = $detect['param'];
540
        if (isset($detect['value'])) {
541
            $value = $detect['value'];
542
543
            return isset($this->params[$key]) ? $this->params[$key] === $value : false;
544
        }
545
        if (isset($detect['options'])) {
546
            return isset($this->params[$key]) ? in_array($this->params[$key], $detect['options'], true) : false;
547
        }
548
549
        return false;
550
    }
551
552
    /**
553
     * Détecte si une variable d'environnement spécifique est présente.
554
     *
555
     * @param array $detect Tableau d'options du détecteur.
556
     *
557
     * @return bool Si la demande est du type que vous vérifiez.
558
     */
559
    protected function _environmentDetector(array $detect): bool
560
    {
561
        if (isset($detect['env'])) {
562
            if (isset($detect['value'])) {
563
                return $this->getEnv($detect['env']) === $detect['value'];
564
            }
565
            if (isset($detect['pattern'])) {
566
                return (bool) preg_match($detect['pattern'], (string) $this->getEnv($detect['env']));
567
            }
568
            if (isset($detect['options'])) {
569
                $pattern = '/' . implode('|', $detect['options']) . '/i';
570
571
                return (bool) preg_match($pattern, (string) $this->getEnv($detect['env']));
572
            }
573
        }
574
575
        return false;
576
    }
577
578
    /**
579
     * Vérifier qu'une requête correspond à tous les types donnés.
580
     *
581
     * Vous permet de tester plusieurs types et d'unir les résultats.
582
     * Voir Request::is() pour savoir comment ajouter des types supplémentaires et le
583
     * types intégrés.
584
     *
585
     * @param string[] $types Les types à vérifier.
586
     *
587
     * @see \BlitzPHP\Http\ServerRequest::is()
588
     */
589
    public function isAll(array $types): bool
590
    {
591
        foreach ($types as $type) {
592
            if (! $this->is($type)) {
593
                return false;
594
            }
595
        }
596
597
        return true;
598
    }
599
600
    /**
601
     * Ajouter un nouveau détecteur à la liste des détecteurs qu'une requête peut utiliser.
602
     * Il existe plusieurs types de détecteurs différents qui peuvent être réglés.
603
     *
604
     * ### Comparaison des rappels
605
     *
606
     * Les détecteurs de rappel vous permettent de fournir un callable pour gérer le chèque.
607
     * Le rappel recevra l'objet de requête comme seul paramètre.
608
     *
609
     * ```
610
     * addDetector('custom', function ($request) { //Renvoyer un booléen });
611
     * ```
612
     *
613
     * ### Comparaison des valeurs d'environnement
614
     *
615
     * Une comparaison de valeur d'environnement, compare une valeur extraite de `env()` à une valeur connue
616
     * la valeur d'environnement est l'égalité vérifiée par rapport à la valeur fournie.
617
     *
618
     * ```
619
     * addDetector('post', ['env' => 'REQUEST_METHOD', 'value' => 'POST']);
620
     * ```
621
     *
622
     * ### Comparaison des paramètres de demande
623
     *
624
     * Permet des détecteurs personnalisés sur les paramètres de demande.
625
     *
626
     * ```
627
     * addDetector('admin', ['param' => 'prefix', 'value' => 'admin']);
628
     * ```
629
     *
630
     * ### Accepter la comparaison
631
     *
632
     * Permet au détecteur de comparer avec la valeur d'en-tête Accepter.
633
     *
634
     * ```
635
     * addDetector('csv', ['accept' => 'text/csv']);
636
     * ```
637
     *
638
     * ### Comparaison d'en-tête
639
     *
640
     * Permet de comparer un ou plusieurs en-têtes.
641
     *
642
     * ```
643
     * addDetector('fancy', ['header' => ['X-Fancy' => 1]);
644
     * ```
645
     *
646
     * Les types `param`, `env` et de comparaison permettent ce qui suit
647
     * options de comparaison de valeur :
648
     *
649
     * ### Comparaison des valeurs de modèle
650
     *
651
     * La comparaison de valeurs de modèles vous permet de comparer une valeur extraite de `env()` à une expression régulière.
652
     *
653
     * ```
654
     * addDetector('iphone', ['env' => 'HTTP_USER_AGENT', 'pattern' => '/iPhone/i']);
655
     * ```
656
     *
657
     * ### Comparaison basée sur les options
658
     *
659
     * Les comparaisons basées sur des options utilisent une liste d'options pour créer une expression régulière. Appels ultérieurs
660
     * ajouter un détecteur d'options déjà défini fusionnera les options.
661
     *
662
     * ```
663
     * addDetector('mobile', ['env' => 'HTTP_USER_AGENT', 'options' => ['Fennec']]);
664
     * ```
665
     *
666
     * Vous pouvez également comparer plusieurs valeurs
667
     * en utilisant la touche `options`. Ceci est utile lorsque vous souhaitez vérifier
668
     * si une valeur de requête se trouve dans une liste d'options.
669
     *
670
     * `addDetector('extension', ['param' => '_ext', 'options' => ['pdf', 'csv']]`
671
     *
672
     * @param array|callable $detector Un callback ou tableau d'options pour la définition du détecteur.
673
     */
674
    public static function addDetector(string $name, $detector): void
675
    {
676
        $name = strtolower($name);
677
        if ($detector instanceof Closure) {
678
            static::$_detectors[$name] = $detector;
679
680
            return;
681
        }
682
        if (isset(static::$_detectors[$name], $detector['options'])) {
683
            /** @var array $data */
684
            $data     = static::$_detectors[$name];
685
            $detector = Arr::merge($data, $detector);
686
        }
687
        static::$_detectors[$name] = $detector;
688
    }
689
690
    /**
691
     * Normaliser un nom d'en-tête dans la version SERVER.
692
     */
693
    protected function normalizeHeaderName(string $name): string
694
    {
695 2
        $name = str_replace('-', '_', strtoupper($name));
696
        if (! in_array($name, ['CONTENT_LENGTH', 'CONTENT_TYPE'], true)) {
697 2
            $name = 'HTTP_' . $name;
698
        }
699
700 2
        return $name;
701
    }
702
703
    /**
704
     * Obtenez tous les en-têtes de la requête.
705
     *
706
     * Renvoie un tableau associatif où les noms d'en-tête sont
707
     * les clés et les valeurs sont une liste de valeurs d'en-tête.
708
     *
709
     * Bien que les noms d'en-tête ne soient pas sensibles à la casse, getHeaders() normalisera
710
     * les en-têtes.
711
     *
712
     * @return array<string[]> Un tableau associatif d'en-têtes et leurs valeurs.
713
     *
714
     * @see http://www.php-fig.org/psr/psr-7/ Cette méthode fait partie de l'interface de requête du serveur PSR-7.
715
     */
716
    public function getHeaders(): array
717
    {
718
        $headers = [];
719
720
        foreach ($this->_environment as $key => $value) {
721
            $name = null;
722
            if (str_starts_with($key, 'HTTP_')) {
723
                $name = substr($key, 5);
724
            }
725
            if (str_starts_with($key, 'CONTENT_')) {
726
                $name = $key;
727
            }
728
            if ($name !== null) {
729
                $name           = str_replace('_', ' ', strtolower($name));
730
                $name           = str_replace(' ', '-', ucwords($name));
731
                $headers[$name] = (array) $value;
732
            }
733
        }
734
735
        return $headers;
736
    }
737
738
    /**
739
     * Vérifiez si un en-tête est défini dans la requête.
740
     *
741
     * @param string $name L'en-tête que vous souhaitez obtenir (insensible à la casse)
742
     *
743
     * @see http://www.php-fig.org/psr/psr-7/ Cette méthode fait partie de l'interface de requête du serveur PSR-7.
744
     */
745
    public function hasHeader(string $name): bool
746
    {
747
        if (isset($this->_environment[$this->normalizeHeaderName($name)])) {
748
            return true;
749
        }
750
751
        return [] !== $this->getHeader($name);
752
    }
753
754
    /**
755
     * Obtenez un seul en-tête de la requête.
756
     *
757
     * Renvoie la valeur de l'en-tête sous forme de tableau. Si l'en-tête
758
     * n'est pas présent, un tableau vide sera retourné.
759
     *
760
     * @param string $name L'en-tête que vous souhaitez obtenir (insensible à la casse)
761
     *
762
     * @return string[] Un tableau associatif d'en-têtes et leurs valeurs.
763
     *                  Si l'en-tête n'existe pas, un tableau vide sera retourné.
764
     *
765
     * @see http://www.php-fig.org/psr/psr-7/ Cette méthode fait partie de l'interface de requête du serveur PSR-7.
766
     */
767
    public function getHeader(string $name): array
768
    {
769 2
        $name = $this->normalizeHeaderName($name);
770
        if (isset($this->_environment[$name])) {
771 2
            return (array) $this->_environment[$name];
772
        }
773
774 2
        return (array) $this->getEnv($name);
775
    }
776
777
    /**
778
     * Obtenez un seul en-tête sous forme de chaîne à partir de la requête.
779
     *
780
     * @param string $name L'en-tête que vous souhaitez obtenir (insensible à la casse)
781
     *
782
     * @return string Les valeurs d'en-tête sont réduites à une chaîne séparée par des virgules.
783
     *
784
     * @see http://www.php-fig.org/psr/psr-7/ Cette méthode fait partie de l'interface de requête du serveur PSR-7.
785
     */
786
    public function getHeaderLine(string $name): string
787
    {
788 2
        $value = $this->getHeader($name);
789
790 2
        return implode(', ', $value);
791
    }
792
793
    /**
794
     * Obtenez une demande modifiée avec l'en-tête fourni.
795
     *
796
     * @param array|string $value
797
     *
798
     * @see http://www.php-fig.org/psr/psr-7/ Cette méthode fait partie de l'interface de requête du serveur PSR-7.
799
     */
800
    public function withHeader(string $name, $value): static
801
    {
802
        $new                      = clone $this;
803
        $name                     = $this->normalizeHeaderName($name);
804
        $new->_environment[$name] = $value;
805
806
        return $new;
807
    }
808
809
    /**
810
     * Obtenez une demande modifiée avec l'en-tête fourni.
811
     *
812
     * Les valeurs d'en-tête existantes seront conservées. La valeur fournie
813
     * sera ajouté aux valeurs existantes.
814
     *
815
     * @param array|string $value
816
     *
817
     * @see http://www.php-fig.org/psr/psr-7/ Cette méthode fait partie de l'interface de requête du serveur PSR-7.
818
     */
819
    public function withAddedHeader(string $name, $value): static
820
    {
821
        $new      = clone $this;
822
        $name     = $this->normalizeHeaderName($name);
823
        $existing = [];
824
        if (isset($new->_environment[$name])) {
825
            $existing = (array) $new->_environment[$name];
826
        }
827
        $existing                 = array_merge($existing, (array) $value);
828
        $new->_environment[$name] = $existing;
829
830
        return $new;
831
    }
832
833
    /**
834
     * Obtenez une demande modifiée sans en-tête fourni.
835
     *
836
     * @see http://www.php-fig.org/psr/psr-7/ Cette méthode fait partie de l'interface de requête du serveur PSR-7.
837
     */
838
    public function withoutHeader(string $name): static
839
    {
840
        $new  = clone $this;
841
        $name = $this->normalizeHeaderName($name);
842
        unset($new->_environment[$name]);
843
844
        return $new;
845
    }
846
847
    /**
848
     * Obtenez la méthode HTTP utilisée pour cette requête.
849
     * Il existe plusieurs manières de spécifier une méthode.
850
     *
851
     * - Si votre client le prend en charge, vous pouvez utiliser des méthodes HTTP natives.
852
     * - Vous pouvez définir l'en-tête HTTP-X-Method-Override.
853
     * - Vous pouvez soumettre une entrée avec le nom `_method`
854
     *
855
     * Chacune de ces 3 approches peut être utilisée pour définir la méthode HTTP utilisée
856
     * par BlitzPHP en interne, et affectera le résultat de cette méthode.
857
     *
858
     * @see http://www.php-fig.org/psr/psr-7/ Cette méthode fait partie de l'interface de requête du serveur PSR-7.
859
     */
860
    public function getMethod(): string
861
    {
862 8
        return (string) $this->getEnv('REQUEST_METHOD');
863
    }
864
865
    /**
866
     * Mettez à jour la méthode de requête et obtenez une nouvelle instance.
867
     *
868
     * @see http://www.php-fig.org/psr/psr-7/ Cette méthode fait partie de l'interface de requête du serveur PSR-7.
869
     */
870
    public function withMethod(string $method): static
871
    {
872 18
        $new = clone $this;
873
874
        if (! preg_match('/^[!#$%&\'*+.^_`\|~0-9a-z-]+$/i', $method)) {
875
            throw new InvalidArgumentException(sprintf(
876
                'Méthode HTTP non prise en charge "%s" fournie',
877
                $method
878 18
            ));
879
        }
880 18
        $new->_environment['REQUEST_METHOD'] = $method;
881
882 18
        return $new;
883
    }
884
885
    /**
886
     * Obtenez tous les paramètres de l'environnement du serveur.
887
     *
888
     * Lire toutes les données 'environnement' ou 'serveur' qui ont été
889
     * utilisé pour créer cette requête.
890
     *
891
     * @see http://www.php-fig.org/psr/psr-7/ Cette méthode fait partie de l'interface de requête du serveur PSR-7.
892
     */
893
    public function getServerParams(): array
894
    {
895
        return $this->_environment;
896
    }
897
898
    /**
899
     * Obtenez tous les paramètres de requête conformément aux spécifications PSR-7. Pour lire des valeurs de requête spécifiques
900
     * utilisez la méthode alternative getQuery().
901
     *
902
     * @see http://www.php-fig.org/psr/psr-7/ Cette méthode fait partie de l'interface de requête du serveur PSR-7.
903
     */
904
    public function getQueryParams(): array
905
    {
906
        return $this->query;
907
    }
908
909
    /**
910
     * Mettez à jour les données de la chaîne de requête et obtenez une nouvelle instance.
911
     *
912
     * @param array $query Les données de la chaîne de requête à utiliser
913
     *
914
     * @see http://www.php-fig.org/psr/psr-7/ Cette méthode fait partie de l'interface de requête du serveur PSR-7.
915
     */
916
    public function withQueryParams(array $query): static
917
    {
918
        $new        = clone $this;
919
        $new->query = $query;
920
921
        return $new;
922
    }
923
924
    /**
925
     * Obtenez l'hôte sur lequel la demande a été traitée.
926
     */
927
    public function host(): ?string
928
    {
929
        if ($this->trustProxy && $this->getEnv('HTTP_X_FORWARDED_HOST')) {
930
            return $this->getEnv('HTTP_X_FORWARDED_HOST');
931
        }
932
933
        return $this->getEnv('HTTP_HOST');
934
    }
935
936
    /**
937
     * Obtenez le port sur lequel la demande a été traitée.
938
     */
939
    public function port(): ?string
940
    {
941
        if ($this->trustProxy && $this->getEnv('HTTP_X_FORWARDED_PORT')) {
942
            return $this->getEnv('HTTP_X_FORWARDED_PORT');
943
        }
944
945
        return $this->getEnv('SERVER_PORT');
946
    }
947
948
    /**
949
     * Obtenez le schéma d'URL actuel utilisé pour la demande.
950
     *
951
     * par exemple. 'http' ou 'https'
952
     */
953
    public function scheme(): ?string
954
    {
955
        if ($this->trustProxy && $this->getEnv('HTTP_X_FORWARDED_PROTO')) {
956
            return $this->getEnv('HTTP_X_FORWARDED_PROTO');
957
        }
958
959
        return $this->getEnv('HTTPS') ? 'https' : 'http';
960
    }
961
962
    /**
963
     * Obtenez le nom de domaine et incluez les segments $tldLength du tld.
964
     *
965
     * @param int $tldLength Nombre de segments que contient votre tld. Par exemple : `example.com` contient 1 tld.
966
     *                       Alors que `example.co.uk` contient 2.
967
     *
968
     * @return string Nom de domaine sans sous-domaines.
969
     */
970
    public function domain(int $tldLength = 1): string
971
    {
972
        $host = $this->host();
973
        if (empty($host)) {
974
            return '';
975
        }
976
977
        $segments = explode('.', $host);
978
        $domain   = array_slice($segments, -1 * ($tldLength + 1));
979
980
        return implode('.', $domain);
981
    }
982
983
    /**
984
     * Obtenez les sous-domaines d'un hôte.
985
     *
986
     * @param int $tldLength Nombre de segments que contient votre tld. Par exemple : `example.com` contient 1 tld.
987
     *                       Alors que `example.co.uk` contient 2.
988
     *
989
     * @return string[] Un tableau de sous-domaines.
990
     */
991
    public function subdomains(int $tldLength = 1): array
992
    {
993
        $host = $this->host();
994
        if (empty($host)) {
995
            return [];
996
        }
997
998
        $segments = explode('.', $host);
999
1000
        return array_slice($segments, 0, -1 * ($tldLength + 1));
1001
    }
1002
1003
1004
    /**
1005
     * Obtient une liste de types de contenu acceptables par le navigateur client dans l'ordre préférable.
1006
     *
1007
     * @return string[]
1008
     */
1009
    public function getAcceptableContentTypes(): array
1010
    {
1011
        $raw    = $this->parseAccept();
1012
        $accept = [];
1013
1014
        foreach ($raw as $types) {
1015
            $accept = array_merge($accept, $types);
1016
        }
1017
1018
		return $accept;
1019
    }
1020
1021
    /**
1022
     * Découvrez quels types de contenu le client accepte ou vérifiez s'il accepte un
1023
     * type particulier de contenu.
1024
     *
1025
     * #### Obtenir tous les types :
1026
     *
1027
     * ```
1028
     * $this->request->accepts();
1029
     * ```
1030
     *
1031
     * #### Vérifier un seul type :
1032
     *
1033
     * ```
1034
     * $this->request->accepts('application/json');
1035
     * ```
1036
     *
1037
     * Cette méthode ordonnera les types de contenu renvoyés par les valeurs de préférence indiquées
1038
     * par le client.
1039
     *
1040
     * @param array|string|null $types Le type de contenu à vérifier. Laissez null pour obtenir tous les types qu'un client accepte.
1041
     *
1042
     * @return bool|string[] Soit un tableau de tous les types acceptés par le client, soit un booléen s'il accepte le type fourni.
1043
     */
1044
    public function accepts(array|string|null $types = null)
1045
    {
1046
        $accept = $this->getAcceptableContentTypes();
1047
1048
        if ($types === null) {
1049
            return $accept;
1050
        }
1051
1052
		foreach ((array) $types as $type) {
1053
			if (in_array($type, $accept, true)) {
1054
				return true;
1055
			}
1056
		}
1057
1058
		return false;
1059
    }
1060
1061
    /**
1062
     * Analyser l'en-tête HTTP_ACCEPT et renvoyer un tableau trié avec les types de contenu
1063
     * comme clés et valeurs pref comme valeurs.
1064
     *
1065
     * Généralement, vous souhaitez utiliser {@link \BlitzPHP\Http\ServerRequest::accepts()} pour obtenir une liste simple
1066
     * des types de contenu acceptés.
1067
     *
1068
     * @return array Un tableau de `prefValue => [contenu/types]`
1069
     */
1070
    public function parseAccept(): array
1071
    {
1072
        return $this->_parseAcceptWithQualifier($this->getHeaderLine('Accept'));
1073
    }
1074
1075
    /**
1076
     * Obtenez les langues acceptées par le client ou vérifiez si une langue spécifique est acceptée.
1077
     *
1078
     * Obtenez la liste des langues acceptées :
1079
     *
1080
     * ``` \BlitzPHP\Http\ServerRequest::acceptLanguage(); ```
1081
     *
1082
     * Vérifiez si une langue spécifique est acceptée :
1083
     *
1084
     * ``` \BlitzPHP\Http\ServerRequest::acceptLanguage('es-es'); ```
1085
     *
1086
     * @return array|bool Si un $language est fourni, un booléen. Sinon, le tableau des langues acceptées.
1087
     */
1088
    public function acceptLanguage(?string $language = null)
1089
    {
1090
        $raw    = $this->_parseAcceptWithQualifier($this->getHeaderLine('Accept-Language'));
1091
        $accept = [];
1092
1093
        foreach ($raw as $languages) {
1094
            foreach ($languages as &$lang) {
1095
                if (strpos($lang, '_')) {
1096
                    $lang = str_replace('_', '-', $lang);
1097
                }
1098
                $lang = strtolower($lang);
1099
            }
1100
            $accept = array_merge($accept, $languages);
1101
        }
1102
        if ($language === null) {
1103
            return $accept;
1104
        }
1105
1106
        return in_array(strtolower($language), $accept, true);
1107
    }
1108
1109
    /**
1110
     * Analysez les en-têtes Accept* avec les options de qualificateur.
1111
     *
1112
     * Seuls les qualificatifs seront extraits, toutes les autres extensions acceptées seront
1113
     * jetés car ils ne sont pas fréquemment utilisés.
1114
     */
1115
    protected function _parseAcceptWithQualifier(string $header): array
1116
    {
1117
        $accept  = [];
1118
        $headers = explode(',', $header);
1119
1120
        foreach (array_filter($headers) as $value) {
1121
            $prefValue = '1.0';
1122
            $value     = trim($value);
1123
1124
            $semiPos = strpos($value, ';');
1125
            if ($semiPos !== false) {
1126
                $params = explode(';', $value);
1127
                $value  = trim($params[0]);
1128
1129
                foreach ($params as $param) {
1130
                    $qPos = strpos($param, 'q=');
1131
                    if ($qPos !== false) {
1132
                        $prefValue = substr($param, $qPos + 2);
1133
                    }
1134
                }
1135
            }
1136
1137
            if (! isset($accept[$prefValue])) {
1138
                $accept[$prefValue] = [];
1139
            }
1140
            if ($prefValue) {
1141
                $accept[$prefValue][] = $value;
1142
            }
1143
        }
1144
        krsort($accept);
1145
1146
        return $accept;
1147
    }
1148
1149
    /**
1150
     * Lire une valeur de requête spécifique ou un chemin en pointillés.
1151
     *
1152
     * Les développeurs sont encouragés à utiliser getQueryParams() s'ils ont besoin de tout le tableau de requête,
1153
     * car il est compatible PSR-7, et cette méthode ne l'est pas. En utilisant Hash::get(), vous pouvez également obtenir des paramètres uniques.
1154
     *
1155
     * ### Alternative PSR-7
1156
     *
1157
     * ```
1158
     * $value = Arr::get($request->getQueryParams(), 'Post.id');
1159
     * ```
1160
     *
1161
     * @param string|null $name    Le nom ou le chemin en pointillé vers le paramètre de requête ou null pour tout lire.
1162
     * @param mixed       $default La valeur par défaut si le paramètre nommé n'est pas défini et que $name n'est pas nul.
1163
     *
1164
     * @return array|string|null Requête de données.
1165
     *
1166
     * @see ServerRequest::getQueryParams()
1167
     */
1168
    public function getQuery(?string $name = null, $default = null)
1169
    {
1170
        if ($name === null) {
1171
            return $this->query;
1172
        }
1173
1174
        return Arr::get($this->query, $name, $default);
1175
    }
1176
1177
    /**
1178
     * Fournit un accesseur sécurisé pour les données de requête. Permet
1179
     * vous permet d'utiliser des chemins compatibles Arr::get().
1180
     *
1181
     * ### Lecture des valeurs.
1182
     *
1183
     * ```
1184
     * // récupère toutes les données
1185
     * $request->getData();
1186
     *
1187
     * // Lire un champ spécifique.
1188
     * $request->getData('Post.title');
1189
     *
1190
     * // Avec une valeur par défaut.
1191
     * $request->getData('Post.not there', 'default value');
1192
     * ```
1193
     *
1194
     * Lors de la lecture des valeurs, vous obtiendrez `null` pour les clés/valeurs qui n'existent pas.
1195
     *
1196
     * Les développeurs sont encouragés à utiliser getParsedBody() s'ils ont besoin de tout le tableau de données,
1197
     * car il est compatible PSR-7, et cette méthode ne l'est pas. En utilisant Hash::get(), vous pouvez également obtenir des paramètres uniques.
1198
     *
1199
     * ### Alternative PSR-7
1200
     *
1201
     * ```
1202
     * $value = Arr::get($request->getParsedBody(), 'Post.id');
1203
     * ```
1204
     *
1205
     * @param string|null $name    Nom séparé par un point de la valeur à lire. Ou null pour lire toutes les données.
1206
     * @param mixed       $default Les données par défaut.
1207
     *
1208
     * @return mixed La valeur en cours de lecture.
1209
     */
1210
    public function getData(?string $name = null, $default = null)
1211
    {
1212
        if ($name === null) {
1213
            return $this->data;
1214
        }
1215
        if (! is_array($this->data)) {
1216
            return $default;
1217
        }
1218
1219
        return Arr::get($this->data, $name, $default);
1220
    }
1221
1222
    /**
1223
     * Lire les données de cookie à partir des données de cookie de la demande.
1224
     *
1225
     * @param string            $key     La clé ou le chemin en pointillés que vous voulez lire.
1226
     * @param array|string|null $default La valeur par défaut si le cookie n'est pas défini.
1227
     *
1228
     * @return array|string|null Soit la valeur du cookie, soit null si la valeur n'existe pas.
1229
     */
1230
    public function getCookie(string $key, $default = null)
1231
    {
1232
        return Arr::get($this->cookies, $key, $default);
1233
    }
1234
1235
    /**
1236
     * Obtenir une collection de cookies basée sur les cookies de la requête
1237
     *
1238
     * La CookieCollection vous permet d'interagir avec les cookies de demande en utilisant
1239
     * Objets `\BlitzPHP\Http\Cookie\Cookie` et peut faire des cookies de demande de conversion
1240
     * dans les cookies de réponse plus facile.
1241
     *
1242
     * Cette méthode créera une nouvelle collection de cookies à chaque appel.
1243
     * Il s'agit d'une optimisation qui permet d'allouer moins d'objets jusqu'à
1244
     * plus la CookieCollection est nécessaire. En général, vous devriez préférer
1245
     * `getCookie()` et `getCookieParams()` sur cette méthode. Utilisation d'une collection de cookies
1246
     * est idéal si vos cookies contiennent des données complexes encodées en JSON.
1247
     */
1248
    public function getCookieCollection(): CookieCollection
1249
    {
1250
        return CookieCollection::createFromServerRequest($this);
1251
    }
1252
1253
    /**
1254
     * Remplacez les cookies de la requête par ceux contenus dans
1255
     * la CookieCollection fournie.
1256
     */
1257
    public function withCookieCollection(CookieCollection $cookies): static
1258
    {
1259
        $new    = clone $this;
1260
        $values = [];
1261
1262
        foreach ($cookies as $cookie) {
1263
            $values[$cookie->getName()] = $cookie->getValue();
1264
        }
1265
        $new->cookies = $values;
1266
1267
        return $new;
1268
    }
1269
1270
    /**
1271
     * Obtenez toutes les données de cookie de la requête.
1272
     *
1273
     * @return array Un tableau de données de cookie.
1274
     */
1275
    public function getCookieParams(): array
1276
    {
1277
        return $this->cookies;
1278
    }
1279
1280
    /**
1281
     * Remplacez les cookies et obtenez une nouvelle instance de requête.
1282
     *
1283
     * @param array $cookies Les nouvelles données de cookie à utiliser.
1284
     */
1285
    public function withCookieParams(array $cookies): static
1286
    {
1287
        $new          = clone $this;
1288
        $new->cookies = $cookies;
1289
1290
        return $new;
1291
    }
1292
1293
    /**
1294
     * Obtenez les données de corps de requête analysées.
1295
     *
1296
     * Si la requête Content-Type est soit application/x-www-form-urlencoded
1297
     * ou multipart/form-data, et la méthode de requête est POST, ce sera le
1298
     * publier des données. Pour les autres types de contenu, il peut s'agir de la requête désérialisée
1299
     * corps.
1300
     *
1301
     * @return array|object|null Les paramètres de corps désérialisés, le cas échéant.
1302
     *                           Il s'agira généralement d'un tableau.
1303
     */
1304
    public function getParsedBody()
1305
    {
1306
        return $this->data;
1307
    }
1308
1309
    /**
1310
     * Mettez à jour le corps analysé et obtenez une nouvelle instance.
1311
     *
1312
     * @param array|object|null $data Les données de corps désérialisées. Cette volonté
1313
     *                                être généralement dans un tableau ou un objet.
1314
     */
1315
    public function withParsedBody($data): static
1316
    {
1317
        $new       = clone $this;
1318
        $new->data = $data;
1319
1320
        return $new;
1321
    }
1322
1323
    /**
1324
     * Récupère la version du protocole HTTP sous forme de chaîne.
1325
     *
1326
     * @return string Version du protocole HTTP.
1327
     */
1328
    public function getProtocolVersion(): string
1329
    {
1330
        if ($this->protocol) {
1331
            return $this->protocol;
1332
        }
1333
1334
        // Remplissez paresseusement ces données car elles ne sont généralement pas utilisées.
1335
        preg_match('/^HTTP\/([\d.]+)$/', (string) $this->getEnv('SERVER_PROTOCOL'), $match);
1336
        $protocol = '1.1';
1337
        if (isset($match[1])) {
1338
            $protocol = $match[1];
1339
        }
1340
        $this->protocol = $protocol;
1341
1342
        return $this->protocol;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->protocol could return the type null which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
1343
    }
1344
1345
    /**
1346
     * Renvoie une instance avec la version de protocole HTTP spécifiée.
1347
     *
1348
     * La chaîne de version DOIT contenir uniquement le numéro de version HTTP (par exemple,
1349
     * "1.1", "1.0").
1350
     *
1351
     * @param string $version Version du protocole HTTP
1352
     */
1353
    public function withProtocolVersion(string $version): static
1354
    {
1355
        if (! preg_match('/^(1\.[01]|2(\.[0])?)$/', $version)) {
1356
            throw new InvalidArgumentException(sprintf('Version de protocole `%s` non prise en charge fournie.', $version));
1357
        }
1358
        $new           = clone $this;
1359
        $new->protocol = $version;
1360
1361
        return $new;
1362
    }
1363
1364
    /**
1365
     * Obtenez une valeur à partir des données d'environnement de la demande.
1366
     * Se replier sur env() si la clé n'est pas définie dans la propriété $environment.
1367
     *
1368
     * @param string      $key     La clé à partir de laquelle vous voulez lire.
1369
     * @param string|null $default Valeur par défaut lors de la tentative de récupération d'un environnement
1370
     *                             valeur de la variable qui n'existe pas.
1371
     *
1372
     * @return string|null Soit la valeur de l'environnement, soit null si la valeur n'existe pas.
1373
     */
1374
    public function getEnv(string $key, ?string $default = null): ?string
1375
    {
1376 10
        $key = strtoupper($key);
1377
        if (! array_key_exists($key, $this->_environment) || null === $this->_environment[$key]) {
1378 2
            $this->_environment[$key] = env($key);
1379
        }
1380
1381 10
        return $this->_environment[$key] !== null ? (string) $this->_environment[$key] : $default;
1382
    }
1383
1384
    /**
1385
     * Mettez à jour la demande avec un nouvel élément de données d'environnement.
1386
     *
1387
     * Renvoie un objet de requête mis à jour. Cette méthode retourne
1388
     * un *nouvel* objet de requête et ne mute pas la requête sur place.
1389
     */
1390
    public function withEnv(string $key, string $value): static
1391
    {
1392
        $new                     = clone $this;
1393
        $new->_environment[$key] = $value;
1394
        $new->clearDetectorCache();
1395
1396
        return $new;
1397
    }
1398
1399
    /**
1400
     * Autoriser uniquement certaines méthodes de requête HTTP, si la méthode de requête ne correspond pas
1401
     * une erreur 405 s'affichera et l'en-tête de réponse "Autoriser" requis sera défini.
1402
     *
1403
     * Exemple:
1404
     *
1405
     * $this->request->allowMethod('post');
1406
     * ou alors
1407
     * $this->request->allowMethod(['post', 'delete']);
1408
     *
1409
     * Si la requête est GET, l'en-tête de réponse "Autoriser : POST, SUPPRIMER" sera défini
1410
     * et une erreur 405 sera renvoyée.
1411
     *
1412
     * @param string|string[] $methods Méthodes de requête HTTP autorisées.
1413
     *
1414
     * @throws HttpException
1415
     */
1416
    public function allowMethod($methods): bool
1417
    {
1418
        $methods = (array) $methods;
1419
1420
        foreach ($methods as $method) {
1421
            if ($this->is($method)) {
1422
                return true;
1423
            }
1424
        }
1425
        $allowed = strtoupper(implode(', ', $methods));
1426
1427
        throw HttpException::methodNotAllowed($allowed);
1428
    }
1429
1430
    /**
1431
     * Mettez à jour la demande avec un nouvel élément de données de demande.
1432
     *
1433
     * Renvoie un objet de requête mis à jour. Cette méthode retourne
1434
     * un *nouvel* objet de requête et ne mute pas la requête sur place.
1435
     *
1436
     * Utilisez `withParsedBody()` si vous devez remplacer toutes les données de la requête.
1437
     *
1438
     * @param string $name Le chemin séparé par des points où insérer $value.
1439
     */
1440
    public function withData(string $name, mixed $value): static
1441
    {
1442
        $copy = clone $this;
1443
1444
        if (is_array($copy->data)) {
1445
            $copy->data = Arr::insert($copy->data, $name, $value);
1446
        }
1447
1448
        return $copy;
1449
    }
1450
1451
    /**
1452
     * Mettre à jour la demande en supprimant un élément de données.
1453
     *
1454
     * Renvoie un objet de requête mis à jour. Cette méthode retourne
1455
     * un *nouvel* objet de requête et ne mute pas la requête sur place.
1456
     *
1457
     * @param string $name Le chemin séparé par des points à supprimer.
1458
     */
1459
    public function withoutData(string $name): static
1460
    {
1461
        $copy = clone $this;
1462
1463
        if (is_array($copy->data)) {
1464
            $copy->data = Arr::remove($copy->data, $name);
1465
        }
1466
1467
        return $copy;
1468
    }
1469
1470
    /**
1471
     * Mettre à jour la requête avec un nouveau paramètre de routage
1472
     *
1473
     * Renvoie un objet de requête mis à jour. Cette méthode retourne
1474
     * un *nouvel* objet de requête et ne mute pas la requête sur place.
1475
     *
1476
     * @param string $name Le chemin séparé par des points où insérer $value.
1477
     */
1478
    public function withParam(string $name, mixed $value): static
1479
    {
1480
        $copy         = clone $this;
1481
        $copy->params = Arr::insert($copy->params, $name, $value);
1482
1483
        return $copy;
1484
    }
1485
1486
    /**
1487
     * Accédez en toute sécurité aux valeurs dans $this->params.
1488
     */
1489
    public function getParam(string $name, mixed $default = null)
1490
    {
1491
        return Arr::get($this->params, $name, $default);
1492
    }
1493
1494
    /**
1495
     * Renvoie une instance avec l'attribut de requête spécifié.
1496
     *
1497
     * @param string $name  Le nom de l'attribut.
1498
     * @param mixed  $value La valeur de l'attribut.
1499
     */
1500
    public function withAttribute(string $name, mixed $value): static
1501
    {
1502
        $new = clone $this;
1503
        if (in_array($name, $this->emulatedAttributes, true)) {
1504
            $new->{$name} = $value;
1505
        } else {
1506
            $new->attributes[$name] = $value;
1507
        }
1508
1509
        return $new;
1510
    }
1511
1512
    /**
1513
     * Renvoie une instance sans l'attribut de requête spécifié.
1514
     *
1515
     * @param string $name Le nom de l'attribut.
1516
     *
1517
     * @throws InvalidArgumentException
1518
     */
1519
    public function withoutAttribute(string $name): static
1520
    {
1521
        $new = clone $this;
1522
        if (in_array($name, $this->emulatedAttributes, true)) {
1523
            throw new InvalidArgumentException(
1524
                "Vous ne pouvez pas supprimer '{$name}'. C'est un attribut BlitzPHP obligatoire."
1525
            );
1526
        }
1527
        unset($new->attributes[$name]);
1528
1529
        return $new;
1530
    }
1531
1532
    /**
1533
     * Tentatives d'obtenir de vieilles données d'entrée qui a été flashé à la session avec redirect_with_input().
1534
     * Il vérifie d'abord les données dans les anciennes données POST, puis les anciennes données GET et enfin vérifier les tableaux de points
1535
     *
1536
     * @return array|string|null
1537
     */
1538
    public function getOldInput(string $key)
1539
    {
1540
        return $this->session()->getOldInput($key);
1541
    }
1542
1543
    /**
1544
     * Lire un attribut de la requête ou obtenir la valeur par défaut
1545
     *
1546
     * @param string $name    Le nom de l'attribut.
1547
     * @param mixed  $default La valeur par défaut si l'attribut n'a pas été défini.
1548
     */
1549
    public function getAttribute(string $name, mixed $default = null): mixed
1550
    {
1551
        if (in_array($name, $this->emulatedAttributes, true)) {
1552
            if ($name === 'here') {
1553
                return $this->base . $this->uri->getPath();
1554
            }
1555
1556
            return $this->{$name};
1557
        }
1558
        if (array_key_exists($name, $this->attributes)) {
1559 4
            return $this->attributes[$name];
1560
        }
1561
1562 4
        return $default;
1563
    }
1564
1565
    /**
1566
     * Obtenez tous les attributs de la requête.
1567
     *
1568
     * Cela inclura les attributs params, webroot, base et here fournis par BlitzPHP.
1569
     */
1570
    public function getAttributes(): array
1571
    {
1572
        $emulated = [
1573
            'params'  => $this->params,
1574
            'webroot' => $this->webroot,
1575
            'base'    => $this->base,
1576
            'here'    => $this->base . $this->uri->getPath(),
1577
        ];
1578
1579
        return $this->attributes + $emulated;
1580
    }
1581
1582
    /**
1583
     * Obtenez le fichier téléchargé à partir d'un chemin en pointillés.
1584
     *
1585
     * @param string $path Le chemin séparé par des points vers le fichier que vous voulez.
1586
     *
1587
     * @return UploadedFileInterface|UploadedFileInterface[]|null
1588
     */
1589
    public function getUploadedFile(string $path)
1590
    {
1591
        $file = Arr::get($this->uploadedFiles, $path);
1592
        if (is_array($file)) {
1593
            foreach ($file as $f) {
1594
                if (! ($f instanceof UploadedFile)) {
1595
                    return null;
1596
                }
1597
            }
1598
1599
            return $file;
1600
        }
1601
1602
        if (! ($file instanceof UploadedFileInterface)) {
1603
            return null;
1604
        }
1605
1606
        return $file;
1607
    }
1608
1609
    /**
1610
     * Obtenez le tableau des fichiers téléchargés à partir de la requête.
1611
     */
1612
    public function getUploadedFiles(): array
1613
    {
1614
        return $this->uploadedFiles;
1615
    }
1616
1617
    /**
1618
     * Mettez à jour la demande en remplaçant les fichiers et en créant une nouvelle instance.
1619
     *
1620
     * @param array $uploadedFiles Un tableau d'objets de fichiers téléchargés.
1621
     *
1622
     * @throws InvalidArgumentException lorsque $files contient un objet invalide.
1623
     */
1624
    public function withUploadedFiles(array $uploadedFiles): static
1625
    {
1626
        $this->validateUploadedFiles($uploadedFiles, '');
1627
        $new                = clone $this;
1628
        $new->uploadedFiles = $uploadedFiles;
1629
1630
        return $new;
1631
    }
1632
1633
    /**
1634
     * Validez de manière récursive les données de fichier téléchargées.
1635
     *
1636
     * @param array  $uploadedFiles Le nouveau tableau de fichiers à valider.
1637
     * @param string $path          Le chemin jusqu'ici.
1638
     *
1639
     * @throws InvalidArgumentException Si des éléments feuilles ne sont pas des fichiers valides.
1640
     */
1641
    protected function validateUploadedFiles(array $uploadedFiles, string $path): void
1642
    {
1643
        foreach ($uploadedFiles as $key => $file) {
1644
            if (is_array($file)) {
1645
                $this->validateUploadedFiles($file, $key . '.');
1646
1647
                continue;
1648
            }
1649
1650
            if (! $file instanceof UploadedFileInterface) {
1651
                throw new InvalidArgumentException("Fichier invalide à '{$path}{$key}'");
1652
            }
1653
        }
1654
    }
1655
1656
    /**
1657
     * Obtient le corps du message.
1658
     */
1659
    public function getBody(): StreamInterface
1660
    {
1661
        return $this->stream;
1662
    }
1663
1664
    /**
1665
     * Renvoie une instance avec le corps de message spécifié.
1666
     */
1667
    public function withBody(StreamInterface $body): static
1668
    {
1669
        $new         = clone $this;
1670
        $new->stream = $body;
1671
1672
        return $new;
1673
    }
1674
1675
    /**
1676
     * Récupère l'instance d'URI.
1677
     */
1678
    public function getUri(): UriInterface
1679
    {
1680
        return $this->uri;
1681
    }
1682
1683
    /**
1684
     * Renvoie une instance avec l'uri spécifié
1685
     *
1686
     * *Attention* Remplacer l'Uri ne mettra pas à jour la `base`, `webroot`,
1687
     * et les attributs `url`.
1688
     *
1689
     * @param bool $preserveHost Indique si l'hôte doit être conservé.
1690
     */
1691
    public function withUri(UriInterface $uri, bool $preserveHost = false): static
1692
    {
1693
        $new      = clone $this;
1694
        $new->uri = $uri;
1695
1696
        if ($preserveHost && $this->hasHeader('Host')) {
1697
            return $new;
1698
        }
1699
1700
        $host = $uri->getHost();
1701
        if (! $host) {
1702
            return $new;
1703
        }
1704
        $port = $uri->getPort();
1705
        if ($port) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $port of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
1706
            $host .= ':' . $port;
1707
        }
1708
        $new->_environment['HTTP_HOST'] = $host;
1709
1710
        return $new;
1711
    }
1712
1713
    /**
1714
     * Créez une nouvelle instance avec une cible de demande spécifique.
1715
     *
1716
     * Vous pouvez utiliser cette méthode pour écraser la cible de la demande qui est
1717
     * déduit de l'Uri de la requête. Cela vous permet également de modifier la demande
1718
     * la forme de la cible en une forme absolue, une forme d'autorité ou une forme d'astérisque
1719
     *
1720
     * @see https://tools.ietf.org/html/rfc7230#section-2.7 (pour les différentes formes de demande-cible autorisées dans les messages de demande)
1721
     *
1722
     * @param string $requestTarget La cible de la requête.
1723
     *
1724
     * @psalm-suppress MoreSpecificImplementedParamType
1725
     */
1726
    public function withRequestTarget(string $requestTarget): static
1727
    {
1728
        $new                = clone $this;
1729
        $new->requestTarget = $requestTarget;
1730
1731
        return $new;
1732
    }
1733
1734
    /**
1735
     * Récupère la cible de la requête.
1736
     *
1737
     * Récupère la cible de la demande du message soit telle qu'elle a été demandée,
1738
     * ou comme défini avec `withRequestTarget()`. Par défaut, cela renverra le
1739
     * chemin relatif de l'application sans répertoire de base et la chaîne de requête
1740
     * défini dans l'environnement SERVER.
1741
     */
1742
    public function getRequestTarget(): string
1743
    {
1744
        if ($this->requestTarget !== null) {
1745
            return $this->requestTarget;
1746
        }
1747
1748
        $target = $this->uri->getPath();
1749
        if ($this->uri->getQuery()) {
1750
            $target .= '?' . $this->uri->getQuery();
1751
        }
1752
1753
        if (empty($target)) {
1754
            $target = '/';
1755
        }
1756
1757
        return $target;
1758
    }
1759
1760
    /**
1761
     * Récupère le chemin de la requête en cours.
1762
     */
1763
    public function getPath(): string
1764
    {
1765
        if ($this->requestTarget === null) {
1766
            return $this->uri->getPath();
1767
        }
1768
1769
        [$path] = explode('?', $this->requestTarget);
1770
1771
        return $path;
1772
    }
1773
1774
    /**
1775
     * Fournit un moyen pratique de travailler avec la classe Negotiate
1776
     * pour la négociation de contenu.
1777
     */
1778
    public function negotiate(string $type, array $supported, bool $strictMatch = false): string
1779
    {
1780
        if (null === $this->negotiator) {
1781
            $this->negotiator = Services::negotiator($this, true);
1782
        }
1783
1784
        switch (strtolower($type)) {
1785
            case 'media':
1786
                return $this->negotiator->media($supported, $strictMatch);
1787
1788
            case 'charset':
1789
                return $this->negotiator->charset($supported);
1790
1791
            case 'encoding':
1792
                return $this->negotiator->encoding($supported);
1793
1794
            case 'language':
1795
                return $this->negotiator->language($supported);
1796
        }
1797
1798
        throw new HttpException($type . ' is not a valid negotiation type. Must be one of: media, charset, encoding, language.');
1799
    }
1800
1801
    /**
1802
     * Définit la chaîne locale pour cette requête.
1803
     */
1804
    public function withLocale(string $locale): static
1805
    {
1806
        $validLocales = config('app.supported_locales');
1807
        // S'il ne s'agit pas d'un paramètre régional valide, définissez-le
1808
        // aux paramètres régionaux par défaut du site.
1809
        if (! in_array($locale, $validLocales, true)) {
0 ignored issues
show
Bug introduced by
$validLocales of type BlitzPHP\Config\Config|null is incompatible with the type array expected by parameter $haystack of in_array(). ( Ignorable by Annotation )

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

1809
        if (! in_array($locale, /** @scrutinizer ignore-type */ $validLocales, true)) {
Loading history...
1810
            $locale = config('app.language');
1811
        }
1812
1813
        Services::translator()->setLocale($locale);
0 ignored issues
show
Bug introduced by
It seems like $locale can also be of type BlitzPHP\Config\Config; however, parameter $locale of BlitzPHP\Translator\Translate::setLocale() does only seem to accept null|string, maybe add an additional type check? ( Ignorable by Annotation )

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

1813
        Services::translator()->setLocale(/** @scrutinizer ignore-type */ $locale);
Loading history...
1814
1815
        return $this->withAttribute('locale', $locale);
1816
    }
1817
1818
    /**
1819
     * Obtient les paramètres régionaux actuels, avec un retour à la valeur par défaut
1820
     * locale si aucune n'est définie.
1821
     */
1822
    public function getLocale(): string
1823
    {
1824 4
        $locale = $this->getAttribute('locale');
1825
        if (empty($locale)) {
1826 4
            $locale = $this->getAttribute('lang');
1827
        }
1828
1829 4
        return $locale ?? Services::translator()->getLocale();
1830
    }
1831
1832
    /**
1833
     * Read data from `php://input`. Useful when interacting with XML or JSON
1834
     * request body content.
1835
     *
1836
     * Getting input with a decoding function:
1837
     *
1838
     * ```
1839
     * $this->request->input('json_decode');
1840
     * ```
1841
     *
1842
     * Getting input using a decoding function, and additional params:
1843
     *
1844
     * ```
1845
     * $this->request->input('Xml::build', ['return' => 'DOMDocument']);
1846
     * ```
1847
     *
1848
     * Any additional parameters are applied to the callback in the order they are given.
1849
     *
1850
     * @param string|null $callback A decoding callback that will convert the string data to another
1851
     *                              representation. Leave empty to access the raw input data. You can also
1852
     *                              supply additional parameters for the decoding callback using var args, see above.
1853
     * @param array       ...$args  The additional arguments
1854
     *
1855
     * @return string The decoded/processed request data.
1856
     */
1857
    public function input($callback = null, ...$args): string
1858
    {
1859
        $this->stream->rewind();
1860
        $input = $this->stream->getContents();
1861
        if ($callback) {
1862
            array_unshift($args, $input);
1863
1864
            return $callback(...$args);
1865
        }
1866
1867
        return $input;
1868
    }
1869
1870
    /**
1871
     * Sets the REQUEST_METHOD environment variable based on the simulated _method
1872
     * HTTP override value. The 'ORIGINAL_REQUEST_METHOD' is also preserved, if you
1873
     * want the read the non-simulated HTTP method the client used.
1874
     *
1875
     * @param array $data Array of post data.
1876
     *
1877
     * @return array
1878
     */
1879
    protected function _processPost(array $data)
1880
    {
1881
        $method   = $this->getEnv('REQUEST_METHOD');
1882
        $override = false;
1883
1884
        if ($_POST) {
1885
            $data = $_POST;
1886
        } elseif (
1887
            in_array($method, ['PUT', 'DELETE', 'PATCH'], true)
1888
            && str_starts_with($this->contentType() ?? '', 'application/x-www-form-urlencoded')
1889
        ) {
1890
            $data = $this->input();
1891
            parse_str($data, $data);
0 ignored issues
show
Bug introduced by
$data of type string is incompatible with the type array expected by parameter $result of parse_str(). ( Ignorable by Annotation )

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

1891
            parse_str($data, /** @scrutinizer ignore-type */ $data);
Loading history...
1892
        }
1893
        if (ini_get('magic_quotes_gpc') === '1') {
1894
            $data = Helpers::stripslashesDeep((array) $this->data);
1895
        }
1896
1897
        if ($this->hasHeader('X-Http-Method-Override')) {
1898
            $data['_method'] = $this->getHeaderLine('X-Http-Method-Override');
1899
            $override        = true;
1900
        }
1901
        $this->_environment['ORIGINAL_REQUEST_METHOD'] = $method;
1902
1903
        if (isset($data['_method'])) {
1904
            $this->_environment['REQUEST_METHOD'] = $data['_method'];
1905
            unset($data['_method']);
1906
            $override = true;
1907
        }
1908
1909
        if ($override && ! in_array($this->_environment['REQUEST_METHOD'], ['PUT', 'POST', 'DELETE', 'PATCH'], true)) {
1910
            $data = [];
1911
        }
1912
1913
        return $data;
1914
    }
1915
1916
    /**
1917
     * Process the GET parameters and move things into the object.
1918
     *
1919
     * @param array  $query       The array to which the parsed keys/values are being added.
1920
     * @param string $queryString A query string from the URL if provided
1921
     *
1922
     * @return array An array containing the parsed query string as keys/values.
1923
     */
1924
    protected function _processGet($query, $queryString = '')
0 ignored issues
show
Unused Code introduced by
The parameter $queryString is not used and could be removed. ( Ignorable by Annotation )

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

1924
    protected function _processGet($query, /** @scrutinizer ignore-unused */ $queryString = '')

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1925
    {
1926
        if (ini_get('magic_quotes_gpc') === '1') {
1927
            $q = Helpers::stripslashesDeep($_GET);
1928
        } else {
1929
            $q = $_GET;
1930
        }
1931
        $query = array_merge($q, $query);
1932
        $url   = (string) $this->uri;
1933
1934
        $unsetUrl = '/' . str_replace(['.', ' '], '_', urldecode($url));
1935
        unset($query[$unsetUrl], $query[$this->base . $unsetUrl]);
1936
1937
        if (str_contains($url, '?')) {
1938
            [, $querystr] = explode('?', $url);
1939
            parse_str($querystr, $queryArgs);
1940
            $query += $queryArgs;
1941
        }
1942
        if (isset($this->params['url'])) {
1943
            $query = array_merge($this->params['url'], $query);
1944
        }
1945
1946
        return $query;
1947
    }
1948
1949
    /**
1950
     * Process uploaded files and move things onto the post data.
1951
     *
1952
     * @param array $post  Post data to merge files onto.
1953
     * @param array $files Uploaded files to merge in.
1954
     *
1955
     * @return array merged post + file data.
1956
     */
1957
    protected function _processFiles(array $post, array $files): array
1958
    {
1959
        if (! is_array($files)) {
0 ignored issues
show
introduced by
The condition is_array($files) is always true.
Loading history...
1960
            return $post;
1961
        }
1962
1963
        $fileData = [];
1964
1965
        foreach ($files as $key => $value) {
1966
            if ($value instanceof UploadedFileInterface) {
1967
                $fileData[$key] = $value;
1968
1969
                continue;
1970
            }
1971
1972
            if (is_array($value) && isset($value['tmp_name'])) {
1973
                $fileData[$key] = $this->_createUploadedFile($value);
1974
1975
                continue;
1976
            }
1977
1978
            throw new InvalidArgumentException(sprintf(
1979
                'Invalid value in FILES "%s"',
1980
                json_encode($value)
1981
            ));
1982
        }
1983
1984
        $this->uploadedFiles = $fileData;
1985
1986
        // Make a flat map that can be inserted into $post for BC.
1987
        $fileMap = Arr::flatten($fileData);
1988
1989
        foreach ($fileMap as $key => $file) {
1990
            $error   = $file->getError();
1991
            $tmpName = '';
1992
1993
            if ($error === UPLOAD_ERR_OK) {
1994
                $tmpName = $file->getStream()->getMetadata('uri');
1995
            }
1996
1997
            $post = Arr::insert($post, $key, [
1998
                'tmp_name' => $tmpName,
1999
                'error'    => $error,
2000
                'name'     => $file->getClientFilename(),
2001
                'type'     => $file->getClientMediaType(),
2002
                'size'     => $file->getSize(),
2003
            ]);
2004
        }
2005
2006
        return $post;
2007
    }
2008
2009
    /**
2010
     * Create an UploadedFile instance from a $_FILES array.
2011
     *
2012
     * If the value represents an array of values, this method will
2013
     * recursively process the data.
2014
     *
2015
     * @param array $value $_FILES struct
2016
     *
2017
     * @return UploadedFile|UploadedFile[]
2018
     */
2019
    protected function _createUploadedFile(array $value)
2020
    {
2021
        if (is_array($value['tmp_name'])) {
2022
            return $this->_normalizeNestedFiles($value);
2023
        }
2024
2025
        return new UploadedFile(
2026
            $value['tmp_name'],
2027
            $value['size'],
2028
            $value['error'],
2029
            $value['name'],
2030
            $value['type']
2031
        );
2032
    }
2033
2034
    /**
2035
     * Normalize an array of file specifications.
2036
     *
2037
     * Loops through all nested files and returns a normalized array of
2038
     * UploadedFileInterface instances.
2039
     *
2040
     * @param array $files The file data to normalize & convert.
2041
     *
2042
     * @return UploadedFile[]
2043
     */
2044
    protected function _normalizeNestedFiles(array $files = []): array
2045
    {
2046
        $normalizedFiles = [];
2047
2048
        foreach (array_keys($files['tmp_name']) as $key) {
2049
            $spec = [
2050
                'tmp_name' => $files['tmp_name'][$key],
2051
                'size'     => $files['size'][$key],
2052
                'error'    => $files['error'][$key],
2053
                'name'     => $files['name'][$key],
2054
                'type'     => $files['type'][$key],
2055
            ];
2056
2057
            $normalizedFiles[$key] = $this->_createUploadedFile($spec);
2058
        }
2059
2060
        return $normalizedFiles;
2061
    }
2062
}
2063