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