├── README.md ├── LICENSE ├── composer.json ├── src ├── RequestException.php ├── ResponseError.php ├── Client.php ├── Response.php └── Request.php └── .phpstorm.meta.php /README.md: -------------------------------------------------------------------------------- 1 | Aplus Framework HTTP Client Library 2 | 3 | # Aplus Framework HTTP Client Library 4 | 5 | - [Home](https://aplus-framework.com/packages/http-client) 6 | - [User Guide](https://docs.aplus-framework.com/guides/libraries/http-client/index.html) 7 | - [API Documentation](https://docs.aplus-framework.com/packages/http-client.html) 8 | 9 | [![tests](https://github.com/aplus-framework/http-client/actions/workflows/tests.yml/badge.svg)](https://github.com/aplus-framework/http-client/actions/workflows/tests.yml) 10 | [![coverage](https://coveralls.io/repos/github/aplus-framework/http-client/badge.svg?branch=master)](https://coveralls.io/github/aplus-framework/http-client?branch=master) 11 | [![packagist](https://img.shields.io/packagist/v/aplus/http-client)](https://packagist.org/packages/aplus/http-client) 12 | [![open-source](https://img.shields.io/badge/open--source-sponsor-magenta)](https://aplus-framework.com/sponsor) 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Natan Felles 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aplus/http-client", 3 | "description": "Aplus Framework HTTP Client Library", 4 | "license": "MIT", 5 | "type": "library", 6 | "keywords": [ 7 | "api", 8 | "http-client", 9 | "http", 10 | "client", 11 | "request", 12 | "response", 13 | "curl" 14 | ], 15 | "authors": [ 16 | { 17 | "name": "Natan Felles", 18 | "email": "natanfelles@gmail.com", 19 | "homepage": "https://natanfelles.github.io" 20 | } 21 | ], 22 | "homepage": "https://aplus-framework.com/packages/http-client", 23 | "support": { 24 | "email": "support@aplus-framework.com", 25 | "issues": "https://github.com/aplus-framework/http-client/issues", 26 | "forum": "https://aplus-framework.com/forum", 27 | "source": "https://github.com/aplus-framework/http-client", 28 | "docs": "https://docs.aplus-framework.com/guides/libraries/http-client/" 29 | }, 30 | "funding": [ 31 | { 32 | "type": "Aplus Sponsor", 33 | "url": "https://aplus-framework.com/sponsor" 34 | } 35 | ], 36 | "require": { 37 | "php": ">=8.3", 38 | "ext-curl": "*", 39 | "ext-fileinfo": "*", 40 | "ext-json": "*", 41 | "aplus/helpers": "^4.0", 42 | "aplus/http": "^6.0" 43 | }, 44 | "require-dev": { 45 | "ext-xdebug": "*", 46 | "aplus/coding-standard": "^2.8", 47 | "ergebnis/composer-normalize": "^2.25", 48 | "jetbrains/phpstorm-attributes": "^1.0", 49 | "phpmd/phpmd": "^2.14", 50 | "phpstan/phpstan": "^1.10", 51 | "phpunit/phpunit": "^10.5" 52 | }, 53 | "minimum-stability": "dev", 54 | "prefer-stable": true, 55 | "autoload": { 56 | "psr-4": { 57 | "Framework\\HTTP\\Client\\": "src/" 58 | } 59 | }, 60 | "autoload-dev": { 61 | "psr-4": { 62 | "Tests\\HTTP\\Client\\": "tests/" 63 | } 64 | }, 65 | "config": { 66 | "allow-plugins": { 67 | "ergebnis/composer-normalize": true 68 | }, 69 | "optimize-autoloader": true, 70 | "preferred-install": "dist", 71 | "sort-packages": true 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/RequestException.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | namespace Framework\HTTP\Client; 11 | 12 | use JetBrains\PhpStorm\ArrayShape; 13 | use RuntimeException; 14 | use Throwable; 15 | 16 | /** 17 | * Class RequestException. 18 | * 19 | * @package http-client 20 | */ 21 | class RequestException extends RuntimeException 22 | { 23 | /** 24 | * @var array 25 | */ 26 | protected array $info; 27 | 28 | /** 29 | * RequestException constructor. 30 | * 31 | * @param string $message 32 | * @param int $code 33 | * @param Throwable|null $previous 34 | * @param array $info 35 | */ 36 | public function __construct( 37 | string $message = '', 38 | int $code = 0, 39 | ?Throwable $previous = null, 40 | array $info = [] 41 | ) { 42 | parent::__construct($message, $code, $previous); 43 | $this->info = $info; 44 | } 45 | 46 | /** 47 | * @return array 48 | */ 49 | #[ArrayShape([ 50 | 'appconnect_time_us' => 'int', 51 | 'certinfo' => 'array', 52 | 'connect_time' => 'float', 53 | 'connect_time_us' => 'int', 54 | 'content_type' => 'string', 55 | 'download_content_length' => 'float', 56 | 'effective_method' => 'string', 57 | 'filetime' => 'int', 58 | 'header_size' => 'int', 59 | 'http_code' => 'int', 60 | 'http_version' => 'int', 61 | 'local_ip' => 'string', 62 | 'local_port' => 'int', 63 | 'namelookup_time' => 'float', 64 | 'namelookup_time_us' => 'int', 65 | 'pretransfer_time' => 'float', 66 | 'pretransfer_time_us' => 'int', 67 | 'primary_ip' => 'string', 68 | 'primary_port' => 'int', 69 | 'protocol' => 'int', 70 | 'redirect_count' => 'int', 71 | 'redirect_time' => 'float', 72 | 'redirect_time_us' => 'int', 73 | 'redirect_url' => 'string', 74 | 'request_size' => 'int', 75 | 'scheme' => 'string', 76 | 'size_download' => 'float', 77 | 'size_upload' => 'float', 78 | 'speed_download' => 'float', 79 | 'speed_upload' => 'float', 80 | 'ssl_verify_result' => 'int', 81 | 'ssl_verifyresult' => 'int', 82 | 'starttransfer_time' => 'float', 83 | 'starttransfer_time_us' => 'int', 84 | 'total_time' => 'float', 85 | 'total_time_us' => 'int', 86 | 'upload_content_length' => 'float', 87 | 'url' => 'string', 88 | ])] 89 | public function getInfo() : array 90 | { 91 | return $this->info; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/ResponseError.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | namespace Framework\HTTP\Client; 11 | 12 | use JetBrains\PhpStorm\ArrayShape; 13 | use Stringable; 14 | 15 | /** 16 | * Class ResponseError. 17 | * 18 | * @package http-client 19 | */ 20 | class ResponseError implements Stringable 21 | { 22 | protected Request $request; 23 | protected string $error; 24 | protected int $errorNumber; 25 | /** 26 | * @var array 27 | */ 28 | protected array $info; 29 | 30 | /** 31 | * @param Request $request 32 | * @param string $error 33 | * @param int $errorNumber 34 | * @param array $info 35 | */ 36 | public function __construct( 37 | Request $request, 38 | string $error, 39 | int $errorNumber, 40 | array $info 41 | ) { 42 | $this->request = $request; 43 | $this->error = $error; 44 | $this->errorNumber = $errorNumber; 45 | $this->info = $info; 46 | } 47 | 48 | public function __toString() : string 49 | { 50 | return 'Error ' . $this->getErrorNumber() . ': ' . $this->getError(); 51 | } 52 | 53 | public function getRequest() : Request 54 | { 55 | return $this->request; 56 | } 57 | 58 | public function getError() : string 59 | { 60 | return $this->error; 61 | } 62 | 63 | public function getErrorNumber() : int 64 | { 65 | return $this->errorNumber; 66 | } 67 | 68 | /** 69 | * @return array 70 | */ 71 | #[ArrayShape([ 72 | 'appconnect_time_us' => 'int', 73 | 'certinfo' => 'array', 74 | 'connect_time' => 'float', 75 | 'connect_time_us' => 'int', 76 | 'content_type' => 'string', 77 | 'download_content_length' => 'float', 78 | 'effective_method' => 'string', 79 | 'filetime' => 'int', 80 | 'header_size' => 'int', 81 | 'http_code' => 'int', 82 | 'http_version' => 'int', 83 | 'local_ip' => 'string', 84 | 'local_port' => 'int', 85 | 'namelookup_time' => 'float', 86 | 'namelookup_time_us' => 'int', 87 | 'pretransfer_time' => 'float', 88 | 'pretransfer_time_us' => 'int', 89 | 'primary_ip' => 'string', 90 | 'primary_port' => 'int', 91 | 'protocol' => 'int', 92 | 'redirect_count' => 'int', 93 | 'redirect_time' => 'float', 94 | 'redirect_time_us' => 'int', 95 | 'redirect_url' => 'string', 96 | 'request_size' => 'int', 97 | 'scheme' => 'string', 98 | 'size_download' => 'float', 99 | 'size_upload' => 'float', 100 | 'speed_download' => 'float', 101 | 'speed_upload' => 'float', 102 | 'ssl_verify_result' => 'int', 103 | 'ssl_verifyresult' => 'int', 104 | 'starttransfer_time' => 'float', 105 | 'starttransfer_time_us' => 'int', 106 | 'total_time' => 'float', 107 | 'total_time_us' => 'int', 108 | 'upload_content_length' => 'float', 109 | 'url' => 'string', 110 | ])] 111 | public function getInfo() : array 112 | { 113 | return $this->info; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/Client.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | namespace Framework\HTTP\Client; 11 | 12 | use CurlHandle; 13 | use Framework\HTTP\Status; 14 | use Framework\HTTP\URL; 15 | use Generator; 16 | use InvalidArgumentException; 17 | 18 | /** 19 | * Class Client. 20 | * 21 | * @see https://www.php.net/manual/en/function.curl-setopt.php 22 | * @see https://curl.se/libcurl/c/curl_easy_setopt.html 23 | * @see https://php.watch/articles/php-curl-security-hardening 24 | * 25 | * @package http-client 26 | */ 27 | class Client 28 | { 29 | /** 30 | * @var array 31 | */ 32 | protected array $parsed = []; 33 | 34 | /** 35 | * Create a new Request instance. 36 | * 37 | * @param URL|string $url 38 | * 39 | * @return Request 40 | */ 41 | public function createRequest(URL | string $url) : Request 42 | { 43 | return new Request($url); 44 | } 45 | 46 | /** 47 | * Run the Request. 48 | * 49 | * @param Request $request 50 | * 51 | * @throws InvalidArgumentException for invalid Request Protocol 52 | * @throws RequestException for curl error 53 | * 54 | * @return Response 55 | */ 56 | public function run(Request $request) : Response 57 | { 58 | $handle = \curl_init(); 59 | $options = $request->getOptions(); 60 | $options[\CURLOPT_HEADERFUNCTION] = [$this, 'parseHeaderLine']; 61 | \curl_setopt_array($handle, $options); 62 | $body = \curl_exec($handle); 63 | $info = []; 64 | if ($request->isGettingInfo()) { 65 | $info = (array) \curl_getinfo($handle); 66 | } 67 | if ($body === false) { 68 | $error = \curl_error($handle); 69 | $errno = \curl_errno($handle); 70 | unset($handle); 71 | throw new RequestException( 72 | $error, 73 | $errno, 74 | info: $info 75 | ); 76 | } 77 | if ($body === true) { 78 | $body = ''; 79 | } 80 | $objectId = \spl_object_id($handle); 81 | unset($handle); 82 | $response = new Response( 83 | $request, 84 | $this->parsed[$objectId]['protocol'], 85 | $this->parsed[$objectId]['code'], 86 | $this->parsed[$objectId]['reason'], 87 | $this->parsed[$objectId]['headers'], 88 | $body, 89 | $info 90 | ); 91 | unset($this->parsed[$objectId]); 92 | return $response; 93 | } 94 | 95 | /** 96 | * Run multiple HTTP Requests. 97 | * 98 | * @param Request[] $requests An associative array of Request instances 99 | * with ids as keys 100 | * 101 | * @return Generator The Requests ids as keys and 102 | * its respective Response or ResponseError as values 103 | */ 104 | public function runMulti(array $requests) : Generator 105 | { 106 | $multiHandle = \curl_multi_init(); 107 | $handles = []; 108 | foreach ($requests as $id => $request) { 109 | $handle = \curl_init(); 110 | $options = $request->getOptions(); 111 | $options[\CURLOPT_HEADERFUNCTION] = [$this, 'parseHeaderLine']; 112 | \curl_setopt_array($handle, $options); 113 | $handles[$id] = $handle; 114 | \curl_multi_add_handle($multiHandle, $handle); 115 | } 116 | do { 117 | $status = \curl_multi_exec($multiHandle, $stillRunning); 118 | $message = \curl_multi_info_read($multiHandle); 119 | if ($message === false) { 120 | continue; 121 | } 122 | foreach ($handles as $id => $handle) { 123 | if ($message['handle'] !== $handle) { 124 | continue; 125 | } 126 | $info = []; 127 | if ($requests[$id]->isGettingInfo()) { 128 | $info = (array) \curl_getinfo($handle); 129 | } 130 | $objectId = \spl_object_id($handle); 131 | if (!isset($this->parsed[$objectId])) { 132 | yield $id => new ResponseError( 133 | $requests[$id], 134 | \curl_error($handle), 135 | \curl_errno($handle), 136 | $info 137 | ); 138 | unset($handles[$id]); 139 | break; 140 | } 141 | yield $id => new Response( 142 | $requests[$id], 143 | $this->parsed[$objectId]['protocol'], 144 | $this->parsed[$objectId]['code'], 145 | $this->parsed[$objectId]['reason'], 146 | $this->parsed[$objectId]['headers'], 147 | (string) \curl_multi_getcontent($message['handle']), 148 | $info 149 | ); 150 | unset($this->parsed[$objectId], $handles[$id]); 151 | break; 152 | } 153 | \curl_multi_remove_handle($multiHandle, $message['handle']); 154 | } while ($stillRunning && $status === \CURLM_OK); 155 | \curl_multi_close($multiHandle); 156 | } 157 | 158 | /** 159 | * Parses Header line. 160 | * 161 | * @param CurlHandle $curlHandle 162 | * @param string $line 163 | * 164 | * @return int 165 | */ 166 | protected function parseHeaderLine(CurlHandle $curlHandle, string $line) : int 167 | { 168 | $trimmedLine = \trim($line); 169 | $lineLength = \strlen($line); 170 | if ($trimmedLine === '') { 171 | return $lineLength; 172 | } 173 | $id = \spl_object_id($curlHandle); 174 | if (!\str_contains($trimmedLine, ':')) { 175 | if (\str_starts_with($trimmedLine, 'HTTP/')) { 176 | $parts = \explode(' ', $trimmedLine, 3); 177 | $this->parsed[$id]['protocol'] = $parts[0]; 178 | $this->parsed[$id]['code'] = (int) ($parts[1] ?? 200); 179 | $this->parsed[$id]['reason'] = $parts[2] 180 | ?? Status::getReason($this->parsed[$id]['code']); 181 | } 182 | return $lineLength; 183 | } 184 | [$name, $value] = \explode(':', $trimmedLine, 2); 185 | $name = \trim($name); 186 | $value = \trim($value); 187 | if ($name !== '' && $value !== '') { 188 | $this->parsed[$id]['headers'][\strtolower($name)][] = $value; 189 | } 190 | return $lineLength; 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /src/Response.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | namespace Framework\HTTP\Client; 11 | 12 | use Exception; 13 | use Framework\HTTP\Cookie; 14 | use Framework\HTTP\Header; 15 | use Framework\HTTP\Message; 16 | use Framework\HTTP\ResponseHeader; 17 | use Framework\HTTP\ResponseInterface; 18 | use InvalidArgumentException; 19 | use JetBrains\PhpStorm\ArrayShape; 20 | use JetBrains\PhpStorm\Pure; 21 | use Override; 22 | 23 | /** 24 | * Class Response. 25 | * 26 | * @package http-client 27 | */ 28 | class Response extends Message implements ResponseInterface 29 | { 30 | protected Request $request; 31 | protected string $protocol; 32 | protected int $statusCode; 33 | protected string $statusReason; 34 | /** 35 | * Response curl info. 36 | * 37 | * @var array 38 | */ 39 | protected array $info = []; 40 | protected int $jsonFlags = 0; 41 | 42 | /** 43 | * Response constructor. 44 | * 45 | * @param Request $request 46 | * @param string $protocol 47 | * @param int $status 48 | * @param string $reason 49 | * @param array> $headers 50 | * @param string $body 51 | * @param array $info 52 | */ 53 | public function __construct( 54 | Request $request, 55 | string $protocol, 56 | int $status, 57 | string $reason, 58 | array $headers, 59 | string $body, 60 | array $info = [] 61 | ) { 62 | $this->request = $request; 63 | $this->setProtocol($protocol); 64 | $this->setStatusCode($status); 65 | $this->setStatusReason($reason); 66 | foreach ($headers as $name => $values) { 67 | foreach ($values as $value) { 68 | $this->appendHeader($name, $value); 69 | } 70 | } 71 | $this->setBody($body); 72 | \ksort($info); 73 | $this->info = $info; 74 | } 75 | 76 | public function getRequest() : Request 77 | { 78 | return $this->request; 79 | } 80 | 81 | /** 82 | * @return array 83 | */ 84 | #[ArrayShape([ 85 | 'appconnect_time_us' => 'int', 86 | 'certinfo' => 'array', 87 | 'connect_time' => 'float', 88 | 'connect_time_us' => 'int', 89 | 'content_type' => 'string', 90 | 'download_content_length' => 'float', 91 | 'effective_method' => 'string', 92 | 'filetime' => 'int', 93 | 'header_size' => 'int', 94 | 'http_code' => 'int', 95 | 'http_version' => 'int', 96 | 'local_ip' => 'string', 97 | 'local_port' => 'int', 98 | 'namelookup_time' => 'float', 99 | 'namelookup_time_us' => 'int', 100 | 'pretransfer_time' => 'float', 101 | 'pretransfer_time_us' => 'int', 102 | 'primary_ip' => 'string', 103 | 'primary_port' => 'int', 104 | 'protocol' => 'int', 105 | 'redirect_count' => 'int', 106 | 'redirect_time' => 'float', 107 | 'redirect_time_us' => 'int', 108 | 'redirect_url' => 'string', 109 | 'request_size' => 'int', 110 | 'scheme' => 'string', 111 | 'size_download' => 'float', 112 | 'size_upload' => 'float', 113 | 'speed_download' => 'float', 114 | 'speed_upload' => 'float', 115 | 'ssl_verify_result' => 'int', 116 | 'ssl_verifyresult' => 'int', 117 | 'starttransfer_time' => 'float', 118 | 'starttransfer_time_us' => 'int', 119 | 'total_time' => 'float', 120 | 'total_time_us' => 'int', 121 | 'upload_content_length' => 'float', 122 | 'url' => 'string', 123 | ])] 124 | public function getInfo() : array 125 | { 126 | return $this->info; 127 | } 128 | 129 | #[Override] 130 | #[Pure] 131 | public function getStatusCode() : int 132 | { 133 | return parent::getStatusCode(); 134 | } 135 | 136 | /** 137 | * @param int $code 138 | * 139 | * @throws InvalidArgumentException if status code is invalid 140 | * 141 | * @return bool 142 | */ 143 | #[Override] 144 | public function isStatusCode(int $code) : bool 145 | { 146 | return parent::isStatusCode($code); 147 | } 148 | 149 | #[Pure] 150 | public function getStatusReason() : string 151 | { 152 | return $this->statusReason; 153 | } 154 | 155 | /** 156 | * @param string $statusReason 157 | * 158 | * @return static 159 | */ 160 | protected function setStatusReason(string $statusReason) : static 161 | { 162 | $this->statusReason = $statusReason; 163 | return $this; 164 | } 165 | 166 | /** 167 | * @param string $name 168 | * @param string $value 169 | * 170 | * @throws Exception if Cookie::setExpires fail 171 | * 172 | * @return static 173 | */ 174 | #[Override] 175 | protected function setHeader(string $name, string $value) : static 176 | { 177 | if (\strtolower($name) === 'set-cookie') { 178 | $values = \str_contains($value, "\n") 179 | ? \explode("\n", $value) 180 | : [$value]; 181 | foreach ($values as $val) { 182 | $cookie = Cookie::parse($val); 183 | if ($cookie) { 184 | $this->setCookie($cookie); 185 | } 186 | } 187 | } 188 | return parent::setHeader($name, $value); 189 | } 190 | 191 | /** 192 | * Get body as decoded JSON. 193 | * 194 | * @param bool $assoc 195 | * @param int|null $flags [optional]

