Passed
Push — main ( 40dc38...b0a2e6 )
by Dimitri
11:06
created

ServerRequest::withMethod()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 16
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

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

299
            $config['url'] = '/' . /** @scrutinizer ignore-type */ $config['url'];
Loading history...
300
        }
301
302
        if (strpos($config['url'], '?') !== false) {
303
            [$config['url'], $config['environment']['QUERY_STRING']] = explode('?', $config['url']);
304
305
            parse_str($config['environment']['QUERY_STRING'], $queryArgs);
306
            $config['query'] += $queryArgs;
307
        }
308
309
        $config['environment']['REQUEST_URI'] = $config['url'];
310
311
        return $config;
312
    }
313
314
    /**
315
     * Obtenez le type de contenu utilisé dans cette requête.
316
     */
317
    public function contentType(): ?string
318
    {
319
        $type = $this->getEnv('CONTENT_TYPE');
320
        if ($type) {
321
            return $type;
322
        }
323
324
        return $this->getEnv('HTTP_CONTENT_TYPE');
325
    }
326
327
    /**
328
     * Renvoie l'instance de l'objet Session pour cette requête
329
     */
330
    public function session(): Session
331
    {
332
        return $this->session;
333
    }
334
335
    /**
336
     * Obtenez l'adresse IP que le client utilise ou dit qu'il utilise.
337
     */
338
    public function clientIp(): string
339
    {
340
        if ($this->trustProxy && $this->getEnv('HTTP_X_FORWARDED_FOR')) {
341
            $addresses = array_map('trim', explode(',', (string) $this->getEnv('HTTP_X_FORWARDED_FOR')));
342
            $trusted   = (count($this->trustedProxies) > 0);
343
            $n         = count($addresses);
344
345
            if ($trusted) {
346
                $trusted = array_diff($addresses, $this->trustedProxies);
347
                $trusted = (count($trusted) === 1);
348
            }
349
350
            if ($trusted) {
351
                return $addresses[0];
352
            }
353
354
            return $addresses[$n - 1];
355
        }
356
357
        if ($this->trustProxy && $this->getEnv('HTTP_X_REAL_IP')) {
358
            $ipaddr = $this->getEnv('HTTP_X_REAL_IP');
359
        } elseif ($this->trustProxy && $this->getEnv('HTTP_CLIENT_IP')) {
360
            $ipaddr = $this->getEnv('HTTP_CLIENT_IP');
361
        } else {
362
            $ipaddr = $this->getEnv('REMOTE_ADDR');
363
        }
364
365
        return trim((string) $ipaddr);
366
    }
367
368
    /**
369
     * Enregistrer des proxys de confiance
370
     *
371
     * @param string[] $proxies ips liste des proxys de confiance
372
     */
373
    public function setTrustedProxies(array $proxies): void
374
    {
375
        $this->trustedProxies = $proxies;
376
        $this->trustProxy     = true;
377
    }
378
379
    /**
380
     * Obtenez les proxys de confiance
381
     */
382
    public function getTrustedProxies(): array
383
    {
384
        return $this->trustedProxies;
385
    }
386
387
    /**
388
     * Renvoie le référent qui a référé cette requête.
389
     *
390
     * @param bool $local Tentative de renvoi d'une adresse locale.
391
     *                    Les adresses locales ne contiennent pas de noms d'hôtes..
392
     */
393
    public function referer(bool $local = true): ?string
394
    {
395
        $ref = $this->getEnv('HTTP_REFERER');
396
397
        $base = /* Configure::read('App.fullBaseUrl') . */ $this->webroot;
398
        if (! empty($ref) && ! empty($base)) {
399
            if ($local && strpos($ref, $base) === 0) {
400
                $ref = substr($ref, strlen($base));
401
                if ($ref === '' || strpos($ref, '//') === 0) {
402
                    $ref = '/';
403
                }
404
                if ($ref[0] !== '/') {
405
                    $ref = '/' . $ref;
406
                }
407
408
                return $ref;
409
            }
410
            if (! $local) {
411
                return $ref;
412
            }
413
        }
414
415
        return null;
416
    }
