Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
88.70% covered (success)
88.70%
157 / 177
82.35% covered (success)
82.35%
42 / 51
CRAP
0.00% covered (danger)
0.00%
0 / 1
Request
88.70% covered (success)
88.70%
157 / 177
82.35% covered (success)
82.35%
42 / 51
136.75
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 create
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 createJson
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 createXml
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 createUrlEncoded
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 createMultipart
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setMethod
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 getMethod
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 withMethod
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 hasMethod
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 clearMethod
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 __clone
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 addHeader
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
9
 setNoContentLength
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 isNoContentLength
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setRawData
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 useRawData
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setQuery
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 addQuery
87.50% covered (success)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
3.02
 getQuery
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasQuery
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 removeQuery
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 removeAllQuery
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 setData
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 addData
83.33% covered (success)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
3.04
 getData
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasData
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 removeData
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 removeAllData
83.33% covered (success)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
3.04
 getUriAsString
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
10
 setRequestType
68.75% covered (warning)
68.75%
11 / 16
0.00% covered (danger)
0.00%
0 / 1
11.47
 getRequestType
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasRequestType
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 removeRequestType
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 resetDataIfAlreadyPrepared
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
3
 createAsJson
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 isJson
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 createAsXml
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 isXml
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 createAsUrlEncoded
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 isUrlEncoded
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 createAsMultipart
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 isMultipart
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 createAsCustomType
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 isCustomType
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
30
 isValidMethod
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasDataContent
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 getDataContent
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getDataContentLength
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 prepareData
75.00% covered (success)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 __call
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2declare(strict_types=1);
3/**
4 * Pop PHP Framework (https://www.popphp.org/)
5 *
6 * @link       https://github.com/popphp/popphp-framework
7 * @author     Nick Sagona, III <nick@popphp.org>
8 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
9 * @license    https://www.popphp.org/license     New BSD License
10 */
11
12/**
13 * @namespace
14 */
15namespace Pop\Http\Client;
16
17use Pop\Http\Uri;
18use Pop\Http\AbstractRequest;
19use Pop\Mime\Part\Header;
20
21/**
22 * HTTP client request class
23 *
24 * @category   Pop
25 * @package    Pop\Http
26 * @author     Nick Sagona, III <nick@popphp.org>
27 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
28 * @license    https://www.popphp.org/license     New BSD License
29 * @version    6.0.0
30 */
31class Request extends AbstractRequest
32{
33
34    /**
35     * Request type constants
36     * @var string
37     */
38    const URLENCODED = 'application/x-www-form-urlencoded';
39    const JSON       = 'application/json';
40    const XML        = 'application/xml';
41    const MULTIPART  = 'multipart/form-data';
42
43    /**
44     * Request method
45     * @var ?string
46     */
47    protected ?string $method = null;
48
49    /**
50     * Request type
51     * @var ?string
52     */
53    protected ?string $requestType = null;
54
55    /**
56     * No Content-Length header flag
57     * @var bool
58     */
59    protected bool $noContentLength = false;
60
61    /**
62     * Raw data flag
63     * @var bool
64     */
65    protected bool $rawData = false;
66
67    /**
68     * Client request query data object
69     *
70     * Can only be a URL-encoded query string on the URI
71     *
72     * @var ?Data
73     */
74    protected ?Data $query = null;
75
76    /**
77     * Client request data object
78     *
79     * Can be any type of supported request data:
80     *     - URL-encoded query string on the URI (GET method)
81     *     - URL-encoded body (any method other than GET)
82     *     - JSON-encoded body
83     *     - XML-encoded body
84     *     - Multipart/form body
85     *
86     * @var ?Data
87     */
88    protected ?Data $data = null;
89
90    /**
91     * Constructor
92     *
93     * Instantiate the request data object
94     *
95     * @param  Uri|string|null $uri
96     * @param  ?string         $method
97     * @param  mixed           $data
98     * @param  ?string         $type
99     * @param  bool            $strict
100     * @throws Exception|\Pop\Http\Exception
101     */
102    public function __construct(Uri|string|null $uri = null, ?string $method = 'GET', mixed $data = null, ?string $type = null, bool $strict = false)
103    {
104        parent::__construct($uri);
105
106        if ($method !== null) {
107            $this->setMethod($method, $strict);
108        }
109        if ($data !== null) {
110            $this->setData($data);
111        }
112        if ($type !== null) {
113            $this->setRequestType($type);
114        }
115    }
116
117    /**
118     * Factory method to create a Request object
119     *
120     * @param  Uri|string|null $uri
121     * @param  string          $method
122     * @param  mixed           $data
123     * @param  ?string         $type
124     * @param  bool            $strict
125     * @throws Exception|\Pop\Http\Exception
126     * @return Request
127     */
128    public static function create(Uri|string|null $uri = null, string $method = 'GET', mixed $data = null, ?string $type = null, bool $strict = false): Request
129    {
130        return new self($uri, $method, $data, $type, $strict);
131    }
132
133    /**
134     * Factory method to create a JSON Request object
135     *
136     * @param  Uri|string|null $uri
137     * @param  string          $method
138     * @param  mixed           $data
139     * @param  bool            $strict
140     * @throws Exception|\Pop\Http\Exception
141     * @return Request
142     */
143    public static function createJson(Uri|string|null $uri = null, string $method = 'POST', mixed $data = null, bool $strict = false): Request
144    {
145        return new self($uri, $method, $data, Request::JSON, $strict);
146    }
147
148    /**
149     * Factory method to create an XML Request object
150     *
151     * @param  Uri|string|null $uri
152     * @param  string          $method
153     * @param  mixed           $data
154     * @param  bool            $strict
155     * @throws Exception|\Pop\Http\Exception
156     * @return Request
157     */
158    public static function createXml(Uri|string|null $uri = null, string $method = 'POST', mixed $data = null, bool $strict = false): Request
159    {
160        return new self($uri, $method, $data, Request::XML, $strict);
161    }
162
163    /**
164     * Factory method to create a URL-encoded Request object
165     *
166     * @param  Uri|string|null $uri
167     * @param  string          $method
168     * @param  mixed           $data
169     * @param  bool            $strict
170     * @throws Exception|\Pop\Http\Exception
171     * @return Request
172     */
173    public static function createUrlEncoded(Uri|string|null $uri = null, string $method = 'GET', mixed $data = null, bool $strict = false): Request
174    {
175        return new self($uri, $method, $data, Request::URLENCODED, $strict);
176    }
177
178    /**
179     * Factory method to create a multipart Request object
180     *
181     * @param  Uri|string|null $uri
182     * @param  string          $method
183     * @param  mixed           $data
184     * @param  bool            $strict
185     * @throws Exception|\Pop\Http\Exception
186     * @return Request
187     */
188    public static function createMultipart(Uri|string|null $uri = null, string $method = 'POST', mixed $data = null, bool $strict = false): Request
189    {
190        return new self($uri, $method, $data, Request::MULTIPART, $strict);
191    }
192
193    /**
194     * Set method
195     *
196     * @param  string $method
197     * @param  bool   $strict
198     * @throws Exception
199     * @return Request
200     */
201    public function setMethod(string $method, bool $strict = false): Request
202    {
203        $method = strtoupper($method);
204
205        if ($strict) {
206            if (!$this->isValidMethod($method)) {
207                throw new Exception('Error: That request method is not valid.');
208            }
209        }
210
211        $this->method = $method;
212        return $this;
213    }
214
215    /**
216     * Get method
217     *
218     * @return string
219     */
220    public function getMethod(): string
221    {
222        return $this->method ?? 'GET';
223    }
224
225    /**
226     * Return an instance with the specified method, per PSR-7
227     *
228     * @param  string $method
229     * @return static
230     */
231    public function withMethod(string $method): static
232    {
233        $clone = clone $this;
234        $clone->setMethod($method);
235        return $clone;
236    }
237
238    /**
239     * Has method
240     *
241     * @return bool
242     */
243    public function hasMethod(): bool
244    {
245        return ($this->method !== null);
246    }
247
248    /**
249     * Clear method
250     *
251     * @return Request
252     */
253    public function clearMethod(): Request
254    {
255        $this->method = null;
256        return $this;
257    }
258
259    /**
260     * Deep-clone the query and data objects so a with*() clone never shares
261     * mutable Data state (or its back-reference) with the instance it was
262     * cloned from
263     *
264     * @return void
265     */
266    public function __clone(): void
267    {
268        parent::__clone();
269
270        if ($this->query !== null) {
271            $this->query = clone $this->query;
272            $this->query->setRequest($this);
273        }
274
275        if ($this->data !== null) {
276            $this->data = clone $this->data;
277            $this->data->setRequest($this);
278        }
279    }
280
281    /**
282     * Add a header
283     *
284     * @param  Header|string|int $header
285     * @param  ?string           $value
286     * @return Request
287     */
288    public function addHeader(Header|string|int $header, ?string $value = null): Request
289    {
290        $contentType = null;
291        if (is_numeric($header) && ($value !== null)) {
292            $header = Header::parse($value);
293            $value  = null;
294        }
295        if (is_string($header) && ($header == 'Content-Type')) {
296            $contentType = $value;
297        } else if (($header instanceof Header) && ($header->getName() == 'Content-Type')) {
298            $contentType = $header->getValue()->getValue();
299        }
300
301        if (($contentType !== null) && ($this->requestType === null)) {
302            $this->requestType = $contentType;
303        }
304
305        parent::addHeader($header, $value);
306        return $this;
307    }
308
309    /**
310     * Set no Content-Length header flag
311     *
312     * @param  bool $noContentLength
313     * @return Request
314     */
315    public function setNoContentLength(bool $noContentLength): Request
316    {
317        $this->noContentLength = $noContentLength;
318        return $this;
319    }
320
321    /**
322     * Is no Content-Length header flag
323     *
324     * @return bool
325     */
326    public function isNoContentLength(): bool
327    {
328        return $this->noContentLength;
329    }
330
331    /**
332     * Set raw data flag
333     *
334     * @param  bool $rawData
335     * @return Request
336     */
337    public function setRawData(bool $rawData): Request
338    {
339        $this->rawData = $rawData;
340        return $this;
341    }
342
343    /**
344     * Use raw data
345     *
346     * @return bool
347     */
348    public function useRawData(): bool
349    {
350        return $this->rawData;
351    }
352
353    /**
354     * Set query data
355     *
356     * @param  mixed $query
357     * @param  mixed $filters
358     * @return Request
359     */
360    public function setQuery(mixed $query, mixed $filters = null): Request
361    {
362        $this->setRequestType(Request::URLENCODED);
363        $this->query = ($query instanceof Data) ? $query : new Data($query, $filters, $this);
364        return $this;
365    }
366
367    /**
368     * Add query data
369     *
370     * @param  mixed $name
371     * @param  mixed $value
372     * @return Request
373     */
374    public function addQuery(mixed $name, mixed $value): Request
375    {
376        $this->setRequestType(Request::URLENCODED);
377        if ($this->query === null) {
378            $this->setRequestType(Request::URLENCODED);
379            $this->query = new Data([], null, $this);
380        } else if (!$this->query->hasRequest()) {
381            $this->query->setRequest($this);
382        }
383        $this->query->addData($name, $value);
384
385        return $this;
386    }
387
388    /**
389     * Get query data
390     *
391     * @return ?Data
392     */
393    public function getQuery(): ?Data
394    {
395        return $this->query;
396    }
397
398    /**
399     * Has query data
400     *
401     * @return bool
402     */
403    public function hasQuery(): bool
404    {
405        return !empty($this->query);
406    }
407
408    /**
409     * Remove query data
410     *
411     * @param  string $key
412     * @return Request
413     */
414    public function removeQuery(string $key): Request
415    {
416        if ($this->query->hasData($key)) {
417            $this->query->removeData($key);
418        }
419        return $this;
420    }
421
422    /**
423     * Remove all query data
424     *
425     * @return Request
426     */
427    public function removeAllQuery(): Request
428    {
429        $this->query = null;
430
431        if ($this->hasHeader('Content-Type')) {
432            $this->removeHeader('Content-Type');
433        }
434        return $this;
435    }
436
437    /**
438     * Set data
439     *
440     * @param  mixed $data
441     * @param  mixed $filters
442     * @return Request
443     */
444    public function setData(mixed $data, mixed $filters = null): Request
445    {
446        if ($data instanceof Data) {
447            if (!$data->hasRequest()) {
448                $data->setRequest($this);
449            }
450        } else {
451            $data = new Data($data, $filters, $this);
452        }
453
454        $this->data = $data;
455
456        return $this;
457    }
458
459    /**
460     * Add data
461     *
462     * @param  mixed $name
463     * @param  mixed $value
464     * @return Request
465     */
466    public function addData(mixed $name, mixed $value): Request
467    {
468        if ($this->data === null) {
469            $this->data = new Data([], null, $this);
470        } else if (!$this->data->hasRequest()) {
471            $this->data->setRequest($this);
472        }
473        $this->data->addData($name, $value);
474
475        return $this;
476    }
477
478    /**
479     * Get data
480     *
481     * @return ?Data
482     */
483    public function getData(): ?Data
484    {
485        return $this->data;
486    }
487
488    /**
489     * Has data
490     *
491     * @return bool
492     */
493    public function hasData(): bool
494    {
495        return !empty($this->data);
496    }
497
498    /**
499     * Remove data
500     *
501     * @param  string $key
502     * @return Request
503     */
504    public function removeData(string $key): Request
505    {
506        if ($this->data->hasData($key)) {
507            $this->data->removeData($key);
508        }
509        return $this;
510    }
511
512    /**
513     * Remove all data
514     *
515     * @return Request
516     */
517    public function removeAllData(): Request
518    {
519        $this->data = null;
520
521        if ($this->hasHeader('Content-Type')) {
522            $this->removeHeader('Content-Type');
523        }
524        if ($this->hasHeader('Content-Length')) {
525            $this->removeHeader('Content-Length');
526        }
527        return $this;
528    }
529
530    /**
531     * Get full URI as string
532     *
533     * @param  bool $query
534     * @return string
535     */
536    public function getUriAsString(bool $query = true): string
537    {
538        $uri = parent::getUriAsString();
539
540        if ($query) {
541            $queryString = null;
542
543            // If request has generic query data
544            if ($this->hasQuery()) {
545                $queryString = $this->query->prepare()->getDataContent();
546            // Else, if request has explicit data configured to be a query string over GET
547            } else if (($this->method == 'GET') && ($this->hasData()) &&
548                (($this->requestType === null) || ($this->requestType == self::URLENCODED))) {
549                $queryString = $this->data->prepare()->getDataContent();
550            }
551
552            if (!empty($queryString) && !str_contains($uri, $queryString)) {
553                $uri .= ((str_contains($uri, '?')) ? '&' : '?') . $queryString;
554            }
555        }
556
557        return $uri;
558    }
559
560    /**
561     * Set request type
562     *
563     * @param  ?string $type
564     * @param  bool    $handleHeader
565     * @return Request
566     */
567    public function setRequestType(?string $type = null, bool $handleHeader = true): Request
568    {
569        switch ($type) {
570            case self::JSON:
571                $this->createAsJson($type, $handleHeader);
572                break;
573            case self::XML:
574                $this->createAsXml($type, $handleHeader);
575                break;
576            case self::URLENCODED:
577                $this->createAsUrlEncoded($type, $handleHeader);
578                break;
579            case self::MULTIPART:
580                $this->createAsMultipart($type);
581                break;
582            default:
583                // Custom content-types
584                if ($type !== null) {
585                    if (strrpos($type, 'json') !== false) {
586                        $this->createAsJson($type);
587                    } else if (strrpos($type, 'xml') !== false) {
588                        $this->createAsXml($type);
589                    } else {
590                        $this->createAsCustomType($type);
591                    }
592                } else {
593                    $this->removeRequestType($handleHeader);
594                }
595
596        }
597
598        return $this;
599    }
600
601    /**
602     * Get request type
603     *
604     * @return ?string
605     */
606    public function getRequestType(): ?string
607    {
608        return $this->requestType;
609    }
610
611    /**
612     * Has request type
613     *
614     * @return bool
615     */
616    public function hasRequestType(): bool
617    {
618        return ($this->requestType !== null);
619    }
620
621    /**
622     * Remove request type
623     *
624     * @param  bool $removeHeader
625     * @return Request
626     */
627    public function removeRequestType(bool $removeHeader = false): Request
628    {
629        $this->requestType = null;
630        if (($removeHeader) && ($this->hasHeader('Content-Type'))) {
631            $this->removeHeader('Content-Type');
632        }
633        return $this;
634    }
635
636    /**
637     * Reset the data object's prepared state if it was already prepared
638     *
639     * Guards against Data::setData() having eagerly prepared its content against
640     * whatever request type was set before this createAsXxx() call - without this,
641     * the stale Content-Type/dataContent baked in at that earlier setData() time
642     * would survive, since the handlers only re-prepare when isPrepared() is false.
643     *
644     * @return void
645     */
646    protected function resetDataIfAlreadyPrepared(): void
647    {
648        if ($this->hasData() && $this->data->isPrepared()) {
649            $this->data->reset();
650        }
651    }
652
653    /**
654     * Create request as JSON
655     *
656     * @param  string $type
657     * @param  bool   $addHeader
658     * @return Request
659     */
660    public function createAsJson(string $type = self::JSON, bool $addHeader = true): Request
661    {
662        $this->resetDataIfAlreadyPrepared();
663
664        $this->requestType = $type;
665
666        if ($this->hasHeader('Content-Type')) {
667            $this->removeHeader('Content-Type');
668        }
669        if ($addHeader) {
670            $this->addHeader('Content-Type', $this->requestType);
671        }
672
673        return $this;
674    }
675
676    /**
677     * Check if request is JSON
678     *
679     * @return bool
680     */
681    public function isJson(): bool
682    {
683        return ($this->requestType !== null) && str_contains(strtolower($this->requestType), 'json');
684    }
685
686    /**
687     * Create request as XML
688     *
689     * @param  string $type
690     * @param  bool   $addHeader
691     * @return Request
692     */
693    public function createAsXml(string $type = self::XML, bool $addHeader = true): Request
694    {
695        $this->resetDataIfAlreadyPrepared();
696
697        $this->requestType = $type;
698
699        if ($this->hasHeader('Content-Type')) {
700            $this->removeHeader('Content-Type');
701        }
702
703        if ($addHeader) {
704            $this->addHeader('Content-Type', $this->requestType);
705        }
706
707        return $this;
708    }
709
710    /**
711     * Check if request is XML
712     *
713     * @return bool
714     */
715    public function isXml(): bool
716    {
717        return ($this->requestType !== null) && str_contains(strtolower($this->requestType), 'xml');
718    }
719
720    /**
721     * Create request as a URL-encoded form
722     *
723     * @param  string $type
724     * @param  bool   $addHeader
725     * @return Request
726     */
727    public function createAsUrlEncoded(string $type = self::URLENCODED, bool $addHeader = true): Request
728    {
729        $this->resetDataIfAlreadyPrepared();
730
731        $this->requestType = $type;
732
733        if ($this->hasHeader('Content-Type')) {
734            $this->removeHeader('Content-Type');
735        }
736
737        if ($addHeader) {
738            $this->addHeader('Content-Type', $this->requestType);
739        }
740
741        return $this;
742    }
743
744    /**
745     * Check if request is a URL-encoded form
746     *
747     * @return bool
748     */
749    public function isUrlEncoded(): bool
750    {
751        return ($this->requestType == self::URLENCODED);
752    }
753
754    /**
755     * Create request as a multipart form
756     *
757     * @param  string $type
758     * @return Request
759     */
760    public function createAsMultipart(string $type = self::MULTIPART): Request
761    {
762        $this->resetDataIfAlreadyPrepared();
763
764        $this->requestType = $type;
765        return $this;
766    }
767
768    /**
769     * Check if request is a multipart form
770     *
771     * @return bool
772     */
773    public function isMultipart(): bool
774    {
775        return ($this->requestType !== null) && str_contains(strtolower($this->requestType), self::MULTIPART);
776    }
777
778    /**
779     * Create request as custom content type
780     *
781     * @param  string $type
782     * @param  bool   $addHeader
783     * @return Request
784     */
785    public function createAsCustomType(string $type, bool $addHeader = true): Request
786    {
787        $this->requestType = $type;
788
789        if ($addHeader) {
790            $this->addHeader('Content-Type', $this->requestType);
791        }
792        return $this;
793    }
794
795    /**
796     * Check if request is a custom content type
797     *
798     * @return bool
799     */
800    public function isCustomType(): bool
801    {
802        return (!empty($this->requestType) && (strrpos($this->requestType, 'json') !== false) &&
803            (strrpos($this->requestType, 'xml') !== false) && ($this->requestType !== self::URLENCODED) &&
804            ($this->requestType !== self::MULTIPART));
805    }
806
807    /**
808     * Is valid method
809     *
810     * @param  string $method
811     * @return bool
812     */
813    public function isValidMethod(string $method): bool
814    {
815        return in_array(strtoupper($method), ['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE', 'CONNECT', 'OPTIONS', 'TRACE']);
816    }
817
818    /**
819     * Has data content
820     *
821     * @return bool
822     */
823    public function hasDataContent(): bool
824    {
825        return (($this->data !== null) && ($this->data->hasDataContent()));
826    }
827
828    /**
829     * Get the data content
830     *
831     * @return ?string
832     */
833    public function getDataContent(): ?string
834    {
835        return $this->data?->getDataContent();
836    }
837
838    /**
839     * Get the data content length
840     *
841     * @param  bool $mb
842     * @return int|null
843     */
844    public function getDataContentLength(bool $mb = false): int|null
845    {
846        return $this->data?->getDataContentLength($mb);
847    }
848
849    /**
850     * Prepare request data
851     *
852     * @return Request
853     */
854    public function prepareData(): Request
855    {
856        if (!$this->data->hasRequest()) {
857            $this->data->setRequest($this);
858        }
859
860        $this->data->prepare();
861
862        return $this;
863    }
864
865    /**
866     * Magic method to check the is[Method](), i.e. $request->isPost();
867     *
868     * @param  string $methodName
869     * @param  ?array $arguments
870     * @throws Exception
871     * @return bool
872     */
873    public function __call(string $methodName, ?array $arguments = null): bool
874    {
875        if (str_starts_with($methodName, 'is')) {
876            $method = strtoupper(substr($methodName, 2));
877            if ($this->isValidMethod($method)) {
878                return ($this->method == $method);
879            } else {
880                throw new Exception('Error: That request method is not valid.');
881            }
882        } else {
883            throw new Exception('Error: That method/function is not valid.');
884        }
885    }
886
887}