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