├── .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="(?
2 | Http
3 |
:cactus: A simple http client wrapper.
6 | 7 | 16 | 17 | [](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 | [](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://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 | --------------------------------------------------------------------------------