417
418
    /**
419
     * Gestionnaire de méthodes manquant, les poignées enveloppent les anciennes méthodes de type isAjax()
420
     *
421
     * @return bool
422
     *
423
     * @throws BadMethodCallException lorsqu'une méthode invalide est appelée.
424
     */
425
    public function __call(string $name, array $params)
426
    {
427
        if (strpos($name, 'is') === 0) {
428
            $type = strtolower(substr($name, 2));
429
430
            array_unshift($params, $type);
431
432
            return $this->is(...$params);
433
        }
434
435
        throw new BadMethodCallException(sprintf('Method "%s()" does not exist', $name));
436
    }
437
438
    /**
439
     * Vérifiez si une demande est d'un certain type.
440
     *
441
     * Utilise les règles de détection intégrées ainsi que des règles supplémentaires
442
     * défini avec {@link \BlitzPHP\Http\ServerRequest::addDetector()}. Tout détecteur peut être appelé
443
     * comme `is($type)` ou `is$Type()`.
444
     *
445
     * @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.
446
     *
447
     * @return bool Si la demande est du type que vous vérifiez.
448
     */
449
    public function is($type, ...$args): bool
450
    {
451
        if (is_array($type)) {
452
            foreach ($type as $_type) {
453
                if ($this->is($_type)) {
454
                    return true;
455
                }
456
            }
457
458
            return false;
459
        }
460
461
        $type = strtolower($type);
462
        if (! isset(static::$_detectors[$type])) {
463
            return false;
464
        }
465
        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...
466
            return $this->_is($type, $args);
467
        }
468
469
        return $this->_detectorCache[$type] = $this->_detectorCache[$type] ?? $this->_is($type, $args);
470
    }
471
472
    /**
473
     * Efface le cache du détecteur d'instance, utilisé par la fonction is()
474
     */
475
    public function clearDetectorCache(): void
476
    {
477
        $this->_detectorCache = [];
478
    }
479
480
    /**
481
     * Worker pour la fonction publique is()
482
     *
483
     * @param string $type Le type de requête que vous souhaitez vérifier.
484
     * @param array  $args Tableau d'arguments de détecteur personnalisés.
485
     *
486
     * @return bool Si la demande est du type que vous vérifiez.
487
     */
488
    protected function _is(string $type, array $args): bool
