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