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