Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
96.93% |
379 / 391 |
|
94.19% |
81 / 86 |
CRAP | |
0.00% |
0 / 1 |
| Client | |
96.93% |
379 / 391 |
|
94.19% |
81 / 86 |
262 | |
0.00% |
0 / 1 |
| __construct | |
100.00% |
33 / 33 |
|
100.00% |
1 / 1 |
20 | |||
| createMulti | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
| fromCurlCommand | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| setRequest | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
| setMethod | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
| getMethod | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
| hasMethod | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
3 | |||
| setOptions | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
| addOptions | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
| addOption | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
| syncRequestFromOptions | |
100.00% |
21 / 21 |
|
100.00% |
1 / 1 |
16 | |||
| getOptions | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| getOption | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| hasOptions | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| hasOption | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| removeOption | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
| removeOptions | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
| setHandler | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
| getHandler | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| hasHandler | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| addMiddleware | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
| getMiddleware | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| hasMiddleware | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| setMultiHandler | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
| getMultiHandler | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| hasMultiHandler | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| setAuth | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
| getAuth | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| hasAuth | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| setHeaders | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
| addHeaders | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
| addHeader | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
| getHeaders | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
3 | |||
| getHeader | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
3 | |||
| hasHeaders | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
| hasHeader | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
| removeHeader | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
| removeAllHeaders | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
| request | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
| getRequest | |
66.67% |
2 / 3 |
|
0.00% |
0 / 1 |
2.15 | |||
| getResponse | |
66.67% |
2 / 3 |
|
0.00% |
0 / 1 |
2.15 | |||
| setData | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
| addData | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
| getData | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
| hasData | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
3 | |||
| removeData | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
| removeAllData | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
3 | |||
| setQuery | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
| addQuery | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
| getQuery | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
| hasQuery | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
3 | |||
| removeQuery | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
| removeAllQuery | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
3 | |||
| setType | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
| getType | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
| hasType | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
| removeType | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
| setFiles | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
5 | |||
| addFile | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
3 | |||
| getFiles | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
7 | |||
| getFile | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| hasFiles | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| hasFile | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| removeFile | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
| removeFiles | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
| setBody | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
| setBodyFromFile | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 | |||
| hasBody | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
| getBody | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
3 | |||
| getBodyContent | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
3 | |||
| getBodyContentLength | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
3 | |||
| removeBody | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
3 | |||
| prepare | |
90.24% |
37 / 41 |
|
0.00% |
0 / 1 |
30.84 | |||
| send | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
3 | |||
| dispatch | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
3 | |||
| dispatchRequest | |
90.91% |
10 / 11 |
|
0.00% |
0 / 1 |
6.03 | |||
| processMiddleware | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
3 | |||
| sendRequest | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
2 | |||
| convertToClientRequest | |
68.75% |
11 / 16 |
|
0.00% |
0 / 1 |
7.10 | |||
| sendAsync | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| render | |
100.00% |
19 / 19 |
|
100.00% |
1 / 1 |
8 | |||
| reset | |
100.00% |
28 / 28 |
|
100.00% |
1 / 1 |
14 | |||
| toCurlCommand | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
| __toString | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| __call | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 | |||
| __callStatic | |
100.00% |
23 / 23 |
|
100.00% |
1 / 1 |
12 | |||
| 1 | <?php |
| 2 | declare(strict_types=1); |
| 3 | /** |
| 4 | * Pop PHP Framework (https://www.popphp.org/) |
| 5 | * |
| 6 | * @link https://github.com/popphp/popphp-framework |
| 7 | * @author Nick Sagona, III <nick@popphp.org> |
| 8 | * @copyright Copyright (c) 2009-2026 Nick Sagona, III |
| 9 | * @license https://www.popphp.org/license New BSD License |
| 10 | */ |
| 11 | |
| 12 | /** |
| 13 | * @namespace |
| 14 | */ |
| 15 | namespace Pop\Http; |
| 16 | |
| 17 | use Pop\Http\Client\Handler\Stream; |
| 18 | use Pop\Http\Client\Request; |
| 19 | use Pop\Http\Client\Response; |
| 20 | use Pop\Http\Client\Handler\Curl; |
| 21 | use Pop\Http\Client\Handler\CurlMulti; |
| 22 | use Pop\Http\Client\Handler\HandlerInterface; |
| 23 | use Pop\Http\Client\Handler\Mock; |
| 24 | use Pop\Http\Client\Middleware\CallableMiddleware; |
| 25 | use Pop\Http\Client\Middleware\MiddlewareInterface; |
| 26 | use Pop\Http\Client\Middleware\Pipeline; |
| 27 | use Pop\Http\Body; |
| 28 | use Pop\Mime\Part\Header; |
| 29 | use Psr\Http\Client\ClientInterface; |
| 30 | use Psr\Http\Message\RequestInterface; |
| 31 | use Psr\Http\Message\ResponseInterface; |
| 32 | |
| 33 | /** |
| 34 | * HTTP client class |
| 35 | * |
| 36 | * @category Pop |
| 37 | * @package Pop\Http |
| 38 | * @author Nick Sagona, III <nick@popphp.org> |
| 39 | * @copyright Copyright (c) 2009-2026 Nick Sagona, III |
| 40 | * @license https://www.popphp.org/license New BSD License |
| 41 | * @version 6.0.0 |
| 42 | */ |
| 43 | class Client extends AbstractHttp implements ClientInterface |
| 44 | { |
| 45 | |
| 46 | /** |
| 47 | * Client options |
| 48 | * @var array |
| 49 | */ |
| 50 | protected array $options = []; |
| 51 | |
| 52 | /** |
| 53 | * Request handler |
| 54 | * @var ?HandlerInterface |
| 55 | */ |
| 56 | protected ?HandlerInterface $handler = null; |
| 57 | |
| 58 | /** |
| 59 | * Request multi-handler |
| 60 | * @var ?CurlMulti |
| 61 | */ |
| 62 | protected ?CurlMulti $multiHandler = null; |
| 63 | |
| 64 | /** |
| 65 | * HTTP auth object |
| 66 | * @var ?Auth |
| 67 | */ |
| 68 | protected ?Auth $auth = null; |
| 69 | |
| 70 | /** |
| 71 | * Registered middleware, in registration order (first-registered = outermost) |
| 72 | * @var array |
| 73 | */ |
| 74 | protected array $middleware = []; |
| 75 | |
| 76 | /** |
| 77 | * Instantiate the client object |
| 78 | * |
| 79 | * Optional parameters are |
| 80 | * - Request URI string |
| 81 | * - Client request instance |
| 82 | * - Client response instance |
| 83 | * - Client handler instance |
| 84 | * - Auth instance |
| 85 | * - Options array |
| 86 | */ |
| 87 | public function __construct() |
| 88 | { |
| 89 | $args = func_get_args(); |
| 90 | $request = null; |
| 91 | $response = null; |
| 92 | $options = null; |
| 93 | $handler = null; |
| 94 | |
| 95 | foreach ($args as $arg) { |
| 96 | if (is_string($arg)) { |
| 97 | $request = new Request($arg); |
| 98 | } else if ($arg instanceof Client\Request) { |
| 99 | $request = $arg; |
| 100 | } else if ($arg instanceof Client\Response) { |
| 101 | $response = $arg; |
| 102 | } else if ($arg instanceof Client\Handler\HandlerInterface) { |
| 103 | $handler = $arg; |
| 104 | } else if ($arg instanceof Auth) { |
| 105 | $this->setAuth($arg); |
| 106 | } else if (is_array($arg)) { |
| 107 | $options = $arg; |
| 108 | } |
| 109 | } |
| 110 | |
| 111 | parent::__construct($request, $response); |
| 112 | |
| 113 | if ($options !== null) { |
| 114 | $this->setOptions($options); |
| 115 | } |
| 116 | |
| 117 | if ($handler !== null) { |
| 118 | if ($handler instanceof CurlMulti) { |
| 119 | $this->setMultiHandler($handler); |
| 120 | } else { |
| 121 | $this->setHandler($handler); |
| 122 | } |
| 123 | } |
| 124 | |
| 125 | if ((!$this->hasRequest()) && (isset($this->options['base_uri']) || $this->hasOption('method') || |
| 126 | $this->hasOption('data') || $this->hasOption('query') || $this->hasOption('headers') || |
| 127 | $this->hasOption('type') || $this->hasOption('files'))) { |
| 128 | $this->request = new Request( |
| 129 | isset($this->options['base_uri']) ? new Uri($this->options['base_uri']) : null, |
| 130 | $this->options['method'] ?? 'GET' |
| 131 | ); |
| 132 | } |
| 133 | |
| 134 | $this->syncRequestFromOptions(); |
| 135 | } |
| 136 | |
| 137 | /** |
| 138 | * Factory to create a multi-handler object |
| 139 | * |
| 140 | * @param array $requests |
| 141 | * @param Client\Handler\CurlMulti $multiHandler |
| 142 | * @return CurlMulti |
| 143 | */ |
| 144 | public static function createMulti( |
| 145 | array $requests, Client\Handler\CurlMulti $multiHandler = new Client\Handler\CurlMulti() |
| 146 | ): CurlMulti |
| 147 | { |
| 148 | foreach ($requests as $request) { |
| 149 | $client = new Client($request); |
| 150 | $client->setMultiHandler($multiHandler); |
| 151 | } |
| 152 | |
| 153 | return $multiHandler; |
| 154 | } |
| 155 | |
| 156 | /** |
| 157 | * Method to convert Curl CLI command to a client object |
| 158 | * |
| 159 | * @param string $command |
| 160 | * @throws Curl\Exception |
| 161 | * @return Client |
| 162 | */ |
| 163 | public static function fromCurlCommand(string $command): Client |
| 164 | { |
| 165 | return Curl\Command::commandToClient($command); |
| 166 | } |
| 167 | |
| 168 | /** |
| 169 | * Set the request object |
| 170 | * |
| 171 | * Overrides AbstractHttp::setRequest() so that any options already set on this client |
| 172 | * (via the constructor or setOptions()/addOption()) get synced onto a request that arrives |
| 173 | * via a direct setRequest() call - not just one materialized internally by prepare(). Without |
| 174 | * this, options set before an explicit setRequest() call were silently dropped, since |
| 175 | * syncRequestFromOptions() is a no-op until a request exists. |
| 176 | * |
| 177 | * @param AbstractRequest $request |
| 178 | * @return Client |
| 179 | */ |
| 180 | public function setRequest(AbstractRequest $request): Client |
| 181 | { |
| 182 | parent::setRequest($request); |
| 183 | $this->syncRequestFromOptions(); |
| 184 | return $this; |
| 185 | } |
| 186 | |
| 187 | /** |
| 188 | * Set method |
| 189 | * |
| 190 | * @param string $method |
| 191 | * @return Client |
| 192 | */ |
| 193 | public function setMethod(string $method): Client |
| 194 | { |
| 195 | if ($this->hasRequest()) { |
| 196 | $this->request()->setMethod($method); |
| 197 | } else { |
| 198 | $this->options['method'] = $method; |
| 199 | } |
| 200 | |
| 201 | return $this; |
| 202 | } |
| 203 | |
| 204 | /** |
| 205 | * Get method |
| 206 | * |
| 207 | * @return ?string |
| 208 | */ |
| 209 | public function getMethod(): ?string |
| 210 | { |
| 211 | if ($this->hasRequest()) { |
| 212 | return $this->request()->getMethod(); |
| 213 | } else { |
| 214 | return $this->options['method'] ?? null; |
| 215 | } |
| 216 | } |
| 217 | |
| 218 | /** |
| 219 | * Has method |
| 220 | * |
| 221 | * @return bool |
| 222 | */ |
| 223 | public function hasMethod(): bool |
| 224 | { |
| 225 | return ((($this->hasRequest()) && !empty($this->request()->getMethod())) || isset($this->options['method'])); |
| 226 | } |
| 227 | |
| 228 | /** |
| 229 | * Set options |
| 230 | * |
| 231 | * Supported options |
| 232 | * - 'base_uri' |
| 233 | * - 'method' |
| 234 | * - 'headers' |
| 235 | * - 'user_agent' |
| 236 | * - 'query' (can only be encoded query string on the URI) |
| 237 | * - 'data' (can be any request data) |
| 238 | * - 'files' |
| 239 | * - 'type' |
| 240 | * - 'auto' |
| 241 | * - 'async' |
| 242 | * - 'verify_peer' |
| 243 | * - 'allow_self_signed' |
| 244 | * - 'no_content_length' |
| 245 | * - 'raw_data' |
| 246 | * - 'force_custom_method' (Curl only - forces CURLOPT_CUSTOMREQUEST) |
| 247 | * |
| 248 | * @param array $options |
| 249 | * @return Client |
| 250 | */ |
| 251 | public function setOptions(array $options): Client |
| 252 | { |
| 253 | $this->options = $options; |
| 254 | $this->syncRequestFromOptions(); |
| 255 | |
| 256 | return $this; |
| 257 | } |
| 258 | |
| 259 | /** |
| 260 | * Add options |
| 261 | * |
| 262 | * @param array $options |
| 263 | * @return Client |
| 264 | */ |
| 265 | public function addOptions(array $options): Client |
| 266 | { |
| 267 | foreach ($options as $name => $value) { |
| 268 | $this->addOption($name, $value); |
| 269 | } |
| 270 | return $this; |
| 271 | } |
| 272 | |
| 273 | /** |
| 274 | * Add an option |
| 275 | * |
| 276 | * @param string $name |
| 277 | * @param mixed $value |
| 278 | * @return Client |
| 279 | */ |
| 280 | public function addOption(string $name, mixed $value): Client |
| 281 | { |
| 282 | $this->options[$name] = $value; |
| 283 | $this->syncRequestFromOptions(); |
| 284 | |
| 285 | return $this; |
| 286 | } |
| 287 | |
| 288 | /** |
| 289 | * Push request-shaped options ('type', 'headers', 'query', 'data', 'files') onto the |
| 290 | * client's request, if one exists. Options are configuration that seeds the request the |
| 291 | * moment both are available - they are not a second, parallel data store - so this is the |
| 292 | * one place that seeding happens, called any time the request or the options change. |
| 293 | * |
| 294 | * 'query' and 'data' are merged onto the request key-by-key (addQuery()/addData()), not |
| 295 | * replaced wholesale (setQuery()/setData()) - this method can run again later (e.g. from a |
| 296 | * later addOption() call for an unrelated key), and a wholesale replace would silently wipe |
| 297 | * out any data/query values added directly in between via addData()/addQuery() or a prior |
| 298 | * sync. 'headers' and 'files' are naturally merge-safe already (addHeaders()/addData() key |
| 299 | * on name), and 'type' is guarded to apply only once (first request type wins). |
| 300 | * |
| 301 | * @return void |
| 302 | */ |
| 303 | protected function syncRequestFromOptions(): void |
| 304 | { |
| 305 | if (!$this->hasRequest()) { |
| 306 | return; |
| 307 | } |
| 308 | |
| 309 | if ($this->hasOption('method')) { |
| 310 | $this->request()->setMethod($this->options['method']); |
| 311 | } |
| 312 | |
| 313 | if ($this->hasOption('type') && !$this->request()->hasRequestType()) { |
| 314 | $this->setType($this->options['type']); |
| 315 | } |
| 316 | |
| 317 | if ($this->hasOption('headers') && is_array($this->options['headers'])) { |
| 318 | $this->addHeaders($this->options['headers']); |
| 319 | } |
| 320 | |
| 321 | if ($this->hasOption('query') && is_array($this->options['query'])) { |
| 322 | foreach ($this->options['query'] as $name => $value) { |
| 323 | $this->addQuery($name, $value); |
| 324 | } |
| 325 | } |
| 326 | |
| 327 | if ($this->hasOption('data') && is_array($this->options['data'])) { |
| 328 | foreach ($this->options['data'] as $name => $value) { |
| 329 | $this->addData($name, $value); |
| 330 | } |
| 331 | } |
| 332 | |
| 333 | if ($this->hasOption('files')) { |
| 334 | foreach ($this->options['files'] as $i => $file) { |
| 335 | $name = is_numeric($i) ? 'file' . ($i + 1) : $i; |
| 336 | $this->addData($name, [ |
| 337 | 'filename' => $file, |
| 338 | 'contentType' => Client\Data::getMimeTypeFromFilename($file) |
| 339 | ]); |
| 340 | } |
| 341 | } |
| 342 | } |
| 343 | |
| 344 | /** |
| 345 | * Get options |
| 346 | * |
| 347 | * @return array |
| 348 | */ |
| 349 | public function getOptions(): array |
| 350 | { |
| 351 | return $this->options; |
| 352 | } |
| 353 | |
| 354 | /** |
| 355 | * Get options |
| 356 | * |
| 357 | * @param string $name |
| 358 | * @return mixed |
| 359 | */ |
| 360 | public function getOption(string $name): mixed |
| 361 | { |
| 362 | return $this->options[$name] ?? null; |
| 363 | } |
| 364 | |
| 365 | /** |
| 366 | * Has options |
| 367 | * |
| 368 | * @return bool |
| 369 | */ |
| 370 | public function hasOptions(): bool |
| 371 | { |
| 372 | return !empty($this->options); |
| 373 | } |
| 374 | |
| 375 | /** |
| 376 | * Has option |
| 377 | * |
| 378 | * @return bool |
| 379 | */ |
| 380 | public function hasOption(string $name): bool |
| 381 | { |
| 382 | return array_key_exists($name, $this->options); |
| 383 | } |
| 384 | |
| 385 | /** |
| 386 | * Remove option |
| 387 | * |
| 388 | * @param string $name |
| 389 | * @return Client |
| 390 | */ |
| 391 | public function removeOption(string $name): Client |
| 392 | { |
| 393 | if (isset($this->options[$name])) { |
| 394 | unset($this->options[$name]); |
| 395 | } |
| 396 | return $this; |
| 397 | } |
| 398 | |
| 399 | /** |
| 400 | * Remove options |
| 401 | * |
| 402 | * @return Client |
| 403 | */ |
| 404 | public function removeOptions(): Client |
| 405 | { |
| 406 | $this->options = []; |
| 407 | return $this; |
| 408 | } |
| 409 | |
| 410 | /** |
| 411 | * Set handler |
| 412 | * |
| 413 | * @param HandlerInterface $handler |
| 414 | * @return Client |
| 415 | */ |
| 416 | public function setHandler(HandlerInterface $handler): Client |
| 417 | { |
| 418 | $this->handler = $handler; |
| 419 | return $this; |
| 420 | } |
| 421 | |
| 422 | /** |
| 423 | * Get handler |
| 424 | * |
| 425 | * @return HandlerInterface|null |
| 426 | */ |
| 427 | public function getHandler(): HandlerInterface|null |
| 428 | { |
| 429 | return $this->handler; |
| 430 | } |
| 431 | |
| 432 | /** |
| 433 | * Has handler |
| 434 | * |
| 435 | * @return bool |
| 436 | */ |
| 437 | public function hasHandler(): bool |
| 438 | { |
| 439 | return ($this->handler !== null); |
| 440 | } |
| 441 | |
| 442 | /** |
| 443 | * Add a middleware to the pipeline. A plain callable is wrapped in |
| 444 | * CallableMiddleware automatically. Registration order is wrap order: the |
| 445 | * first-registered middleware is outermost. |
| 446 | * |
| 447 | * @param MiddlewareInterface|callable $middleware |
| 448 | * @return Client |
| 449 | */ |
| 450 | public function addMiddleware(MiddlewareInterface|callable $middleware): Client |
| 451 | { |
| 452 | $this->middleware[] = ($middleware instanceof MiddlewareInterface) |
| 453 | ? $middleware : new CallableMiddleware($middleware); |
| 454 | |
| 455 | return $this; |
| 456 | } |
| 457 | |
| 458 | /** |
| 459 | * Get the registered middleware, in registration order |
| 460 | * |
| 461 | * @return array |
| 462 | */ |
| 463 | public function getMiddleware(): array |
| 464 | { |
| 465 | return $this->middleware; |
| 466 | } |
| 467 | |
| 468 | /** |
| 469 | * Has any middleware registered |
| 470 | * |
| 471 | * @return bool |
| 472 | */ |
| 473 | public function hasMiddleware(): bool |
| 474 | { |
| 475 | return !empty($this->middleware); |
| 476 | } |
| 477 | |
| 478 | /** |
| 479 | * Set multi-handler |
| 480 | * |
| 481 | * @param CurlMulti $multiHandler |
| 482 | * @return Client |
| 483 | */ |
| 484 | public function setMultiHandler(CurlMulti $multiHandler): Client |
| 485 | { |
| 486 | $this->multiHandler = $multiHandler; |
| 487 | |
| 488 | if (!($this->handler instanceof Curl)) { |
| 489 | $this->handler = new Curl(); |
| 490 | $this->multiHandler->addClient($this); |
| 491 | } |
| 492 | |
| 493 | return $this; |
| 494 | } |
| 495 | |
| 496 | /** |
| 497 | * Get multi-handler |
| 498 | * |
| 499 | * @return CurlMulti |
| 500 | */ |
| 501 | public function getMultiHandler(): CurlMulti |
| 502 | { |
| 503 | return $this->multiHandler; |
| 504 | } |
| 505 | |
| 506 | /** |
| 507 | * Has multi-handler |
| 508 | * |
| 509 | * @return bool |
| 510 | */ |
| 511 | public function hasMultiHandler(): bool |
| 512 | { |
| 513 | return ($this->multiHandler !== null); |
| 514 | } |
| 515 | |
| 516 | /** |
| 517 | * Set auth |
| 518 | * |
| 519 | * @param Auth $auth |
| 520 | * @return Client |
| 521 | */ |
| 522 | public function setAuth(Auth $auth): Client |
| 523 | { |
| 524 | $this->auth = $auth; |
| 525 | return $this; |
| 526 | } |
| 527 | |
| 528 | /** |
| 529 | * Get auth |
| 530 | * |
| 531 | * @return ?Auth |
| 532 | */ |
| 533 | public function getAuth(): ?Auth |
| 534 | { |
| 535 | return $this->auth; |
| 536 | } |
| 537 | |
| 538 | /** |
| 539 | * Has auth |
| 540 | * |
| 541 | * @return bool |
| 542 | */ |
| 543 | public function hasAuth(): bool |
| 544 | { |
| 545 | return ($this->auth !== null); |
| 546 | } |
| 547 | |
| 548 | /** |
| 549 | * Set headers (clear out any existing headers) |
| 550 | * |
| 551 | * @param array $headers |
| 552 | * @return Client |
| 553 | */ |
| 554 | public function setHeaders(array $headers): Client |
| 555 | { |
| 556 | $this->request()->setHeaders($headers); |
| 557 | return $this; |
| 558 | } |
| 559 | |
| 560 | /** |
| 561 | * Add headers |
| 562 | * |
| 563 | * @param array $headers |
| 564 | * @return Client |
| 565 | */ |
| 566 | public function addHeaders(array $headers): Client |
| 567 | { |
| 568 | $this->request()->addHeaders($headers); |
| 569 | return $this; |
| 570 | } |
| 571 | |
| 572 | /** |
| 573 | * Add header |
| 574 | * |
| 575 | * @param Header|string|int $header |
| 576 | * @param mixed $value |
| 577 | * @return Client |
| 578 | */ |
| 579 | public function addHeader(Header|string|int $header, mixed $value = null): Client |
| 580 | { |
| 581 | $this->request()->addHeader($header, $value); |
| 582 | return $this; |
| 583 | } |
| 584 | |
| 585 | /** |
| 586 | * Get headers |
| 587 | * |
| 588 | * @return mixed |
| 589 | */ |
| 590 | public function getHeaders(): mixed |
| 591 | { |
| 592 | return ($this->hasRequest() && $this->request()->hasHeaders()) ? $this->request()->getHeaderObjects() : null; |
| 593 | } |
| 594 | |
| 595 | /** |
| 596 | * Get header |
| 597 | * |
| 598 | * @param string $name |
| 599 | * @return mixed |
| 600 | */ |
| 601 | public function getHeader(string $name): mixed |
| 602 | { |
| 603 | return ($this->hasRequest() && $this->request()->hasHeader($name)) ? $this->request()->getHeaderObject($name) : null; |
| 604 | } |
| 605 | |
| 606 | /** |
| 607 | * Has headers |
| 608 | * |
| 609 | * @return bool |
| 610 | */ |
| 611 | public function hasHeaders(): bool |
| 612 | { |
| 613 | return $this->hasRequest() && $this->request()->hasHeaders(); |
| 614 | } |
| 615 | |
| 616 | /** |
| 617 | * Has header |
| 618 | * |
| 619 | * @param string $name |
| 620 | * @return bool |
| 621 | */ |
| 622 | public function hasHeader(string $name): bool |
| 623 | { |
| 624 | return $this->hasRequest() && $this->request()->hasHeader($name); |
| 625 | } |
| 626 | |
| 627 | /** |
| 628 | * Remove header |
| 629 | * |
| 630 | * @param string $name |
| 631 | * @return Client |
| 632 | */ |
| 633 | public function removeHeader(string $name): Client |
| 634 | { |
| 635 | if ($this->hasHeader($name)) { |
| 636 | $this->request()->removeHeader($name); |
| 637 | } |
| 638 | return $this; |
| 639 | } |
| 640 | |
| 641 | /** |
| 642 | * Remove all headers |
| 643 | * |
| 644 | * @return Client |
| 645 | */ |
| 646 | public function removeAllHeaders(): Client |
| 647 | { |
| 648 | if ($this->hasHeaders()) { |
| 649 | $this->request()->removeHeaders(); |
| 650 | } |
| 651 | return $this; |
| 652 | } |
| 653 | |
| 654 | /** |
| 655 | * Get the client's request, materializing an empty one if none exists yet - |
| 656 | * every data-touching setter goes through this, so there is exactly one |
| 657 | * place a Client\Request gets created on demand instead of the old |
| 658 | * options-array-vs-request dual bookkeeping. |
| 659 | * |
| 660 | * @return Client\Request |
| 661 | */ |
| 662 | protected function request(): Client\Request |
| 663 | { |
| 664 | if (!($this->request instanceof Request)) { |
| 665 | $this->request = new Request(); |
| 666 | } |
| 667 | return $this->request; |
| 668 | } |
| 669 | |
| 670 | /** |
| 671 | * Get the request |
| 672 | * |
| 673 | * @throws Exception |
| 674 | * @return Request |
| 675 | */ |
| 676 | public function getRequest(): Request |
| 677 | { |
| 678 | if (!($this->request instanceof Request)) { |
| 679 | throw new Exception('Error: The request object has not been created.'); |
| 680 | } |
| 681 | return $this->request; |
| 682 | } |
| 683 | |
| 684 | /** |
| 685 | * Get the response |
| 686 | * |
| 687 | * @throws Exception |
| 688 | * @return Response |
| 689 | */ |
| 690 | public function getResponse(): Response |
| 691 | { |
| 692 | if (!($this->response instanceof Response)) { |
| 693 | throw new Exception('Error: The response object has not been created.'); |
| 694 | } |
| 695 | return $this->response; |
| 696 | } |
| 697 | |
| 698 | /** |
| 699 | * Set data |
| 700 | * |
| 701 | * @param array $data |
| 702 | * @return Client |
| 703 | */ |
| 704 | public function setData(array $data): Client |
| 705 | { |
| 706 | $this->request()->setData($data); |
| 707 | return $this; |
| 708 | } |
| 709 | |
| 710 | /** |
| 711 | * Add data |
| 712 | * |
| 713 | * @param string $name |
| 714 | * @param mixed $value |
| 715 | * @return Client |
| 716 | */ |
| 717 | public function addData(string $name, mixed $value): Client |
| 718 | { |
| 719 | $this->request()->addData($name, $value); |
| 720 | return $this; |
| 721 | } |
| 722 | |
| 723 | /** |
| 724 | * Get data |
| 725 | * |
| 726 | * @param ?string $key |
| 727 | * @return mixed |
| 728 | */ |
| 729 | public function getData(?string $key = null): mixed |
| 730 | { |
| 731 | return $this->hasRequest() ? $this->request()->getData()?->getData($key) : null; |
| 732 | } |
| 733 | |
| 734 | /** |
| 735 | * Has data |
| 736 | * |
| 737 | * @param ?string $key |
| 738 | * @return bool |
| 739 | */ |
| 740 | public function hasData(?string $key = null): bool |
| 741 | { |
| 742 | return $this->hasRequest() && $this->request()->hasData() && $this->request()->getData()->hasData($key); |
| 743 | } |
| 744 | |
| 745 | /** |
| 746 | * Remove data |
| 747 | * |
| 748 | * @param string $key |
| 749 | * @return Client |
| 750 | */ |
| 751 | public function removeData(string $key): Client |
| 752 | { |
| 753 | if ($this->hasData($key)) { |
| 754 | $this->request()->removeData($key); |
| 755 | } |
| 756 | return $this; |
| 757 | } |
| 758 | |
| 759 | /** |
| 760 | * Remove all data |
| 761 | * |
| 762 | * @return Client |
| 763 | */ |
| 764 | public function removeAllData(): Client |
| 765 | { |
| 766 | if ($this->hasRequest() && $this->request()->hasData()) { |
| 767 | $this->request()->removeAllData(); |
| 768 | } |
| 769 | return $this; |
| 770 | } |
| 771 | |
| 772 | /** |
| 773 | * Set query |
| 774 | * |
| 775 | * @param array $query |
| 776 | * @return Client |
| 777 | */ |
| 778 | public function setQuery(array $query): Client |
| 779 | { |
| 780 | $this->request()->setQuery($query); |
| 781 | return $this; |
| 782 | } |
| 783 | |
| 784 | /** |
| 785 | * Add query |
| 786 | * |
| 787 | * @param string $name |
| 788 | * @param mixed $value |
| 789 | * @return Client |
| 790 | */ |
| 791 | public function addQuery(string $name, mixed $value): Client |
| 792 | { |
| 793 | $this->request()->addQuery($name, $value); |
| 794 | return $this; |
| 795 | } |
| 796 | |
| 797 | /** |
| 798 | * Get query |
| 799 | * |
| 800 | * @param ?string $key |
| 801 | * @return mixed |
| 802 | */ |
| 803 | public function getQuery(?string $key = null): mixed |
| 804 | { |
| 805 | return $this->hasRequest() ? $this->request()->getQuery()?->getData($key) : null; |
| 806 | } |
| 807 | |
| 808 | /** |
| 809 | * Has query |
| 810 | * |
| 811 | * @param ?string $key |
| 812 | * @return bool |
| 813 | */ |
| 814 | public function hasQuery(?string $key = null): bool |
| 815 | { |
| 816 | return $this->hasRequest() && $this->request()->hasQuery() && $this->request()->getQuery()->hasData($key); |
| 817 | } |
| 818 | |
| 819 | /** |
| 820 | * Remove query |
| 821 | * |
| 822 | * @param string $key |
| 823 | * @return Client |
| 824 | */ |
| 825 | public function removeQuery(string $key): Client |
| 826 | { |
| 827 | if ($this->hasQuery($key)) { |
| 828 | $this->request()->removeQuery($key); |
| 829 | } |
| 830 | return $this; |
| 831 | } |
| 832 | |
| 833 | /** |
| 834 | * Remove all query data |
| 835 | * |
| 836 | * @return Client |
| 837 | */ |
| 838 | public function removeAllQuery(): Client |
| 839 | { |
| 840 | if ($this->hasRequest() && $this->request()->hasQuery()) { |
| 841 | $this->request()->removeAllQuery(); |
| 842 | } |
| 843 | return $this; |
| 844 | } |
| 845 | |
| 846 | /** |
| 847 | * Set type |
| 848 | * |
| 849 | * @param string $type |
| 850 | * @param bool $addHeader |
| 851 | * @return Client |
| 852 | */ |
| 853 | public function setType(string $type, bool $addHeader = true): Client |
| 854 | { |
| 855 | $this->request()->setRequestType($type, $addHeader); |
| 856 | return $this; |
| 857 | } |
| 858 | |
| 859 | /** |
| 860 | * Get type |
| 861 | * |
| 862 | * @return mixed |
| 863 | */ |
| 864 | public function getType(): mixed |
| 865 | { |
| 866 | return $this->hasRequest() ? $this->request()->getRequestType() : null; |
| 867 | } |
| 868 | |
| 869 | /** |
| 870 | * Has type |
| 871 | * |
| 872 | * @return bool |
| 873 | */ |
| 874 | public function hasType(): bool |
| 875 | { |
| 876 | return $this->hasRequest() && $this->request()->hasRequestType(); |
| 877 | } |
| 878 | |
| 879 | /** |
| 880 | * Remove type |
| 881 | * |
| 882 | * @return Client |
| 883 | */ |
| 884 | public function removeType(): Client |
| 885 | { |
| 886 | if ($this->hasType()) { |
| 887 | $this->request()->removeRequestType(); |
| 888 | } |
| 889 | return $this; |
| 890 | } |
| 891 | |
| 892 | /** |
| 893 | * Set files |
| 894 | * |
| 895 | * @param array|string $files |
| 896 | * @param bool $multipart |
| 897 | * @throws Exception |
| 898 | * @return Client |
| 899 | */ |
| 900 | public function setFiles(array|string $files, bool $multipart = true): Client |
| 901 | { |
| 902 | if (is_string($files)) { |
| 903 | $files = [$files]; |
| 904 | } |
| 905 | |
| 906 | // Replace semantics, consistent with setData()/setHeaders()/setQuery() - a set*() call |
| 907 | // supersedes what was there before; add*() is the accumulating counterpart. |
| 908 | $this->removeFiles(); |
| 909 | |
| 910 | foreach ($files as $i => $file) { |
| 911 | $this->addFile($file, is_numeric($i) ? null : $i); |
| 912 | } |
| 913 | |
| 914 | if ($multipart) { |
| 915 | $this->setType(Client\Request::MULTIPART); |
| 916 | } |
| 917 | |
| 918 | return $this; |
| 919 | } |
| 920 | |
| 921 | /** |
| 922 | * Add file |
| 923 | * |
| 924 | * @param string $file |
| 925 | * @param ?string $name |
| 926 | * @throws Exception |
| 927 | * @return Client |
| 928 | */ |
| 929 | public function addFile(string $file, ?string $name = null): Client |
| 930 | { |
| 931 | if (!file_exists($file)) { |
| 932 | throw new Exception("Error: The file '" . $file . "' does not exist."); |
| 933 | } |
| 934 | |
| 935 | if ($name === null) { |
| 936 | // hasFile($name) rescans and rebuilds the entire request data array on every candidate |
| 937 | // name - an O(n) getFiles() call per attempt. Data::hasData() is a direct isset() check |
| 938 | // against the same underlying array, so this is O(1) per attempt instead. It also avoids |
| 939 | // a name colliding with any existing data key, not just file-shaped ones (hasFile() only |
| 940 | // matches file-shaped entries). |
| 941 | $data = $this->request()->getData(); |
| 942 | $i = 1; |
| 943 | do { |
| 944 | $name = 'file' . $i; |
| 945 | $i++; |
| 946 | } while ($data?->hasData($name)); |
| 947 | } |
| 948 | |
| 949 | $this->addData($name, ['filename' => $file, 'contentType' => Client\Data::getMimeTypeFromFilename($file)]); |
| 950 | |
| 951 | return $this; |
| 952 | } |
| 953 | |
| 954 | /** |
| 955 | * Get files |
| 956 | * |
| 957 | * @return array|null |
| 958 | */ |
| 959 | public function getFiles(): array|null |
| 960 | { |
| 961 | if (!$this->hasRequest() || !$this->request()->hasData()) { |
| 962 | return null; |
| 963 | } |
| 964 | |
| 965 | $files = []; |
| 966 | foreach ($this->request()->getData()->getData() as $name => $value) { |
| 967 | if (is_array($value) && isset($value['filename'])) { |
| 968 | $files[$name] = $value['filename']; |
| 969 | } |
| 970 | } |
| 971 | |
| 972 | return !empty($files) ? $files : null; |
| 973 | } |
| 974 | |
| 975 | /** |
| 976 | * Get file |
| 977 | * |
| 978 | * @param string $key |
| 979 | * @return ?string |
| 980 | */ |
| 981 | public function getFile(string $key): ?string |
| 982 | { |
| 983 | return $this->getFiles()[$key] ?? null; |
| 984 | } |
| 985 | |
| 986 | /** |
| 987 | * Has files |
| 988 | * |
| 989 | * @return bool |
| 990 | */ |
| 991 | public function hasFiles(): bool |
| 992 | { |
| 993 | return !empty($this->getFiles()); |
| 994 | } |
| 995 | |
| 996 | /** |
| 997 | * Has file |
| 998 | * |
| 999 | * @param string $key |
| 1000 | * @return bool |
| 1001 | */ |
| 1002 | public function hasFile(string $key): bool |
| 1003 | { |
| 1004 | return array_key_exists($key, $this->getFiles() ?? []); |
| 1005 | } |
| 1006 | |
| 1007 | /** |
| 1008 | * Remove file |
| 1009 | * |
| 1010 | * @param string $key |
| 1011 | * @return Client |
| 1012 | */ |
| 1013 | public function removeFile(string $key): Client |
| 1014 | { |
| 1015 | if ($this->hasFile($key)) { |
| 1016 | $this->removeData($key); |
| 1017 | } |
| 1018 | return $this; |
| 1019 | } |
| 1020 | |
| 1021 | /** |
| 1022 | * Remove all files |
| 1023 | * |
| 1024 | * @return Client |
| 1025 | */ |
| 1026 | public function removeFiles(): Client |
| 1027 | { |
| 1028 | foreach (array_keys($this->getFiles() ?? []) as $key) { |
| 1029 | $this->removeData($key); |
| 1030 | } |
| 1031 | return $this; |
| 1032 | } |
| 1033 | |
| 1034 | /** |
| 1035 | * Set request body |
| 1036 | * |
| 1037 | * @param string $body |
| 1038 | * @throws Exception |
| 1039 | * @return Client |
| 1040 | */ |
| 1041 | public function setBody(string $body): Client |
| 1042 | { |
| 1043 | if ($this->request === null) { |
| 1044 | throw new Exception('Error: The request object has not been created.'); |
| 1045 | } |
| 1046 | |
| 1047 | $this->request()->setBody($body); |
| 1048 | |
| 1049 | return $this; |
| 1050 | } |
| 1051 | |
| 1052 | /** |
| 1053 | * Set request body from file |
| 1054 | * |
| 1055 | * @param string $file |
| 1056 | * @throws Exception |
| 1057 | * @return Client |
| 1058 | */ |
| 1059 | public function setBodyFromFile(string $file): Client |
| 1060 | { |
| 1061 | if ($this->request === null) { |
| 1062 | throw new Exception('Error: The request object has not been created.'); |
| 1063 | } |
| 1064 | if (!file_exists($file)) { |
| 1065 | throw new Exception("Error: The file '" . $file . "' does not exist."); |
| 1066 | } |
| 1067 | |
| 1068 | $this->request()->setBody(file_get_contents($file)); |
| 1069 | |
| 1070 | return $this; |
| 1071 | } |
| 1072 | |
| 1073 | /** |
| 1074 | * Has request body |
| 1075 | * |
| 1076 | * @return bool |
| 1077 | */ |
| 1078 | public function hasBody(): bool |
| 1079 | { |
| 1080 | return (($this->request !== null) && ($this->request()->hasBody())); |
| 1081 | } |
| 1082 | |
| 1083 | /** |
| 1084 | * Get request body |
| 1085 | * |
| 1086 | * @return ?Body |
| 1087 | */ |
| 1088 | public function getBody(): ?Body |
| 1089 | { |
| 1090 | return (($this->request !== null) && ($this->request()->hasBody())) ? $this->request()->getBody() : null; |
| 1091 | } |
| 1092 | |
| 1093 | /** |
| 1094 | * Get request body content |
| 1095 | * |
| 1096 | * @return ?string |
| 1097 | */ |
| 1098 | public function getBodyContent(): ?string |
| 1099 | { |
| 1100 | return (($this->request !== null) && ($this->request()->hasBody())) ? $this->request()->getBodyContent() : null; |
| 1101 | } |
| 1102 | |
| 1103 | /** |
| 1104 | * Get request body content length |
| 1105 | * |
| 1106 | * @param bool $mb |
| 1107 | * @return int |
| 1108 | */ |
| 1109 | public function getBodyContentLength(bool $mb = false): int |
| 1110 | { |
| 1111 | return (($this->request !== null) && ($this->request()->hasBody())) ? $this->request()->getBodyContentLength($mb) : 0; |
| 1112 | } |
| 1113 | |
| 1114 | /** |
| 1115 | * Remove the body |
| 1116 | * |
| 1117 | * @return Client |
| 1118 | */ |
| 1119 | public function removeBody(): Client |
| 1120 | { |
| 1121 | if (($this->request !== null) && ($this->request()->hasBody())) { |
| 1122 | $this->request()->removeBody(); |
| 1123 | } |
| 1124 | return $this; |
| 1125 | } |
| 1126 | |
| 1127 | /** |
| 1128 | * Prepare the client request |
| 1129 | * |
| 1130 | * @param ?string $uri |
| 1131 | * @param ?string $method |
| 1132 | * @throws Exception|Client\Exception |
| 1133 | * @return Client |
| 1134 | */ |
| 1135 | public function prepare(?string $uri = null, ?string $method = null): Client |
| 1136 | { |
| 1137 | // Check that there is a request object or a URI |
| 1138 | if ((!$this->hasRequest()) && ($uri === null) && !isset($this->options['base_uri'])) { |
| 1139 | throw new Exception('Error: There is no request URI to send.'); |
| 1140 | } |
| 1141 | |
| 1142 | if (($method === null) && isset($this->options['method'])) { |
| 1143 | $method = $this->options['method']; |
| 1144 | } |
| 1145 | |
| 1146 | if ($this->hasRequest()) { |
| 1147 | // Set request URI |
| 1148 | if ($uri !== null) { |
| 1149 | if (isset($this->options['base_uri']) && !str_starts_with($uri, $this->options['base_uri'])) { |
| 1150 | $uri = $this->options['base_uri'] . $uri; |
| 1151 | } |
| 1152 | $this->request()->setUri(new Uri($uri)); |
| 1153 | // Else, check and adjust for base_uri |
| 1154 | } else if (isset($this->options['base_uri']) && !str_starts_with($this->request()->getUriAsString(), $this->options['base_uri'])) { |
| 1155 | $this->request()->setUri($this->options['base_uri'] . $this->request()->getUriAsString()); |
| 1156 | } |
| 1157 | |
| 1158 | // Set method |
| 1159 | if ($method !== null) { |
| 1160 | $this->request()->setMethod($method); |
| 1161 | } |
| 1162 | // Create new request object |
| 1163 | } else { |
| 1164 | if (($uri === null) && isset($this->options['base_uri'])) { |
| 1165 | $uri = $this->options['base_uri']; |
| 1166 | } else if (isset($this->options['base_uri']) && !str_starts_with($uri, $this->options['base_uri'])) { |
| 1167 | $uri = $this->options['base_uri'] . $uri; |
| 1168 | } |
| 1169 | $this->setRequest(new Request(new Uri($uri), ($method ?? 'GET'))); |
| 1170 | |
| 1171 | // A request has just been materialized for the first time, so seed it from any |
| 1172 | // request-shaped options. syncRequestFromOptions() is a no-op without a request, so |
| 1173 | // options set on a client that had no request yet (e.g. `new Client(new Curl())` then |
| 1174 | // `addOption('data', [...])`) would otherwise be silently dropped. This deliberately |
| 1175 | // does NOT run when a request already existed: addOption()/setOptions() sync |
| 1176 | // themselves at call time in that case, and re-running the sync here would merge |
| 1177 | // stale option data back over any explicit setData()/removeData() made since. |
| 1178 | $this->syncRequestFromOptions(); |
| 1179 | } |
| 1180 | |
| 1181 | // An explicit method argument to prepare() outranks a 'method' option the sync just applied |
| 1182 | if ($method !== null) { |
| 1183 | $this->request()->setMethod($method); |
| 1184 | } |
| 1185 | |
| 1186 | // Set request type |
| 1187 | if ($this->hasOption('type')) { |
| 1188 | $this->request()->setRequestType($this->options['type']); |
| 1189 | } |
| 1190 | |
| 1191 | // Set no Content-Length header flag |
| 1192 | if ($this->hasOption('no_content_length')) { |
| 1193 | $this->request()->setNoContentLength($this->options['no_content_length']); |
| 1194 | } |
| 1195 | |
| 1196 | // Set raw data flag |
| 1197 | if ($this->hasOption('raw_data')) { |
| 1198 | $this->request()->setRawData($this->options['raw_data']); |
| 1199 | } |
| 1200 | |
| 1201 | // Set (or reset) handler |
| 1202 | if (!$this->hasHandler()) { |
| 1203 | $this->setHandler(new Curl()); |
| 1204 | } else { |
| 1205 | $this->getHandler()->reset(); |
| 1206 | } |
| 1207 | |
| 1208 | // Set user-agent |
| 1209 | if ($this->hasOption('user_agent')) { |
| 1210 | if ($this->handler instanceof Curl) { |
| 1211 | $this->handler->setOption(CURLOPT_USERAGENT, $this->options['user_agent']); |
| 1212 | } else if ($this->handler instanceof Stream) { |
| 1213 | $this->handler->addContextOption('http', ['user_agent' => $this->options['user_agent']]); |
| 1214 | } |
| 1215 | } |
| 1216 | |
| 1217 | // Handle SSL options |
| 1218 | if (($this->handler instanceof Curl) || ($this->handler instanceof Stream) || ($this->handler instanceof Mock)) { |
| 1219 | if ($this->hasOption('verify_peer')) { |
| 1220 | $this->handler->setVerifyPeer((bool)$this->options['verify_peer']); |
| 1221 | } |
| 1222 | if ($this->hasOption('allow_self_signed')) { |
| 1223 | $this->handler->allowSelfSigned((bool)$this->options['allow_self_signed']); |
| 1224 | } |
| 1225 | } |
| 1226 | |
| 1227 | return $this; |
| 1228 | } |
| 1229 | |
| 1230 | /** |
| 1231 | * Send the client request |
| 1232 | * |
| 1233 | * @param ?string $uri |
| 1234 | * @param ?string $method |
| 1235 | * @throws Exception|Client\Exception|Client\Handler\Exception |
| 1236 | * @return Response|Promise|array|string |
| 1237 | */ |
| 1238 | public function send(?string $uri = null, ?string $method = null): Response|Promise|array|string |
| 1239 | { |
| 1240 | if (isset($this->options['async']) && ($this->options['async'] === true)) { |
| 1241 | return $this->sendAsync(); |
| 1242 | } else { |
| 1243 | return $this->dispatch($uri, $method); |
| 1244 | } |
| 1245 | } |
| 1246 | |
| 1247 | /** |
| 1248 | * Perform the actual synchronous dispatch, always - unlike send(), this does |
| 1249 | * NOT redirect to sendAsync() when the 'async' option is set. Promise::wait()/ |
| 1250 | * resolve() call this (not send()) to perform the real request: since a Promise |
| 1251 | * wraps a Client whose 'async' option is still true for the lifetime of that |
| 1252 | * Client, calling send() from inside wait()/resolve() would re-enter the async |
| 1253 | * branch above and return a brand-new Promise instead of ever dispatching. |
| 1254 | * |
| 1255 | * @param ?string $uri |
| 1256 | * @param ?string $method |
| 1257 | * @throws Exception|Client\Exception|Client\Handler\Exception |
| 1258 | * @return Response|array|string |
| 1259 | */ |
| 1260 | public function dispatch(?string $uri = null, ?string $method = null): Response|array|string |
| 1261 | { |
| 1262 | $this->prepare($uri, $method); |
| 1263 | $this->response = $this->processMiddleware($this->request); |
| 1264 | |
| 1265 | return (($this->hasOption('auto')) && ($this->options['auto'])) ? |
| 1266 | $this->response->getParsedResponse() : $this->response; |
| 1267 | } |
| 1268 | |
| 1269 | /** |
| 1270 | * Perform the actual handler dispatch for the given request - the terminal |
| 1271 | * handler at the center of the middleware pipeline. If $request is a |
| 1272 | * Client\Request, it's re-pointed as $this->request first, so any |
| 1273 | * middleware-applied modifications (e.g. an injected header) are reflected |
| 1274 | * in what's actually sent. A foreign (non-Client\Request) PSR-7 request is |
| 1275 | * converted first, reusing the same conversion sendRequest() uses. |
| 1276 | * |
| 1277 | * Note: this is public (so a terminal callable passed into Pipeline::process() |
| 1278 | * can reach it), but it assumes prepare() has already run - it does not create |
| 1279 | * a handler itself, and depends on $this->handler already being set. |
| 1280 | * |
| 1281 | * @param RequestInterface $request |
| 1282 | * @throws Exception|Client\Exception|Client\Handler\Exception |
| 1283 | * @return Response |
| 1284 | */ |
| 1285 | public function dispatchRequest(RequestInterface $request): Response |
| 1286 | { |
| 1287 | if (!($request instanceof Request)) { |
| 1288 | $request = $this->convertToClientRequest($request); |
| 1289 | } |
| 1290 | |
| 1291 | if ($request !== $this->request) { |
| 1292 | parent::setRequest($request); |
| 1293 | } |
| 1294 | |
| 1295 | $response = (isset($this->options['force_custom_method']) && ($this->handler instanceof Curl)) ? |
| 1296 | $this->handler->prepare($request, $this->auth, (bool)$this->options['force_custom_method'])->send() : |
| 1297 | $this->handler->prepare($request, $this->auth)->send(); |
| 1298 | |
| 1299 | if (!($response instanceof Response)) { |
| 1300 | throw new Client\Exception('Error: The handler did not return a valid response.'); |
| 1301 | } |
| 1302 | |
| 1303 | $this->response = $response; |
| 1304 | |
| 1305 | return $this->response; |
| 1306 | } |
| 1307 | |
| 1308 | /** |
| 1309 | * Run the request through the middleware pipeline (if any), terminating in |
| 1310 | * dispatchRequest(). Shared by dispatch() and sendRequest() so middleware |
| 1311 | * behaves identically regardless of entry point. |
| 1312 | * |
| 1313 | * MiddlewareInterface::process() is typed to the broader PSR-7 ResponseInterface |
| 1314 | * (see that interface's docblock), but this method's return type is narrowed to |
| 1315 | * Client\Response, since that's the concrete type Client's internals require. If |
| 1316 | * a middleware short-circuits with some other ResponseInterface implementation, |
| 1317 | * that mismatch is caught below and re-thrown as a clear Client\Exception rather |
| 1318 | * than surfacing as a raw TypeError. |
| 1319 | * |
| 1320 | * @param RequestInterface $request |
| 1321 | * @throws Exception|Client\Exception|Client\Handler\Exception |
| 1322 | * @return Response |
| 1323 | */ |
| 1324 | protected function processMiddleware(RequestInterface $request): Response |
| 1325 | { |
| 1326 | if (!$this->hasMiddleware()) { |
| 1327 | return $this->dispatchRequest($request); |
| 1328 | } |
| 1329 | |
| 1330 | $pipeline = new Pipeline($this->middleware); |
| 1331 | $response = $pipeline->process($request, fn(RequestInterface $req) => $this->dispatchRequest($req)); |
| 1332 | |
| 1333 | if (!($response instanceof Response)) { |
| 1334 | throw new Client\Exception( |
| 1335 | 'Error: A middleware returned a ' . get_class($response) . |
| 1336 | ', but Pop\Http\Client requires middleware to return a Pop\Http\Client\Response ' . |
| 1337 | '(see Pop\Http\Client\Middleware\MiddlewareInterface::process()).' |
| 1338 | ); |
| 1339 | } |
| 1340 | |
| 1341 | return $response; |
| 1342 | } |
| 1343 | |
| 1344 | /** |
| 1345 | * Send a PSR-7 request and return a PSR-7 response, per PSR-18 |
| 1346 | * |
| 1347 | * @param RequestInterface $request |
| 1348 | * @throws Exception|Client\Exception|Client\RequestException|Client\Handler\Exception |
| 1349 | * @return ResponseInterface |
| 1350 | */ |
| 1351 | public function sendRequest(RequestInterface $request): ResponseInterface |
| 1352 | { |
| 1353 | $clientRequest = $this->convertToClientRequest($request); |
| 1354 | |
| 1355 | if (!$clientRequest->hasUri()) { |
| 1356 | throw new Client\RequestException('Error: There is no request URI to send.', $clientRequest); |
| 1357 | } |
| 1358 | |
| 1359 | $this->setRequest($clientRequest); |
| 1360 | $this->prepare(); |
| 1361 | |
| 1362 | $this->response = $this->processMiddleware($this->request); |
| 1363 | |
| 1364 | return $this->response; |
| 1365 | } |
| 1366 | |
| 1367 | /** |
| 1368 | * Convert a foreign (non-Client\Request) PSR-7 RequestInterface into a |
| 1369 | * Client\Request. A Client\Request passed in is returned as-is. Shared by |
| 1370 | * sendRequest() (the PSR-18 entry point) and dispatchRequest() (so a |
| 1371 | * middleware that substitutes a foreign PSR-7 request is honored rather |
| 1372 | * than silently dropped). |
| 1373 | * |
| 1374 | * @param RequestInterface $request |
| 1375 | * @throws Client\Exception |
| 1376 | * @return Request |
| 1377 | */ |
| 1378 | protected function convertToClientRequest(RequestInterface $request): Request |
| 1379 | { |
| 1380 | if ($request instanceof Request) { |
| 1381 | return $request; |
| 1382 | } |
| 1383 | |
| 1384 | try { |
| 1385 | $clientRequest = new Request((string)$request->getUri(), $request->getMethod()); |
| 1386 | foreach ($request->getHeaders() as $name => $values) { |
| 1387 | $clientRequest->addHeader($name, array_shift($values)); |
| 1388 | foreach ($values as $value) { |
| 1389 | $clientRequest->getHeaderObject($name)->addValue($value); |
| 1390 | } |
| 1391 | } |
| 1392 | $body = (string)$request->getBody(); |
| 1393 | if ($body !== '') { |
| 1394 | $clientRequest->setBody($body); |
| 1395 | } |
| 1396 | } catch (\Throwable $e) { |
| 1397 | throw new Client\Exception( |
| 1398 | 'Error: Unable to convert the given ' . get_class($request) . |
| 1399 | ' PSR-7 request into a Pop\Http\Client\Request: ' . $e->getMessage(), 0, $e |
| 1400 | ); |
| 1401 | } |
| 1402 | |
| 1403 | return $clientRequest; |
| 1404 | } |
| 1405 | |
| 1406 | /** |
| 1407 | * Method to send the request asynchronously |
| 1408 | * |
| 1409 | * @return Promise |
| 1410 | */ |
| 1411 | public function sendAsync(): Promise |
| 1412 | { |
| 1413 | return new Promise($this); |
| 1414 | } |
| 1415 | |
| 1416 | /** |
| 1417 | * Method to render the request as a string |
| 1418 | * |
| 1419 | * @return string |
| 1420 | */ |
| 1421 | public function render(): string |
| 1422 | { |
| 1423 | $this->prepare(); |
| 1424 | |
| 1425 | if (isset($this->options['force_custom_method']) && ($this->handler instanceof Curl)) { |
| 1426 | $this->handler->prepare($this->request(), $this->auth, (bool)$this->options['force_custom_method']); |
| 1427 | } else { |
| 1428 | $this->handler->prepare($this->request(), $this->auth); |
| 1429 | } |
| 1430 | |
| 1431 | $uri = $this->handler->getUriObject(); |
| 1432 | $uriString = $uri->getUri(); |
| 1433 | if ($uri->hasQuery()) { |
| 1434 | $uriString .= '?' . $uri->getQuery(); |
| 1435 | } |
| 1436 | |
| 1437 | $request = $this->request()->getMethod() . ' ' . $uriString . ' HTTP/' . $this->handler->getHttpVersion() . "\r\n" . |
| 1438 | 'Host: ' . $uri->getFullHost() . "\r\n" . $this->request()->getHeadersAsString() . "\r\n"; |
| 1439 | |
| 1440 | if ($this->request()->hasDataContent()) { |
| 1441 | $request .= $this->request()->getDataContent(); |
| 1442 | // Multipart data is prepared lazily - prepareData() only mints the boundary and declares |
| 1443 | // the Content-Type header, leaving the body to the handler's own (streaming) path - so |
| 1444 | // there is no buffered data content to display here. render() is a one-shot debug/ |
| 1445 | // inspection method rather than part of the send path, so building the rendered body on |
| 1446 | // demand here is fine, and reusing the already-declared boundary keeps the header and |
| 1447 | // the displayed body in agreement. |
| 1448 | } else if (($this->request()->hasData()) && ($this->request()->isMultipart())) { |
| 1449 | $boundary = null; |
| 1450 | $contentType = $this->request()->getHeaderValueAsString('Content-Type'); |
| 1451 | if ($contentType !== null) { |
| 1452 | $boundary = Parser::parseMediaType($contentType)['params']['boundary'] ?? null; |
| 1453 | } |
| 1454 | |
| 1455 | $request .= Body\Multipart::build($this->request()->getData()->getData(), $boundary)->getContent(); |
| 1456 | } |
| 1457 | return $request; |
| 1458 | } |
| 1459 | |
| 1460 | /** |
| 1461 | * Method to reset the client |
| 1462 | * |
| 1463 | * @param bool $data |
| 1464 | * @param bool $headers |
| 1465 | * @param bool $clear |
| 1466 | * @return static |
| 1467 | */ |
| 1468 | public function reset(?bool $data = true, ?bool $headers = false, bool $clear = false): static |
| 1469 | { |
| 1470 | // Fully clear out and reset of client object |
| 1471 | if ($clear) { |
| 1472 | $this->request = null; |
| 1473 | $this->response = null; |
| 1474 | $this->handler = null; |
| 1475 | $this->multiHandler = null; |
| 1476 | $this->auth = null; |
| 1477 | $this->options = []; |
| 1478 | // Clear out basic client info & data |
| 1479 | } else { |
| 1480 | if ($data) { |
| 1481 | if (isset($this->options['query'])) { |
| 1482 | unset($this->options['query']); |
| 1483 | } |
| 1484 | if (isset($this->options['data'])) { |
| 1485 | unset($this->options['data']); |
| 1486 | } |
| 1487 | if (isset($this->options['files'])) { |
| 1488 | unset($this->options['files']); |
| 1489 | } |
| 1490 | if ($this->request !== null) { |
| 1491 | if ($this->request()->hasQuery()) { |
| 1492 | $this->request()->removeAllQuery(); |
| 1493 | } |
| 1494 | if ($this->request()->hasData()) { |
| 1495 | $this->request()->removeAllData(); |
| 1496 | } |
| 1497 | } |
| 1498 | } |
| 1499 | if ($headers) { |
| 1500 | if (isset($this->options['headers'])) { |
| 1501 | unset($this->options['headers']); |
| 1502 | } |
| 1503 | if (isset($this->options['user_agent'])) { |
| 1504 | unset($this->options['user_agent']); |
| 1505 | } |
| 1506 | if ($this->request !== null) { |
| 1507 | if ($this->request()->hasHeaders()) { |
| 1508 | $this->request()->removeHeaders(); |
| 1509 | } |
| 1510 | } |
| 1511 | } |
| 1512 | } |
| 1513 | |
| 1514 | return $this; |
| 1515 | } |
| 1516 | |
| 1517 | /** |
| 1518 | * Method to convert client object to a Curl command for the CLI |
| 1519 | * |
| 1520 | * @throws Exception|Curl\Exception |
| 1521 | * @return string |
| 1522 | */ |
| 1523 | public function toCurlCommand(): string |
| 1524 | { |
| 1525 | if ($this->handler instanceof Stream) { |
| 1526 | throw new Exception('Error: The client handler must be an instance of Curl'); |
| 1527 | } |
| 1528 | |
| 1529 | if (!$this->hasHandler()) { |
| 1530 | $this->setHandler(new Curl()); |
| 1531 | } |
| 1532 | |
| 1533 | return Curl\Command::clientToCommand($this); |
| 1534 | } |
| 1535 | |
| 1536 | /** |
| 1537 | * To string magic method to render the client request to a raw string |
| 1538 | * |
| 1539 | */ |
| 1540 | public function __toString(): string |
| 1541 | { |
| 1542 | return $this->render(); |
| 1543 | } |
| 1544 | |
| 1545 | /** |
| 1546 | * Magic method to send requests by the method name, i.e. $client->get('http://localhost/'); |
| 1547 | * |
| 1548 | * @param string $methodName |
| 1549 | * @param array $arguments |
| 1550 | * @throws Exception|Client\Exception|Client\Handler\Exception |
| 1551 | * @return Response|Promise|array|string |
| 1552 | */ |
| 1553 | public function __call(string $methodName, array $arguments): Response|Promise|array|string |
| 1554 | { |
| 1555 | if (str_contains($methodName, 'Async')) { |
| 1556 | if (isset($arguments[0])) { |
| 1557 | $methodName = strtoupper(substr($methodName, 0, strpos($methodName, 'Async'))); |
| 1558 | $this->prepare($arguments[0], $methodName); |
| 1559 | } |
| 1560 | return $this->sendAsync(); |
| 1561 | } else { |
| 1562 | return $this->send(($arguments[0] ?? null), strtoupper($methodName)); |
| 1563 | } |
| 1564 | } |
| 1565 | |
| 1566 | /** |
| 1567 | * Magic method to send requests by the static method name, i.e. Client::get('http://localhost/'); |
| 1568 | * |
| 1569 | * @param string $methodName |
| 1570 | * @param array $arguments |
| 1571 | * @throws Exception|Client\Exception|Client\Handler\Exception |
| 1572 | * @return Response|Promise|array|string |
| 1573 | */ |
| 1574 | public static function __callStatic(string $methodName, array $arguments): Response|Promise|array|string |
| 1575 | { |
| 1576 | $client = new static(); |
| 1577 | $uri = null; |
| 1578 | |
| 1579 | if (count($arguments) > 1) { |
| 1580 | foreach ($arguments as $arg) { |
| 1581 | if (is_string($arg)) { |
| 1582 | $client->setRequest(new Client\Request($arg)); |
| 1583 | } else if ($arg instanceof Client\Request) { |
| 1584 | $client->setRequest($arg); |
| 1585 | } else if ($arg instanceof Client\Response) { |
| 1586 | $client->setResponse($arg); |
| 1587 | } else if ($arg instanceof Client\Handler\HandlerInterface) { |
| 1588 | $client->setHandler($arg); |
| 1589 | } else if ($arg instanceof Auth) { |
| 1590 | $client->setAuth($arg); |
| 1591 | } else if (is_array($arg)) { |
| 1592 | $client->setOptions($arg); |
| 1593 | } |
| 1594 | } |
| 1595 | } |
| 1596 | |
| 1597 | if ((!$client->hasRequest()) && isset($arguments[0])) { |
| 1598 | $uri = ($arguments[0]); |
| 1599 | } |
| 1600 | |
| 1601 | if (str_contains($methodName, 'Async')) { |
| 1602 | $methodName = strtoupper(substr($methodName, 0, strpos($methodName, 'Async'))); |
| 1603 | $client->prepare($uri, $methodName); |
| 1604 | return $client->sendAsync(); |
| 1605 | } else { |
| 1606 | return $client->send($uri, strtoupper($methodName)); |
| 1607 | } |
| 1608 | } |
| 1609 | |
| 1610 | } |