Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
427 / 427
100.00% covered (success)
100.00%
40 / 40
CRAP
100.00% covered (success)
100.00%
1 / 1
Request
100.00% covered (success)
100.00%
427 / 427
100.00% covered (success)
100.00%
40 / 40
78
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 __toString
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
8
 getMultipartBody
100.00% covered (success)
100.00%
39 / 39
100.00% covered (success)
100.00%
1 / 1
4
 getFileInfo
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
5
 setUrl
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getUrl
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getMethod
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isMethod
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setMethod
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setProtocol
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setBody
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 setJson
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 setPost
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 hasFiles
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getFiles
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setFiles
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 setContentType
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 setCookie
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 setCookies
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 removeCookie
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 removeCookies
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 setCookieHeader
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 setHeader
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setHeaders
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 removeHeader
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 removeHeaders
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setBasicAuth
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 setBearerAuth
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 setUserAgent
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setDownloadFunction
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 setOption
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 setOptions
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getOptions
100.00% covered (success)
100.00%
26 / 26
100.00% covered (success)
100.00%
1 / 1
5
 getOption
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getPostAndFiles
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
1 / 1
8
 setGetResponseInfo
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 isGettingResponseInfo
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setCheckOptions
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 isCheckingOptions
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 checkOption
100.00% covered (success)
100.00%
220 / 220
100.00% covered (success)
100.00%
1 / 1
6
1<?php declare(strict_types=1);
2/*
3 * This file is part of Aplus Framework HTTP Client Library.
4 *
5 * (c) Natan Felles <natanfelles@gmail.com>
6 *
7 * For the full copyright and license information, please view the LICENSE
8 * file that was distributed with this source code.
9 */
10namespace Framework\HTTP\Client;
11
12use CURLFile;
13use CURLStringFile;
14use Framework\Helpers\ArraySimple;
15use Framework\HTTP\Cookie;
16use Framework\HTTP\Header;
17use Framework\HTTP\Message;
18use Framework\HTTP\Method;
19use Framework\HTTP\Protocol;
20use Framework\HTTP\RequestHeader;
21use Framework\HTTP\RequestInterface;
22use Framework\HTTP\URL;
23use InvalidArgumentException;
24use JetBrains\PhpStorm\ArrayShape;
25use JetBrains\PhpStorm\Pure;
26use JsonException;
27use OutOfBoundsException;
28use SensitiveParameter;
29
30/**
31 * Class Request.
32 *
33 * @package http-client
34 */
35class Request extends Message implements RequestInterface
36{
37    /**
38     * HTTP Request Method.
39     */
40    protected string $method = Method::GET;
41    /**
42     * HTTP Request URL.
43     */
44    protected URL $url;
45    /**
46     * POST files.
47     *
48     * @var array<string,array<mixed>|string>
49     */
50    protected array $files = [];
51    /**
52     * Client default curl options.
53     *
54     * @var array<int,mixed>
55     */
56    protected array $defaultOptions = [
57        \CURLOPT_CONNECTTIMEOUT => 10,
58        \CURLOPT_TIMEOUT => 60,
59        \CURLOPT_FOLLOWLOCATION => false,
60        \CURLOPT_MAXREDIRS => 1,
61        \CURLOPT_AUTOREFERER => true,
62        \CURLOPT_RETURNTRANSFER => true,
63        \CURLOPT_ENCODING => '',
64    ];
65    /**
66     * Custom curl options.
67     *
68     * @var array<int,mixed>
69     */
70    protected array $options = [];
71    protected bool $checkOptions = false;
72    protected bool $getResponseInfo = false;
73
74    /**
75     * Request constructor.
76     *
77     * @param string|URL $url
78     */
79    public function __construct(URL | string $url)
80    {
81        $this->setUrl($url);
82    }
83
84    public function __toString() : string
85    {
86        if ($this->parseContentType() === 'multipart/form-data') {
87            $this->setBody($this->getMultipartBody());
88        }
89        if ( ! $this->hasHeader(RequestHeader::ACCEPT)) {
90            $accept = '*/*';
91            $this->setHeader(RequestHeader::ACCEPT, $accept);
92        }
93        $options = $this->getOptions();
94        if (isset($options[\CURLOPT_ENCODING])
95            && ! $this->hasHeader(RequestHeader::ACCEPT_ENCODING)
96        ) {
97            $encoding = $options[\CURLOPT_ENCODING] === ''
98                ? 'deflate, gzip, br, zstd'
99                : $options[\CURLOPT_ENCODING];
100            $this->setHeader(RequestHeader::ACCEPT_ENCODING, $encoding);
101        }
102        $message = parent::__toString();
103        if (isset($accept)) {
104            $this->removeHeader(RequestHeader::ACCEPT);
105        }
106        if (isset($encoding)) {
107            $this->removeHeader(RequestHeader::ACCEPT_ENCODING);
108        }
109        return $message;
110    }
111
112    protected function getMultipartBody() : string
113    {
114        $bodyParts = [];
115        \parse_str($this->getBody(), $post);
116        /**
117         * @var array<string,string> $post
118         */
119        $post = ArraySimple::convert($post);
120        foreach ($post as $field => $value) {
121            $field = \htmlspecialchars($field, \ENT_QUOTES | \ENT_HTML5);
122            $bodyParts[] = \implode("\r\n", [
123                "Content-Disposition: form-data; name=\"{$field}\"",
124                '',
125                $value,
126            ]);
127        }
128        /**
129         * @var array<string,CURLFile|string> $files
130         */
131        $files = ArraySimple::convert($this->getFiles());
132        foreach ($files as $field => $file) {
133            $field = (string) $field;
134            $field = \htmlspecialchars($field, \ENT_QUOTES | \ENT_HTML5);
135            $info = $this->getFileInfo($file);
136            $filename = \htmlspecialchars($info['filename'], \ENT_QUOTES | \ENT_HTML5);
137            $bodyParts[] = \implode("\r\n", [
138                'Content-Disposition: form-data; name="' . $field . '"; filename="' . $filename . '"',
139                'Content-Type: ' . $info['mime'],
140                '',
141                $info['data'],
142            ]);
143        }
144        unset($info);
145        $boundary = \str_repeat('-', 24) . \substr(\md5(\implode("\r\n", $bodyParts)), 0, 16);
146        $this->setHeader(
147            Header::CONTENT_TYPE,
148            'multipart/form-data; charset=UTF-8; boundary=' . $boundary
149        );
150        foreach ($bodyParts as &$part) {
151            $part = "--{$boundary}\r\n{$part}";
152        }
153        unset($part);
154        $bodyParts[] = "--{$boundary}--";
155        $bodyParts[] = '';
156        $bodyParts = \implode("\r\n", $bodyParts);
157        $this->setHeader(
158            Header::CONTENT_LENGTH,
159            (string) \strlen($bodyParts)
160        );
161        return $bodyParts;
162    }
163
164    /**
165     * @param CURLFile|CURLStringFile|string $file
166     *
167     * @return array<string,string>
168     */
169    #[ArrayShape(['filename' => 'string', 'data' => 'string', 'mime' => 'string'])]
170    protected function getFileInfo(CURLFile | CURLStringFile | string $file) : array
171    {
172        if ($file instanceof CURLFile) {
173            return [
174                'filename' => $file->getPostFilename(),
175                'data' => (string) \file_get_contents($file->getFilename()),
176                'mime' => $file->getMimeType() ?: 'application/octet-stream',
177            ];
178        }
179        if ($file instanceof CURLStringFile) {
180            return [
181                'filename' => $file->postname,
182                'data' => $file->data,
183                'mime' => $file->mime,
184            ];
185        }
186        return [
187            'filename' => \basename($file),
188            'data' => (string) \file_get_contents($file),
189            'mime' => \mime_content_type($file) ?: 'application/octet-stream',
190        ];
191    }
192
193    /**
194     * @param string|URL $url
195     *
196     * @return static
197     */
198    public function setUrl(string | URL $url) : static
199    {
200        if ( ! $url instanceof URL) {
201            $url = new URL($url);
202        }
203        $this->setHeader(RequestHeader::HOST, $url->getHost());
204        return parent::setUrl($url);
205    }
206
207    #[Pure]
208    public function getUrl() : URL
209    {
210        return parent::getUrl();
211    }
212
213    #[Pure]
214    public function getMethod() : string
215    {
216        return parent::getMethod();
217    }
218
219    /**
220     * @param string $method
221     *
222     * @throws InvalidArgumentException for invalid method
223     *
224     * @return bool
225     */
226    public function isMethod(string $method) : bool
227    {
228        return parent::isMethod($method);
229    }
230
231    /**
232     * @param string $method
233     *
234     * @return static
235     */
236    public function setMethod(string $method) : static
237    {
238        return parent::setMethod($method);
239    }
240
241    /**
242     * @param string $protocol
243     *
244     * @return static
245     */
246    public function setProtocol(string $protocol) : static
247    {
248        return parent::setProtocol($protocol);
249    }
250
251    /**
252     * Set the request body.
253     *
254     * @param array<string,mixed>|string $body
255     *
256     * @return static
257     */
258    public function setBody(array | string $body) : static
259    {
260        if (\is_array($body)) {
261            $body = \http_build_query($body);
262        }
263        return parent::setBody($body);
264    }
265
266    /**
267     * Set body with JSON data.
268     *
269     * @param mixed $data
270     * @param int $options [optional] <p>
271     * Bitmask consisting of <b>JSON_HEX_QUOT</b>,
272     * <b>JSON_HEX_TAG</b>,
273     * <b>JSON_HEX_AMP</b>,
274     * <b>JSON_HEX_APOS</b>,
275     * <b>JSON_NUMERIC_CHECK</b>,
276     * <b>JSON_PRETTY_PRINT</b>,
277     * <b>JSON_UNESCAPED_SLASHES</b>,
278     * <b>JSON_FORCE_OBJECT</b>,
279     * <b>JSON_UNESCAPED_UNICODE</b>.
280     * <b>JSON_THROW_ON_ERROR</b>
281     * </p>
282     * <p>Default is <b>JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE</b>
283     * when null</p>
284     * @param int<1,max> $depth [optional] Set the maximum depth. Must be greater than zero.
285     *
286     * @throws JsonException if json_encode() fails
287     *
288     * @return static
289     */
290    public function setJson(mixed $data, int $options = null, int $depth = 512) : static
291    {
292        if ($options === null) {
293            $options = \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE;
294        }
295        $data = \json_encode($data, $options | \JSON_THROW_ON_ERROR, $depth);
296        $this->setContentType('application/json');
297        $this->setBody($data);
298        return $this;
299    }
300
301    /**
302     * Set POST data simulating a browser request.
303     *
304     * @param array<string,mixed> $data
305     *
306     * @return static
307     */
308    public function setPost(array $data) : static
309    {
310        $this->setMethod(Method::POST);
311        $this->setBody($data);
312        return $this;
313    }
314
315    #[Pure]
316    public function hasFiles() : bool
317    {
318        return ! empty($this->files);
319    }
320
321    /**
322     * Get files for upload.
323     *
324     * @return array<mixed>
325     */
326    #[Pure]
327    public function getFiles() : array
328    {
329        return $this->files;
330    }
331
332    /**
333     * Set files for upload.
334     *
335     * @param array<mixed> $files Fields as keys, files (CURLFile,
336     * CURLStringFile or string filename) as values.
337     * Multi-dimensional array is allowed.
338     *
339     * @throws InvalidArgumentException for invalid file path
340     *
341     * @return static
342     */
343    public function setFiles(array $files) : static
344    {
345        $this->setMethod(Method::POST);
346        $this->setContentType('multipart/form-data');
347        $this->files = $files;
348        return $this;
349    }
350
351    /**
352     * Set the Content-Type header.
353     *
354     * @param string $mime
355     * @param string $charset
356     *
357     * @return static
358     */
359    public function setContentType(string $mime, string $charset = 'UTF-8') : static
360    {
361        $this->setHeader(
362            Header::CONTENT_TYPE,
363            $mime . ($charset ? '; charset=' . $charset : '')
364        );
365        return $this;
366    }
367
368    /**
369     * @param Cookie $cookie
370     *
371     * @return static
372     */
373    public function setCookie(Cookie $cookie) : static
374    {
375        parent::setCookie($cookie);
376        $this->setCookieHeader();
377        return $this;
378    }
379
380    /**
381     * @param array<int,Cookie> $cookies
382     *
383     * @return static
384     */
385    public function setCookies(array $cookies) : static
386    {
387        return parent::setCookies($cookies);
388    }
389
390    /**
391     * @param string $name
392     *
393     * @return static
394     */
395    public function removeCookie(string $name) : static
396    {
397        parent::removeCookie($name);
398        $this->setCookieHeader();
399        return $this;
400    }
401
402    /**
403     * @param array<int,string> $names
404     *
405     * @return static
406     */
407    public function removeCookies(array $names) : static
408    {
409        parent::removeCookies($names);
410        $this->setCookieHeader();
411        return $this;
412    }
413
414    /**
415     * @return static
416     */
417    protected function setCookieHeader() : static
418    {
419        $line = [];
420        foreach ($this->getCookies() as $cookie) {
421            $line[] = $cookie->getName() . '=' . $cookie->getValue();
422        }
423        if ($line) {
424            $line = \implode('; ', $line);
425            return $this->setHeader(RequestHeader::COOKIE, $line);
426        }
427        return $this->removeHeader(RequestHeader::COOKIE);
428    }
429
430    /**
431     * @param string $name
432     * @param string $value
433     *
434     * @return static
435     */
436    public function setHeader(string $name, string $value) : static
437    {
438        return parent::setHeader($name, $value);
439    }
440
441    /**
442     * @param array<string,string> $headers
443     *
444     * @return static
445     */
446    public function setHeaders(array $headers) : static
447    {
448        return parent::setHeaders($headers);
449    }
450
451    /**
452     * @param string $name
453     *
454     * @return static
455     */
456    public function removeHeader(string $name) : static
457    {
458        return parent::removeHeader($name);
459    }
460
461    /**
462     * @return static
463     */
464    public function removeHeaders() : static
465    {
466        return parent::removeHeaders();
467    }
468
469    /**
470     * Set Authorization header with Basic type.
471     *
472     * @param string $username
473     * @param string $password
474     *
475     * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization
476     *
477     * @return static
478     */
479    public function setBasicAuth(
480        string $username,
481        #[SensitiveParameter] string $password
482    ) : static {
483        return $this->setHeader(
484            RequestHeader::AUTHORIZATION,
485            'Basic ' . \base64_encode($username . ':' . $password)
486        );
487    }
488
489    /**
490     * Set Authorization header with Bearer type.
491     *
492     * @param string $token
493     *
494     * @return static
495     */
496    public function setBearerAuth(#[SensitiveParameter] string $token) : static
497    {
498        return $this->setHeader(
499            RequestHeader::AUTHORIZATION,
500            'Bearer ' . $token
501        );
502    }
503
504    /**
505     * Set the User-Agent header.
506     *
507     * @param string|null $userAgent
508     *
509     * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent
510     *
511     * @return static
512     */
513    public function setUserAgent(string $userAgent = null) : static
514    {
515        $userAgent ??= 'Aplus HTTP Client';
516        return $this->setHeader(RequestHeader::USER_AGENT, $userAgent);
517    }
518
519    /**
520     * Set a callback to write the response body with chunks.
521     *
522     * Used to write data to files, databases, etc...
523     *
524     * NOTE: Using this function makes the Response body, returned in the
525     * {@see Client::run()} method, be set with an empty string.
526     *
527     * @param callable $callback A callback with the response body $data chunk
528     * as first argument and the CurlHandle as the second. Return is not
529     * necessary.
530     *
531     * @return static
532     */
533    public function setDownloadFunction(callable $callback) : static
534    {
535        $function = static function (\CurlHandle $handle, string $data) use ($callback) : int {
536            $callback($data, $handle);
537            return \strlen($data);
538        };
539        $this->setOption(\CURLOPT_WRITEFUNCTION, $function);
540        return $this;
541    }
542
543    /**
544     * Set curl options.
545     *
546     * @param int $option A curl constant
547     * @param mixed $value
548     *
549     * @see Client::$defaultOptions
550     *
551     * @return static
552     */
553    public function setOption(int $option, mixed $value) : static
554    {
555        if ($this->isCheckingOptions()) {
556            $this->checkOption($option, $value);
557        }
558        $this->options[$option] = $value;
559        return $this;
560    }
561
562    /**
563     * Set many curl options.
564     *
565     * @param array<int,mixed> $options Curl constants as keys and their respective values
566     *
567     * @return static
568     */
569    public function setOptions(array $options) : static
570    {
571        foreach ($options as $option => $value) {
572            $this->setOption($option, $value);
573        }
574        return $this;
575    }
576
577    /**
578     * Get default options replaced by custom.
579     *
580     * @return array<int,mixed>
581     */
582    public function getOptions() : array
583    {
584        $options = \array_replace($this->defaultOptions, $this->options);
585        $options[\CURLOPT_PROTOCOLS] = \CURLPROTO_HTTPS | \CURLPROTO_HTTP;
586        $options[\CURLOPT_HTTP_VERSION] = match ($this->getProtocol()) {
587            Protocol::HTTP_1_0 => \CURL_HTTP_VERSION_1_0,
588            Protocol::HTTP_1_1 => \CURL_HTTP_VERSION_1_1,
589            Protocol::HTTP_2_0 => \CURL_HTTP_VERSION_2_0,
590            Protocol::HTTP_2 => \CURL_HTTP_VERSION_2,
591            default => throw new InvalidArgumentException(
592                'Invalid Request Protocol: ' . $this->getProtocol()
593            )
594        };
595        switch ($this->getMethod()) {
596            case Method::POST:
597                $options[\CURLOPT_POST] = true;
598                $options[\CURLOPT_POSTFIELDS] = $this->getPostAndFiles();
599                break;
600            case Method::DELETE:
601            case Method::PATCH:
602            case Method::PUT:
603                $options[\CURLOPT_POSTFIELDS] = $this->getBody();
604                break;
605        }
606        $options[\CURLOPT_CUSTOMREQUEST] = $this->getMethod();
607        $options[\CURLOPT_HEADER] = false;
608        $options[\CURLOPT_URL] = $this->getUrl()->toString();
609        $options[\CURLOPT_HTTPHEADER] = $this->getHeaderLines();
610        return $options;
611    }
612
613    public function getOption(int $option) : mixed
614    {
615        return $this->getOptions()[$option] ?? null;
616    }
617
618    /**
619     * Returns string if the Request has not files and curl will set the
620     * Content-Type header to application/x-www-form-urlencoded. If the Request
621     * has files, returns an array and curl will set the Content-Type to
622     * multipart/form-data.
623     *
624     * If the Request has files, the $post and $files arrays are converted to
625     * the array_simple format. Because curl does not understand the PHP
626     * multi-dimensional arrays.
627     *
628     * @see https://www.php.net/manual/en/function.curl-setopt.php CURLOPT_POSTFIELDS
629     * @see ArraySimple::convert()
630     *
631     * @return array<string,mixed>|string
632     */
633    public function getPostAndFiles() : array | string
634    {
635        if ( ! $this->hasFiles()) {
636            return $this->getBody();
637        }
638        \parse_str($this->getBody(), $post);
639        $post = ArraySimple::convert($post);
640        foreach ($post as &$value) {
641            $value = (string) $value;
642        }
643        unset($value);
644        $files = ArraySimple::convert($this->getFiles());
645        foreach ($files as $field => &$file) {
646            if ($file instanceof CURLFile
647                || $file instanceof CURLStringFile
648            ) {
649                continue;
650            }
651            if ( ! \is_file($file)) {
652                throw new InvalidArgumentException(
653                    "Field '{$field}' does not match a file: {$file}"
654                );
655            }
656            $file = new CURLFile(
657                $file,
658                \mime_content_type($file) ?: 'application/octet-stream',
659                \basename($file)
660            );
661        }
662        unset($file);
663        return \array_replace($post, $files);
664    }
665
666    public function setGetResponseInfo(bool $get = true) : static
667    {
668        $this->getResponseInfo = $get;
669        return $this;
670    }
671
672    public function isGettingResponseInfo() : bool
673    {
674        return $this->getResponseInfo;
675    }
676
677    /**
678     * @param bool $check
679     *
680     * @return static
681     */
682    public function setCheckOptions(bool $check = true) : static
683    {
684        $this->checkOptions = $check;
685        return $this;
686    }
687
688    public function isCheckingOptions() : bool
689    {
690        return $this->checkOptions;
691    }
692
693    /**
694     * @param int $option The curl option
695     * @param mixed $value The curl option value
696     *
697     * @throws InvalidArgumentException if the option value does not match the
698     * expected type
699     * @throws OutOfBoundsException if the option is invalid
700     */
701    protected function checkOption(int $option, mixed $value) : void
702    {
703        $types = [
704            'bool' => [
705                \CURLOPT_AUTOREFERER,
706                \CURLOPT_COOKIESESSION,
707                \CURLOPT_CERTINFO,
708                \CURLOPT_CONNECT_ONLY,
709                \CURLOPT_CRLF,
710                \CURLOPT_DISALLOW_USERNAME_IN_URL,
711                \CURLOPT_DNS_SHUFFLE_ADDRESSES,
712                \CURLOPT_HAPROXYPROTOCOL,
713                \CURLOPT_SSH_COMPRESSION,
714                \CURLOPT_DNS_USE_GLOBAL_CACHE,
715                \CURLOPT_FAILONERROR,
716                \CURLOPT_SSL_FALSESTART,
717                \CURLOPT_FILETIME,
718                \CURLOPT_FOLLOWLOCATION,
719                \CURLOPT_FORBID_REUSE,
720                \CURLOPT_FRESH_CONNECT,
721                \CURLOPT_FTP_USE_EPRT,
722                \CURLOPT_FTP_USE_EPSV,
723                \CURLOPT_FTP_CREATE_MISSING_DIRS,
724                \CURLOPT_FTPAPPEND,
725                \CURLOPT_TCP_NODELAY,
726                // CURLOPT_FTPASCII,
727                \CURLOPT_FTPLISTONLY,
728                \CURLOPT_HEADER,
729                \CURLINFO_HEADER_OUT,
730                \CURLOPT_HTTP09_ALLOWED,
731                \CURLOPT_HTTPGET,
732                \CURLOPT_HTTPPROXYTUNNEL,
733                \CURLOPT_HTTP_CONTENT_DECODING,
734                \CURLOPT_KEEP_SENDING_ON_ERROR,
735                // CURLOPT_MUTE,
736                \CURLOPT_NETRC,
737                \CURLOPT_NOBODY,
738                \CURLOPT_NOPROGRESS,
739                \CURLOPT_NOSIGNAL,
740                \CURLOPT_PATH_AS_IS,
741                \CURLOPT_PIPEWAIT,
742                \CURLOPT_POST,
743                \CURLOPT_PUT,
744                \CURLOPT_RETURNTRANSFER,
745                \CURLOPT_SASL_IR,
746                \CURLOPT_SSL_ENABLE_ALPN,
747                \CURLOPT_SSL_ENABLE_NPN,
748                \CURLOPT_SSL_VERIFYPEER,
749                \CURLOPT_SSL_VERIFYSTATUS,
750                \CURLOPT_PROXY_SSL_VERIFYPEER,
751                \CURLOPT_SUPPRESS_CONNECT_HEADERS,
752                \CURLOPT_TCP_FASTOPEN,
753                \CURLOPT_TFTP_NO_OPTIONS,
754                \CURLOPT_TRANSFERTEXT,
755                \CURLOPT_UNRESTRICTED_AUTH,
756                \CURLOPT_UPLOAD,
757                \CURLOPT_VERBOSE,
758            ],
759            'int' => [
760                \CURLOPT_BUFFERSIZE,
761                \CURLOPT_CONNECTTIMEOUT,
762                \CURLOPT_CONNECTTIMEOUT_MS,
763                \CURLOPT_DNS_CACHE_TIMEOUT,
764                \CURLOPT_EXPECT_100_TIMEOUT_MS,
765                \CURLOPT_HAPPY_EYEBALLS_TIMEOUT_MS,
766                \CURLOPT_FTPSSLAUTH,
767                \CURLOPT_HEADEROPT,
768                \CURLOPT_HTTP_VERSION,
769                \CURLOPT_HTTPAUTH,
770                \CURLOPT_INFILESIZE,
771                \CURLOPT_LOW_SPEED_LIMIT,
772                \CURLOPT_LOW_SPEED_TIME,
773                \CURLOPT_MAXCONNECTS,
774                \CURLOPT_MAXREDIRS,
775                \CURLOPT_PORT,
776                \CURLOPT_POSTREDIR,
777                \CURLOPT_PROTOCOLS,
778                \CURLOPT_PROXYAUTH,
779                \CURLOPT_PROXYPORT,
780                \CURLOPT_PROXYTYPE,
781                \CURLOPT_REDIR_PROTOCOLS,
782                \CURLOPT_RESUME_FROM,
783                \CURLOPT_SOCKS5_AUTH,
784                \CURLOPT_SSL_OPTIONS,
785                \CURLOPT_SSL_VERIFYHOST,
786                \CURLOPT_SSLVERSION,
787                \CURLOPT_PROXY_SSL_OPTIONS,
788                \CURLOPT_PROXY_SSL_VERIFYHOST,
789                \CURLOPT_PROXY_SSLVERSION,
790                \CURLOPT_STREAM_WEIGHT,
791                \CURLOPT_TCP_KEEPALIVE,
792                \CURLOPT_TCP_KEEPIDLE,
793                \CURLOPT_TCP_KEEPINTVL,
794                \CURLOPT_TIMECONDITION,
795                \CURLOPT_TIMEOUT,
796                \CURLOPT_TIMEOUT_MS,
797                \CURLOPT_TIMEVALUE,
798                \CURLOPT_TIMEVALUE_LARGE,
799                \CURLOPT_MAX_RECV_SPEED_LARGE,
800                \CURLOPT_MAX_SEND_SPEED_LARGE,
801                \CURLOPT_SSH_AUTH_TYPES,
802                \CURLOPT_IPRESOLVE,
803                \CURLOPT_FTP_FILEMETHOD,
804            ],
805            'string' => [
806                \CURLOPT_ABSTRACT_UNIX_SOCKET,
807                \CURLOPT_CAINFO,
808                \CURLOPT_CAPATH,
809                \CURLOPT_COOKIE,
810                \CURLOPT_COOKIEFILE,
811                \CURLOPT_COOKIEJAR,
812                \CURLOPT_COOKIELIST,
813                \CURLOPT_CUSTOMREQUEST,
814                \CURLOPT_DEFAULT_PROTOCOL,
815                \CURLOPT_DNS_INTERFACE,
816                \CURLOPT_DNS_LOCAL_IP4,
817                \CURLOPT_DNS_LOCAL_IP6,
818                \CURLOPT_DOH_URL,
819                \CURLOPT_EGDSOCKET,
820                \CURLOPT_ENCODING,
821                \CURLOPT_FTPPORT,
822                \CURLOPT_INTERFACE,
823                \CURLOPT_KEYPASSWD,
824                \CURLOPT_KRB4LEVEL,
825                \CURLOPT_LOGIN_OPTIONS,
826                \CURLOPT_PINNEDPUBLICKEY,
827                \CURLOPT_POSTFIELDS,
828                \CURLOPT_PRIVATE,
829                \CURLOPT_PRE_PROXY,
830                \CURLOPT_PROXY,
831                \CURLOPT_PROXY_SERVICE_NAME,
832                \CURLOPT_PROXY_CAINFO,
833                \CURLOPT_PROXY_CAPATH,
834                \CURLOPT_PROXY_CRLFILE,
835                \CURLOPT_PROXY_KEYPASSWD,
836                \CURLOPT_PROXY_PINNEDPUBLICKEY,
837                \CURLOPT_PROXY_SSLCERT,
838                \CURLOPT_PROXY_SSLCERTTYPE,
839                \CURLOPT_PROXY_SSL_CIPHER_LIST,
840                \CURLOPT_PROXY_TLS13_CIPHERS,
841                \CURLOPT_PROXY_SSLKEY,
842                \CURLOPT_PROXY_SSLKEYTYPE,
843                \CURLOPT_PROXY_TLSAUTH_PASSWORD,
844                \CURLOPT_PROXY_TLSAUTH_TYPE,
845                \CURLOPT_PROXY_TLSAUTH_USERNAME,
846                \CURLOPT_PROXYUSERPWD,
847                \CURLOPT_RANDOM_FILE,
848                \CURLOPT_RANGE,
849                \CURLOPT_REFERER,
850                \CURLOPT_SERVICE_NAME,
851                \CURLOPT_SSH_HOST_PUBLIC_KEY_MD5,
852                \CURLOPT_SSH_PUBLIC_KEYFILE,
853                \CURLOPT_SSH_PRIVATE_KEYFILE,
854                \CURLOPT_SSL_CIPHER_LIST,
855                \CURLOPT_SSLCERT,
856                \CURLOPT_SSLCERTPASSWD,
857                \CURLOPT_SSLCERTTYPE,
858                \CURLOPT_SSLENGINE,
859                \CURLOPT_SSLENGINE_DEFAULT,
860                \CURLOPT_SSLKEY,
861                \CURLOPT_SSLKEYPASSWD,
862                \CURLOPT_SSLKEYTYPE,
863                \CURLOPT_TLS13_CIPHERS,
864                \CURLOPT_UNIX_SOCKET_PATH,
865                \CURLOPT_URL,
866                \CURLOPT_USERAGENT,
867                \CURLOPT_USERNAME,
868                \CURLOPT_PASSWORD,
869                \CURLOPT_USERPWD,
870                \CURLOPT_XOAUTH2_BEARER,
871            ],
872            'array' => [
873                \CURLOPT_CONNECT_TO,
874                \CURLOPT_HTTP200ALIASES,
875                \CURLOPT_HTTPHEADER,
876                \CURLOPT_POSTQUOTE,
877                \CURLOPT_PROXYHEADER,
878                \CURLOPT_QUOTE,
879                \CURLOPT_RESOLVE,
880            ],
881            'fopen' => [
882                \CURLOPT_FILE,
883                \CURLOPT_INFILE,
884                \CURLOPT_STDERR,
885                \CURLOPT_WRITEHEADER,
886            ],
887            'function' => [
888                \CURLOPT_HEADERFUNCTION,
889                // CURLOPT_PASSWDFUNCTION,
890                \CURLOPT_PROGRESSFUNCTION,
891                \CURLOPT_READFUNCTION,
892                \CURLOPT_WRITEFUNCTION,
893            ],
894            'curl_share_init' => [
895                \CURLOPT_SHARE,
896            ],
897        ];
898        foreach ($types as $type => $constants) {
899            foreach ($constants as $constant) {
900                if ($option !== $constant) {
901                    continue;
902                }
903                if ($value === null) {
904                    return;
905                }
906                $valid = match ($type) {
907                    'bool' => \is_bool($value),
908                    'int' => \is_int($value),
909                    'string' => \is_string($value),
910                    'array' => \is_array($value),
911                    'fopen' => \is_resource($value),
912                    'function' => \is_callable($value),
913                    'curl_share_init' => $value instanceof \CurlShareHandle
914                };
915                if ($valid) {
916                    return;
917                }
918                $message = match ($type) {
919                    'bool' => 'The value of option %d should be of bool type',
920                    'int' => 'The value of option %d should be of int type',
921                    'string' => 'The value of option %d should be of string type',
922                    'array' => 'The value of option %d should be of array type',
923                    'fopen' => 'The value of option %d should be a fopen() resource',
924                    'function' => 'The value of option %d should be a callable',
925                    'curl_share_init' => 'The value of option %d should be a result of curl_share_init()'
926                };
927                throw new InvalidArgumentException(\sprintf($message, $option));
928            }
929        }
930        throw new OutOfBoundsException('Invalid curl constant option: ' . $option);
931    }
932}