Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
99.21% covered (success)
99.21%
252 / 254
96.49% covered (success)
96.49%
55 / 57
CRAP
0.00% covered (danger)
0.00%
0 / 1
Client
99.21% covered (success)
99.21%
252 / 254
96.49% covered (success)
96.49%
55 / 57
169
0.00% covered (danger)
0.00%
0 / 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%
5 / 5
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
 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
 setData
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 addData
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getData
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
4
 hasData
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 removeData
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 removeAllData
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 setQuery
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 addQuery
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getQuery
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
4
 hasQuery
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 removeQuery
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 removeAllQuery
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 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
3
 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
2
 removeFile
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 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
97.78% covered (success)
97.78%
44 / 45
0.00% covered (danger)
0.00%
0 / 1
26
 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
92.31% covered (success)
92.31%
12 / 13
0.00% covered (danger)
0.00%
0 / 1
5.01
 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\Handler\Stream;
17use Pop\Http\Client\Request;
18use Pop\Http\Client\Response;
19use Pop\Http\Client\Handler\Curl;
20use Pop\Http\Client\Handler\CurlMulti;
21use Pop\Http\Client\Handler\HandlerInterface;
22use Pop\Mime\Part\Body;
23
24/**
25 * HTTP client class
26 *
27 * @category   Pop
28 * @package    Pop\Http
29 * @author     Nick Sagona, III <dev@nolainteractive.com>
30 * @copyright  Copyright (c) 2009-2024 NOLA Interactive, LLC. (http://www.nolainteractive.com)
31 * @license    http://www.popphp.org/license     New BSD License
32 * @version    5.0.0
33 */
34class Client extends AbstractHttp
35{
36
37    /**
38     * Client options
39     * @var array
40     */
41    protected array $options = [];
42
43    /**
44     * Request handler
45     * @var ?HandlerInterface
46     */
47    protected ?HandlerInterface $handler = null;
48
49    /**
50     * Request multi-handler
51     * @var ?CurlMulti
52     */
53    protected ?CurlMulti $multiHandler = null;
54
55    /**
56     * HTTP auth object
57     * @var ?Auth
58     */
59    protected ?Auth $auth = null;
60
61    /**
62     * Instantiate the client object
63     *
64     * Optional parameters are a request URI string, client request instance, a client response instance,
65     * a client handler instance, an auth instance, or an options array
66     */
67    public function __construct()
68    {
69        $args     = func_get_args();
70        $request  = null;
71        $response = null;
72        $handler  = null;
73
74        foreach ($args as $arg) {
75            if (is_string($arg)) {
76                $request = new Request($arg);
77            } else if ($arg instanceof Client\Request) {
78                $request = $arg;
79            } else if ($arg instanceof Client\Response) {
80                $response = $arg;
81            } else if ($arg instanceof Client\Handler\HandlerInterface) {
82                $handler = $arg;
83            } else if ($arg instanceof Auth) {
84                $this->setAuth($arg);
85            } else if (is_array($arg)) {
86                $this->setOptions($arg);
87            }
88        }
89
90        parent::__construct($request, $response);
91
92        if ($handler !== null) {
93            if ($handler instanceof CurlMulti) {
94                $this->setMultiHandler($handler);
95            } else {
96                $this->setHandler($handler);
97            }
98        }
99    }
100
101    /**
102     * Factory to create a multi-handler object
103     *
104     * @param  array $requests
105     * @return CurlMulti
106     */
107    public static function createMulti(array $requests): CurlMulti
108    {
109        $multiHandler = new Client\Handler\CurlMulti();
110
111        foreach ($requests as $request) {
112            $client = new Client($request);
113            $client->setMultiHandler($multiHandler);
114        }
115
116        return $multiHandler;
117    }
118
119    /**
120     * Method to convert Curl CLI command to a client object
121     *
122     * @param  string $command
123     * @throws Exception
124     * @return Client
125     */
126    public static function fromCurlCommand(string $command): Client
127    {
128        return Curl\Command::commandToClient($command);
129    }
130
131    /**
132     * Set method
133     *
134     * @param  string $method
135     * @return Client
136     */
137    public function setMethod(string $method): Client
138    {
139        if ($this->hasRequest()) {
140            $this->request->setMethod($method);
141        } else {
142            $this->options['method'] = $method;
143        }
144
145        return $this;
146    }
147
148    /**
149     * Get method
150     *
151     * @return string|null
152     */
153    public function getMethod(): string|null
154    {
155        if ($this->hasRequest()) {
156            return $this->request->getMethod();
157        } else {
158            return $this->options['method'] ?? null;
159        }
160    }
161
162    /**
163     * Has method
164     *
165     * @return bool
166     */
167    public function hasMethod(): bool
168    {
169        return ((($this->hasRequest()) && !empty($this->request->getMethod())) || isset($this->options['method']));
170    }
171
172    /**
173     * Set options
174     *
175     * Supported options
176     *  - 'base_uri'
177     *  - 'method'
178     *  - 'headers'
179     *  - 'user_agent'
180     *  - 'query' (can only be encoded query string on the URI)
181     *  - 'data' (can be any request data)
182     *  - 'files'
183     *  - 'type'
184     *  - 'auto'
185     *  - 'async'
186     *  - 'verify_peer'
187     *  - 'allow_self_signed'
188     *  - 'force_custom_method' (Curl only - forces CURLOPT_CUSTOMREQUEST)
189     *
190     * @param  array $options
191     * @return Client
192     */
193    public function setOptions(array $options): Client
194    {
195        $this->options = $options;
196        return $this;
197    }
198
199    /**
200     * Add options
201     *
202     * @param  array $options
203     * @return Client
204     */
205    public function addOptions(array $options): Client
206    {
207        foreach ($options as $name => $value) {
208            $this->addOption($name, $value);
209        }
210        return $this;
211    }
212
213    /**
214     * Add an option
215     *
216     * @param  mixed  $value
217     * @return Client
218     */
219    public function addOption(string $name, mixed $value): Client
220    {
221        $this->options[$name] = $value;
222        return $this;
223    }
224
225    /**
226     * Get options
227     *
228     * @return array
229     */
230    public function getOptions(): array
231    {
232        return $this->options;
233    }
234
235    /**
236     * Get options
237     *
238     * @param  string $name
239     * @return mixed
240     */
241    public function getOption(string $name): mixed
242    {
243        return $this->options[$name] ?? null;
244    }
245
246    /**
247     * Has options
248     *
249     * @return bool
250     */
251    public function hasOptions(): bool
252    {
253        return !empty($this->options);
254    }
255
256    /**
257     * Has option
258     *
259     * @return bool
260     */
261    public function hasOption(string $name): bool
262    {
263        return isset($this->options[$name]);
264    }
265
266    /**
267     * Set handler
268     *
269     * @param  HandlerInterface $handler
270     * @return Client
271     */
272    public function setHandler(HandlerInterface $handler): Client
273    {
274        $this->handler = $handler;
275        return $this;
276    }
277
278    /**
279     * Get handler
280     *
281     * @return HandlerInterface|null
282     */
283    public function getHandler(): HandlerInterface|null
284    {
285        return $this->handler;
286    }
287
288    /**
289     * Has handler
290     *
291     * @return bool
292     */
293    public function hasHandler(): bool
294    {
295        return ($this->handler !== null);
296    }
297
298    /**
299     * Set multi-handler
300     *
301     * @param  CurlMulti $multiHandler
302     * @return Client
303     */
304    public function setMultiHandler(CurlMulti $multiHandler): Client
305    {
306        $this->multiHandler = $multiHandler;
307
308        if (!($this->handler instanceof Curl)) {
309            $this->handler = new Curl();
310            $this->multiHandler->addClient($this);
311        }
312
313        return $this;
314    }
315
316    /**
317     * Get multi-handler
318     *
319     * @return CurlMulti
320     */
321    public function getMultiHandler(): CurlMulti
322    {
323        return $this->multiHandler;
324    }
325
326    /**
327     * Has multi-handler
328     *
329     * @return bool
330     */
331    public function hasMultiHandler(): bool
332    {
333        return ($this->multiHandler !== null);
334    }
335
336    /**
337     * Set auth
338     *
339     * @param  Auth $auth
340     * @return Client
341     */
342    public function setAuth(Auth $auth): Client
343    {
344        $this->auth = $auth;
345        return $this;
346    }
347
348    /**
349     * Get auth
350     *
351     * @return Auth|null
352     */
353    public function getAuth(): Auth|null
354    {
355        return $this->auth;
356    }
357
358    /**
359     * Has auth
360     *
361     * @return bool
362     */
363    public function hasAuth(): bool
364    {
365        return ($this->auth !== null);
366    }
367
368    /**
369     * Set data
370     *
371     * @param  array $data
372     * @return Client
373     */
374    public function setData(array $data): Client
375    {
376        $this->options['data'] = $data;
377        return $this;
378    }
379
380    /**
381     * Add data
382     *
383     * @param  string $name
384     * @param  mixed  $value
385     * @return Client
386     */
387    public function addData(string $name, mixed $value): Client
388    {
389        if (!isset($this->options['data'])) {
390            $this->options['data'] = [];
391        }
392        $this->options['data'][$name] = $value;
393        return $this;
394    }
395
396    /**
397     * Get data
398     *
399     * @param  ?string $key
400     * @return mixed
401     */
402    public function getData(?string $key = null): mixed
403    {
404        if ($key !== null) {
405            return (isset($this->options['data']) && isset($this->options['data'][$key])) ?
406                $this->options['data'][$key] : null;
407        } else {
408            return $this->options['data'] ?? null;
409        }
410    }
411
412    /**
413     * Has data
414     *
415     * @param  ?string $key
416     * @return bool
417     */
418    public function hasData(?string $key = null): bool
419    {
420        if ($key !== null) {
421            return (isset($this->options['data']) && isset($this->options['data'][$key]));
422        } else {
423            return isset($this->options['data']);
424        }
425    }
426
427    /**
428     * Remove data
429     *
430     * @param  string $key
431     * @return Client
432     */
433    public function removeData(string $key): Client
434    {
435        if (isset($this->options['data']) && isset($this->options['data'][$key])) {
436            unset($this->options['data'][$key]);
437        }
438
439        return $this;
440    }
441
442    /**
443     * Remove all data
444     *
445     * @return Client
446     */
447    public function removeAllData(): Client
448    {
449        if (isset($this->options['data'])) {
450            unset($this->options['data']);
451        }
452
453        return $this;
454    }
455
456    /**
457     * Set query
458     *
459     * @param  array $query
460     * @return Client
461     */
462    public function setQuery(array $query): Client
463    {
464        $this->options['query'] = $query;
465        return $this;
466    }
467
468    /**
469     * Add query
470     *
471     * @param  string $name
472     * @param  mixed  $value
473     * @return Client
474     */
475    public function addQuery(string $name, mixed $value): Client
476    {
477        if (!isset($this->options['query'])) {
478            $this->options['query'] = [];
479        }
480        $this->options['query'][$name] = $value;
481        return $this;
482    }
483
484    /**
485     * Get query
486     *
487     * @param  ?string $key
488     * @return mixed
489     */
490    public function getQuery(?string $key = null): mixed
491    {
492        if ($key !== null) {
493            return (isset($this->options['query']) && isset($this->options['query'][$key])) ?
494                $this->options['query'][$key] : null;
495        } else {
496            return $this->options['query'] ?? null;
497        }
498    }
499
500    /**
501     * Has query
502     *
503     * @param  ?string $key
504     * @return bool
505     */
506    public function hasQuery(?string $key = null): bool
507    {
508        if ($key !== null) {
509            return (isset($this->options['query']) && isset($this->options['query'][$key]));
510        } else {
511            return isset($this->options['query']);
512        }
513    }
514
515    /**
516     * Remove query
517     *
518     * @param  string $key
519     * @return Client
520     */
521    public function removeQuery(string $key): Client
522    {
523        if (isset($this->options['query']) && isset($this->options['query'][$key])) {
524            unset($this->options['query'][$key]);
525        }
526
527        return $this;
528    }
529
530    /**
531     * Remove all query data
532     *
533     * @return Client
534     */
535    public function removeAllQuery(): Client
536    {
537        if (isset($this->options['query'])) {
538            unset($this->options['query']);
539        }
540
541        return $this;
542    }
543
544    /**
545     * Set files
546     *
547     * @param  array|string $files
548     * @param  bool         $multipart
549     * @throws Exception
550     * @return Client
551     */
552    public function setFiles(array|string $files, bool $multipart = true): Client
553    {
554        if (is_string($files)) {
555            $files = [$files];
556        }
557
558        $filenames = [];
559
560        foreach ($files as $i => $file) {
561            if (!file_exists($file)) {
562                throw new Exception("Error: The file '" . $file . "' does not exist.");
563            }
564
565            $name = (is_numeric($i)) ? 'file' . ($i + 1) : $i;
566            $filenames[$name] = $file;
567        }
568
569        $this->options['files'] = $filenames;
570
571        if ($multipart) {
572            $this->options['type'] = Request::MULTIPART;
573        }
574        return $this;
575    }
576
577    /**
578     * Add file
579     *
580     * @param  string  $file
581     * @param  ?string $name
582     * @throws Exception
583     * @return Client
584     */
585    public function addFile(string $file, ?string $name = null): Client
586    {
587        if (!file_exists($file)) {
588            throw new Exception("Error: The file '" . $file . "' does not exist.");
589        }
590
591        if (!isset($this->options['files'])) {
592            $this->options['files'] = [];
593        }
594
595        if ($name === null) {
596            $i = 1;
597            $name = 'file' . $i;
598            while (isset($this->options['files'][$name])) {
599                $i++;
600                $name = 'file' . $i;
601            }
602        }
603
604        $this->options['files'][$name] = $file;
605        return $this;
606    }
607
608    /**
609     * Get files
610     *
611     * @return array|null
612     */
613    public function getFiles(): array|null
614    {
615        return $this->options['files'] ?? null;
616    }
617
618    /**
619     * Get file
620     *
621     * @param  string $key
622     * @return string|null
623     */
624    public function getFile(string $key): string|null
625    {
626        return (isset($this->options['files']) && isset($this->options['files'][$key])) ?
627                $this->options['files'][$key] : null;
628    }
629
630    /**
631     * Has files
632     *
633     * @return bool
634     */
635    public function hasFiles(): bool
636    {
637        return isset($this->options['files']);
638    }
639
640    /**
641     * Has file
642     *
643     * @param  string $key
644     * @return bool
645     */
646    public function hasFile(string $key): bool
647    {
648        return (isset($this->options['files']) && isset($this->options['files'][$key]));
649    }
650
651    /**
652     * Remove file
653     *
654     * @param  string $key
655     * @return Client
656     */
657    public function removeFile(string $key): Client
658    {
659        if (isset($this->options['files']) && isset($this->options['files'][$key])) {
660            unset($this->options['files'][$key]);
661        }
662
663        return $this;
664    }
665
666    /**
667     * Remove all files
668     *
669     * @return Client
670     */
671    public function removeFiles(): Client
672    {
673        if (isset($this->options['files'])) {
674            unset($this->options['files']);
675        }
676
677        return $this;
678    }
679
680    /**
681     * Set request body
682     *
683     * @param  string $body
684     * @return Client
685     */
686    public function setBody(string $body): Client
687    {
688        if ($this->request === null) {
689            throw new Exception('Error: The request object has not been created.');
690        }
691
692        $this->request->setBody($body);
693
694        return $this;
695    }
696
697    /**
698     * Set request body from file
699     *
700     * @param  string $file
701     * @return Client
702     */
703    public function setBodyFromFile(string $file): Client
704    {
705        if ($this->request === null) {
706            throw new Exception('Error: The request object has not been created.');
707        }
708        if (!file_exists($file)) {
709            throw new Exception("Error: The file '" . $file . "' does not exist.");
710        }
711
712        $this->request->setBody(file_get_contents($file));
713
714        return $this;
715    }
716
717    /**
718     * Has request body
719     *
720     * @return bool
721     */
722    public function hasBody(): bool
723    {
724        return (($this->request !== null) && ($this->request->hasBody()));
725    }
726
727    /**
728     * Get request body
729     *
730     * @return Body|null
731     */
732    public function getBody(): Body|null
733    {
734        return (($this->request !== null) && ($this->request->hasBody())) ? $this->request->getBody() : null;
735    }
736
737    /**
738     * Get request body content
739     *
740     * @return string|null
741     */
742    public function getBodyContent(): string|null
743    {
744        return (($this->request !== null) && ($this->request->hasBody())) ? $this->request->getBodyContent() : null;
745    }
746
747    /**
748     * Get request body content length
749     *
750     * @return int
751     */
752    public function getBodyContentLength(): int
753    {
754        return (($this->request !== null) && ($this->request->hasBody())) ? $this->request->getBodyContentLength() : 0;
755    }
756
757    /**
758     * Remove the body
759     *
760     * @return Client
761     */
762    public function removeBody(): Client
763    {
764        if (($this->request !== null) && ($this->request->hasBody())) {
765            $this->request->removeBody();
766        }
767        return $this;
768    }
769
770    /**
771     * Prepare the client request
772     *
773     * @param  ?string $uri
774     * @param  ?string  $method
775     * @throws Exception|Client\Exception
776     * @return Client
777     */
778    public function prepare(?string $uri = null, string $method = null): Client
779    {
780        if ((!$this->hasRequest()) && ($uri === null)) {
781            throw new Exception('Error: There is no request URI to send.');
782        }
783
784        if (($method === null) && isset($this->options['method'])) {
785            $method = $this->options['method'];
786        }
787
788        // Set request URI
789        if ($uri !== null) {
790            if ($this->hasRequest()) {
791                $this->request->setUri(new Uri($uri));
792            } else {
793                $request = new Request(new Uri($uri), ($method ?? 'GET'));
794                $this->setRequest($request);
795            }
796        } else if ($method !== null) {
797            $this->request->setMethod($method);
798        }
799
800        // Add headers
801        if (($this->hasOption('headers')) && is_array($this->options['headers'])) {
802            $this->request->addHeaders($this->options['headers']);
803        }
804
805        // Add data and files
806        $data = [];
807        if ($this->hasOption('data')) {
808            $data = $this->options['data'];
809        }
810
811        if ($this->hasOption('files')) {
812            $files = $this->options['files'];
813            foreach ($files as $file => $value) {
814                $file = (is_numeric($file)) ? 'file' . ($file + 1) : $file;
815                $data[$file] = [
816                    'filename'    => $value,
817                    'contentType' => Client\Data::getMimeTypeFromFilename($value)
818                ];
819            }
820        }
821
822        if (!empty($data)) {
823            $this->request->setData($data);
824        }
825
826        // Add query
827        if ($this->hasOption('query')) {
828            $this->request->setQuery($this->options['query']);
829        }
830
831        // Set request type
832        if ($this->hasOption('type')) {
833            $this->request->setRequestType($this->options['type']);
834        }
835
836        // Set handler
837        if (!$this->hasHandler()) {
838            $this->setHandler(new Curl());
839        }
840
841        // Set user-agent
842        if ($this->hasOption('user_agent')) {
843            if ($this->handler instanceof Curl) {
844                $this->handler->setOption(CURLOPT_USERAGENT, $this->options['user_agent']);
845            } else if ($this->handler instanceof Stream) {
846                $this->handler->addContextOption('http', ['user_agent' => $this->options['user_agent']]);
847            }
848        }
849
850        // Handle SSL options
851        if (!($this->handler instanceof CurlMulti)) {
852            if ($this->hasOption('verify_peer')) {
853                $this->handler->setVerifyPeer((bool)$this->options['verify_peer']);
854            }
855            if ($this->hasOption('allow_self_signed')) {
856                $this->handler->allowSelfSigned((bool)$this->options['allow_self_signed']);
857            }
858        }
859
860        // Adjust for base_uri
861        if (($this->hasOption('base_uri')) && !str_starts_with($this->request->getUriAsString(), $this->getOption('base_uri'))) {
862            $this->request->setUri($this->getOption('base_uri') . $this->request->getUriAsString());
863        }
864
865        return $this;
866    }
867
868    /**
869     * Send the client request
870     *
871     * @param  ?string $uri
872     * @param  ?string $method
873     * @throws Exception|Client\Exception|Client\Handler\Exception
874     * @return Response|Promise|array|string
875     */
876    public function send(?string $uri = null, string $method = null): Response|Promise|array|string
877    {
878        if (isset($this->options['async']) && ($this->options['async'] === true)) {
879            return $this->sendAsync();
880        } else {
881            $this->prepare($uri, $method);
882            $this->response = (isset($this->options['force_custom_method']) && ($this->handler instanceof Curl)) ?
883                $this->handler->prepare($this->request, $this->auth, (bool)$this->options['force_custom_method'])->send() :
884                $this->handler->prepare($this->request, $this->auth)->send();
885
886            return (($this->hasOption('auto')) && ($this->options['auto']) && ($this->response instanceof Response)) ?
887                $this->response->getParsedResponse() : $this->response;
888        }
889    }
890
891    /**
892     * Method to send the request asynchronously
893     *
894     * @return Promise
895     */
896    public function sendAsync(): Promise
897    {
898        return new Promise($this);
899    }
900
901    /**
902     * Method to render the request as a string
903     *
904     * @return string
905     */
906    public function render(): string
907    {
908        $this->prepare();
909
910        if (isset($this->options['force_custom_method']) && ($this->handler instanceof Curl)) {
911            $this->handler->prepare($this->request, $this->auth, (bool)$this->options['force_custom_method']);
912        } else {
913            $this->handler->prepare($this->request, $this->auth);
914        }
915
916        $uri       = $this->handler->getUriObject();
917        $uriString = $uri->getUri();
918        if ($uri->hasQuery()) {
919            $uriString .= '?' . $uri->getQuery();
920        }
921
922        $request = $this->request->getMethod() . ' ' . $uriString . ' HTTP/' . $this->handler->getHttpVersion() . "\r\n" .
923            'Host: ' . $uri->getFullHost() . "\r\n" . $this->request->getHeadersAsString() . "\r\n";
924
925        if ($this->request->hasDataContent()) {
926            $request .= $this->request->getDataContent();
927        }
928        return $request;
929    }
930
931    /**
932     * Method to convert client object to a Curl command for the CLI
933     *
934     * @throws Exception|Curl\Exception
935     * @return string
936     */
937    public function toCurlCommand(): string
938    {
939        if ($this->handler instanceof Stream) {
940            throw new Exception('Error: The client handler must be an instance of Curl');
941        }
942
943        if (!$this->hasHandler()) {
944            $this->setHandler(new Curl());
945        }
946
947        return Curl\Command::clientToCommand($this);
948    }
949
950    /**
951     * To string magic method to render the client request to a raw string
952     *
953     */
954    public function __toString(): string
955    {
956        return $this->render();
957    }
958
959    /**
960     * Magic method to send requests by the method name, i.e. $client->get('http://localhost/');
961     *
962     * @param  string $methodName
963     * @param  array  $arguments
964     * @throws Exception|Client\Exception
965     * @return Response|Promise|array|string
966     */
967    public function __call(string $methodName, array $arguments): Response|Promise|array|string
968    {
969        if (str_contains($methodName, 'Async')) {
970            if (isset($arguments[0])) {
971                $methodName = strtoupper(substr($methodName, 0, strpos($methodName, 'Async')));
972                $this->prepare($arguments[0], $methodName);
973            }
974            return $this->sendAsync();
975        } else {
976            return $this->send(($arguments[0] ?? null), strtoupper($methodName));
977        }
978    }
979
980    /**
981     * Magic method to send requests by the static method name, i.e. Client::get('http://localhost/');
982     *
983     * @param  string $methodName
984     * @param  array  $arguments
985     * @throws Exception|Client\Exception
986     * @return Response|Promise|array|string
987     */
988    public static function __callStatic(string $methodName, array $arguments): Response|Promise|array|string
989    {
990        $client = new static();
991        $uri    = null;
992
993        if (count($arguments) > 1) {
994            foreach ($arguments as $arg) {
995                if (is_string($arg)) {
996                    $client->setRequest(new Client\Request($arg));
997                } else if ($arg instanceof Client\Request) {
998                    $client->setRequest($arg);
999                } else if ($arg instanceof Client\Response) {
1000                    $client->setResponse($arg);
1001                } else if ($arg instanceof Client\Handler\HandlerInterface) {
1002                    $client->setHandler($arg);
1003                } else if ($arg instanceof Auth) {
1004                    $client->setAuth($arg);
1005                } else if (is_array($arg)) {
1006                    $client->setOptions($arg);
1007                }
1008            }
1009        }
1010
1011        if ((!$client->hasRequest()) && isset($arguments[0])) {
1012            $uri = ($arguments[0]);
1013        }
1014
1015        if (str_contains($methodName, 'Async')) {
1016            $methodName = strtoupper(substr($methodName, 0, strpos($methodName, 'Async')));
1017            $client->prepare($uri, $methodName);
1018            return $client->sendAsync();
1019        } else {
1020            return $client->send($uri, strtoupper($methodName));
1021        }
1022    }
1023
1024}