Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
95.21% covered (success)
95.21%
139 / 146
92.50% covered (success)
92.50%
37 / 40
CRAP
0.00% covered (danger)
0.00%
0 / 1
Request
95.21% covered (success)
95.21%
139 / 146
92.50% covered (success)
92.50%
37 / 40
95
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
 addHeader
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
11
 setQuery
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 addQuery
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 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
75.00% covered (success)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 setData
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 addData
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 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
75.00% covered (success)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 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
13.05
 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%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 createAsJson
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 isJson
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 createAsXml
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 isXml
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 createAsUrlEncoded
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 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%
2 / 2
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
1
 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
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
7
 __call
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2/**
3 * Pop PHP Framework (http://www.popphp.org/)
4 *
5 * @link       https://github.com/popphp/popphp-framework
6 * @author     Nick Sagona, III <dev@nolainteractive.com>
7 * @copyright  Copyright (c) 2009-2024 NOLA Interactive, LLC. (http://www.nolainteractive.com)
8 * @license    http://www.popphp.org/license     New BSD License
9 */
10
11/**
12 * @namespace
13 */
14namespace Pop\Http\Client;
15
16use Pop\Http\Uri;
17use Pop\Http\AbstractRequest;
18use Pop\Mime\Message;
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 <dev@nolainteractive.com>
27 * @copyright  Copyright (c) 2009-2024 NOLA Interactive, LLC. (http://www.nolainteractive.com)
28 * @license    http://www.popphp.org/license     New BSD License
29 * @version    5.2.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     * Client request query data object
57     *
58     * Can only be a URL-encoded query string on the URI
59     *
60     * @var ?Data
61     */
62    protected ?Data $query = null;
63
64    /**
65     * Client request data object
66     *
67     * Can be any type of supported request data:
68     *     - URL-encoded query string on the URI (GET method)
69     *     - URL-encoded body (any method other than GET)
70     *     - JSON-encoded body
71     *     - XML-encoded body
72     *     - Multipart/form body
73     *
74     * @var ?Data
75     */
76    protected ?Data $data = null;
77
78    /**
79     * Constructor
80     *
81     * Instantiate the request data object
82     *
83     * @param  Uri|string|null $uri
84     * @param  string          $method
85     * @param  mixed           $data
86     * @param  ?string         $type
87     * @throws Exception
88     */
89    public function __construct(Uri|string|null $uri = null, string $method = 'GET', mixed $data = null, ?string $type = null)
90    {
91        parent::__construct($uri);
92
93        if ($method !== null) {
94            $this->setMethod($method);
95        }
96        if ($data !== null) {
97            $this->setData($data);
98        }
99        if ($type !== null) {
100            $this->setRequestType($type);
101        }
102    }
103
104    /**
105     * Factory method to create a Request object
106     *
107     * @param  Uri|string|null $uri
108     * @param  string          $method
109     * @param  mixed           $data
110     * @param  ?string         $type
111     * @throws Exception
112     * @return Request
113     */
114    public static function create(Uri|string|null $uri = null, string $method = 'GET', mixed $data = null, ?string $type = null): Request
115    {
116        return new self($uri, $method, $data, $type);
117    }
118
119    /**
120     * Factory method to create a JSON Request object
121     *
122     * @param  Uri|string|null $uri
123     * @param  string          $method
124     * @param  mixed           $data
125     * @throws Exception
126     * @return Request
127     */
128    public static function createJson(Uri|string|null $uri = null, string $method = 'POST', mixed $data = null): Request
129    {
130        return new self($uri, $method, $data, Request::JSON);
131    }
132
133    /**
134     * Factory method to create an XML Request object
135     *
136     * @param  Uri|string|null $uri
137     * @param  string          $method
138     * @param  mixed           $data
139     * @throws Exception
140     * @return Request
141     */
142    public static function createXml(Uri|string|null $uri = null, string $method = 'POST', mixed $data = null): Request
143    {
144        return new self($uri, $method, $data, Request::XML);
145    }
146
147    /**
148     * Factory method to create a URL-encoded Request object
149     *
150     * @param  Uri|string|null $uri
151     * @param  string          $method
152     * @param  mixed           $data
153     * @throws Exception
154     * @return Request
155     */
156    public static function createUrlEncoded(Uri|string|null $uri = null, string $method = 'GET', mixed $data = null): Request
157    {
158        return new self($uri, $method, $data, Request::URLENCODED);
159    }
160
161    /**
162     * Factory method to create a multipart Request object
163     *
164     * @param  Uri|string|null $uri
165     * @param  string          $method
166     * @param  mixed           $data
167     * @throws Exception
168     * @return Request
169     */
170    public static function createMultipart(Uri|string|null $uri = null, string $method = 'POST', mixed $data = null): Request
171    {
172        return new self($uri, $method, $data, Request::MULTIPART);
173    }
174
175    /**
176     * Set method
177     *
178     * @param  string $method
179     * @param  bool   $strict
180     * @throws Exception
181     * @return Request
182     */
183    public function setMethod(string $method, bool $strict = true): Request
184    {
185        $method = strtoupper($method);
186
187        if ($strict) {
188            if (!$this->isValidMethod($method)) {
189                throw new Exception('Error: That request method is not valid.');
190            }
191        }
192
193        $this->method = $method;
194        return $this;
195    }
196
197    /**
198     * Get method
199     *
200     * @return string
201     */
202    public function getMethod(): string
203    {
204        return $this->method;
205    }
206
207    /**
208     * Add a header
209     *
210     * @param  Header|string|int $header
211     * @param  ?string           $value
212     * @return Request
213     */
214    public function addHeader(Header|string|int $header, ?string $value = null): Request
215    {
216        $contentType = null;
217        if (is_numeric($header) && ($value !== null)) {
218            $header = Header::parse($value);
219            $value  = null;
220        }
221        if (is_string($header) && ($header == 'Content-Type')) {
222            $contentType = $value;
223        } else if (($header instanceof Header) && ($header->getName() == 'Content-Type')) {
224            $contentType = $header->getValue()->getValue();
225        }
226
227        switch ($contentType) {
228            case Request::JSON:
229                $this->requestType = Request::JSON;
230                break;
231            case Request::XML:
232                $this->requestType = Request::XML;
233                break;
234            case Request::URLENCODED:
235                $this->requestType = Request::URLENCODED;
236                break;
237            case Request::MULTIPART:
238                $this->requestType = Request::MULTIPART;
239                break;
240        }
241
242        parent::addHeader($header, $value);
243        return $this;
244    }
245
246    /**
247     * Set query data
248     *
249     * @param  mixed $query
250     * @param  mixed $filters
251     * @return Request
252     */
253    public function setQuery(mixed $query, mixed $filters = null): Request
254    {
255        $this->query = ($query instanceof Data) ? $query : new Data($query, $filters, Request::URLENCODED);
256        return $this;
257    }
258
259    /**
260     * Add query data
261     *
262     * @param  mixed $name
263     * @param  mixed $value
264     * @return Request
265     */
266    public function addQuery(mixed $name, mixed $value): Request
267    {
268        if ($this->data === null) {
269            $this->query = new Data([], null, Request::URLENCODED);
270        }
271        $this->query->addData($name, $value);
272
273        return $this;
274    }
275
276    /**
277     * Get query data
278     *
279     * @return ?Data
280     */
281    public function getQuery(): ?Data
282    {
283        return $this->query;
284    }
285
286    /**
287     * Has query data
288     *
289     * @return bool
290     */
291    public function hasQuery(): bool
292    {
293        return !empty($this->query);
294    }
295
296    /**
297     * Remove query data
298     *
299     * @param  string $key
300     * @return Request
301     */
302    public function removeQuery(string $key): Request
303    {
304        if ($this->query->hasData($key)) {
305            $this->query->removeData($key);
306        }
307        return $this;
308    }
309
310    /**
311     * Remove all query data
312     *
313     * @return Request
314     */
315    public function removeAllQuery(): Request
316    {
317        $this->query = null;
318
319        if ($this->hasHeader('Content-Type')) {
320            $this->removeHeader('Content-Type');
321        }
322        return $this;
323    }
324
325    /**
326     * Set data
327     *
328     * @param  mixed $data
329     * @param  mixed $filters
330     * @return Request
331     */
332    public function setData(mixed $data, mixed $filters = null): Request
333    {
334        $this->data = ($data instanceof Data) ? $data : new Data($data, $filters, $this->requestType);
335        if ($this->data->hasContentTypeHeader()) {
336            $this->addHeader($this->data->getContentTypeHeader());
337        }
338        if ($this->data->hasContentLengthHeader()) {
339            $this->addHeader($this->data->getContentLengthHeader());
340        }
341        return $this;
342    }
343
344    /**
345     * Add data
346     *
347     * @param  mixed $name
348     * @param  mixed $value
349     * @return Request
350     */
351    public function addData(mixed $name, mixed $value): Request
352    {
353        if ($this->data === null) {
354            $this->data = new Data();
355        }
356        $this->data->addData($name, $value);
357
358        return $this;
359    }
360
361    /**
362     * Get data
363     *
364     * @return ?Data
365     */
366    public function getData(): ?Data
367    {
368        return $this->data;
369    }
370
371    /**
372     * Has data
373     *
374     * @return bool
375     */
376    public function hasData(): bool
377    {
378        return !empty($this->data);
379    }
380
381    /**
382     * Remove data
383     *
384     * @param  string $key
385     * @return Request
386     */
387    public function removeData(string $key): Request
388    {
389        if ($this->data->hasData($key)) {
390            $this->data->removeData($key);
391        }
392        return $this;
393    }
394
395    /**
396     * Remove all data
397     *
398     * @return Request
399     */
400    public function removeAllData(): Request
401    {
402        $this->data = null;
403
404        if ($this->hasHeader('Content-Type')) {
405            $this->removeHeader('Content-Type');
406        }
407        return $this;
408    }
409
410    /**
411     * Get full URI as string
412     *
413     * @param  bool $query
414     * @return string
415     */
416    public function getUriAsString(bool $query = true): string
417    {
418        $uri = parent::getUriAsString();
419
420        if ($query) {
421            $queryString = null;
422
423            // If request has generic query data
424            if ($this->hasQuery()) {
425                $queryString = $this->query->prepare()->getDataContent();
426            // Else, if request has explicit data configured to be a query string over GET
427            } else if (($this->method == 'GET') && ($this->hasData()) &&
428                (($this->requestType === null) || ($this->requestType == self::URLENCODED))) {
429                $queryString = $this->data->prepare()->getDataContent();
430            }
431
432            if (!empty($queryString) && !str_contains($uri, $queryString)) {
433                $uri .= ((str_contains($uri, '?')) ? '&' : '?') . $queryString;
434            }
435        }
436
437        return $uri;
438    }
439
440    /**
441     * Set request type
442     *
443     * @param  ?string $type
444     * @return Request
445     */
446    public function setRequestType(string $type = null): Request
447    {
448        switch ($type) {
449            case self::JSON:
450                $this->createAsJson();
451                break;
452            case self::XML:
453                $this->createAsXml();
454                break;
455            case self::URLENCODED:
456                $this->createAsUrlEncoded();
457                break;
458            case self::MULTIPART:
459                $this->createAsMultipart();
460                break;
461            default:
462                $this->requestType = null;
463                if ($this->hasHeader('Content-Type')) {
464                    $this->removeHeader('Content-Type');
465                }
466                if ($this->hasHeader('Content-Length')) {
467                    $this->removeHeader('Content-Length');
468                }
469        }
470
471        if (($this->requestType !== null) && ($this->hasData())) {
472            $this->data->setType($this->requestType);
473        }
474
475        return $this;
476    }
477
478    /**
479     * Get request type
480     *
481     * @return string
482     */
483    public function getRequestType(): string
484    {
485        return $this->requestType;
486    }
487
488    /**
489     * Has request type
490     *
491     * @return bool
492     */
493    public function hasRequestType(): bool
494    {
495        return ($this->requestType !== null);
496    }
497
498    /**
499     * Remove request type
500     *
501     * @return Request
502     */
503    public function removeRequestType(): Request
504    {
505        $this->requestType = null;
506        return $this;
507    }
508
509    /**
510     * Create request as JSON
511     *
512     * @return Request
513     */
514    public function createAsJson(): Request
515    {
516        $this->requestType = self::JSON;
517
518        if ($this->hasHeader('Content-Type')) {
519            $this->removeHeader('Content-Type');
520        }
521        $this->addHeader('Content-Type', $this->requestType);
522
523        return $this;
524    }
525
526    /**
527     * Check if request is JSON
528     *
529     * @return bool
530     */
531    public function isJson(): bool
532    {
533        return ($this->requestType == self::JSON);
534    }
535
536    /**
537     * Create request as XML
538     *
539     * @return Request
540     */
541    public function createAsXml(): Request
542    {
543        $this->requestType = self::XML;
544
545        if ($this->hasHeader('Content-Type')) {
546            $this->removeHeader('Content-Type');
547        }
548        $this->addHeader('Content-Type', $this->requestType);
549
550        return $this;
551    }
552
553    /**
554     * Check if request is XML
555     *
556     * @return bool
557     */
558    public function isXml(): bool
559    {
560        return ($this->requestType == self::XML);
561    }
562
563    /**
564     * Create request as a URL-encoded form
565     *
566     * @return Request
567     */
568    public function createAsUrlEncoded(): Request
569    {
570        $this->requestType = self::URLENCODED;
571
572        if ($this->hasHeader('Content-Type')) {
573            $this->removeHeader('Content-Type');
574        }
575        $this->addHeader('Content-Type', $this->requestType);
576
577        return $this;
578    }
579
580    /**
581     * Check if request is a URL-encoded form
582     *
583     * @return bool
584     */
585    public function isUrlEncoded(): bool
586    {
587        return ($this->requestType == self::URLENCODED);
588    }
589
590    /**
591     * Create request as a multipart form
592     *
593     * @return Request
594     */
595    public function createAsMultipart(): Request
596    {
597        $this->requestType = self::MULTIPART;
598        return $this;
599    }
600
601    /**
602     * Check if request is a multipart form
603     *
604     * @return bool
605     */
606    public function isMultipart(): bool
607    {
608        return ($this->requestType == self::MULTIPART);
609    }
610
611    /**
612     * Is valid method
613     *
614     * @param  string $method
615     * @return bool
616     */
617    public function isValidMethod(string $method): bool
618    {
619        return in_array(strtoupper($method), ['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE', 'CONNECT', 'OPTIONS', 'TRACE']);
620    }
621
622    /**
623     * Has data content
624     *
625     * @return bool
626     */
627    public function hasDataContent(): bool
628    {
629        return (($this->data !== null) && ($this->data->hasDataContent()));
630    }
631
632    /**
633     * Get the data content
634     *
635     * @return ?string
636     */
637    public function getDataContent(): ?string
638    {
639        return $this->data?->getDataContent();
640    }
641
642    /**
643     * Get the data content length
644     *
645     * @param  bool $mb
646     * @return int|null
647     */
648    public function getDataContentLength(bool $mb = false): int|null
649    {
650        return $this->data?->getDataContentLength($mb);
651    }
652
653    /**
654     * Prepare request data
655     *
656     * @return Request
657     */
658    public function prepareData(): Request
659    {
660        if ($this->requestType !== null) {
661            $this->data->setType($this->requestType);
662        }
663
664        $this->data->prepare();
665
666        if ($this->data->hasContentTypeHeader()) {
667            if ($this->hasHeader('Content-Type')) {
668                $this->removeHeader('Content-Type');
669            }
670            $this->addHeader($this->data->getContentTypeHeader());
671        }
672
673        if ($this->method != 'GET') {
674            if ($this->getDataContentLength() !== null) {
675                if ($this->hasHeader('Content-Length')) {
676                    $this->removeHeader('Content-Length');
677                }
678                $this->addHeader('Content-Length', $this->getDataContentLength());
679            }
680        }
681
682        return $this;
683    }
684
685    /**
686     * Magic method to check the is[Method](), i.e. $request->isPost();
687     *
688     * @param  string $methodName
689     * @param  ?array $arguments
690     * @throws Exception
691     * @return bool
692     */
693    public function __call(string $methodName, ?array $arguments = null): bool
694    {
695        if (str_starts_with($methodName, 'is')) {
696            $method = strtoupper(substr($methodName, 2));
697            if ($this->isValidMethod($method)) {
698                return ($this->method == $method);
699            } else {
700                throw new Exception('Error: That request method is not valid.');
701            }
702        } else {
703            throw new Exception('Error: That method/function is not valid.');
704        }
705    }
706
707}