489
    {
490
        $detect = static::$_detectors[$type];
491
        if (is_callable($detect)) {
492
            array_unshift($args, $this);
493
494
            return $detect(...$args);
495
        }
496
        if (isset($detect['env']) && $this->_environmentDetector($detect)) {
0 ignored issues
show
Bug introduced by
It seems like $detect can also be of type callable; however, parameter $detect of BlitzPHP\Http\ServerRequ...:_environmentDetector() does only seem to accept array, 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

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

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

1553
        return $this->session()->/** @scrutinizer ignore-call */ getOldInput($key);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
1554
    }
1555
1556
    /**
1557
     * Lire un attribut de la requête ou obtenir la valeur par défaut
1558
     *
1559
     * @param string     $name    Le nom de l'attribut.
1560
     * @param mixed|null $default La valeur par défaut si l'attribut n'a pas été défini.
1561
     *
1562
     * @return mixed
1563
     */
1564
    public function getAttribute($name, $default = null)
1565
    {
1566
        if (in_array($name, $this->emulatedAttributes, true)) {
1567
            if ($name === 'here') {
1568
                return $this->base . $this->uri->getPath();
1569
            }
1570
1571
            return $this->{$name};
1572
        }
1573
        if (array_key_exists($name, $this->attributes)) {
1574
            return $this->attributes[$name];
1575
        }
1576
1577
        return $default;
1578
    }
1579
1580
    /**
1581
     * Obtenez tous les attributs de la requête.
1582
     *
1583
     * Cela inclura les attributs params, webroot, base et here fournis par BlitzPHP.
1584
     */
1585
    public function getAttributes(): array
1586
    {
1587
        $emulated = [
1588
            'params'  => $this->params,
1589
            'webroot' => $this->webroot,
1590
            'base'    => $this->base,
1591
            'here'    => $this->base . $this->uri->getPath(),
1592
        ];
1593
1594
        return $this->attributes + $emulated;
1595
    }
1596
1597
    /**
1598
     * Obtenez le fichier téléchargé à partir d'un chemin en pointillés.
1599
     *
1600
     * @param string $path Le chemin séparé par des points vers le fichier que vous voulez.
1601
     *
1602
     * @return UploadedFileInterface|UploadedFileInterface[]|null
1603
     */
1604
    public function getUploadedFile(string $path)
1605
    {
1606
        $file = Arr::get($this->uploadedFiles, $path);
1607
        if (is_array($file)) {
1608
            foreach ($file as $f) {
1609
                if (! ($f instanceof UploadedFile)) {
1610
                    return null;
1611
                }
1612
            }
1613
1614
            return $file;
1615
        }
1616
1617
        if (! ($file instanceof UploadedFileInterface)) {
1618
            return null;
1619
        }
1620
1621
        return $file;
1622
    }
1623
1624
    /**
1625
     * Obtenez le tableau des fichiers téléchargés à partir de la requête.
1626
     */
1627
    public function getUploadedFiles(): array
1628
    {
1629
        return $this->uploadedFiles;
1630
    }
1631
1632
    /**
1633
     * Mettez à jour la demande en remplaçant les fichiers et en créant une nouvelle instance.
1634
     *
1635
     * @param array $uploadedFiles Un tableau d'objets de fichiers téléchargés.
1636
     *
1637
     * @throws InvalidArgumentException lorsque $files contient un objet invalide.
1638
     */
1639
    public function withUploadedFiles(array $uploadedFiles): self
1640
    {
1641
        $this->validateUploadedFiles($uploadedFiles, '');
1642
        $new                = clone $this;
1643
        $new->uploadedFiles = $uploadedFiles;
1644
1645
        return $new;
1646
    }
1647
1648
    /**
1649
     * Validez de manière récursive les données de fichier téléchargées.
1650
     *
1651
     * @param array  $uploadedFiles Le nouveau tableau de fichiers à valider.
1652
     * @param string $path          Le chemin jusqu'ici.
1653
     *
1654
     * @throws InvalidArgumentException Si des éléments feuilles ne sont pas des fichiers valides.
1655
     */
1656
    protected function validateUploadedFiles(array $uploadedFiles, string $path): void
1657
    {
1658
        foreach ($uploadedFiles as $key => $file) {
1659
            if (is_array($file)) {
1660
                $this->validateUploadedFiles($file, $key . '.');
1661
1662
                continue;
1663
            }
1664
1665
            if (! $file instanceof UploadedFileInterface) {
1666
                throw new InvalidArgumentException("Invalid file at '{$path}{$key}'");
1667
            }
1668
        }
1669
    }
1670
1671
    /**
1672
     * Obtient le corps du message.
1673
     */
1674
    public function getBody(): StreamInterface
1675
    {
1676
        return $this->stream;
1677
    }
1678
1679
    /**
1680
     * Renvoie une instance avec le corps de message spécifié.
1681
     */
1682
    public function withBody(StreamInterface $body): self
1683
    {
1684
        $new         = clone $this;
1685
        $new->stream = $body;
1686
1687
        return $new;
1688
    }
1689
1690
    /**
1691
     * Récupère l'instance d'URI.
1692
     */
1693
    public function getUri(): UriInterface
1694
    {
1695
        return $this->uri;
1696
    }
1697
1698
    /**
1699
     * Renvoie une instance avec l'uri spécifié
1700
     *
1701
     * *Attention* Remplacer l'Uri ne mettra pas à jour la `base`, `webroot`,
1702
     * et les attributs `url`.
1703
     *
1704
     * @param bool $preserveHost Indique si l'hôte doit être conservé.
1705
     */
1706
    public function withUri(UriInterface $uri, $preserveHost = false): self
1707
    {
1708
        $new      = clone $this;
1709
        $new->uri = $uri;
1710
1711
        if ($preserveHost && $this->hasHeader('Host')) {
1712
            return $new;
1713
        }
1714
1715
        $host = $uri->getHost();
1716
        if (! $host) {
1717
            return $new;
1718
        }
1719
        $port = $uri->getPort();
1720
        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...
1721
            $host .= ':' . $port;
1722
        }
1723
        $new->_environment['HTTP_HOST'] = $host;
1724
1725
        return $new;
1726
    }
