Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
373 / 373
100.00% covered (success)
100.00%
73 / 73
CRAP
100.00% covered (success)
100.00%
1 / 1
Client
100.00% covered (success)
100.00%
373 / 373
100.00% covered (success)
100.00%
73 / 73
264
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
1 / 1
10
 createMulti
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 fromCurlCommand
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setMethod
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getMethod
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 hasMethod
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
3
 setOptions
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 addOptions
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 addOption
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getOptions
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getOption
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasOptions
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasOption
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 removeOption
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 removeOptions
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setHandler
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getHandler
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasHandler
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setMultiHandler
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 getMultiHandler
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasMultiHandler
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setAuth
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getAuth
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasAuth
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setHeaders
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 addHeaders
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 addHeader
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 getHeaders
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 getHeader
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
4
 hasHeaders
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 hasHeader
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 removeHeader
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 removeAllHeaders
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 setData
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 addData
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 getData
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
7
 hasData
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
6
 removeData
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
6
 removeAllData
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
5
 setQuery
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 addQuery
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 getQuery
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
7
 hasQuery
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
6
 removeQuery
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
6
 removeAllQuery
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
5
 setType
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 getType
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 hasType
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
3
 removeType
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 setFiles
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
6
 addFile
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
5
 getFiles
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getFile
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 hasFiles
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasFile
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 removeFile
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 removeFiles
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 setBody
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 setBodyFromFile
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 hasBody
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 getBody
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
3
 getBodyContent
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
3
 getBodyContentLength
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
3
 removeBody
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 prepare
100.00% covered (success)
100.00%
51 / 51
100.00% covered (success)
100.00%
1 / 1
33
 send
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
8
 sendAsync
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 render
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
5
 reset
