├── .github ├── FUNDING.yml └── dependabot.yml ├── src ├── Exceptions │ ├── RuntimeException.php │ ├── InvalidArgumentException.php │ ├── InvalidConfigException.php │ ├── Exception.php │ └── HttpException.php ├── Traits │ ├── HasHttpRequests.php │ ├── CreatesDefaultHttpClient.php │ └── ResponseCastable.php ├── Responses │ ├── StreamResponse.php │ └── Response.php ├── Config.php ├── Support │ ├── XML.php │ └── Collection.php └── Client.php ├── .editorconfig ├── composer.json └── README.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [overtrue] 2 | -------------------------------------------------------------------------------- /src/Exceptions/RuntimeException.php: -------------------------------------------------------------------------------- 1 | response = $response; 17 | $this->formattedResponse = $formattedResponse; 18 | 19 | $response?->getBody()->rewind(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Traits/HasHttpRequests.php: -------------------------------------------------------------------------------- 1 | [ 14 | CURLOPT_IPRESOLVE => CURL_IPRESOLVE_V4, 15 | ], 16 | ]; 17 | 18 | public static function setDefaultOptions(array $defaults = []) 19 | { 20 | self::$defaults = $defaults; 21 | } 22 | 23 | public static function getDefaultOptions(): array 24 | { 25 | return self::$defaults; 26 | } 27 | 28 | public function setHttpClient(ClientInterface $httpClient): static 29 | { 30 | $this->httpClient = $httpClient; 31 | 32 | return $this; 33 | } 34 | 35 | public function getHttpClient(): ClientInterface 36 | { 37 | if (!$this->httpClient) { 38 | $this->httpClient = new Client(['handler' => $this->getHandlerStack()]); 39 | } 40 | 41 | return $this->httpClient; 42 | } 43 | 44 | public function request(string $uri, string $method = 'GET', array $options = [], bool $async = false) 45 | { 46 | return $this->getHttpClient()->{ $async ? 'requestAsync' : 'request' }(strtoupper($method), $uri, array_merge(self::$defaults, $options)); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Responses/StreamResponse.php: -------------------------------------------------------------------------------- 1 | getBody()->rewind(); 15 | 16 | $directory = rtrim($directory, '/'); 17 | 18 | if (!is_dir($directory)) { 19 | mkdir($directory, 0755, true); // @codeCoverageIgnore 20 | } 21 | 22 | if (!is_writable($directory)) { 23 | throw new InvalidArgumentException(sprintf("'%s' is not writable.", $directory)); 24 | } 25 | 26 | $contents = $this->getBody()->getContents(); 27 | 28 | if (empty($filename)) { 29 | if (preg_match('/filename="(?.*?)"/', $this->getHeaderLine('Content-Disposition'), $match)) { 30 | $filename = $match['filename']; 31 | } else { 32 | $filename = md5($contents); 33 | } 34 | } 35 | 36 | if (empty(pathinfo($filename, PATHINFO_EXTENSION))) { 37 | $filename .= File::getStreamExt($contents); 38 | } 39 | 40 | file_put_contents($directory.'/'.$filename, $contents); 41 | 42 | return $filename; 43 | } 44 | 45 | /** 46 | * @throws \Overtrue\Http\Exceptions\InvalidArgumentException 47 | */ 48 | public function saveAs(string $directory, string $filename): string 49 | { 50 | return $this->save($directory, $filename); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "overtrue/http", 3 | "description": "A simple http client wrapper.", 4 | "type": "library", 5 | "require": { 6 | "php": ">=8.0.2", 7 | "guzzlehttp/guzzle": "^7.4", 8 | "ext-curl": "*", 9 | "ext-json": "*", 10 | "ext-simplexml": "*", 11 | "ext-libxml": "*" 12 | }, 13 | "require-dev": { 14 | "phpunit/phpunit": "^9.5", 15 | "mockery/mockery": "^1.0", 16 | "overtrue/phplint": "^3.0", 17 | "brainmaestro/composer-git-hooks": "^2.7", 18 | "friendsofphp/php-cs-fixer": "^3.0" 19 | }, 20 | "autoload": { 21 | "psr-4": { 22 | "Overtrue\\Http\\": "src" 23 | } 24 | }, 25 | "license": "MIT", 26 | "authors": [ 27 | { 28 | "name": "overtrue", 29 | "email": "anzhengchao@gmail.com" 30 | } 31 | ], 32 | "scripts": { 33 | "post-update-cmd": [ 34 | "cghooks update" 35 | ], 36 | "post-merge": "composer install", 37 | "post-install-cmd": [ 38 | "cghooks add --ignore-lock", 39 | "cghooks update" 40 | ], 41 | "cghooks": "vendor/bin/cghooks", 42 | "check-style": "php-cs-fixer fix --using-cache=no --diff --dry-run --ansi", 43 | "fix-style": "php-cs-fixer fix --using-cache=no --ansi", 44 | "test": "vendor/bin/phpunit --colors=always" 45 | }, 46 | "scripts-descriptions": { 47 | "test": "Run all tests.", 48 | "check-style": "Run style checks (only dry run - no fixing!).", 49 | "fix-style": "Run style checks and fix violations." 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Traits/CreatesDefaultHttpClient.php: -------------------------------------------------------------------------------- 1 | $this->getHandlerStack(), 17 | ], $options)); 18 | } 19 | 20 | public function pushMiddleware(callable $middleware, string $name = null): static 21 | { 22 | if (!is_null($name)) { 23 | $this->middlewares[$name] = $middleware; 24 | } else { 25 | $this->middlewares[] = $middleware; 26 | } 27 | 28 | return $this; 29 | } 30 | 31 | public function getMiddlewares(): array 32 | { 33 | return $this->middlewares; 34 | } 35 | 36 | public function setMiddlewares(array $middlewares): static 37 | { 38 | $this->middlewares = $middlewares; 39 | 40 | return $this; 41 | } 42 | 43 | public function setHandlerStack(HandlerStack $handlerStack): static 44 | { 45 | $this->handlerStack = $handlerStack; 46 | 47 | return $this; 48 | } 49 | 50 | public function getHandlerStack(): HandlerStack 51 | { 52 | if ($this->handlerStack) { 53 | return $this->handlerStack; 54 | } 55 | 56 | $this->handlerStack = HandlerStack::create(); 57 | 58 | foreach ($this->middlewares as $name => $middleware) { 59 | $this->handlerStack->push($middleware, $name); 60 | } 61 | 62 | return $this->handlerStack; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Traits/ResponseCastable.php: -------------------------------------------------------------------------------- 1 | getBody()->rewind(); 20 | 21 | switch ($type ?? 'array') { 22 | case 'collection': 23 | return $response->toCollection(); 24 | case 'array': 25 | return $response->toArray(); 26 | case 'object': 27 | return $response->toObject(); 28 | } 29 | } 30 | 31 | /** 32 | * @throws \Overtrue\Http\Exceptions\InvalidArgumentException 33 | */ 34 | protected function detectAndCastResponseToType($response, $type = null) 35 | { 36 | $response = match (true) { 37 | $response instanceof ResponseInterface => Response::buildFromPsrResponse($response), 38 | ($response instanceof Collection) || is_array($response) || is_object($response) => new Response( 39 | 200, 40 | [], 41 | json_encode($response) 42 | ), 43 | is_scalar($response) => new Response(200, [], $response), 44 | default => throw new InvalidArgumentException(sprintf( 45 | 'Unsupported response type "%s"', 46 | gettype($response) 47 | )), 48 | }; 49 | 50 | return $this->castResponseToType($response, $type); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Responses/Response.php: -------------------------------------------------------------------------------- 1 | getBody()->rewind(); 15 | $contents = $this->getBody()->getContents(); 16 | $this->getBody()->rewind(); 17 | 18 | return $contents; 19 | } 20 | 21 | public static function buildFromPsrResponse(ResponseInterface $response): self 22 | { 23 | return new static( 24 | $response->getStatusCode(), 25 | $response->getHeaders(), 26 | $response->getBody(), 27 | $response->getProtocolVersion(), 28 | $response->getReasonPhrase() 29 | ); 30 | } 31 | 32 | public function toJson(): string 33 | { 34 | return json_encode($this->toArray()); 35 | } 36 | 37 | public function toArray(): array 38 | { 39 | $content = $this->getBodyContents(); 40 | 41 | if (false !== stripos($this->getHeaderLine('Content-Type'), 'xml') || 0 === stripos($content, 'getBodyContents(), true); 46 | 47 | if (JSON_ERROR_NONE === json_last_error()) { 48 | return (array) $array; 49 | } 50 | 51 | return []; 52 | } 53 | 54 | public function toCollection(): Collection 55 | { 56 | return new Collection($this->toArray()); 57 | } 58 | 59 | public function toObject(): object 60 | { 61 | return json_decode($this->getBodyContents()); 62 | } 63 | 64 | public function __toString(): string 65 | { 66 | return $this->getBodyContents(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Config.php: -------------------------------------------------------------------------------- 1 | null, 12 | 'timeout' => 3000, 13 | 'connect_timeout' => 3000, 14 | 'proxy' => [], 15 | ]; 16 | 17 | protected bool $autoTrimEndpointSlash = true; 18 | 19 | public function __construct(array $options = []) 20 | { 21 | $this->options = array_merge($this->options, $options); 22 | } 23 | 24 | public function getBaseUri(): string 25 | { 26 | return $this->options['base_uri'] ?? ''; 27 | } 28 | 29 | public function setBaseUri(string $baseUri): static 30 | { 31 | $this->options['base_uri'] = $baseUri; 32 | 33 | return $this; 34 | } 35 | 36 | public function getTimeout(): int 37 | { 38 | return $this->options['timeout'] ?? 3000; 39 | } 40 | 41 | public function setTimeout(float|int $timeout): static 42 | { 43 | $this->options['timeout'] = $timeout; 44 | 45 | return $this; 46 | } 47 | 48 | public function getConnectTimeout(): int 49 | { 50 | return $this->options['connect_timeout'] ?? 3000; 51 | } 52 | 53 | public function setConnectTimeout(float|int $connectTimeout): static 54 | { 55 | $this->options['connect_timeout'] = $connectTimeout; 56 | 57 | return $this; 58 | } 59 | 60 | public function getProxy(): array 61 | { 62 | return $this->options['proxy'] ?? []; 63 | } 64 | 65 | public function setProxy(array $proxy): static 66 | { 67 | $this->options['proxy'] = $proxy; 68 | 69 | return $this; 70 | } 71 | 72 | public function toArray(): array 73 | { 74 | return $this->options; 75 | } 76 | 77 | public function setOption(string $key, mixed $value): static 78 | { 79 | $this->options[$key] = $value; 80 | 81 | return $this; 82 | } 83 | 84 | public function getOption(string $key, mixed $default = null) 85 | { 86 | return $this->options[$key] ?? $default; 87 | } 88 | 89 | public function mergeOptions(array $options): static 90 | { 91 | $this->options = array_merge($this->options, $options); 92 | 93 | return $this; 94 | } 95 | 96 | public function setOptions(array $options): static 97 | { 98 | $this->options = $options; 99 | 100 | return $this; 101 | } 102 | 103 | public function getOptions(): array 104 | { 105 | return $this->options; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/Support/XML.php: -------------------------------------------------------------------------------- 1 | $value) { 25 | $_attr[] = "{$key}=\"{$value}\""; 26 | } 27 | 28 | $attr = implode(' ', $_attr); 29 | } 30 | 31 | $attr = trim($attr); 32 | $attr = empty($attr) ? '' : " {$attr}"; 33 | $xml = "<{$root}{$attr}>"; 34 | $xml .= self::data2Xml($data, $item, $id); 35 | $xml .= ""; 36 | 37 | return $xml; 38 | } 39 | 40 | public static function cdata($string): string 41 | { 42 | return sprintf('', $string); 43 | } 44 | 45 | protected static function normalize($obj) 46 | { 47 | $result = null; 48 | 49 | if (is_object($obj)) { 50 | $obj = (array) $obj; 51 | } 52 | 53 | if (is_array($obj)) { 54 | foreach ($obj as $key => $value) { 55 | $res = self::normalize($value); 56 | if (('@attributes' === $key) && ($key)) { 57 | $result = $res; // @codeCoverageIgnore 58 | } else { 59 | $result[$key] = $res; 60 | } 61 | } 62 | } else { 63 | $result = $obj; 64 | } 65 | 66 | return $result; 67 | } 68 | 69 | protected static function data2Xml($data, $item = 'item', $id = 'id'): string 70 | { 71 | $xml = $attr = ''; 72 | 73 | foreach ($data as $key => $val) { 74 | if (is_numeric($key)) { 75 | $id && $attr = " {$id}=\"{$key}\""; 76 | $key = $item; 77 | } 78 | 79 | $xml .= "<{$key}{$attr}>"; 80 | 81 | if ((is_array($val) || is_object($val))) { 82 | $xml .= self::data2Xml((array) $val, $item, $id); 83 | } else { 84 | $xml .= is_numeric($val) ? $val : self::cdata($val); 85 | } 86 | 87 | $xml .= ""; 88 | } 89 | 90 | return $xml; 91 | } 92 | 93 | public static function sanitize($xml): array|string|null 94 | { 95 | return preg_replace('/[^\x{9}\x{A}\x{D}\x{20}-\x{D7FF}\x{E000}-\x{FFFD}\x{10000}-\x{10FFFF}]+/u', '', $xml); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |

Http

3 |

4 | 5 |

:cactus: A simple http client wrapper.

6 | 7 |

8 | Build Status 9 | Latest Stable Version 10 | Latest Unstable Version 11 | Build Status 12 | Scrutinizer Code Quality 13 | Total Downloads 14 | License 15 |

16 | 17 | [![Sponsor me](https://github.com/overtrue/overtrue/blob/master/sponsor-me.svg?raw=true)](https://github.com/sponsors/overtrue) 18 | 19 | ## Installing 20 | 21 | ```shell 22 | $ composer require overtrue/http -vvv 23 | ``` 24 | 25 | ## Usage 26 | 27 | ```php 28 | get('https://httpbin.org/ip'); 35 | //{ 36 | // "ip": "1.2.3.4" 37 | //} 38 | ``` 39 | 40 | ### Configuration: 41 | 42 | ```php 43 | 44 | use Overtrue\Http\Client; 45 | 46 | $config = [ 47 | 'base_uri' => 'https://www.easyhttp.com/apiV2/', 48 | 'timeout' => 3000, 49 | 'headers' => [ 50 | 'User-Agent' => 'MyClient/1.0', 51 | 'Content-Type' => 'application/json' 52 | ] 53 | //... 54 | ]; 55 | 56 | $client = Client::create($config); // or new Client($config); 57 | 58 | //... 59 | ``` 60 | 61 | ### Custom response type 62 | 63 | ```php 64 | $config = new Config([ 65 | 'base_uri' => 'https://www.easyhttp.com/apiV2/', 66 | 67 | // array(default)/collection/object/raw 68 | 'response_type' => 'collection', 69 | ]); 70 | 71 | //... 72 | ``` 73 | 74 | ### Logging request and response 75 | 76 | Install monolog: 77 | 78 | ```bash 79 | $ composer require monolog/monolog 80 | ``` 81 | Add logger middleware: 82 | 83 | ```php 84 | use Overtrue\Http\Client; 85 | 86 | $client = Client::create(); 87 | 88 | $logger = new \Monolog\Logger('my-logger'); 89 | 90 | $logger->pushHandler( 91 | new \Monolog\Handler\RotatingFileHandler('/tmp/my-log.log') 92 | ); 93 | 94 | $client->pushMiddleware(\GuzzleHttp\Middleware::log( 95 | $logger, 96 | new \GuzzleHttp\MessageFormatter(\GuzzleHttp\MessageFormatter::DEBUG) 97 | )); 98 | 99 | $response = $client->get('https://httpbin.org/ip'); 100 | ``` 101 | 102 | ## :heart: Sponsor me 103 | 104 | [![Sponsor me](https://github.com/overtrue/overtrue/blob/master/sponsor-me.svg?raw=true)](https://github.com/sponsors/overtrue) 105 | 106 | 如果你喜欢我的项目并想支持它,[点击这里 :heart:](https://github.com/sponsors/overtrue) 107 | 108 | 109 | ## Project supported by JetBrains 110 | 111 | Many thanks to Jetbrains for kindly providing a license for me to work on this and other open-source projects. 112 | 113 | [![](https://resources.jetbrains.com/storage/products/company/brand/logos/jb_beam.svg)](https://www.jetbrains.com/?from=https://github.com/overtrue) 114 | 115 | ## PHP 扩展包开发 116 | 117 | > 想知道如何从零开始构建 PHP 扩展包? 118 | > 119 | > 请关注我的实战课程,我会在此课程中分享一些扩展开发经验 —— [《PHP 扩展包实战教程 - 从入门到发布》](https://learnku.com/courses/creating-package) 120 | 121 | ## License 122 | 123 | MIT 124 | -------------------------------------------------------------------------------- /src/Support/Collection.php: -------------------------------------------------------------------------------- 1 | $value) { 19 | $this->set($key, $value); 20 | } 21 | } 22 | 23 | public function all(): array 24 | { 25 | return $this->items; 26 | } 27 | 28 | public function only(array $keys): self 29 | { 30 | $return = []; 31 | 32 | foreach ($keys as $key) { 33 | $value = $this->get($key); 34 | 35 | if (!is_null($value)) { 36 | $return[$key] = $value; 37 | } 38 | } 39 | 40 | return new static($return); 41 | } 42 | 43 | public function except(string|array $keys): self 44 | { 45 | $keys = is_array($keys) ? $keys : func_get_args(); 46 | 47 | return new static(array_diff($this->items, array_combine($keys, array_pad([], count($keys), null)))); 48 | } 49 | 50 | public function merge(array|Collection $items): self 51 | { 52 | foreach ($items as $key => $value) { 53 | $this->set($key, $value); 54 | } 55 | 56 | return new static($this->all()); 57 | } 58 | 59 | public function has(string $key): bool 60 | { 61 | return !is_null($this->dotGet($this->items, $key)); 62 | } 63 | 64 | public function first(): mixed 65 | { 66 | return reset($this->items); 67 | } 68 | 69 | public function last(): mixed 70 | { 71 | $end = end($this->items); 72 | 73 | reset($this->items); 74 | 75 | return $end; 76 | } 77 | 78 | public function add(string $key, mixed $value) 79 | { 80 | $this->dotSet($this->items, $key, $value); 81 | } 82 | 83 | public function set(string $key, mixed $value) 84 | { 85 | $this->dotSet($this->items, $key, $value); 86 | } 87 | 88 | public function forget(string $key) 89 | { 90 | $this->dotRemove($this->items, $key); 91 | } 92 | 93 | public function get(string $key, mixed $default = null) 94 | { 95 | return $this->dotGet($this->items, $key, $default); 96 | } 97 | 98 | public function dotGet(array $array, string $key, mixed $default = null) 99 | { 100 | if (array_key_exists($key, $array)) { 101 | return $array[$key]; 102 | } 103 | foreach (explode('.', $key) as $segment) { 104 | if (array_key_exists($segment, $array)) { 105 | $array = $array[$segment]; 106 | } else { 107 | return $default; 108 | } 109 | } 110 | } 111 | 112 | public function dotSet(array &$array, string $key, mixed $value): array 113 | { 114 | $keys = explode('.', $key); 115 | while (count($keys) > 1) { 116 | $key = array_shift($keys); 117 | if (!isset($array[$key]) || !is_array($array[$key])) { 118 | $array[$key] = []; 119 | } 120 | $array = &$array[$key]; 121 | } 122 | $array[array_shift($keys)] = $value; 123 | 124 | return $array; 125 | } 126 | 127 | public function dotRemove(array &$array, array|string $keys) 128 | { 129 | $original = &$array; 130 | $keys = (array) $keys; 131 | if (0 === count($keys)) { 132 | return; 133 | } 134 | 135 | foreach ($keys as $key) { 136 | if (array_key_exists($key, $array)) { 137 | unset($array[$key]); 138 | continue; 139 | } 140 | $parts = explode('.', $key); 141 | 142 | $array = &$original; 143 | 144 | while (count($parts) > 1) { 145 | $part = array_shift($parts); 146 | if (isset($array[$part]) && is_array($array[$part])) { 147 | $array = &$array[$part]; 148 | } else { 149 | continue 2; 150 | } 151 | } 152 | unset($array[array_shift($parts)]); 153 | } 154 | } 155 | 156 | #[Pure] 157 | public function toArray(): array 158 | { 159 | return $this->all(); 160 | } 161 | 162 | public function toJson(int $option = JSON_UNESCAPED_UNICODE): string 163 | { 164 | return json_encode($this->all(), $option); 165 | } 166 | 167 | public function __toString(): string 168 | { 169 | return $this->toJson(); 170 | } 171 | 172 | public function jsonSerialize(): array 173 | { 174 | return $this->items; 175 | } 176 | 177 | public function getIterator(): ArrayIterator 178 | { 179 | return new ArrayIterator($this->items); 180 | } 181 | 182 | public function count(): int 183 | { 184 | return count($this->items); 185 | } 186 | 187 | public function __get($key) 188 | { 189 | return $this->get($key); 190 | } 191 | 192 | public function __set($key, $value) 193 | { 194 | $this->set($key, $value); 195 | } 196 | 197 | public function __isset($key): bool 198 | { 199 | return $this->has($key); 200 | } 201 | 202 | public function __unset($key) 203 | { 204 | $this->forget($key); 205 | } 206 | 207 | public static function __set_state($array): object 208 | { 209 | return new self($array); 210 | } 211 | 212 | public function offsetExists($offset): bool 213 | { 214 | return $this->has($offset); 215 | } 216 | 217 | public function offsetUnset($offset): void 218 | { 219 | if ($this->offsetExists($offset)) { 220 | $this->forget($offset); 221 | } 222 | } 223 | 224 | public function offsetGet($offset): mixed 225 | { 226 | return $this->offsetExists($offset) ? $this->get($offset) : null; 227 | } 228 | 229 | public function offsetSet($offset, $value): void 230 | { 231 | $this->set($offset, $value); 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /src/Client.php: -------------------------------------------------------------------------------- 1 | config = $this->normalizeConfig($config); 29 | } 30 | 31 | public function get(string $uri, array $options = [], bool $async = false) 32 | { 33 | return $this->request($uri, 'GET', $options, $async); 34 | } 35 | 36 | public function getAsync(string $uri, array $options = []) 37 | { 38 | return $this->get($uri, $options, true); 39 | } 40 | 41 | public function post(string $uri, array $data = [], array $options = [], bool $async = false) 42 | { 43 | return $this->request($uri, 'POST', \array_merge($options, ['form_params' => $data]), $async); 44 | } 45 | 46 | public function postAsync(string $uri, array $data = [], array $options = []) 47 | { 48 | return $this->post($uri, $data, $options, true); 49 | } 50 | 51 | public function patch(string $uri, array $data = [], array $options = [], bool $async = false) 52 | { 53 | return $this->request($uri, 'PATCH', \array_merge($options, ['form_params' => $data]), $async); 54 | } 55 | 56 | public function patchAsync(string $uri, array $data = [], array $options = []) 57 | { 58 | return $this->patch($uri, $data, $options, true); 59 | } 60 | 61 | public function put(string $uri, array $data = [], array $options = [], bool $async = false) 62 | { 63 | return $this->request($uri, 'PUT', \array_merge($options, ['form_params' => $data]), $async); 64 | } 65 | 66 | public function putAsync(string $uri, array $data = [], array $options = []) 67 | { 68 | return $this->put($uri, $data, $options, true); 69 | } 70 | 71 | public function options(string $uri, array $options = [], bool $async = false) 72 | { 73 | return $this->request($uri, 'OPTIONS', $options, $async); 74 | } 75 | 76 | public function optionsAsync(string $uri, array $options = []) 77 | { 78 | return $this->options($uri, $options, true); 79 | } 80 | 81 | public function head(string $uri, array $options = [], bool $async = false) 82 | { 83 | return $this->request($uri, 'HEAD', $options, $async); 84 | } 85 | 86 | public function headAsync(string $uri, array $options = []) 87 | { 88 | return $this->head($uri, $options, true); 89 | } 90 | 91 | public function delete(string $uri, array $options = [], bool $async = false) 92 | { 93 | return $this->request($uri, 'DELETE', $options, $async); 94 | } 95 | 96 | public function deleteAsync(string $uri, array $options = []) 97 | { 98 | return $this->delete($uri, $options, true); 99 | } 100 | 101 | public function upload(string $uri, array $files = [], array $form = [], array $options = [], bool $async = false) 102 | { 103 | $multipart = []; 104 | 105 | foreach ($files as $name => $contents) { 106 | $contents = \is_resource($contents) ? $contents : \fopen($contents, 'r'); 107 | $multipart[] = \compact('name', 'contents'); 108 | } 109 | 110 | foreach ($form as $name => $contents) { 111 | $multipart = array_merge($multipart, $this->normalizeMultipartField($name, $contents)); 112 | } 113 | 114 | return $this->request($uri, 'POST', \array_merge($options, ['multipart' => $multipart]), $async); 115 | } 116 | 117 | public function uploadAsync(string $uri, array $files = [], array $form = [], array $options = []) 118 | { 119 | return $this->upload($uri, $files, $form, $options, true); 120 | } 121 | 122 | public function request(string $uri, string $method = 'GET', array $options = [], bool $async = false) 123 | { 124 | $result = $this->requestRaw($uri, $method, $options, $async); 125 | 126 | $transformer = function ($response) { 127 | return $this->castResponseToType($response, $this->config->getOption('response_type')); 128 | }; 129 | 130 | return $async ? $result->then($transformer) : $transformer($result); 131 | } 132 | 133 | public function requestRaw(string $uri, string $method = 'GET', array $options = [], bool $async = false) 134 | { 135 | if ($this->baseUri) { 136 | $options['base_uri'] = $this->baseUri; 137 | } 138 | 139 | return $this->performRequest($uri, $method, $options, $async); 140 | } 141 | 142 | public function getHttpClient(): ClientInterface 143 | { 144 | if (!$this->httpClient) { 145 | $this->httpClient = $this->createDefaultHttClient($this->config->toArray()); 146 | } 147 | 148 | return $this->httpClient; 149 | } 150 | 151 | public function getConfig(): Config 152 | { 153 | return $this->config; 154 | } 155 | 156 | public function setConfig(Config $config): static 157 | { 158 | $this->config = $config; 159 | 160 | return $this; 161 | } 162 | 163 | public function normalizeMultipartField(string $name, mixed $contents): array 164 | { 165 | $field = []; 166 | 167 | if (!is_array($contents)) { 168 | return [compact('name', 'contents')]; 169 | } 170 | 171 | foreach ($contents as $key => $value) { 172 | $key = sprintf('%s[%s]', $name, $key); 173 | $field = array_merge($field, is_array($value) ? $this->normalizeMultipartField($key, $value) : [['name' => $key, 'contents' => $value]]); 174 | } 175 | 176 | return $field; 177 | } 178 | 179 | protected function normalizeConfig(array|Config $config): Config 180 | { 181 | if (\is_array($config)) { 182 | $config = new Config($config); 183 | } 184 | 185 | if (!($config instanceof Config)) { 186 | throw new \InvalidArgumentException('config must be array or instance of Overtrue\Http\Config.'); 187 | } 188 | 189 | return $config; 190 | } 191 | } 192 | --------------------------------------------------------------------------------