196 | * Bitmask consisting of JSON_BIGINT_AS_STRING, 197 | * JSON_INVALID_UTF8_IGNORE, 198 | * JSON_INVALID_UTF8_SUBSTITUTE, 199 | * JSON_OBJECT_AS_ARRAY, 200 | * JSON_THROW_ON_ERROR. 201 | *

202 | *

Default is none when null.

203 | * @param int<1,max> $depth 204 | * 205 | * @see https://www.php.net/manual/en/function.json-decode.php 206 | * @see https://www.php.net/manual/en/json.constants.php 207 | * 208 | * @return array|false|object 209 | */ 210 | public function getJson(bool $assoc = false, ?int $flags = null, int $depth = 512) : array | false | object 211 | { 212 | if ($flags === null) { 213 | $flags = $this->getJsonFlags(); 214 | } 215 | $body = \json_decode($this->getBody(), $assoc, $depth, $flags); 216 | if (\json_last_error() !== \JSON_ERROR_NONE) { 217 | return false; 218 | } 219 | return $body; 220 | } 221 | 222 | #[Pure] 223 | public function isJson() : bool 224 | { 225 | return $this->parseContentType() === 'application/json'; 226 | } 227 | 228 | #[Pure] 229 | public function getStatus() : string 230 | { 231 | return $this->getStatusCode() . ' ' . $this->getStatusReason(); 232 | } 233 | 234 | /** 235 | * Get parsed Link header as array. 236 | * 237 | * NOTE: To be parsed, links must be in the GitHub REST API format. 238 | * 239 | * @see https://docs.github.com/en/rest/guides/using-pagination-in-the-rest-api#using-link-headers 240 | * @see https://docs.aplus-framework.com/guides/libraries/pagination/index.html#http-header-link 241 | * @see https://datatracker.ietf.org/doc/html/rfc5988 242 | * 243 | * @return array Associative array with rel as keys and links 244 | * as values 245 | */ 246 | public function getLinks() : array 247 | { 248 | $link = $this->getHeader(ResponseHeader::LINK); 249 | $result = []; 250 | if ($link) { 251 | $result = $this->parseLinkHeader($link); 252 | } 253 | return $result; 254 | } 255 | 256 | /** 257 | * @param string $headerLink 258 | * 259 | * @return array 260 | */ 261 | protected function parseLinkHeader(string $headerLink) : array 262 | { 263 | $links = []; 264 | $parts = \explode(',', $headerLink, 10); 265 | foreach ($parts as $part) { 266 | $section = \explode(';', $part, 10); 267 | if (\count($section) !== 2) { 268 | continue; 269 | } 270 | $url = \preg_replace('#<(.*)>#', '$1', $section[0]); 271 | $name = \preg_replace('#rel="(.*)"#', '$1', $section[1]); 272 | $url = \trim($url); 273 | $name = \trim($name); 274 | $links[$name] = $url; 275 | } 276 | return $links; 277 | } 278 | } 279 | -------------------------------------------------------------------------------- /.phpstorm.meta.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | namespace PHPSTORM_META; 11 | 12 | registerArgumentsSet( 13 | 'curl_options', 14 | // TODO: Comment non-HTTP constants 15 | \CURLINFO_HEADER_OUT, 16 | \CURLOPT_ABSTRACT_UNIX_SOCKET, 17 | \CURLOPT_AUTOREFERER, 18 | \CURLOPT_BUFFERSIZE, 19 | \CURLOPT_CAINFO, 20 | \CURLOPT_CAPATH, 21 | \CURLOPT_CERTINFO, 22 | \CURLOPT_CONNECTTIMEOUT, 23 | \CURLOPT_CONNECTTIMEOUT_MS, 24 | \CURLOPT_CONNECT_ONLY, 25 | //\CURLOPT_CONNECT_TO, 26 | //\CURLOPT_COOKIE, 27 | \CURLOPT_COOKIEFILE, 28 | \CURLOPT_COOKIEJAR, 29 | \CURLOPT_COOKIELIST, 30 | \CURLOPT_COOKIESESSION, 31 | \CURLOPT_CRLF, 32 | //\CURLOPT_DEFAULT_PROTOCOL, 33 | \CURLOPT_DISALLOW_USERNAME_IN_URL, 34 | \CURLOPT_DNS_CACHE_TIMEOUT, 35 | \CURLOPT_DNS_INTERFACE, 36 | \CURLOPT_DNS_LOCAL_IP4, 37 | \CURLOPT_DNS_LOCAL_IP6, 38 | \CURLOPT_DNS_SHUFFLE_ADDRESSES, 39 | \CURLOPT_DNS_USE_GLOBAL_CACHE, 40 | \CURLOPT_DOH_URL, 41 | \CURLOPT_EGDSOCKET, 42 | \CURLOPT_ENCODING, 43 | \CURLOPT_EXPECT_100_TIMEOUT_MS, 44 | \CURLOPT_FAILONERROR, 45 | \CURLOPT_FILE, 46 | \CURLOPT_FILETIME, 47 | \CURLOPT_FOLLOWLOCATION, 48 | \CURLOPT_FORBID_REUSE, 49 | \CURLOPT_FRESH_CONNECT, 50 | //\CURLOPT_FTPAPPEND, 51 | //\CURLOPT_FTPASCII, 52 | //\CURLOPT_FTPLISTONLY, 53 | //\CURLOPT_FTPPORT, 54 | //\CURLOPT_FTPSSLAUTH, 55 | //\CURLOPT_FTP_CREATE_MISSING_DIRS, 56 | //\CURLOPT_FTP_FILEMETHOD, 57 | //\CURLOPT_FTP_USE_EPRT, 58 | //\CURLOPT_FTP_USE_EPSV, 59 | \CURLOPT_HAPPY_EYEBALLS_TIMEOUT_MS, 60 | \CURLOPT_HAPROXYPROTOCOL, 61 | \CURLOPT_HEADEROPT, 62 | \CURLOPT_HTTP09_ALLOWED, 63 | \CURLOPT_HTTP200ALIASES, 64 | \CURLOPT_HTTPAUTH, 65 | //\CURLOPT_HTTPGET, 66 | \CURLOPT_HTTPPROXYTUNNEL, 67 | \CURLOPT_HTTP_CONTENT_DECODING, 68 | \CURLOPT_INFILE, 69 | \CURLOPT_INFILESIZE, 70 | \CURLOPT_INTERFACE, 71 | \CURLOPT_IPRESOLVE, 72 | \CURLOPT_KEEP_SENDING_ON_ERROR, 73 | \CURLOPT_KEYPASSWD, 74 | \CURLOPT_KRB4LEVEL, 75 | \CURLOPT_LOGIN_OPTIONS, 76 | \CURLOPT_LOW_SPEED_LIMIT, 77 | \CURLOPT_LOW_SPEED_TIME, 78 | \CURLOPT_MAXCONNECTS, 79 | \CURLOPT_MAXREDIRS, 80 | \CURLOPT_MAX_RECV_SPEED_LARGE, 81 | \CURLOPT_MAX_SEND_SPEED_LARGE, 82 | \CURLOPT_MUTE, 83 | \CURLOPT_NETRC, 84 | \CURLOPT_NOBODY, 85 | \CURLOPT_NOPROGRESS, 86 | \CURLOPT_NOSIGNAL, 87 | \CURLOPT_PASSWDFUNCTION, 88 | \CURLOPT_PASSWORD, 89 | \CURLOPT_PATH_AS_IS, 90 | \CURLOPT_PINNEDPUBLICKEY, 91 | \CURLOPT_PIPEWAIT, 92 | \CURLOPT_PORT, 93 | \CURLOPT_POSTQUOTE, 94 | \CURLOPT_POSTREDIR, 95 | \CURLOPT_PRE_PROXY, 96 | \CURLOPT_PRIVATE, 97 | \CURLOPT_PROGRESSFUNCTION, 98 | \CURLOPT_PROTOCOLS, 99 | \CURLOPT_PROXY, 100 | \CURLOPT_PROXYAUTH, 101 | \CURLOPT_PROXYHEADER, 102 | \CURLOPT_PROXYPORT, 103 | \CURLOPT_PROXYTYPE, 104 | \CURLOPT_PROXYUSERPWD, 105 | \CURLOPT_PROXY_CAINFO, 106 | \CURLOPT_PROXY_CAPATH, 107 | \CURLOPT_PROXY_CRLFILE, 108 | \CURLOPT_PROXY_KEYPASSWD, 109 | \CURLOPT_PROXY_PINNEDPUBLICKEY, 110 | \CURLOPT_PROXY_SERVICE_NAME, 111 | \CURLOPT_PROXY_SSLCERT, 112 | \CURLOPT_PROXY_SSLCERTTYPE, 113 | \CURLOPT_PROXY_SSLKEY, 114 | \CURLOPT_PROXY_SSLKEYTYPE, 115 | \CURLOPT_PROXY_SSLVERSION, 116 | \CURLOPT_PROXY_SSL_CIPHER_LIST, 117 | \CURLOPT_PROXY_SSL_OPTIONS, 118 | \CURLOPT_PROXY_SSL_VERIFYHOST, 119 | \CURLOPT_PROXY_SSL_VERIFYPEER, 120 | \CURLOPT_PROXY_TLS13_CIPHERS, 121 | \CURLOPT_PROXY_TLSAUTH_PASSWORD, 122 | \CURLOPT_PROXY_TLSAUTH_TYPE, 123 | \CURLOPT_PROXY_TLSAUTH_USERNAME, 124 | \CURLOPT_PUT, 125 | \CURLOPT_QUOTE, 126 | \CURLOPT_RANDOM_FILE, 127 | \CURLOPT_RANGE, 128 | \CURLOPT_READFUNCTION, 129 | \CURLOPT_REDIR_PROTOCOLS, 130 | \CURLOPT_REFERER, 131 | \CURLOPT_RESOLVE, 132 | \CURLOPT_RESUME_FROM, 133 | \CURLOPT_RETURNTRANSFER, 134 | \CURLOPT_SASL_IR, 135 | \CURLOPT_SERVICE_NAME, 136 | \CURLOPT_SHARE, 137 | \CURLOPT_SOCKS5_AUTH, 138 | \CURLOPT_SSH_AUTH_TYPES, 139 | \CURLOPT_SSH_COMPRESSION, 140 | \CURLOPT_SSH_HOST_PUBLIC_KEY_MD5, 141 | \CURLOPT_SSH_PRIVATE_KEYFILE, 142 | \CURLOPT_SSH_PUBLIC_KEYFILE, 143 | \CURLOPT_SSLCERT, 144 | \CURLOPT_SSLCERTPASSWD, 145 | \CURLOPT_SSLCERTTYPE, 146 | \CURLOPT_SSLENGINE, 147 | \CURLOPT_SSLENGINE_DEFAULT, 148 | \CURLOPT_SSLKEY, 149 | \CURLOPT_SSLKEYPASSWD, 150 | \CURLOPT_SSLKEYTYPE, 151 | \CURLOPT_SSLVERSION, 152 | \CURLOPT_SSL_CIPHER_LIST, 153 | \CURLOPT_SSL_ENABLE_ALPN, 154 | \CURLOPT_SSL_ENABLE_NPN, 155 | \CURLOPT_SSL_FALSESTART, 156 | \CURLOPT_SSL_OPTIONS, 157 | \CURLOPT_SSL_VERIFYHOST, 158 | \CURLOPT_SSL_VERIFYPEER, 159 | \CURLOPT_SSL_VERIFYSTATUS, 160 | \CURLOPT_STDERR, 161 | \CURLOPT_STREAM_WEIGHT, 162 | \CURLOPT_SUPPRESS_CONNECT_HEADERS, 163 | \CURLOPT_TCP_FASTOPEN, 164 | \CURLOPT_TCP_KEEPALIVE, 165 | \CURLOPT_TCP_KEEPIDLE, 166 | \CURLOPT_TCP_KEEPINTVL, 167 | \CURLOPT_TCP_NODELAY, 168 | \CURLOPT_TFTP_NO_OPTIONS, 169 | \CURLOPT_TIMECONDITION, 170 | \CURLOPT_TIMEOUT, 171 | \CURLOPT_TIMEOUT_MS, 172 | \CURLOPT_TIMEVALUE, 173 | \CURLOPT_TIMEVALUE_LARGE, 174 | \CURLOPT_TLS13_CIPHERS, 175 | \CURLOPT_TRANSFERTEXT, 176 | \CURLOPT_UNIX_SOCKET_PATH, 177 | \CURLOPT_UNRESTRICTED_AUTH, 178 | \CURLOPT_UPLOAD, 179 | \CURLOPT_USERAGENT, 180 | \CURLOPT_USERNAME, 181 | \CURLOPT_USERPWD, 182 | \CURLOPT_VERBOSE, 183 | \CURLOPT_WRITEFUNCTION, 184 | \CURLOPT_WRITEHEADER, 185 | \CURLOPT_XOAUTH2_BEARER, 186 | // The following options are overwritten in the Request::getOptions() 187 | // So, we disable it from code-completion... 188 | //\CURLOPT_CUSTOMREQUEST, 189 | //\CURLOPT_HEADER, 190 | //\CURLOPT_HTTPHEADER, 191 | //\CURLOPT_HTTP_VERSION, 192 | //\CURLOPT_POST, 193 | //\CURLOPT_POSTFIELDS, 194 | //\CURLOPT_URL, 195 | // The following are overwritten in the Client: 196 | //\CURLOPT_HEADERFUNCTION, 197 | ); 198 | expectedArguments( 199 | \Framework\HTTP\Client\Request::getOption(), 200 | 0, 201 | argumentsSet('curl_options') 202 | ); 203 | expectedArguments( 204 | \Framework\HTTP\Client\Request::setOption(), 205 | 0, 206 | argumentsSet('curl_options') 207 | ); 208 | registerArgumentsSet( 209 | 'json_decode_flags', 210 | \JSON_BIGINT_AS_STRING, 211 | \JSON_INVALID_UTF8_IGNORE, 212 | \JSON_INVALID_UTF8_SUBSTITUTE, 213 | \JSON_OBJECT_AS_ARRAY, 214 | \JSON_THROW_ON_ERROR, 215 | ); 216 | expectedArguments( 217 | \Framework\HTTP\Client\Response::getJson(), 218 | 1, 219 | argumentsSet('json_decode_flags') 220 | ); 221 | expectedArguments( 222 | \Framework\HTTP\Client\Response::setJsonFlags(), 223 | 0, 224 | argumentsSet('json_decode_flags') 225 | ); 226 | registerArgumentsSet( 227 | 'json_encode_flags', 228 | \JSON_FORCE_OBJECT, 229 | \JSON_HEX_AMP, 230 | \JSON_HEX_APOS, 231 | \JSON_HEX_QUOT, 232 | \JSON_HEX_TAG, 233 | \JSON_INVALID_UTF8_IGNORE, 234 | \JSON_INVALID_UTF8_SUBSTITUTE, 235 | \JSON_NUMERIC_CHECK, 236 | \JSON_PARTIAL_OUTPUT_ON_ERROR, 237 | \JSON_PRESERVE_ZERO_FRACTION, 238 | \JSON_PRETTY_PRINT, 239 | \JSON_THROW_ON_ERROR, 240 | \JSON_UNESCAPED_LINE_TERMINATORS, 241 | \JSON_UNESCAPED_SLASHES, 242 | \JSON_UNESCAPED_UNICODE, 243 | ); 244 | expectedArguments( 245 | \Framework\HTTP\Client\Request::setJson(), 246 | 1, 247 | argumentsSet('json_encode_flags') 248 | ); 249 | expectedArguments( 250 | \Framework\HTTP\Client\Request::setJsonFlags(), 251 | 0, 252 | argumentsSet('json_encode_flags') 253 | ); 254 | registerArgumentsSet( 255 | 'content_types', 256 | 'application/json', 257 | 'application/octet-stream', 258 | 'application/x-www-form-urlencoded', 259 | 'application/xml', 260 | 'multipart/form-data', 261 | 'text/css', 262 | 'text/html', 263 | 'text/javascript', 264 | 'text/markdown', 265 | 'text/plain', 266 | 'text/x-php', 267 | 'text/x-rst', 268 | 'text/xml', 269 | ); 270 | expectedArguments( 271 | \Framework\HTTP\Client\Request::setContentType(), 272 | 0, 273 | argumentsSet('content_types') 274 | ); 275 | registerArgumentsSet( 276 | 'response_headers', 277 | \Framework\HTTP\ResponseHeader::ACCEPT_RANGES, 278 | \Framework\HTTP\ResponseHeader::ACCESS_CONTROL_ALLOW_CREDENTIALS, 279 | \Framework\HTTP\ResponseHeader::ACCESS_CONTROL_ALLOW_HEADERS, 280 | \Framework\HTTP\ResponseHeader::ACCESS_CONTROL_ALLOW_METHODS, 281 | \Framework\HTTP\ResponseHeader::ACCESS_CONTROL_ALLOW_ORIGIN, 282 | \Framework\HTTP\ResponseHeader::ACCESS_CONTROL_EXPOSE_HEADERS, 283 | \Framework\HTTP\ResponseHeader::ACCESS_CONTROL_MAX_AGE, 284 | \Framework\HTTP\ResponseHeader::AGE, 285 | \Framework\HTTP\ResponseHeader::ALLOW, 286 | \Framework\HTTP\ResponseHeader::CACHE_CONTROL, 287 | \Framework\HTTP\ResponseHeader::CLEAR_SITE_DATA, 288 | \Framework\HTTP\ResponseHeader::CONNECTION, 289 | \Framework\HTTP\ResponseHeader::CONTENT_DISPOSITION, 290 | \Framework\HTTP\ResponseHeader::CONTENT_ENCODING, 291 | \Framework\HTTP\ResponseHeader::CONTENT_LANGUAGE, 292 | \Framework\HTTP\ResponseHeader::CONTENT_LENGTH, 293 | \Framework\HTTP\ResponseHeader::CONTENT_LOCATION, 294 | \Framework\HTTP\ResponseHeader::CONTENT_RANGE, 295 | \Framework\HTTP\ResponseHeader::CONTENT_SECURITY_POLICY, 296 | \Framework\HTTP\ResponseHeader::CONTENT_SECURITY_POLICY_REPORT_ONLY, 297 | \Framework\HTTP\ResponseHeader::CONTENT_TYPE, 298 | \Framework\HTTP\ResponseHeader::DATE, 299 | \Framework\HTTP\ResponseHeader::ETAG, 300 | \Framework\HTTP\ResponseHeader::EXPECT_CT, 301 | \Framework\HTTP\ResponseHeader::EXPIRES, 302 | \Framework\HTTP\ResponseHeader::FEATURE_POLICY, 303 | \Framework\HTTP\ResponseHeader::KEEP_ALIVE, 304 | \Framework\HTTP\ResponseHeader::LAST_MODIFIED, 305 | \Framework\HTTP\ResponseHeader::LINK, 306 | \Framework\HTTP\ResponseHeader::LOCATION, 307 | \Framework\HTTP\ResponseHeader::PRAGMA, 308 | \Framework\HTTP\ResponseHeader::PROXY_AUTHENTICATE, 309 | \Framework\HTTP\ResponseHeader::PUBLIC_KEY_PINS, 310 | \Framework\HTTP\ResponseHeader::PUBLIC_KEY_PINS_REPORT_ONLY, 311 | \Framework\HTTP\ResponseHeader::REFERRER_POLICY, 312 | \Framework\HTTP\ResponseHeader::RETRY_AFTER, 313 | \Framework\HTTP\ResponseHeader::SERVER, 314 | \Framework\HTTP\ResponseHeader::SET_COOKIE, 315 | \Framework\HTTP\ResponseHeader::SOURCEMAP, 316 | \Framework\HTTP\ResponseHeader::STRICT_TRANSPORT_SECURITY, 317 | \Framework\HTTP\ResponseHeader::TIMING_ALLOW_ORIGIN, 318 | \Framework\HTTP\ResponseHeader::TK, 319 | \Framework\HTTP\ResponseHeader::TRAILER, 320 | \Framework\HTTP\ResponseHeader::TRANSFER_ENCODING, 321 | \Framework\HTTP\ResponseHeader::UPGRADE, 322 | \Framework\HTTP\ResponseHeader::VARY, 323 | \Framework\HTTP\ResponseHeader::VIA, 324 | \Framework\HTTP\ResponseHeader::WARNING, 325 | \Framework\HTTP\ResponseHeader::WWW_AUTHENTICATE, 326 | \Framework\HTTP\ResponseHeader::X_CONTENT_TYPE_OPTIONS, 327 | \Framework\HTTP\ResponseHeader::X_DNS_PREFETCH_CONTROL, 328 | \Framework\HTTP\ResponseHeader::X_FRAME_OPTIONS, 329 | \Framework\HTTP\ResponseHeader::X_POWERED_BY, 330 | \Framework\HTTP\ResponseHeader::X_REQUEST_ID, 331 | \Framework\HTTP\ResponseHeader::X_XSS_PROTECTION, 332 | 'Accept-Ranges', 333 | 'Access-Control-Allow-Credentials', 334 | 'Access-Control-Allow-Headers', 335 | 'Access-Control-Allow-Methods', 336 | 'Access-Control-Allow-Origin', 337 | 'Access-Control-Expose-Headers', 338 | 'Access-Control-Max-Age', 339 | 'Age', 340 | 'Allow', 341 | 'Cache-Control', 342 | 'Clear-Site-Data', 343 | 'Connection', 344 | 'Content-Disposition', 345 | 'Content-Encoding', 346 | 'Content-Language', 347 | 'Content-Length', 348 | 'Content-Location', 349 | 'Content-Range', 350 | 'Content-Security-Policy', 351 | 'Content-Security-Policy-Report-Only', 352 | 'Content-Type', 353 | 'Date', 354 | 'ETag', 355 | 'Expect-CT', 356 | 'Expires', 357 | 'Feature-Policy', 358 | 'Keep-Alive', 359 | 'Last-Modified', 360 | 'Link', 361 | 'Location', 362 | 'Pragma', 363 | 'Proxy-Authenticate', 364 | 'Public-Key-Pins', 365 | 'Public-Key-Pins-Report-Only', 366 | 'Referrer-Policy', 367 | 'Retry-After', 368 | 'Server', 369 | 'Set-Cookie', 370 | 'SourceMap', 371 | 'Strict-Transport-Security', 372 | 'Timing-Allow-Origin', 373 | 'Tk', 374 | 'Trailer', 375 | 'Transfer-Encoding', 376 | 'Upgrade', 377 | 'Vary', 378 | 'Via', 379 | 'WWW-Authenticate', 380 | 'Warning', 381 | 'X-Content-Type-Options', 382 | 'X-DNS-Prefetch-Control', 383 | 'X-Frame-Options', 384 | 'X-Powered-By', 385 | 'X-Request-ID', 386 | 'X-XSS-Protection', 387 | ); 388 | expectedArguments( 389 | \Framework\HTTP\Client\Response::getHeader(), 390 | 0, 391 | argumentsSet('response_headers') 392 | ); 393 | expectedArguments( 394 | \Framework\HTTP\Client\Response::getHeaderLine(), 395 | 0, 396 | argumentsSet('response_headers') 397 | ); 398 | 399 | -------------------------------------------------------------------------------- /src/Request.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | namespace Framework\HTTP\Client; 11 | 12 | use CURLFile; 13 | use CURLStringFile; 14 | use Framework\Helpers\ArraySimple; 15 | use Framework\HTTP\Cookie; 16 | use Framework\HTTP\Message; 17 | use Framework\HTTP\Method; 18 | use Framework\HTTP\Protocol; 19 | use Framework\HTTP\RequestHeader; 20 | use Framework\HTTP\RequestInterface; 21 | use Framework\HTTP\URL; 22 | use InvalidArgumentException; 23 | use JetBrains\PhpStorm\ArrayShape; 24 | use JetBrains\PhpStorm\Pure; 25 | use JsonException; 26 | use OutOfBoundsException; 27 | use Override; 28 | use RuntimeException; 29 | use SensitiveParameter; 30 | 31 | /** 32 | * Class Request. 33 | * 34 | * @package http-client 35 | */ 36 | class Request extends Message implements RequestInterface 37 | { 38 | /** 39 | * HTTP Request Method. 40 | */ 41 | protected string $method = Method::GET; 42 | /** 43 | * HTTP Request URL. 44 | */ 45 | protected URL $url; 46 | /** 47 | * POST files. 48 | * 49 | * @var array|string> 50 | */ 51 | protected array $files = []; 52 | /** 53 | * Client default curl options. 54 | * 55 | * @var array 56 | */ 57 | protected array $defaultOptions = [ 58 | \CURLOPT_CONNECTTIMEOUT => 10, 59 | \CURLOPT_TIMEOUT => 60, 60 | \CURLOPT_FOLLOWLOCATION => false, 61 | \CURLOPT_MAXREDIRS => 1, 62 | \CURLOPT_AUTOREFERER => true, 63 | \CURLOPT_RETURNTRANSFER => true, 64 | \CURLOPT_ENCODING => '', 65 | ]; 66 | /** 67 | * Custom curl options. 68 | * 69 | * @var array 70 | */ 71 | protected array $options = []; 72 | protected bool $checkOptions = false; 73 | protected bool $getInfo = false; 74 | 75 | /** 76 | * Request constructor. 77 | * 78 | * @param URL|string $url 79 | */ 80 | public function __construct(URL | string $url) 81 | { 82 | $this->setUrl($url); 83 | } 84 | 85 | #[Override] 86 | public function __toString() : string 87 | { 88 | if ($this->parseContentType() === 'multipart/form-data') { 89 | $this->setBody($this->getMultipartBody()); 90 | } 91 | if (!$this->hasHeader(RequestHeader::ACCEPT)) { 92 | $accept = '*/*'; 93 | $this->setHeader(RequestHeader::ACCEPT, $accept); 94 | } 95 | $options = $this->getOptions(); 96 | if (isset($options[\CURLOPT_ENCODING]) 97 | && !$this->hasHeader(RequestHeader::ACCEPT_ENCODING) 98 | ) { 99 | $encoding = $options[\CURLOPT_ENCODING] === '' 100 | ? 'deflate, gzip, br, zstd' 101 | : $options[\CURLOPT_ENCODING]; 102 | $this->setHeader(RequestHeader::ACCEPT_ENCODING, $encoding); 103 | } 104 | $message = parent::__toString(); 105 | if (isset($accept)) { 106 | $this->removeHeader(RequestHeader::ACCEPT); 107 | } 108 | if (isset($encoding)) { 109 | $this->removeHeader(RequestHeader::ACCEPT_ENCODING); 110 | } 111 | return $message; 112 | } 113 | 114 | protected function getMultipartBody() : string 115 | { 116 | $bodyParts = []; 117 | \parse_str($this->getBody(), $post); 118 | /** 119 | * @var array $post 120 | */ 121 | $post = ArraySimple::convert($post); 122 | foreach ($post as $field => $value) { 123 | $field = \htmlspecialchars($field, \ENT_QUOTES | \ENT_HTML5); 124 | $bodyParts[] = \implode("\r\n", [ 125 | "Content-Disposition: form-data; name=\"{$field}\"", 126 | '', 127 | $value, 128 | ]); 129 | } 130 | /** 131 | * @var array $files 132 | */ 133 | $files = ArraySimple::convert($this->getFiles()); 134 | foreach ($files as $field => $file) { 135 | $field = (string) $field; 136 | $field = \htmlspecialchars($field, \ENT_QUOTES | \ENT_HTML5); 137 | $info = $this->getFileInfo($file); 138 | $filename = \htmlspecialchars($info['filename'], \ENT_QUOTES | \ENT_HTML5); 139 | $bodyParts[] = \implode("\r\n", [ 140 | 'Content-Disposition: form-data; name="' . $field . '"; filename="' . $filename . '"', 141 | 'Content-Type: ' . $info['mime'], 142 | '', 143 | $info['data'], 144 | ]); 145 | } 146 | unset($info); 147 | $boundary = \str_repeat('-', 24) . \substr(\md5(\implode("\r\n", $bodyParts)), 0, 16); 148 | $this->setHeader( 149 | RequestHeader::CONTENT_TYPE, 150 | 'multipart/form-data; charset=UTF-8; boundary=' . $boundary 151 | ); 152 | foreach ($bodyParts as &$part) { 153 | $part = "--{$boundary}\r\n{$part}"; 154 | } 155 | unset($part); 156 | $bodyParts[] = "--{$boundary}--"; 157 | $bodyParts[] = ''; 158 | $bodyParts = \implode("\r\n", $bodyParts); 159 | $this->setHeader( 160 | RequestHeader::CONTENT_LENGTH, 161 | (string) \strlen($bodyParts) 162 | ); 163 | return $bodyParts; 164 | } 165 | 166 | /** 167 | * @param CURLFile|CURLStringFile|string $file 168 | * 169 | * @return array 170 | */ 171 | #[ArrayShape(['filename' => 'string', 'data' => 'string', 'mime' => 'string'])] 172 | protected function getFileInfo(CURLFile | CURLStringFile | string $file) : array 173 | { 174 | if ($file instanceof CURLFile) { 175 | return [ 176 | 'filename' => $file->getPostFilename(), 177 | 'data' => (string) \file_get_contents($file->getFilename()), 178 | 'mime' => $file->getMimeType() ?: 'application/octet-stream', 179 | ]; 180 | } 181 | // @phpstan-ignore-next-line 182 | if ($file instanceof CURLStringFile) { 183 | return [ 184 | 'filename' => $file->postname, 185 | 'data' => $file->data, 186 | 'mime' => $file->mime, 187 | ]; 188 | } 189 | return [ 190 | 'filename' => \basename($file), 191 | 'data' => (string) \file_get_contents($file), 192 | 'mime' => \mime_content_type($file) ?: 'application/octet-stream', 193 | ]; 194 | } 195 | 196 | /** 197 | * @param URL|string $url 198 | * 199 | * @return static 200 | */ 201 | #[Override] 202 | public function setUrl(URL | string $url) : static 203 | { 204 | if (!$url instanceof URL) { 205 | $url = new URL($url); 206 | } 207 | $this->setHeader(RequestHeader::HOST, $url->getHost()); 208 | return parent::setUrl($url); 209 | } 210 | 211 | #[Override] 212 | #[Pure] 213 | public function getUrl() : URL 214 | { 215 | return parent::getUrl(); 216 | } 217 | 218 | #[Override] 219 | #[Pure] 220 | public function getMethod() : string 221 | { 222 | return parent::getMethod(); 223 | } 224 | 225 | /** 226 | * @param string $method 227 | * 228 | * @throws InvalidArgumentException for invalid method 229 | * 230 | * @return bool 231 | */ 232 | #[Override] 233 | public function isMethod(string $method) : bool 234 | { 235 | return parent::isMethod($method); 236 | } 237 | 238 | /** 239 | * @param string $method 240 | * 241 | * @return static 242 | */ 243 | #[Override] 244 | public function setMethod(string $method) : static 245 | { 246 | return parent::setMethod($method); 247 | } 248 | 249 | /** 250 | * @param string $protocol 251 | * 252 | * @return static 253 | */ 254 | #[Override] 255 | public function setProtocol(string $protocol) : static 256 | { 257 | return parent::setProtocol($protocol); 258 | } 259 | 260 | /** 261 | * Set the request body. 262 | * 263 | * @param array|string $body 264 | * 265 | * @return static 266 | */ 267 | #[Override] 268 | public function setBody(array | string $body) : static 269 | { 270 | if (\is_array($body)) { 271 | $body = \http_build_query($body); 272 | } 273 | return parent::setBody($body); 274 | } 275 | 276 | /** 277 | * Set body with JSON data. 278 | * 279 | * @param mixed $data 280 | * @param int|null $flags [optional]