100.00% covered (success)
100.00%
27 / 27
100.00% covered (success)
100.00%
1 / 1
14
 toCurlCommand
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 __toString
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 __call
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 __callStatic
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
1 / 1
12
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;
15
16use Pop\Http\Client\Data;
17use Pop\Http\Client\Handler\Stream;
18use Pop\Http\Client\Request;
19use Pop\Http\Client\Response;
20use Pop\Http\Client\Handler\Curl;
21use Pop\Http\Client\Handler\CurlMulti;
22use Pop\Http\Client\Handler\HandlerInterface;
23use Pop\Mime\Part\Body;
24use Pop\Mime\Part\Header;
25
26/**
27 * HTTP client class
28 *
29 * @category   Pop
30 * @package    Pop\Http
31 * @author     Nick Sagona, III <dev@nolainteractive.com>
32 * @copyright  Copyright (c) 2009-2024 NOLA Interactive, LLC. (http://www.nolainteractive.com)
33 * @license    http://www.popphp.org/license     New BSD License
34 * @version    5.2.0
35 */
36class Client extends AbstractHttp
37{
38
39    /**
40     * Client options
41     * @var array
42     */
43    protected array $options = [];
44
45    /**
46     * Request handler
47     * @var ?HandlerInterface
48     */
49    protected ?HandlerInterface $handler = null;
50
51    /**
52     * Request multi-handler
53     * @var ?CurlMulti
54     */
55    protected ?CurlMulti $multiHandler = null;
56
57    /**
58     * HTTP auth object
59     * @var ?Auth
60     */
61    protected ?Auth $auth = null;
62
63    /**
64     * Instantiate the client object
65     *
66     * Optional parameters are
67     *  - Request URI string
68     *  - Client request instance
69     *  - Client response instance
70     *  - Client handler instance
71     *  - Auth instance
72     *  - Options array
73     */
74    public function __construct()
75    {
76        $args     = func_get_args();
77        $request  = null;
78        $response = null;
79        $handler  = null;
80
81        foreach ($args as $arg) {
82            if (is_string($arg)) {
83                $request = new Request($arg);
84            } else if ($arg instanceof Client\Request) {
85                $request = $arg;
86            } else if ($arg instanceof Client\Response) {
87                $response = $arg;
88            } else if ($arg instanceof Client\Handler\HandlerInterface) {
89                $handler = $arg;
90            } else if ($arg instanceof Auth) {
91                $this->setAuth($arg);
92            } else if (is_array($arg)) {
93                $this->setOptions($arg);
94            }
95        }
96
97        parent::__construct($request, $response);
98
99        if ($handler !== null) {
100            if ($handler instanceof CurlMulti) {
101                $this->setMultiHandler($handler);
102            } else {
103                $this->setHandler($handler);
104            }
105        }
106    }
107
108    /**
109     * Factory to create a multi-handler object
110     *
111     * @param  array                    $requests
112     * @param  Client\Handler\CurlMulti $multiHandler
113     * @return CurlMulti
114     */
115    public static function createMulti(
116        array $requests, Client\Handler\CurlMulti $multiHandler = new Client\Handler\CurlMulti()
117    ): CurlMulti
118    {
119        foreach ($requests as $request) {
120            $client = new Client($request);
121            $client->setMultiHandler($multiHandler);
122        }
123
124        return $multiHandler;
125    }
126
127    /**
128     * Method to convert Curl CLI command to a client object
129     *
130     * @param  string $command
131     * @throws Curl\Exception
132     * @return Client
133     */
134    public static function fromCurlCommand(string $command): Client
135    {
136        return Curl\Command::commandToClient($command);
137    }
138
139    /**
140     * Set method
141     *
142     * @param  string $method
143     * @return Client
144     */
145    public function setMethod(string $method): Client
146    {
147        if ($this->hasRequest()) {
148            $this->request->setMethod($method);
149        } else {
150            $this->options['method'] = $method;
151        }
152
153        return $this;
154    }
155
156    /**
157     * Get method
158     *
159     * @return ?string
160     */
161    public function getMethod(): ?string
162    {
163        if ($this->hasRequest()) {
164            return $this->request->getMethod();
165        } else {
166            return $this->options['method'] ?? null;
167        }
168    }
169
170    /**
171     * Has method
172     *
173     * @return bool
174     */
175    public function hasMethod(): bool
176    {
177        return ((($this->hasRequest()) && !empty($this->request->getMethod())) || isset($this->options['method']));
178    }
179
180    /**
181     * Set options
182     *
183     * Supported options
184     *  - 'base_uri'
185     *  - 'method'
186     *  - 'headers'
187     *  - 'user_agent'
188     *  - 'query' (can only be encoded query string on the URI)
189     *  - 'data' (can be any request data)
190     *  - 'files'
191     *  - 'type'
192     *  - 'auto'
193     *  - 'async'
194     *  - 'verify_peer'
195     *  - 'allow_self_signed'
196     *  - 'force_custom_method' (Curl only - forces CURLOPT_CUSTOMREQUEST)
197     *
198     * @param  array $options
199     * @return Client
200     */
201    public function setOptions(array $options): Client
202    {
203        $this->options = $options;
204        return $this;
205    }
206
207    /**
208     * Add options
209     *
210     * @param  array $options
211     * @return Client
212     */
213    public function addOptions(array $options): Client
214    {
215        foreach ($options as $name => $value) {
216            $this->addOption($name, $value);
217        }
218        return $this;
219    }
220
221    /**
222     * Add an option
223     *
224     * @param  string $name
225     * @param  mixed  $value
226     * @return Client
227     */
228    public function addOption(string $name, mixed $value): Client
229    {
230        $this->options[$name] = $value;
231        return $this;
232    }
233
234    /**
235     * Get options
236     *
237     * @return array
238     */
239    public function getOptions(): array
240    {
241        return $this->options;
242    }
243
244    /**
245     * Get options
246     *
247     * @param  string $name
248     * @return mixed
249     */
250    public function getOption(string $name): mixed
251    {
252        return $this->options[$name] ?? null;
253    }
254
255    /**
256     * Has options
257     *
258     * @return bool
259     */
260    public function hasOptions(): bool
261    {
262        return !empty($this->options);
263    }
264
265    /**
266     * Has option
267     *
268     * @return bool
269     */
270    public function hasOption(string $name): bool
271    {
272        return array_key_exists($name, $this->options);
273    }
274
275    /**
276     * Remove option
277     *
278     * @param  string $name
279     * @return Client
280     */
281    public function removeOption(string $name): Client
282    {
283        if (isset($this->options[$name])) {
284            unset($this->options[$name]);
285        }
286        return $this;
287    }
288
289    /**
290     * Remove options
291     *
292     * @return Client
293     */
294    public function removeOptions(): Client
295    {
296        $this->options = [];
297        return $this;
298    }
299
300    /**
301     * Set handler
302     *
303     * @param  HandlerInterface $handler
304     * @return Client
305     */
306    public function setHandler(HandlerInterface $handler): Client
307    {
308        $this->handler = $handler;
309        return $this;
310    }
311
312    /**
313     * Get handler
314     *
315     * @return HandlerInterface|null
316     */
317    public function getHandler(): HandlerInterface|null
318    {
319        return $this->handler;
320    }
321
322    /**
323     * Has handler
324     *
325     * @return bool
326     */
327    public function hasHandler(): bool
328    {
329        return ($this->handler !== null);
330    }
331
332    /**
333     * Set multi-handler
334     *
335     * @param  CurlMulti $multiHandler
336     * @return Client
337     */
338    public function setMultiHandler(CurlMulti $multiHandler): Client
339    {
340        $this->multiHandler = $multiHandler;
341
342        if (!($this->handler instanceof Curl)) {
343            $this->handler = new Curl();
344            $this->multiHandler->addClient($this);
345        }
346
347        return $this;
348    }
349
350    /**
351     * Get multi-handler
352     *
353     * @return CurlMulti
354     */
355    public function getMultiHandler(): CurlMulti
356    {
357        return $this->multiHandler;
358    }
359
360    /**
361     * Has multi-handler
362     *
363     * @return bool
364     */
365    public function hasMultiHandler(): bool
366    {
367        return ($this->multiHandler !== null);
368    }
369
370    /**
371     * Set auth
372     *
373     * @param  Auth $auth
374     * @return Client
375     */
376    public function setAuth(Auth $auth): Client
377    {
378        $this->auth = $auth;
379        return $this;
380    }
381
382    /**
383     * Get auth
384     *
385     * @return ?Auth
386     */
387    public function getAuth(): ?Auth
388    {
389        return $this->auth;
390    }
391
392    /**
393     * Has auth
394     *
395     * @return bool
396     */
397    public function hasAuth(): bool
398    {
399        return ($this->auth !== null);
400    }
401
402    /**
403     * Set headers (clear out any existing headers)
404     *
405     * @param  array $headers
406     * @return Client
407     */
408    public function setHeaders(array $headers): Client
409    {
410        if ($this->hasRequest()) {
411            $this->request->setHeaders($headers);
412        } else {
413            $this->options['headers'] = $headers;
414        }
415        return $this;
416    }
417
418    /**
419     * Add headers
420     *
421     * @param  array $headers
422     * @return Client
423     */
424    public function addHeaders(array $headers): Client
425    {
426        if ($this->hasRequest()) {
427            $this->request->addHeaders($headers);
428        } else {
429            $this->options['headers'] = $headers;
430        }
431        return $this;
432    }
433
434    /**
435     * Add header
436     *
437     * @param  Header|string|int $header
438     * @param  mixed             $value
439     * @return Client
440     */
441    public function addHeader(Header|string|int $header, mixed $value = null): Client
442    {
443        if ($this->hasRequest()) {
444            $this->request->addHeader($header, $value);
445        } else {
446            if (!isset($this->options['headers'])) {
447                $this->options['headers'] = [];
448            }
449            if ($header instanceof Header) {
450                $this->options['headers'][$header->getName()] = $header;
451            } else {
452                $this->options['headers'][$header] = $value;
453            }
454        }
455
456        return $this;
457    }
458
459    /**
460     * Get headers
461     *
462     * @return mixed
463     */
464    public function getHeaders(): mixed
465    {
466        if (($this->hasRequest()) && ($this->request->hasHeaders())) {
467            return $this->request->getHeaders();
468        } else {
469            return $this->options['headers'] ?? null;
470        }
471    }
472
473    /**
474     * Get header
475     *
476     * @param  string $name
477     * @return mixed
478     */
479    public function getHeader(string $name): mixed
480    {
481        if (($this->hasRequest()) && ($this->request->hasHeader($name))) {
482            return $this->request->getHeader($name);
483        } else {
484            return (isset($this->options['headers'][$name])) ? $this->options['headers'][$name] : null;
485        }
486    }
487
488    /**
489     * Has headers
490     *
491     * @return bool
492     */
493    public function hasHeaders(): bool
494    {
495        if (($this->hasRequest()) && ($this->request->hasHeaders())) {
496            return true;
497        } else {
498            return !empty($this->options['headers']);
499        }
500    }
501
502    /**
503     * Has header
504     *
505     * @param  string $name
506     * @return bool
507     */
508    public function hasHeader(string $name): bool
509    {
510        if (($this->hasRequest()) && ($this->request->hasHeader($name))) {
511            return true;
512        } else {
513            return isset($this->options['headers'][$name]);
514        }
515    }
516
517    /**
518     * Remove header
519     *
520     * @param  string $name
521     * @return Client
522     */
523    public function removeHeader(string $name): Client
524    {
525        if (($this->hasRequest()) && ($this->request->hasHeader($name))) {
526            $this->request->removeHeader($name);
527        }
528        if (isset($this->options['headers'][$name])) {
529            unset($this->options['headers'][$name]);
530        }
531
532        return $this;
533    }
534
535    /**
536     * Remove all headers
537     *
538     * @return Client
539     */
540    public function removeAllHeaders(): Client
541    {
542        if (($this->hasRequest()) && ($this->request->hasHeaders())) {
543            $this->request->removeHeaders();
544        }
545        if (isset($this->options['headers'])) {
546            unset($this->options['headers']);
547        }
548
549        return $this;
550    }
551
552    /**
553     * Set data
554     *
555     * @param  array $data
556     * @return Client
557     */
558    public function setData(array $data): Client
559    {
560        if (($this->hasRequest()) && ($this->request instanceof Client\Request)) {
561            $this->request->setData($data);
562        } else {
563            $this->options['data'] = $data;
564        }
565
566        return $this;
567    }
568
569    /**
570     * Add data
571     *
572     * @param  string $name
573     * @param  mixed  $value
574     * @return Client
575     */
576    public function addData(string $name, mixed $value): Client
577    {
578        if (($this->hasRequest()) && ($this->request instanceof Client\Request)) {
579            $this->request->addData($name, $value);
580        } else {
581            if (!isset($this->options['data'])) {
582                $this->options['data'] = [];
583            }
584            $this->options['data'][$name] = $value;
585        }
586
587        return $this;
588    }
589
590    /**
591     * Get data
592     *
593     * @param  ?string $key
594     * @return mixed
595     */
596    public function getData(?string $key = null): mixed
597    {
598        $data = null;
599
600        if (($this->hasRequest()) && ($this->request instanceof Client\Request)) {
601            $data = $this->request->getData()->getData($key);
602        }
603
604        if (($data === null) && isset($this->options['data'])) {
605            if ($key !== null) {
606                $data = (isset($this->options['data'][$key])) ?
607                    $this->options['data'][$key] : null;
608            } else {
609                $data = $this->options['data'] ?? null;
610            }
611        }
612
613        return $data;
614    }
615
616    /**
617     * Has data
618     *
619     * @param  ?string $key
620     * @return bool
621     */
622    public function hasData(?string $key = null): bool
623    {
624        if (($this->hasRequest()) && ($this->request instanceof Client\Request) &&
625            ($this->request->hasData()) && ($this->request->getData()->hasData($key))) {
626            return true;
627        } else {
628            return ($key !== null) ? isset($this->options['data'][$key]) : isset($this->options['data']);
629        }
630    }
631
632    /**
633     * Remove data
634     *
635     * @param  string $key
636     * @return Client
637     */
638    public function removeData(string $key): Client
639    {
640        if (($this->hasRequest()) && ($this->request instanceof Client\Request) &&
641            ($this->request->hasData()) && ($this->request->getData()->hasData($key))) {
642            $this->request->removeData($key);
643        }
644        if (isset($this->options['data'][$key])) {
645            unset($this->options['data'][$key]);
646        }
647
648        return $this;
649    }
650
651    /**
652     * Remove all data
653     *
654     * @return Client
655     */
656    public function removeAllData(): Client
657    {
658        if (($this->hasRequest()) && ($this->request instanceof Client\Request) && ($this->request->hasData())) {
659            $this->request->removeAllData();
660        }
661        if (isset($this->options['data'])) {
662            unset($this->options['data']);
663        }
664
665        return $this;
666    }
667
668    /**
669     * Set query
670     *
671     * @param  array $query
672     * @return Client
673     */
674    public function setQuery(array $query): Client
675    {
676        if (($this->hasRequest()) && ($this->request instanceof Client\Request)) {
677            $this->request->setQuery(new Data($query));
678        } else {
679            $this->options['query'] = $query;
680        }
681
682        return $this;
683    }
684
685    /**
686     * Add query
687     *
688     * @param  string $name
689     * @param  mixed  $value
690     * @return Client
691     */
692    public function addQuery(string $name, mixed $value): Client
693    {
694        if (($this->hasRequest()) && ($this->request instanceof Client\Request)) {
695            $this->request->addQuery($name, $value);
696        } else {
697            if (!isset($this->options['query'])) {
698                $this->options['query'] = [];
699            }
700            $this->options['query'][$name] = $value;
701        }
702
703        return $this;
704    }
705
706    /**
707     * Get query
708     *
709     * @param  ?string $key
710     * @return mixed
711     */
712    public function getQuery(?string $key = null): mixed
713    {
714        $query = null;
715
716        if (($this->hasRequest()) && ($this->request instanceof Client\Request)) {
717            $query = $this->request->getQuery()->getData($key);
718        }
719
720        if (($query === null) && isset($this->options['query'])) {
721            if ($key !== null) {
722                $query = (isset($this->options['query'][$key])) ?
723                    $this->options['query'][$key] : null;
724            } else {
725                $query = $this->options['query'] ?? null;
726            }
727        }
728
729        return $query;
730    }
731
732    /**
733     * Has query
734     *
735     * @param  ?string $key
736     * @return bool
737     */
738    public function hasQuery(?string $key = null): bool
739    {
740        if (($this->hasRequest()) && ($this->request instanceof Client\Request) &&
741            ($this->request->hasQuery()) && ($this->request->getQuery()->hasData($key))) {
742            return true;
743        } else {
744            return ($key !== null) ? isset($this->options['query'][$key]) : isset($this->options['query']);
745        }
746    }
747
748    /**
749     * Remove query
750     *
751     * @param  string $key
752     * @return Client
753     */
754    public function removeQuery(string $key): Client
755    {
756        if (($this->hasRequest()) && ($this->request instanceof Client\Request) &&
757            ($this->request->hasQuery()) && ($this->request->getQuery()->hasData($key))) {
758            $this->request->removeQuery($key);
759        }
760        if (isset($this->options['query'][$key])) {
761            unset($this->options['query'][$key]);
762        }
763
764        return $this;
765    }
766
767    /**
768     * Remove all query data
769     *
770     * @return Client
771     */
772    public function removeAllQuery(): Client
773    {
774        if (($this->hasRequest()) && ($this->request instanceof Client\Request) && ($this->request->hasQuery())) {
775            $this->request->removeAllQuery();
776        }
777        if (isset($this->options['query'])) {
778            unset($this->options['query']);
779        }
780
781        return $this;
782    }
783
784    /**
785     * Set type
786     *
787     * @param  string $type
788     * @return Client
789     */
790    public function setType(string $type): Client
791    {
792        if (($this->hasRequest()) && ($this->request instanceof Client\Request)) {
793            $this->request->setRequestType($type);
794        } else {
795            $this->options['type'] = $type;
796        }
797
798        return $this;
799    }
800
801    /**
802     * Get type
803     *
804     * @return mixed
805     */
806    public function getType(): mixed
807    {
808        if (($this->hasRequest()) && ($this->request instanceof Client\Request)) {
809            return $this->request->getRequestType();
810        } else {
811            return $this->options['type'] ?? null;
812        }
813    }
814
815    /**
816     * Has type
817     *
818     * @return bool
819     */
820    public function hasType(): bool
821    {
822        return ((($this->hasRequest()) && ($this->request instanceof Client\Request))) ?
823            $this->request->hasRequestType() : isset($this->options['type']);
824    }
825
826    /**
827     * Remove type
828     *
829     * @return Client
830     */
831    public function removeType(): Client
832    {
833        if (($this->hasRequest()) && ($this->request instanceof Client\Request)) {
834            $this->request->removeRequestType();
835        }
836        if (isset($this->options['type'])) {
837            unset($this->options['type']);
838        }
839
840        return $this;
841    }
842
843    /**
844     * Set files
845     *
846     * @param  array|string $files
847     * @param  bool         $multipart
848     * @throws Exception
849     * @return Client
850     */
851    public function setFiles(array|string $files, bool $multipart = true): Client
852    {
853        if (is_string($files)) {
854            $files = [$files];
855        }
856
857        $filenames = [];
858
859        foreach ($files as $i => $file) {
860            if (!file_exists($file)) {
861                throw new Exception("Error: The file '" . $file . "' does not exist.");
862            }
863
864            $name = (is_numeric($i)) ? 'file' . ($i + 1) : $i;
865            $filenames[$name] = $file;
866        }
867
868        $this->options['files'] = $filenames;
869
870        if ($multipart) {
871            $this->options['type'] = Request::MULTIPART;
872        }
873        return $this;
874    }
875
876    /**
877     * Add file
878     *
879     * @param  string  $file
880     * @param  ?string $name
881     * @throws Exception
882     * @return Client
883     */
884    public function addFile(string $file, ?string $name = null): Client
885    {
886        if (!file_exists($file)) {
887            throw new Exception("Error: The file '" . $file . "' does not exist.");
888        }
889
890        if (!isset($this->options['files'])) {
891            $this->options['files'] = [];
892        }
893
894        if ($name === null) {
895            $i = 1;
896            $name = 'file' . $i;
897            while (isset($this->options['files'][$name])) {
898                $i++;
899                $name = 'file' . $i;
900            }
901        }
902
903        $this->options['files'][$name] = $file;
904        return $this;
905    }
906
907    /**
908     * Get files
909     *
910     * @return array|null
911     */
912    public function getFiles(): array|null
913    {
914        return $this->options['files'] ?? null;
915    }
916
917    /**
918     * Get file
919     *
920     * @param  string $key
921     * @return ?string
922     */
923    public function getFile(string $key): ?string
924    {
925        return (isset($this->options['files'][$key])) ?
926                $this->options['files'][$key] : null;
927    }
928
929    /**
930     * Has files
931     *
932     * @return bool
933     */
934    public function hasFiles(): bool
935    {
936        return isset($this->options['files']);
937    }
938
939    /**
940     * Has file
941     *
942     * @param  string $key
943     * @return bool
944     */
945    public function hasFile(string $key): bool
946    {
947        return (isset($this->options['files'][$key]));
948    }
949
950    /**
951     * Remove file
952     *
953     * @param  string $key
954     * @return Client
955     */
956    public function removeFile(string $key): Client
957    {
958        if (isset($this->options['files'][$key])) {
959            unset($this->options['files'][$key]);
960        }
961
962        return $this;
963    }
964
965    /**
966     * Remove all files
967     *
968     * @return Client
969     */
970    public function removeFiles(): Client
971    {
972        if (isset($this->options['files'])) {
973            unset($this->options['files']);
974        }
975
976        return $this;
977    }
978
979    /**
980     * Set request body
981     *
982     * @param  string $body
983     * @throws Exception
984     * @return Client
985     */
986    public function setBody(string $body): Client
987    {
988        if ($this->request === null) {
989            throw new Exception('Error: The request object has not been created.');
990        }
991
992        $this->request->setBody($body);
993
994        return $this;
995    }
996
997    /**
998     * Set request body from file
999     *
1000     * @param  string $file
1001     * @throws Exception
1002     * @return Client
1003     */
1004    public function setBodyFromFile(string $file): Client
1005    {
1006        if ($this->request === null) {
1007            throw new Exception('Error: The request object has not been created.');
1008        }
1009        if (!file_exists($file)) {
1010            throw new Exception("Error: The file '" . $file . "' does not exist.");
1011        }
1012
1013        $this->request->setBody(file_get_contents($file));
1014
1015        return $this;
1016    }
1017
1018    /**
1019     * Has request body
1020     *
1021     * @return bool
1022     */
1023    public function hasBody(): bool
1024    {
1025        return (($this->request !== null) && ($this->request->hasBody()));
1026    }
1027
1028    /**
1029     * Get request body
1030     *
1031     * @return ?Body
1032     */
1033    public function getBody(): ?Body
1034    {
1035        return (($this->request !== null) && ($this->request->hasBody())) ? $this->request->getBody() : null;
1036    }
1037
1038    /**
1039     * Get request body content
1040     *
1041     * @return ?string
1042     */
1043    public function getBodyContent(): ?string
1044    {
1045        return (($this->request !== null) && ($this->request->hasBody())) ? $this->request->getBodyContent() : null;
1046    }
1047
1048    /**
1049     * Get request body content length
1050     *
1051     * @param  bool $mb
1052     * @return int
1053     */
1054    public function getBodyContentLength(bool $mb = false): int
1055    {
1056        return (($this->request !== null) && ($this->request->hasBody())) ? $this->request->getBodyContentLength($mb) : 0;
1057    }
1058
1059    /**
1060     * Remove the body
1061     *
1062     * @return Client
1063     */
1064    public function removeBody(): Client
1065    {
1066        if (($this->request !== null) && ($this->request->hasBody())) {
1067            $this->request->removeBody();
1068        }
1069        return $this;
1070    }
1071
1072    /**
1073     * Prepare the client request
1074     *
1075     * @param  ?string $uri
1076     * @param  ?string $method
1077     * @throws Exception|Client\Exception
1078     * @return Client
1079     */
1080    public function prepare(?string $uri = null, string $method = null): Client
1081    {
1082        // Check that there is a request object or a URI
1083        if ((!$this->hasRequest()) && ($uri === null) && !isset($this->options['base_uri'])) {
1084            throw new Exception('Error: There is no request URI to send.');
1085        }
1086
1087        if (($method === null) && isset($this->options['method'])) {
1088            $method = $this->options['method'];
1089        }
1090
1091        if ($this->hasRequest()) {
1092            // Set request URI
1093            if ($uri !== null) {
1094                if (isset($this->options['base_uri']) && !str_starts_with($uri, $this->options['base_uri'])) {
1095                    $uri = $this->options['base_uri'] . $uri;
1096                }
1097                $this->request->setUri(new Uri($uri));
1098            // Else, check and adjust for base_uri
1099            } else if (isset($this->options['base_uri']) && !str_starts_with($this->request->getUriAsString(), $this->options['base_uri'])) {
1100                $this->request->setUri($this->options['base_uri'] . $this->request->getUriAsString());
1101            }
1102
1103            // Set method
1104            if ($method !== null) {
1105                $this->request->setMethod($method);
1106            }
1107        // Create new request object
1108        } else {
1109            if (($uri === null) && isset($this->options['base_uri'])) {
1110                $uri = $this->options['base_uri'];
1111            } else if (isset($this->options['base_uri']) && !str_starts_with($uri, $this->options['base_uri'])) {
1112                $uri = $this->options['base_uri'] . $uri;
1113            }
1114            $this->setRequest(new Request(new Uri($uri), ($method ?? 'GET')));
1115        }
1116
1117        // Set request type
1118        if ($this->hasOption('type')) {
1119            $this->request->setRequestType($this->options['type']);
1120        }
1121
1122        // Add headers
1123        if (($this->hasOption('headers')) && is_array($this->options['headers'])) {
1124            $this->request->addHeaders($this->options['headers']);
1125        }
1126
1127        // Add query
1128        if ($this->hasOption('query')) {
1129            $this->request->setQuery($this->options['query']);
1130        }
1131
1132        // Add data
1133        $data = [];
1134        if ($this->hasOption('data')) {
1135            $data = $this->options['data'];
1136        }
1137
1138        // Add files
1139        if ($this->hasOption('files')) {
1140            $files = $this->options['files'];
1141            foreach ($files as $file => $value) {
1142                $file = (is_numeric($file)) ? 'file' . ($file + 1) : $file;
1143                $data[$file] = [
1144                    'filename'    => $value,
1145                    'contentType' => Client\Data::getMimeTypeFromFilename($value)
1146                ];
1147            }
1148        }
1149
1150        if (!empty($data)) {
1151            $this->request->setData($data);
1152        }
1153
1154        // Set (or reset) handler
1155        if (!$this->hasHandler()) {
1156            $this->setHandler(new Curl());
1157        } else {
1158            $this->getHandler()->reset();
1159        }
1160
1161        // Set user-agent
1162        if ($this->hasOption('user_agent')) {
1163            if ($this->handler instanceof Curl) {
1164                $this->handler->setOption(CURLOPT_USERAGENT, $this->options['user_agent']);
1165            } else if ($this->handler instanceof Stream) {
1166                $this->handler->addContextOption('http', ['user_agent' => $this->options['user_agent']]);
1167            }
1168        }
1169
1170        // Handle SSL options
1171        if (!($this->handler instanceof CurlMulti)) {
1172            if ($this->hasOption('verify_peer')) {
1173                $this->handler->setVerifyPeer((bool)$this->options['verify_peer']);
1174            }
1175            if ($this->hasOption('allow_self_signed')) {
1176                $this->handler->allowSelfSigned((bool)$this->options['allow_self_signed']);
1177            }
1178        }
1179
1180        return $this;
1181    }
1182
1183    /**
1184     * Send the client request
1185     *
1186     * @param  ?string $uri
1187     * @param  ?string $method
1188     * @throws Exception|Client\Exception|Client\Handler\Exception
1189     * @return Response|Promise|array|string
1190     */
1191    public function send(?string $uri = null, string $method = null): Response|Promise|array|string
1192    {
1193        if (isset($this->options['async']) && ($this->options['async'] === true)) {
1194            return $this->sendAsync();
1195        } else {
1196            $this->prepare($uri, $method);
1197            $this->response = (isset($this->options['force_custom_method']) && ($this->handler instanceof Curl)) ?
1198                $this->handler->prepare($this->request, $this->auth, (bool)$this->options['force_custom_method'])->send() :
1199                $this->handler->prepare($this->request, $this->auth)->send();
1200
1201            return (($this->hasOption('auto')) && ($this->options['auto']) && ($this->response instanceof Response)) ?
1202                $this->response->getParsedResponse() : $this->response;
1203        }
1204    }
1205
1206    /**
1207     * Method to send the request asynchronously
1208     *
1209     * @return Promise
1210     */
1211    public function sendAsync(): Promise
1212    {
1213        return new Promise($this);
1214    }
1215
1216    /**
1217     * Method to render the request as a string
1218     *
1219     * @return string
1220     */
1221    public function render(): string
1222    {
1223        $this->prepare();
1224
1225        if (isset($this->options['force_custom_method']) && ($this->handler instanceof Curl)) {
1226            $this->handler->prepare($this->request, $this->auth, (bool)$this->options['force_custom_method']);
1227        } else {
1228            $this->handler->prepare($this->request, $this->auth);
1229        }
1230
1231        $uri       = $this->handler->getUriObject();
1232        $uriString = $uri->getUri();
1233        if ($uri->hasQuery()) {
1234            $uriString .= '?' . $uri->getQuery();
1235        }
1236
1237        $request = $this->request->getMethod() . ' ' . $uriString . ' HTTP/' . $this->handler->getHttpVersion() . "\r\n" .
1238            'Host: ' . $uri->getFullHost() . "\r\n" . $this->request->getHeadersAsString() . "\r\n";
1239
1240        if ($this->request->hasDataContent()) {
1241            $request .= $this->request->getDataContent();
1242        }
1243        return $request;
1244    }
1245
1246    /**
1247     * Method to reset the client
1248     *
1249     * @param  bool $data
1250     * @param  bool $headers
1251     * @param  bool $clear
1252     * @return static
1253     */
1254    public function reset(?bool $data = true, ?bool $headers = false, bool $clear = false): static
1255    {
1256        // Fully clear out and reset of client object
1257        if ($clear) {
1258            $this->request  = null;
1259            $this->response = null;
1260            $this->handler  = null;
1261            $this->auth     = null;
1262            $this->options  = [];
1263        // Clear out basic client info & data
1264        } else {
1265            if ($data) {
1266                if (isset($this->options['query'])) {
1267                    unset($this->options['query']);
1268                }
1269                if (isset($this->options['data'])) {
1270                    unset($this->options['data']);
1271                }
1272                if (isset($this->options['files'])) {
1273                    unset($this->options['files']);
1274                }
1275                if ($this->request !== null) {
1276                    if ($this->request->hasQuery()) {
1277                        $this->request->removeAllQuery();
1278                    }
1279                    if ($this->request->hasData()) {
1280                        $this->request->removeAllData();
1281                    }
1282                }
1283            }
1284            if ($headers) {
1285                if (isset($this->options['headers'])) {
1286                    unset($this->options['headers']);
1287                }
1288                if (isset($this->options['user_agent'])) {
1289                    unset($this->options['user_agent']);
1290                }
1291                if ($this->request !== null) {
1292                    if ($this->request->hasHeaders()) {
1293                        $this->request->removeHeaders();
1294                    }
1295                }
1296            }
1297        }
1298
1299        return $this;
1300    }
1301
1302    /**
1303     * Method to convert client object to a Curl command for the CLI
1304     *
1305     * @throws Exception|Curl\Exception
1306     * @return string
1307     */
1308    public function toCurlCommand(): string
1309    {
1310        if ($this->handler instanceof Stream) {
1311            throw new Exception('Error: The client handler must be an instance of Curl');
1312        }
1313
1314        if (!$this->hasHandler()) {
1315            $this->setHandler(new Curl());
1316        }
1317
1318        return Curl\Command::clientToCommand($this);
1319    }
1320
1321    /**
1322     * To string magic method to render the client request to a raw string
1323     *
1324     */
1325    public function __toString(): string
1326    {
1327        return $this->render();
1328    }
1329
1330    /**
1331     * Magic method to send requests by the method name, i.e. $client->get('http://localhost/');
1332     *
1333     * @param  string $methodName
1334     * @param  array  $arguments
1335     * @throws Exception|Client\Exception|Client\Handler\Exception
1336     * @return Response|Promise|array|string
1337     */
1338    public function __call(string $methodName, array $arguments): Response|Promise|array|string
1339    {
1340        if (str_contains($methodName, 'Async')) {
1341            if (isset($arguments[0])) {
1342                $methodName = strtoupper(substr($methodName, 0, strpos($methodName, 'Async')));
1343                $this->prepare($arguments[0], $methodName);
1344            }
1345            return $this->sendAsync();
1346        } else {
1347            return $this->send(($arguments[0] ?? null), strtoupper($methodName));
1348        }
1349    }
1350
1351    /**
1352     * Magic method to send requests by the static method name, i.e. Client::get('http://localhost/');
1353     *
1354     * @param  string $methodName
1355     * @param  array  $arguments
1356     * @throws Exception|Client\Exception|Client\Handler\Exception
1357     * @return Response|Promise|array|string
1358     */
1359    public static function __callStatic(string $methodName, array $arguments): Response|Promise|array|string
1360    {
1361        $client = new static();
1362        $uri    = null;
1363
1364        if (count($arguments