Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
97.14% covered (success)
97.14%
170 / 175
92.31% covered (success)
92.31%
36 / 39
CRAP
0.00% covered (danger)
0.00%
0 / 1
Request
97.14% covered (success)
97.14%
170 / 175
92.31% covered (success)
92.31%
36 / 39
114
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
 addHeaders
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 setData
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 getData
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 hasData
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setQuery
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getQuery
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 hasQuery
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getUriAsString
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
11
 setRequestType
85.71% covered (success)
85.71%
12 / 14
0.00% covered (danger)
0.00%
0 / 1
8.19
 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
 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
1
 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%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 prepareData
96.43% covered (success)
96.43%
27 / 28
0.00% covered (danger)
0.00%
0 / 1
15
 prepareJson
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
11
 prepareXml
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
7
 prepareUrlEncoded
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 prepareMultipart
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 prepareRawData
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 __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.0.0
30 */
31class Request extends AbstractRequest
32{
33
34    /**
35     * Request type constants
36     * @var string
37     */
38    const JSON      = 'application/json';
39    const XML       = 'application/xml';
40    const URLFORM   = 'application/x-www-form-urlencoded';
41    const MULTIPART = 'multipart/form-data';
42
43    /**
44     * Request method
45     * @var ?string
46     */
47    protected ?string $method = null;
48
49    /**
50     * Client request data object
51     *
52     * Can be any type of supported request data:
53     *     - URI-based encoded query string
54     *     - URL-encoded body
55     *     - JSON-encoded body
56     *     - XML-encoded body
57     *     - Multipart/form body
58     *
59     * @var ?Data
60     */
61    protected ?Data $data = null;
62
63    /**
64     * Client request query data
65     *
66     *  Differs from the Data object, as it can only be an encoded query string on the URI.
67     *  It can be used in combination of the main request Data object to force certain data
68     *  to be appended to the URI.
69     *
70     * @var array
71     */
72    protected array $query = [];
73
74    /**
75     * Request type
76     * @var ?string
77     */
78    protected ?string $requestType = null;
79
80    /**
81     * Request data content
82     * @var string|array|null
83     */
84    protected string|array|null $dataContent = null;
85
86    /**
87     * Constructor
88     *
89     * Instantiate the request data object
90     *
91     * @param  Uri|string|null $uri
92     * @param  string          $method
93     * @param  array|Data|null $data
94     * @param  ?string         $type
95     * @throws Exception
96     */
97    public function __construct(Uri|string|null $uri = null, string $method = 'GET', array|Data|null $data = null, ?string $type = null)
98    {
99        parent::__construct($uri);
100
101        if ($method !== null) {
102            $this->setMethod($method);
103        }
104        if ($data !== null) {
105            $this->setData($data);
106        }
107        if ($type !== null) {
108            $this->setRequestType($type);
109        }
110    }
111
112    /**
113     * Factory method to create a Request object
114     *
115     * @param  Uri|string|null $uri
116     * @param  string $method
117     * @param  array|Data|null $data
118     * @param  ?string $type
119     * @throws Exception
120     * @return Request
121     */
122    public static function create(
123        Uri|string|null $uri = null, string $method = 'GET', array|Data|null $data = null, ?string $type = null
124    ): Request
125    {
126        return new self($uri, $method, $data, $type);
127    }
128
129    /**
130     * Factory method to create a JSON Request object
131     *
132     * @param  Uri|string|null $uri
133     * @param  string $method
134     * @param  array|Data|null $data
135     * @throws Exception
136     * @return Request
137     */
138    public static function createJson(Uri|string|null $uri = null, string $method = 'GET', array|Data|null $data = null): Request
139    {
140        return new self($uri, $method, $data, Request::JSON);
141    }
142
143    /**
144     * Factory method to create an XML Request object
145     *
146     * @param  Uri|string|null $uri
147     * @param  string $method
148     * @param  array|Data|null $data
149     * @throws Exception
150     * @return Request
151     */
152    public static function createXml(Uri|string|null $uri = null, string $method = 'GET', array|Data|null $data = null): Request
153    {
154        return new self($uri, $method, $data, Request::XML);
155    }
156
157    /**
158     * Factory method to create a URL-encoded Request object
159     *
160     * @param  Uri|string|null $uri
161     * @param  string $method
162     * @param  array|Data|null $data
163     * @throws Exception
164     * @return Request
165     */
166    public static function createUrlEncoded(Uri|string|null $uri = null, string $method = 'GET', array|Data|null $data = null): Request
167    {
168        return new self($uri, $method, $data, Request::URLFORM);
169    }
170
171    /**
172     * Factory method to create a multipart Request object
173     *
174     * @param  Uri|string|null $uri
175     * @param  string $method
176     * @param  array|Data|null $data
177     * @throws Exception
178     * @return Request
179     */
180    public static function createMultipart(Uri|string|null $uri = null, string $method = 'GET', array|Data|null $data = null): Request
181    {
182        return new self($uri, $method, $data, Request::MULTIPART);
183    }
184
185    /**
186     * Set method
187     *
188     * @param  string $method
189     * @param  bool   $strict
190     * @throws Exception
191     * @return Request
192     */
193    public function setMethod(string $method, bool $strict = true): Request
194    {
195        $method = strtoupper($method);
196
197        if ($strict) {
198            if (!$this->isValidMethod($method)) {
199                throw new Exception('Error: That request method is not valid.');
200            }
201        }
202
203        $this->method = $method;
204        return $this;
205    }
206
207    /**
208     * Get method
209     *
210     * @return string
211     */
212    public function getMethod(): string
213    {
214        return $this->method;
215    }
216
217    /**
218     * Add a header
219     *
220     * @param  Header|string|int $header
221     * @param  ?string           $value
222     * @return Request
223     */
224    public function addHeader(Header|string|int $header, ?string $value = null): Request
225    {
226        $contentType = null;
227        if (is_numeric($header) && ($value !== null)) {
228            $header = Header::parse($value);
229            $value  = null;
230        }
231        if (is_string($header) && ($header == 'Content-Type')) {
232            $contentType = $value;
233        } else if (($header instanceof Header) && ($header->getName() == 'Content-Type')) {
234            $contentType = $header->getValue()->getValue();
235        }
236
237        switch ($contentType) {
238            case Request::JSON:
239                $this->requestType = Request::JSON;
240                break;
241            case Request::XML:
242                $this->requestType = Request::XML;
243                break;
244            case Request::URLFORM:
245                $this->requestType = Request::URLFORM;
246                break;
247            case Request::MULTIPART:
248                $this->requestType = Request::MULTIPART;
249                break;
250        }
251
252        parent::addHeader($header, $value);
253        return $this;
254    }
255
256    /**
257     * Add all headers
258     *
259     * @param  array $headers
260     * @return Request
261     */
262    public function addHeaders(array $headers): Request
263    {
264        foreach ($headers as $header => $value) {
265            if ($value instanceof Header) {
266                $this->addHeader($value);
267            } else {
268                $this->addHeader($header, $value);
269            }
270        }
271        return $this;
272    }
273
274    /**
275     * Set data
276     *
277     * @param  array|string|Data $data
278     * @return Request
279     */
280    public function setData(array|string|Data $data): Request
281    {
282        if (is_string($data)) {
283            $this->data = new Data();
284            $this->data->setData($data);
285        } else {
286            $this->data = (is_array($data)) ? new Data($data) : $data;
287        }
288        $this->dataContent = null;
289        return $this;
290    }
291
292    /**
293     * Get data
294     *
295     * @param  bool $asArray
296     * @return Data|array
297     */
298    public function getData(bool $asArray = false): Data|array
299    {
300        return ($asArray) ? $this->data->getData() : $this->data;
301    }
302
303    /**
304     * Has data
305     *
306     * @return bool
307     */
308    public function hasData(): bool
309    {
310        return !empty($this->data);
311    }
312
313    /**
314     * Set query
315     *
316     * @param  array $query
317     * @return Request
318     */
319    public function setQuery(array $query): Request
320    {
321        $this->query = $query;
322        return $this;
323    }
324
325    /**
326     * Get query
327     *
328     * @param  ?string $name
329     * @return mixed
330     */
331    public function getQuery(?string $name = null): mixed
332    {
333        if ($name !== null) {
334            return $this->query[$name] ?? null;
335        } else {
336            return $this->query;
337        }
338    }
339
340    /**
341     * Has query
342     *
343     * @return bool
344     */
345    public function hasQuery(): bool
346    {
347        return !empty($this->query);
348    }
349
350    /**
351     * Get full URI as string
352     *
353     * @param  bool $query
354     * @return string
355     */
356    public function getUriAsString(bool $query = true): string
357    {
358        $uri = parent::getUriAsString();
359
360        if (($this->method == 'GET') && ($query) && ($this->data !== null)) {
361            $queryString = null;
362            if ($this->dataContent !== null) {
363                $queryString = ((is_array($this->dataContent)) ? http_build_query($this->dataContent) : $this->dataContent);
364            } else if (($this->requestType === null) || ($this->requestType == self::URLFORM)) {
365                $queryString = $this->data->prepareQueryString($this->query);
366            }
367
368            if (!empty($queryString) && !str_contains($uri, $queryString)) {
369                $uri .= ((str_contains($uri, '?')) ? '&' : '?') . $queryString;
370            }
371        }
372
373        return $uri;
374    }
375
376    /**
377     * Set request type
378     *
379     * @param  ?string $type
380     * @return Request
381     */
382    public function setRequestType(string $type = null): Request
383    {
384        switch ($type) {
385            case self::JSON:
386                $this->createAsJson();
387                break;
388            case self::XML:
389                $this->createAsXml();
390                break;
391            case self::URLFORM:
392                $this->createAsUrlEncoded();
393                break;
394            case self::MULTIPART:
395                $this->createAsMultipart();
396                break;
397            default:
398                $this->requestType = null;
399                if ($this->hasHeader('Content-Type')) {
400                    $this->removeHeader('Content-Type');
401                }
402                if ($this->hasHeader('Content-Length')) {
403                    $this->removeHeader('Content-Length');
404                }
405        }
406
407        return $this;
408    }
409
410    /**
411     * Get request type
412     *
413     * @return string
414     */
415    public function getRequestType(): string
416    {
417        return $this->requestType;
418    }
419
420    /**
421     * Has request type
422     *
423     * @return bool
424     */
425    public function hasRequestType(): bool
426    {
427        return ($this->requestType !== null);
428    }
429
430    /**
431     * Create request as JSON
432     *
433     * @return Request
434     */
435    public function createAsJson(): Request
436    {
437        $this->requestType = self::JSON;
438
439        if ($this->hasHeader('Content-Type')) {
440            $this->removeHeader('Content-Type');
441        }
442        $this->addHeader('Content-Type', $this->requestType);
443
444        return $this;
445    }
446
447    /**
448     * Check if request is JSON
449     *
450     * @return bool
451     */
452    public function isJson(): bool
453    {
454        return ($this->requestType == self::JSON);
455    }
456
457    /**
458     * Create request as XML
459     *
460     * @return Request
461     */
462    public function createAsXml(): Request
463    {
464        $this->requestType = self::XML;
465
466        if ($this->hasHeader('Content-Type')) {
467            $this->removeHeader('Content-Type');
468        }
469        $this->addHeader('Content-Type', $this->requestType);
470
471        return $this;
472    }
473
474    /**
475     * Check if request is XML
476     *
477     * @return bool
478     */
479    public function isXml(): bool
480    {
481        return ($this->requestType == self::XML);
482    }
483
484    /**
485     * Create request as a URL-encoded form
486     *
487     * @return Request
488     */
489    public function createAsUrlEncoded(): Request
490    {
491        $this->requestType = self::URLFORM;
492
493        if ($this->hasHeader('Content-Type')) {
494            $this->removeHeader('Content-Type');
495        }
496        $this->addHeader('Content-Type', $this->requestType);
497
498        return $this;
499    }
500
501    /**
502     * Check if request is a URL-encoded form
503     *
504     * @return bool
505     */
506    public function isUrlEncoded(): bool
507    {
508        return ($this->requestType == self::URLFORM);
509    }
510
511    /**
512     * Create request as a multipart form
513     *
514     * @return Request
515     */
516    public function createAsMultipart(): Request
517    {
518        $this->requestType = self::MULTIPART;
519        return $this;
520    }
521
522    /**
523     * Check if request is a multipart form
524     *
525     * @return bool
526     */
527    public function isMultipart(): bool
528    {
529        return ($this->requestType == self::MULTIPART);
530    }
531
532    /**
533     * Is valid method
534     *
535     * @param  string $method
536     * @return bool
537     */
538    public function isValidMethod(string $method): bool
539    {
540        return in_array(strtoupper($method), ['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE', 'CONNECT', 'OPTIONS', 'TRACE']);
541    }
542
543    /**
544     * Has data content
545     *
546     * @return bool
547     */
548    public function hasDataContent(): bool
549    {
550        return ($this->dataContent !== null);
551    }
552
553    /**
554     * Get the data content
555     *
556     * @return string|array|null
557     */
558    public function getDataContent(): string|array|null
559    {
560        return $this->dataContent;
561    }
562
563    /**
564     * Get the content length
565     *
566     * @return int|null
567     */
568    public function getDataContentLength(bool $mb = false): int|null
569    {
570        if (!is_array($this->dataContent)) {
571            return ($mb) ? mb_strlen($this->dataContent) : strlen($this->dataContent);
572        } else {
573            return null;
574        }
575    }
576
577    /**
578     * Prepare request data
579     *
580     * @return void
581     */
582    public function prepareData(): void
583    {
584        $dataPrepared = false;
585
586        // Prepare the GET query string
587        if (($this->method == 'GET') && ((!$this->hasHeader('Content-Type')) ||
588                ($this->getHeaderValue('Content-Type') == 'application/x-www-form-urlencoded'))) {
589            $this->dataContent = ($this->data->hasRawData()) ?
590                $this->data->getRawData() : $this->data->prepareQueryString($this->query);
591            $dataPrepared = true;
592            // Else, prepare the data content
593        } else if ($this->method != 'GET') {
594            switch ($this->requestType) {
595                case Request::JSON:
596                    $this->prepareJson();
597                    $dataPrepared = true;
598                    break;
599                case Request::XML:
600                    $this->prepareXml();
601                    $dataPrepared = true;
602                    break;
603                case Request::URLFORM:
604                    $this->prepareUrlEncoded();
605                    $dataPrepared = true;
606                    break;
607                case Request::MULTIPART:
608                    $this->prepareMultipart();
609                    $dataPrepared = true;
610                    break;
611            }
612
613            // Fallback
614            if (!$dataPrepared) {
615                // If the request has raw data
616                if ($this->data->hasRawData()) {
617                    $this->prepareRawData();
618                // Else, use basic data
619                } else if ($this->data !== null) {
620                    $this->dataContent = $this->getData(true);
621                }
622            }
623        }
624
625        if ($this->method != 'GET') {
626            if ($this->getDataContentLength() !== null) {
627                $this->addHeader('Content-Length', $this->getDataContentLength());
628            }
629        }
630    }
631
632    /**
633     * Method to prepare JSON data content
634     *
635     * @return Request
636     */
637    public function prepareJson(): Request
638    {
639        if ($this->data->hasRawData()) {
640            $jsonContent = $this->data->getRawData();
641        } else {
642            $jsonData    = $this->getData(true);
643            $jsonContent = [];
644
645            // Check for JSON files
646            foreach ($jsonData as $jsonDatum) {
647                if (isset($jsonDatum['filename']) && isset($jsonDatum['contentType']) &&
648                    ($jsonDatum['contentType'] == 'application/json') && file_exists($jsonDatum['filename'])) {
649                    $jsonContent = array_merge($jsonContent, json_decode(file_get_contents($jsonDatum['filename']), true));
650                }
651            }
652
653            // Else, use JSON data
654            if (empty($jsonContent)) {
655                $jsonContent = $jsonData;
656            }
657        }
658
659        // Only encode if the data isn't already encoded
660        if (!((is_string($jsonContent) && (json_decode($jsonContent) !== false)) && (json_last_error() == JSON_ERROR_NONE))) {
661            $this->dataContent = json_encode($jsonContent, JSON_PRETTY_PRINT);
662        } else {
663            $this->dataContent = $jsonContent;
664        }
665
666        return $this;
667    }
668
669    /**
670     * Method to prepare XML data content
671     *
672     * @return Request
673     */
674    public function prepareXml(): Request
675    {
676        if ($this->data->hasRawData()) {
677            $xmlContent = $this->data->getRawData();
678        } else {
679            $xmlData    = $this->getData(true);
680            $xmlContent = null;
681
682            // Check for XML files
683            foreach ($xmlData as $xmlDatum) {
684                $xmlContent .= (isset($xmlDatum['filename']) && isset($xmlDatum['contentType']) &&
685                    ($xmlDatum['contentType'] == 'application/xml') && file_exists($xmlDatum['filename'])) ?
686                    file_get_contents($xmlDatum['filename']) : $xmlDatum;
687            }
688        }
689
690        $this->dataContent = $xmlContent;
691
692        return $this;
693    }
694
695    /**
696     * Method to prepare URL-encoded data content
697     *
698     * @return Request
699     */
700    public function prepareUrlEncoded(): Request
701    {
702        $this->dataContent = $this->data->prepareQueryString();
703        return $this;
704    }
705
706    /**
707     * Method to prepare multipart data content
708     *
709     * @return Request
710     */
711    public function prepareMultipart(): Request
712    {
713        $formMessage       = Message::createForm($this->data->getData());
714        $this->dataContent = $formMessage->renderRaw();
715        $this->addHeader($formMessage->getHeader('Content-Type'));
716
717
718
719        return $this;
720    }
721
722    /**
723     * Method to prepare raw data content
724     *
725     * @return Request
726     */
727    public function prepareRawData(): Request
728    {
729        $this->dataContent = $this->data->getRawData();
730        return $this;
731    }
732
733    /**
734     * Magic method to check the is[Method](), i.e. $request->isPost();
735     *
736     * @param  string $methodName
737     * @param  ?array $arguments
738     * @throws Exception
739     * @return bool
740     */
741    public function __call(string $methodName, ?array $arguments = null): bool
742    {
743        if (str_starts_with($methodName, 'is')) {
744            $method = strtoupper(substr($methodName, 2));
745            if ($this->isValidMethod($method)) {
746                return ($this->method == $method);
747            } else {
748                throw new Exception('Error: That request method is not valid.');
749            }
750        } else {
751            throw new Exception('Error: That method/function is not valid.');
752        }
753    }
754
755}