281 | * Bitmask consisting of JSON_FORCE_OBJECT, 282 | * JSON_HEX_AMP, 283 | * JSON_HEX_APOS, 284 | * JSON_HEX_QUOT, 285 | * JSON_HEX_TAG, 286 | * JSON_INVALID_UTF8_IGNORE, 287 | * JSON_INVALID_UTF8_SUBSTITUTE, 288 | * JSON_INVALID_UTF8_SUBSTITUTE, 289 | * JSON_NUMERIC_CHECK, 290 | * JSON_PARTIAL_OUTPUT_ON_ERROR, 291 | * JSON_PRESERVE_ZERO_FRACTION, 292 | * JSON_PRETTY_PRINT, 293 | * JSON_THROW_ON_ERROR, 294 | * JSON_UNESCAPED_LINE_TERMINATORS, 295 | * JSON_UNESCAPED_SLASHES, 296 | * JSON_UNESCAPED_UNICODE. 297 | *

298 | *

Default is JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE 299 | * when null. JSON_THROW_ON_ERROR is enforced by default.

300 | * @param int<1,max> $depth [optional] Set the maximum depth. Must be greater than zero. 301 | * 302 | * @see https://www.php.net/manual/en/function.json-encode.php 303 | * @see https://www.php.net/manual/en/json.constants.php 304 | * 305 | * @throws JsonException if json_encode() fails 306 | * 307 | * @return static 308 | */ 309 | public function setJson(mixed $data, ?int $flags = null, int $depth = 512) : static 310 | { 311 | if ($flags === null) { 312 | $flags = $this->getJsonFlags(); 313 | } 314 | $data = \json_encode($data, $flags | \JSON_THROW_ON_ERROR, $depth); 315 | $this->setContentType('application/json'); 316 | $this->setBody($data); 317 | return $this; 318 | } 319 | 320 | /** 321 | * Set POST data simulating a browser request. 322 | * 323 | * @param array $data 324 | * 325 | * @return static 326 | */ 327 | public function setPost(array $data) : static 328 | { 329 | $this->setMethod(Method::POST); 330 | $this->setBody($data); 331 | return $this; 332 | } 333 | 334 | #[Pure] 335 | public function hasFiles() : bool 336 | { 337 | return !empty($this->files); 338 | } 339 | 340 | /** 341 | * Get files for upload. 342 | * 343 | * @return array 344 | */ 345 | #[Pure] 346 | public function getFiles() : array 347 | { 348 | return $this->files; 349 | } 350 | 351 | /** 352 | * Set files for upload. 353 | * 354 | * @param array $files Fields as keys, files (CURLFile, 355 | * CURLStringFile or string filename) as values. 356 | * Multi-dimensional array is allowed. 357 | * 358 | * @throws InvalidArgumentException for invalid file path 359 | * 360 | * @return static 361 | */ 362 | public function setFiles(array $files) : static 363 | { 364 | $this->setMethod(Method::POST); 365 | $this->setContentType('multipart/form-data'); 366 | $this->files = $files; 367 | return $this; 368 | } 369 | 370 | /** 371 | * Set the Content-Type header. 372 | * 373 | * @param string $mime 374 | * @param string|null $charset 375 | * 376 | * @return static 377 | */ 378 | public function setContentType(string $mime, ?string $charset = 'UTF-8') : static 379 | { 380 | $this->setHeader( 381 | RequestHeader::CONTENT_TYPE, 382 | $mime . ($charset ? '; charset=' . $charset : '') 383 | ); 384 | return $this; 385 | } 386 | 387 | /** 388 | * @param Cookie $cookie 389 | * 390 | * @return static 391 | */ 392 | #[Override] 393 | public function setCookie(Cookie $cookie) : static 394 | { 395 | parent::setCookie($cookie); 396 | $this->setCookieHeader(); 397 | return $this; 398 | } 399 | 400 | /** 401 | * @param array $cookies 402 | * 403 | * @return static 404 | */ 405 | #[Override] 406 | public function setCookies(array $cookies) : static 407 | { 408 | return parent::setCookies($cookies); 409 | } 410 | 411 | /** 412 | * @param string $name 413 | * 414 | * @return static 415 | */ 416 | #[Override] 417 | public function removeCookie(string $name) : static 418 | { 419 | parent::removeCookie($name); 420 | $this->setCookieHeader(); 421 | return $this; 422 | } 423 | 424 | /** 425 | * @param array $names 426 | * 427 | * @return static 428 | */ 429 | #[Override] 430 | public function removeCookies(array $names) : static 431 | { 432 | parent::removeCookies($names); 433 | $this->setCookieHeader(); 434 | return $this; 435 | } 436 | 437 | /** 438 | * @return static 439 | */ 440 | protected function setCookieHeader() : static 441 | { 442 | $line = []; 443 | foreach ($this->getCookies() as $cookie) { 444 | $line[] = $cookie->getName() . '=' . $cookie->getValue(); 445 | } 446 | if ($line) { 447 | $line = \implode('; ', $line); 448 | return $this->setHeader(RequestHeader::COOKIE, $line); 449 | } 450 | return $this->removeHeader(RequestHeader::COOKIE); 451 | } 452 | 453 | /** 454 | * @param string $name 455 | * @param string $value 456 | * 457 | * @return static 458 | */ 459 | #[Override] 460 | public function setHeader(string $name, string $value) : static 461 | { 462 | return parent::setHeader($name, $value); 463 | } 464 | 465 | /** 466 | * @param array $headers 467 | * 468 | * @return static 469 | */ 470 | #[Override] 471 | public function setHeaders(array $headers) : static 472 | { 473 | return parent::setHeaders($headers); 474 | } 475 | 476 | /** 477 | * @param string $name 478 | * 479 | * @return static 480 | */ 481 | #[Override] 482 | public function removeHeader(string $name) : static 483 | { 484 | return parent::removeHeader($name); 485 | } 486 | 487 | /** 488 | * @return static 489 | */ 490 | #[Override] 491 | public function removeHeaders() : static 492 | { 493 | return parent::removeHeaders(); 494 | } 495 | 496 | /** 497 | * Set Authorization header with Basic type. 498 | * 499 | * @param string $username 500 | * @param string $password 501 | * 502 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization 503 | * 504 | * @return static 505 | */ 506 | public function setBasicAuth( 507 | string $username, 508 | #[SensitiveParameter] 509 | string $password 510 | ) : static { 511 | return $this->setHeader( 512 | RequestHeader::AUTHORIZATION, 513 | 'Basic ' . \base64_encode($username . ':' . $password) 514 | ); 515 | } 516 | 517 | /** 518 | * Set Authorization header with Bearer type. 519 | * 520 | * @param string $token 521 | * 522 | * @return static 523 | */ 524 | public function setBearerAuth(#[SensitiveParameter] string $token) : static 525 | { 526 | return $this->setHeader( 527 | RequestHeader::AUTHORIZATION, 528 | 'Bearer ' . $token 529 | ); 530 | } 531 | 532 | /** 533 | * Set the User-Agent header. 534 | * 535 | * @param string|null $userAgent 536 | * 537 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent 538 | * 539 | * @return static 540 | */ 541 | public function setUserAgent(?string $userAgent = null) : static 542 | { 543 | $userAgent ??= 'Aplus HTTP Client'; 544 | return $this->setHeader(RequestHeader::USER_AGENT, $userAgent); 545 | } 546 | 547 | /** 548 | * Set a callback to write the response body with chunks. 549 | * 550 | * Used to write data to files, databases, etc... 551 | * 552 | * NOTE: Using this function makes the Response body, returned in the 553 | * {@see Client::run()} method, be set with an empty string. 554 | * 555 | * @param callable $callback A callback with the response body $data chunk 556 | * as first argument and the CurlHandle as the second. Return is not 557 | * necessary. 558 | * 559 | * @return static 560 | */ 561 | public function setDownloadFunction(callable $callback) : static 562 | { 563 | $function = static function (\CurlHandle $handle, string $data) use ($callback) : int { 564 | $callback($data, $handle); 565 | return \strlen($data); 566 | }; 567 | $this->setOption(\CURLOPT_WRITEFUNCTION, $function); 568 | return $this; 569 | } 570 | 571 | /** 572 | * Set a filename to download the file. 573 | * 574 | * @param string $filename The filename 575 | * @param bool $overwrite Set true to allow to overwrite the file 576 | * 577 | * @throws RuntimeException if $overwrite is false and the file exists 578 | * 579 | * @return static 580 | */ 581 | public function setDownloadFile(string $filename, bool $overwrite = false) : static 582 | { 583 | $isFile = \is_file($filename); 584 | if ($isFile && $overwrite === false) { 585 | throw new RuntimeException('File path already exists: ' . $filename); 586 | } 587 | if ($isFile) { 588 | \unlink($filename); 589 | } 590 | $this->setDownloadFunction(static function (string $data) use ($filename) : void { 591 | \file_put_contents($filename, $data, \FILE_APPEND); 592 | }); 593 | return $this; 594 | } 595 | 596 | /** 597 | * Set curl options. 598 | * 599 | * @param int $option A curl constant 600 | * @param mixed $value 601 | * 602 | * @see Client::$defaultOptions 603 | * 604 | * @return static 605 | */ 606 | public function setOption(int $option, mixed $value) : static 607 | { 608 | if ($this->isCheckingOptions()) { 609 | $this->checkOption($option, $value); 610 | } 611 | $this->options[$option] = $value; 612 | return $this; 613 | } 614 | 615 | /** 616 | * Set many curl options. 617 | * 618 | * @param array $options Curl constants as keys and their respective values 619 | * 620 | * @return static 621 | */ 622 | public function setOptions(array $options) : static 623 | { 624 | foreach ($options as $option => $value) { 625 | $this->setOption($option, $value); 626 | } 627 | return $this; 628 | } 629 | 630 | /** 631 | * Get default options replaced by custom. 632 | * 633 | * @return array 634 | */ 635 | public function getOptions() : array 636 | { 637 | $options = \array_replace($this->defaultOptions, $this->options); 638 | $options[\CURLOPT_PROTOCOLS] = \CURLPROTO_HTTPS | \CURLPROTO_HTTP; 639 | $options[\CURLOPT_HTTP_VERSION] = match ($this->getProtocol()) { 640 | Protocol::HTTP_1_0 => \CURL_HTTP_VERSION_1_0, 641 | Protocol::HTTP_1_1 => \CURL_HTTP_VERSION_1_1, 642 | Protocol::HTTP_2_0 => \CURL_HTTP_VERSION_2_0, 643 | Protocol::HTTP_2 => \CURL_HTTP_VERSION_2, 644 | default => throw new InvalidArgumentException( 645 | 'Invalid Request Protocol: ' . $this->getProtocol() 646 | ) 647 | }; 648 | switch ($this->getMethod()) { 649 | case Method::POST: 650 | $options[\CURLOPT_POST] = true; 651 | $options[\CURLOPT_POSTFIELDS] = $this->getPostAndFiles(); 652 | break; 653 | case Method::DELETE: 654 | case Method::PATCH: 655 | case Method::PUT: 656 | $options[\CURLOPT_POSTFIELDS] = $this->getBody(); 657 | break; 658 | } 659 | $options[\CURLOPT_CUSTOMREQUEST] = $this->getMethod(); 660 | $options[\CURLOPT_HEADER] = false; 661 | $options[\CURLOPT_URL] = $this->getUrl()->toString(); 662 | $options[\CURLOPT_HTTPHEADER] = $this->getHeaderLines(); 663 | return $options; 664 | } 665 | 666 | public function getOption(int $option) : mixed 667 | { 668 | return $this->getOptions()[$option] ?? null; 669 | } 670 | 671 | /** 672 | * Returns string if the Request has not files and curl will set the 673 | * Content-Type header to application/x-www-form-urlencoded. If the Request 674 | * has files, returns an array and curl will set the Content-Type to 675 | * multipart/form-data. 676 | * 677 | * If the Request has files, the $post and $files arrays are converted to 678 | * the array_simple format. Because curl does not understand the PHP 679 | * multi-dimensional arrays. 680 | * 681 | * @see https://www.php.net/manual/en/function.curl-setopt.php CURLOPT_POSTFIELDS 682 | * @see ArraySimple::convert() 683 | * 684 | * @return array|string 685 | */ 686 | public function getPostAndFiles() : array | string 687 | { 688 | if (!$this->hasFiles()) { 689 | return $this->getBody(); 690 | } 691 | \parse_str($this->getBody(), $post); 692 | $post = ArraySimple::convert($post); 693 | foreach ($post as &$value) { 694 | $value = (string) $value; 695 | } 696 | unset($value); 697 | $files = ArraySimple::convert($this->getFiles()); 698 | foreach ($files as $field => &$file) { 699 | // @phpstan-ignore-next-line 700 | if ($file instanceof CURLFile || $file instanceof CURLStringFile) { 701 | continue; 702 | } 703 | if (!\is_file($file)) { 704 | throw new InvalidArgumentException( 705 | "Field '{$field}' does not match a file: {$file}" 706 | ); 707 | } 708 | $file = new CURLFile( 709 | $file, 710 | \mime_content_type($file) ?: 'application/octet-stream', 711 | \basename($file) 712 | ); 713 | } 714 | unset($file); 715 | return \array_replace($post, $files); 716 | } 717 | 718 | public function setGetInfo(bool $get = true) : static 719 | { 720 | $this->getInfo = $get; 721 | return $this; 722 | } 723 | 724 | public function isGettingInfo() : bool 725 | { 726 | return $this->getInfo; 727 | } 728 | 729 | /** 730 | * @param bool $check 731 | * 732 | * @return static 733 | */ 734 | public function setCheckOptions(bool $check = true) : static 735 | { 736 | $this->checkOptions = $check; 737 | return $this; 738 | } 739 | 740 | public function isCheckingOptions() : bool 741 | { 742 | return $this->checkOptions; 743 | } 744 | 745 | /** 746 | * @param int $option The curl option 747 | * @param mixed $value The curl option value 748 | * 749 | * @throws InvalidArgumentException if the option value does not match the 750 | * expected type 751 | * @throws OutOfBoundsException if the option is invalid 752 | */ 753 | protected function checkOption(int $option, mixed $value) : void 754 | { 755 | $types = [ 756 | 'bool' => [ 757 | \CURLOPT_AUTOREFERER, 758 | \CURLOPT_COOKIESESSION, 759 | \CURLOPT_CERTINFO, 760 | \CURLOPT_CONNECT_ONLY, 761 | \CURLOPT_CRLF, 762 | \CURLOPT_DISALLOW_USERNAME_IN_URL, 763 | \CURLOPT_DNS_SHUFFLE_ADDRESSES, 764 | \CURLOPT_HAPROXYPROTOCOL, 765 | \CURLOPT_SSH_COMPRESSION, 766 | \CURLOPT_DNS_USE_GLOBAL_CACHE, 767 | \CURLOPT_FAILONERROR, 768 | \CURLOPT_SSL_FALSESTART, 769 | \CURLOPT_FILETIME, 770 | \CURLOPT_FOLLOWLOCATION, 771 | \CURLOPT_FORBID_REUSE, 772 | \CURLOPT_FRESH_CONNECT, 773 | \CURLOPT_FTP_USE_EPRT, 774 | \CURLOPT_FTP_USE_EPSV, 775 | \CURLOPT_FTP_CREATE_MISSING_DIRS, 776 | \CURLOPT_FTPAPPEND, 777 | \CURLOPT_TCP_NODELAY, 778 | // CURLOPT_FTPASCII, 779 | \CURLOPT_FTPLISTONLY, 780 | \CURLOPT_HEADER, 781 | \CURLINFO_HEADER_OUT, 782 | \CURLOPT_HTTP09_ALLOWED, 783 | \CURLOPT_HTTPGET, 784 | \CURLOPT_HTTPPROXYTUNNEL, 785 | \CURLOPT_HTTP_CONTENT_DECODING, 786 | \CURLOPT_KEEP_SENDING_ON_ERROR, 787 | // CURLOPT_MUTE, 788 | \CURLOPT_NETRC, 789 | \CURLOPT_NOBODY, 790 | \CURLOPT_NOPROGRESS, 791 | \CURLOPT_NOSIGNAL, 792 | \CURLOPT_PATH_AS_IS, 793 | \CURLOPT_PIPEWAIT, 794 | \CURLOPT_POST, 795 | \CURLOPT_PUT, 796 | \CURLOPT_RETURNTRANSFER, 797 | \CURLOPT_SASL_IR, 798 | \CURLOPT_SSL_ENABLE_ALPN, 799 | \CURLOPT_SSL_ENABLE_NPN, 800 | \CURLOPT_SSL_VERIFYPEER, 801 | \CURLOPT_SSL_VERIFYSTATUS, 802 | \CURLOPT_PROXY_SSL_VERIFYPEER, 803 | \CURLOPT_SUPPRESS_CONNECT_HEADERS, 804 | \CURLOPT_TCP_FASTOPEN, 805 | \CURLOPT_TFTP_NO_OPTIONS, 806 | \CURLOPT_TRANSFERTEXT, 807 | \CURLOPT_UNRESTRICTED_AUTH, 808 | \CURLOPT_UPLOAD, 809 | \CURLOPT_VERBOSE, 810 | ], 811 | 'int' => [ 812 | \CURLOPT_BUFFERSIZE, 813 | \CURLOPT_CONNECTTIMEOUT, 814 | \CURLOPT_CONNECTTIMEOUT_MS, 815 | \CURLOPT_DNS_CACHE_TIMEOUT, 816 | \CURLOPT_EXPECT_100_TIMEOUT_MS, 817 | \CURLOPT_HAPPY_EYEBALLS_TIMEOUT_MS, 818 | \CURLOPT_FTPSSLAUTH, 819 | \CURLOPT_HEADEROPT, 820 | \CURLOPT_HTTP_VERSION, 821 | \CURLOPT_HTTPAUTH, 822 | \CURLOPT_INFILESIZE, 823 | \CURLOPT_LOW_SPEED_LIMIT, 824 | \CURLOPT_LOW_SPEED_TIME, 825 | \CURLOPT_MAXCONNECTS, 826 | \CURLOPT_MAXREDIRS, 827 | \CURLOPT_PORT, 828 | \CURLOPT_POSTREDIR, 829 | \CURLOPT_PROTOCOLS, 830 | \CURLOPT_PROXYAUTH, 831 | \CURLOPT_PROXYPORT, 832 | \CURLOPT_PROXYTYPE, 833 | \CURLOPT_REDIR_PROTOCOLS, 834 | \CURLOPT_RESUME_FROM, 835 | \CURLOPT_SOCKS5_AUTH, 836 | \CURLOPT_SSL_OPTIONS, 837 | \CURLOPT_SSL_VERIFYHOST, 838 | \CURLOPT_SSLVERSION, 839 | \CURLOPT_PROXY_SSL_OPTIONS, 840 | \CURLOPT_PROXY_SSL_VERIFYHOST, 841 | \CURLOPT_PROXY_SSLVERSION, 842 | \CURLOPT_STREAM_WEIGHT, 843 | \CURLOPT_TCP_KEEPALIVE, 844 | \CURLOPT_TCP_KEEPIDLE, 845 | \CURLOPT_TCP_KEEPINTVL, 846 | \CURLOPT_TIMECONDITION, 847 | \CURLOPT_TIMEOUT, 848 | \CURLOPT_TIMEOUT_MS, 849 | \CURLOPT_TIMEVALUE, 850 | \CURLOPT_TIMEVALUE_LARGE, 851 | \CURLOPT_MAX_RECV_SPEED_LARGE, 852 | \CURLOPT_MAX_SEND_SPEED_LARGE, 853 | \CURLOPT_SSH_AUTH_TYPES, 854 | \CURLOPT_IPRESOLVE, 855 | \CURLOPT_FTP_FILEMETHOD, 856 | ], 857 | 'string' => [ 858 | \CURLOPT_ABSTRACT_UNIX_SOCKET, 859 | \CURLOPT_CAINFO, 860 | \CURLOPT_CAPATH, 861 | \CURLOPT_COOKIE, 862 | \CURLOPT_COOKIEFILE, 863 | \CURLOPT_COOKIEJAR, 864 | \CURLOPT_COOKIELIST, 865 | \CURLOPT_CUSTOMREQUEST, 866 | \CURLOPT_DEFAULT_PROTOCOL, 867 | \CURLOPT_DNS_INTERFACE, 868 | \CURLOPT_DNS_LOCAL_IP4, 869 | \CURLOPT_DNS_LOCAL_IP6, 870 | \CURLOPT_DOH_URL, 871 | \CURLOPT_EGDSOCKET, 872 | \CURLOPT_ENCODING, 873 | \CURLOPT_FTPPORT, 874 | \CURLOPT_INTERFACE, 875 | \CURLOPT_KEYPASSWD, 876 | \CURLOPT_KRB4LEVEL, 877 | \CURLOPT_LOGIN_OPTIONS, 878 | \CURLOPT_PINNEDPUBLICKEY, 879 | \CURLOPT_POSTFIELDS, 880 | \CURLOPT_PRIVATE, 881 | \CURLOPT_PRE_PROXY, 882 | \CURLOPT_PROXY, 883 | \CURLOPT_PROXY_SERVICE_NAME, 884 | \CURLOPT_PROXY_CAINFO, 885 | \CURLOPT_PROXY_CAPATH, 886 | \CURLOPT_PROXY_CRLFILE, 887 | \CURLOPT_PROXY_KEYPASSWD, 888 | \CURLOPT_PROXY_PINNEDPUBLICKEY, 889 | \CURLOPT_PROXY_SSLCERT, 890 | \CURLOPT_PROXY_SSLCERTTYPE, 891 | \CURLOPT_PROXY_SSL_CIPHER_LIST, 892 | \CURLOPT_PROXY_TLS13_CIPHERS, 893 | \CURLOPT_PROXY_SSLKEY, 894 | \CURLOPT_PROXY_SSLKEYTYPE, 895 | \CURLOPT_PROXY_TLSAUTH_PASSWORD, 896 | \CURLOPT_PROXY_TLSAUTH_TYPE, 897 | \CURLOPT_PROXY_TLSAUTH_USERNAME, 898 | \CURLOPT_PROXYUSERPWD, 899 | \CURLOPT_RANDOM_FILE, 900 | \CURLOPT_RANGE, 901 | \CURLOPT_REFERER, 902 | \CURLOPT_SERVICE_NAME, 903 | \CURLOPT_SSH_HOST_PUBLIC_KEY_MD5, 904 | \CURLOPT_SSH_PUBLIC_KEYFILE, 905 | \CURLOPT_SSH_PRIVATE_KEYFILE, 906 | \CURLOPT_SSL_CIPHER_LIST, 907 | \CURLOPT_SSLCERT, 908 | \CURLOPT_SSLCERTPASSWD, 909 | \CURLOPT_SSLCERTTYPE, 910 | \CURLOPT_SSLENGINE, 911 | \CURLOPT_SSLENGINE_DEFAULT, 912 | \CURLOPT_SSLKEY, 913 | \CURLOPT_SSLKEYPASSWD, 914 | \CURLOPT_SSLKEYTYPE, 915 | \CURLOPT_TLS13_CIPHERS, 916 | \CURLOPT_UNIX_SOCKET_PATH, 917 | \CURLOPT_URL, 918 | \CURLOPT_USERAGENT, 919 | \CURLOPT_USERNAME, 920 | \CURLOPT_PASSWORD, 921 | \CURLOPT_USERPWD, 922 | \CURLOPT_XOAUTH2_BEARER, 923 | ], 924 | 'array' => [ 925 | \CURLOPT_CONNECT_TO, 926 | \CURLOPT_HTTP200ALIASES, 927 | \CURLOPT_HTTPHEADER, 928 | \CURLOPT_POSTQUOTE, 929 | \CURLOPT_PROXYHEADER, 930 | \CURLOPT_QUOTE, 931 | \CURLOPT_RESOLVE, 932 | ], 933 | 'fopen' => [ 934 | \CURLOPT_FILE, 935 | \CURLOPT_INFILE, 936 | \CURLOPT_STDERR, 937 | \CURLOPT_WRITEHEADER, 938 | ], 939 | 'function' => [ 940 | \CURLOPT_HEADERFUNCTION, 941 | // CURLOPT_PASSWDFUNCTION, 942 | \CURLOPT_PROGRESSFUNCTION, 943 | \CURLOPT_READFUNCTION, 944 | \CURLOPT_WRITEFUNCTION, 945 | ], 946 | 'curl_share_init' => [ 947 | \CURLOPT_SHARE, 948 | ], 949 | ]; 950 | foreach ($types as $type => $constants) { 951 | foreach ($constants as $constant) { 952 | if ($option !== $constant) { 953 | continue; 954 | } 955 | if ($value === null) { 956 | return; 957 | } 958 | $valid = match ($type) { 959 | 'bool' => \is_bool($value), 960 | 'int' => \is_int($value), 961 | 'string' => \is_string($value), 962 | 'array' => \is_array($value), 963 | 'fopen' => \is_resource($value), 964 | 'function' => \is_callable($value), 965 | 'curl_share_init' => $value instanceof \CurlShareHandle 966 | }; 967 | if ($valid) { 968 | return; 969 | } 970 | $message = match ($type) { 971 | 'bool' => 'The value of option %d should be of bool type', 972 | 'int' => 'The value of option %d should be of int type', 973 | 'string' => 'The value of option %d should be of string type', 974 | 'array' => 'The value of option %d should be of array type', 975 | 'fopen' => 'The value of option %d should be a fopen() resource', 976 | 'function' => 'The value of option %d should be a callable', 977 | 'curl_share_init' => 'The value of option %d should be a result of curl_share_init()' 978 | }; 979 | throw new InvalidArgumentException(\sprintf($message, $option)); 980 | } 981 | } 982 | throw new OutOfBoundsException('Invalid curl constant option: ' . $option); 983 | } 984 | } 985 | --------------------------------------------------------------------------------