1727
1728
    /**
1729
     * Créez une nouvelle instance avec une cible de demande spécifique.
1730
     *
1731
     * Vous pouvez utiliser cette méthode pour écraser la cible de la demande qui est
1732
     * déduit de l'Uri de la requête. Cela vous permet également de modifier la demande
1733
     * la forme de la cible en une forme absolue, une forme d'autorité ou une forme d'astérisque
1734
     *
1735
     * @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)
1736
     *
1737
     * @param string $requestTarget La cible de la requête.
1738
     *
1739
     * @psalm-suppress MoreSpecificImplementedParamType
1740
     */
1741
    public function withRequestTarget($requestTarget): self
1742
    {
1743
        $new                = clone $this;
1744
        $new->requestTarget = $requestTarget;
1745
1746
        return $new;
1747
    }
1748
1749
    /**
1750
     * Récupère la cible de la requête.
1751
     *
1752
     * Récupère la cible de la demande du message soit telle qu'elle a été demandée,
1753
     * ou comme défini avec `withRequestTarget()`. Par défaut, cela renverra le
1754
     * chemin relatif de l'application sans répertoire de base et la chaîne de requête
1755
     * défini dans l'environnement SERVER.
1756
     */
1757
    public function getRequestTarget(): string
1758
    {
1759
        if ($this->requestTarget !== null) {
1760
            return $this->requestTarget;
1761
        }
1762
1763
        $target = $this->uri->getPath();
1764
        if ($this->uri->getQuery()) {
1765
            $target .= '?' . $this->uri->getQuery();
1766
        }
1767
1768
        if (empty($target)) {
1769
            $target = '/';
1770
        }
1771
1772
        return $target;
1773
    }
1774
1775
    /**
1776
     * Récupère le chemin de la requête en cours.
1777
     */
1778
    public function getPath(): string
1779
    {
1780
        if ($this->requestTarget === null) {
1781
            return $this->uri->getPath();
1782
        }
1783
1784
        [$path] = explode('?', $this->requestTarget);
1785
1786
        return $path;
1787
    }
1788
1789
    /**
1790
     * Fournit un moyen pratique de travailler avec la classe Negotiate
1791
     * pour la négociation de contenu.
1792
     */
1793
    public function negotiate(string $type, array $supported, bool $strictMatch = false): string
1794
    {
1795
        if (null === $this->negotiator) {
1796
            $this->negotiator = Services::negotiator($this, true);
1797
        }
1798
1799
        switch (strtolower($type)) {
1800
            case 'media':
1801
                return $this->negotiator->media($supported, $strictMatch);
1802
1803
            case 'charset':
1804
                return $this->negotiator->charset($supported);
1805
1806
            case 'encoding':
1807
                return $this->negotiator->encoding($supported);
1808
1809
            case 'language':
1810
                return $this->negotiator->language($supported);
1811
        }
1812
1813
        throw new HttpException($type . ' is not a valid negotiation type. Must be one of: media, charset, encoding, language.');
1814
    }
1815
1816
    /**
1817
     * Définit la chaîne locale pour cette requête.
1818
     */
1819
    public function withLocale(string $locale): self
1820
    {
1821
        $validLocales = config('app.supported_locales');
1822
        // S'il ne s'agit pas d'un paramètre régional valide, définissez-le
1823
        // aux paramètres régionaux par défaut du site.
1824
        if (! in_array($locale, $validLocales, true)) {
1825
            $locale = config('app.language');
1826
        }
1827
1828
        Services::language()->setLocale($locale);
1829
1830
        return $this->withAttribute('locale', $locale);
1831
    }
1832
1833
    /**
1834
     * Obtient les paramètres régionaux actuels, avec un retour à la valeur par défaut
1835
     * locale si aucune n'est définie.
1836
     */
1837
    public function getLocale(): string
1838
    {
1839
        $locale = $this->getAttribute('locale');
1840
        if (empty($locale)) {
1841
            $locale = $this->getAttribute('lang');
1842
        }
1843
1844
        return $locale ?? Services::language()->getLocale();
1845
    }
1846
1847
    /**
1848
     * Read data from `php://input`. Useful when interacting with XML or JSON
1849
     * request body content.
1850
     *
1851
     * Getting input with a decoding function:
1852
     *
1853
     * ```
1854
     * $this->request->input('json_decode');
1855
     * ```
1856
     *
1857
     * Getting input using a decoding function, and additional params:
1858
     *
1859
     * ```
1860
     * $this->request->input('Xml::build', ['return' => 'DOMDocument']);
1861
     * ```
1862
     *
1863
     * Any additional parameters are applied to the callback in the order they are given.
1864
     *
1865
     * @param string|null $callback A decoding callback that will convert the string data to another
1866
     *                              representation. Leave empty to access the raw input data. You can also
1867
     *                              supply additional parameters for the decoding callback using var args, see above.
1868
     * @param array       ...$args  The additional arguments
1869
     *
1870
     * @return string The decoded/processed request data.
1871
     */
1872
    public function input($callback = null, ...$args): string
1873
    {
1874
        $this->stream->rewind();
1875
        $input = $this->stream->getContents();
1876
        if ($callback) {
1877
            array_unshift($args, $input);
1878
1879
            return $callback(...$args);
1880
        }
1881
1882
        return $input;
1883
    }
1884
1885
    /**
1886
     * Sets the REQUEST_METHOD environment variable based on the simulated _method
1887
     * HTTP override value. The 'ORIGINAL_REQUEST_METHOD' is also preserved, if you
1888
     * want the read the non-simulated HTTP method the client used.
1889
     *
1890
     * @param array $data Array of post data.
1891
     *
1892
     * @return array
1893
     */
1894
    protected function _processPost(array $data)
1895
    {
1896
        $method   = $this->getEnv('REQUEST_METHOD');
1897
        $override = false;
1898
1899
        if ($_POST) {
1900
            $data = $_POST;
1901
        } elseif (
1902
            in_array($method, ['PUT', 'DELETE', 'PATCH'], true)
1903
            && strpos($this->contentType() ?? '', 'application/x-www-form-urlencoded') === 0
1904
        ) {
1905
            $data = $this->input();
1906
            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

1906
            parse_str($data, /** @scrutinizer ignore-type */ $data);
Loading history...
1907
        }
1908
        if (ini_get('magic_quotes_gpc') === '1') {
1909
            $data = Helpers::stripslashesDeep((array) $this->data);
1910
        }
1911
1912
        if ($this->hasHeader('X-Http-Method-Override')) {
1913
            $data['_method'] = $this->getHeaderLine('X-Http-Method-Override');
1914
            $override        = true;
1915
        }
1916
        $this->_environment['ORIGINAL_REQUEST_METHOD'] = $method;
1917
1918
        if (isset($data['_method'])) {
1919
            $this->_environment['REQUEST_METHOD'] = $data['_method'];
1920
            unset($data['_method']);
1921
            $override = true;
1922
        }
1923
1924
        if ($override && ! in_array($this->_environment['REQUEST_METHOD'], ['PUT', 'POST', 'DELETE', 'PATCH'], true)) {
1925
            $data = [];
1926
        }
1927
1928
        return $data;
1929
    }
1930
1931
    /**
1932
     * Process the GET parameters and move things into the object.
1933
     *
1934
     * @param array  $query       The array to which the parsed keys/values are being added.
1935
     * @param string $queryString A query string from the URL if provided
1936
     *
1937
     * @return array An array containing the parsed query string as keys/values.
1938
     */
1939
    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

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