Total Complexity | 89 |
Total Lines | 512 |
Duplicated Lines | 0 % |
Changes | 0 |
Complex classes like StreamHandler often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use StreamHandler, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
18 | class StreamHandler |
||
19 | { |
||
20 | private $lastHeaders = []; |
||
21 | |||
22 | /** |
||
23 | * Sends an HTTP request. |
||
24 | * |
||
25 | * @param RequestInterface $request Request to send. |
||
26 | * @param array $options Request transfer options. |
||
27 | * |
||
28 | * @return PromiseInterface |
||
29 | */ |
||
30 | public function __invoke(RequestInterface $request, array $options) |
||
31 | { |
||
32 | // Sleep if there is a delay specified. |
||
33 | if (isset($options['delay'])) { |
||
34 | usleep($options['delay'] * 1000); |
||
35 | } |
||
36 | |||
37 | $startTime = isset($options['on_stats']) ? microtime(true) : null; |
||
38 | |||
39 | try { |
||
40 | // Does not support the expect header. |
||
41 | $request = $request->withoutHeader('Expect'); |
||
42 | |||
43 | // Append a content-length header if body size is zero to match |
||
44 | // cURL's behavior. |
||
45 | if (0 === $request->getBody()->getSize()) { |
||
46 | $request = $request->withHeader('Content-Length', 0); |
||
47 | } |
||
48 | |||
49 | return $this->createResponse( |
||
50 | $request, |
||
51 | $options, |
||
52 | $this->createStream($request, $options), |
||
53 | $startTime |
||
54 | ); |
||
55 | } catch (\InvalidArgumentException $e) { |
||
56 | throw $e; |
||
57 | } catch (\Exception $e) { |
||
58 | // Determine if the error was a networking error. |
||
59 | $message = $e->getMessage(); |
||
60 | // This list can probably get more comprehensive. |
||
61 | if (strpos($message, 'getaddrinfo') // DNS lookup failed |
||
62 | || strpos($message, 'Connection refused') |
||
63 | || strpos($message, "couldn't connect to host") // error on HHVM |
||
64 | ) { |
||
65 | $e = new ConnectException($e->getMessage(), $request, $e); |
||
66 | } |
||
67 | $e = RequestException::wrapException($request, $e); |
||
68 | $this->invokeStats($options, $request, $startTime, null, $e); |
||
69 | |||
70 | return \GuzzleHttp\Promise\rejection_for($e); |
||
|
|||
71 | } |
||
72 | } |
||
73 | |||
74 | private function invokeStats( |
||
75 | array $options, |
||
76 | RequestInterface $request, |
||
77 | $startTime, |
||
78 | ResponseInterface $response = null, |
||
79 | $error = null |
||
80 | ) { |
||
81 | if (isset($options['on_stats'])) { |
||
82 | $stats = new TransferStats( |
||
83 | $request, |
||
84 | $response, |
||
85 | microtime(true) - $startTime, |
||
86 | $error, |
||
87 | [] |
||
88 | ); |
||
89 | call_user_func($options['on_stats'], $stats); |
||
90 | } |
||
91 | } |
||
92 | |||
93 | private function createResponse( |
||
94 | RequestInterface $request, |
||
95 | array $options, |
||
96 | $stream, |
||
97 | $startTime |
||
98 | ) { |
||
99 | $hdrs = $this->lastHeaders; |
||
100 | $this->lastHeaders = []; |
||
101 | $parts = explode(' ', array_shift($hdrs), 3); |
||
102 | $ver = explode('/', $parts[0])[1]; |
||
103 | $status = $parts[1]; |
||
104 | $reason = isset($parts[2]) ? $parts[2] : null; |
||
105 | $headers = \GuzzleHttp\headers_from_lines($hdrs); |
||
106 | list ($stream, $headers) = $this->checkDecode($options, $headers, $stream); |
||
107 | $stream = Psr7\stream_for($stream); |
||
108 | $sink = $stream; |
||
109 | |||
110 | if (strcasecmp('HEAD', $request->getMethod())) { |
||
111 | $sink = $this->createSink($stream, $options); |
||
112 | } |
||
113 | |||
114 | $response = new Psr7\Response($status, $headers, $sink, $ver, $reason); |
||
115 | |||
116 | if (isset($options['on_headers'])) { |
||
117 | try { |
||
118 | $options['on_headers']($response); |
||
119 | } catch (\Exception $e) { |
||
120 | $msg = 'An error was encountered during the on_headers event'; |
||
121 | $ex = new RequestException($msg, $request, $response, $e); |
||
122 | return \GuzzleHttp\Promise\rejection_for($ex); |
||
123 | } |
||
124 | } |
||
125 | |||
126 | // Do not drain when the request is a HEAD request because they have |
||
127 | // no body. |
||
128 | if ($sink !== $stream) { |
||
129 | $this->drain( |
||
130 | $stream, |
||
131 | $sink, |
||
132 | $response->getHeaderLine('Content-Length') |
||
133 | ); |
||
134 | } |
||
135 | |||
136 | $this->invokeStats($options, $request, $startTime, $response, null); |
||
137 | |||
138 | return new FulfilledPromise($response); |
||
139 | } |
||
140 | |||
141 | private function createSink(StreamInterface $stream, array $options) |
||
142 | { |
||
143 | if (!empty($options['stream'])) { |
||
144 | return $stream; |
||
145 | } |
||
146 | |||
147 | $sink = isset($options['sink']) |
||
148 | ? $options['sink'] |
||
149 | : fopen('php://temp', 'r+'); |
||
150 | |||
151 | return is_string($sink) |
||
152 | ? new Psr7\LazyOpenStream($sink, 'w+') |
||
153 | : Psr7\stream_for($sink); |
||
154 | } |
||
155 | |||
156 | private function checkDecode(array $options, array $headers, $stream) |
||
157 | { |
||
158 | // Automatically decode responses when instructed. |
||
159 | if (!empty($options['decode_content'])) { |
||
160 | $normalizedKeys = \GuzzleHttp\normalize_header_keys($headers); |
||
161 | if (isset($normalizedKeys['content-encoding'])) { |
||
162 | $encoding = $headers[$normalizedKeys['content-encoding']]; |
||
163 | if ($encoding[0] === 'gzip' || $encoding[0] === 'deflate') { |
||
164 | $stream = new Psr7\InflateStream( |
||
165 | Psr7\stream_for($stream) |
||
166 | ); |
||
167 | $headers['x-encoded-content-encoding'] |
||
168 | = $headers[$normalizedKeys['content-encoding']]; |
||
169 | // Remove content-encoding header |
||
170 | unset($headers[$normalizedKeys['content-encoding']]); |
||
171 | // Fix content-length header |
||
172 | if (isset($normalizedKeys['content-length'])) { |
||
173 | $headers['x-encoded-content-length'] |
||
174 | = $headers[$normalizedKeys['content-length']]; |
||
175 | |||
176 | $length = (int) $stream->getSize(); |
||
177 | if ($length === 0) { |
||
178 | unset($headers[$normalizedKeys['content-length']]); |
||
179 | } else { |
||
180 | $headers[$normalizedKeys['content-length']] = [$length]; |
||
181 | } |
||
182 | } |
||
183 | } |
||
184 | } |
||
185 | } |
||
186 | |||
187 | return [$stream, $headers]; |
||
188 | } |
||
189 | |||
190 | /** |
||
191 | * Drains the source stream into the "sink" client option. |
||
192 | * |
||
193 | * @param StreamInterface $source |
||
194 | * @param StreamInterface $sink |
||
195 | * @param string $contentLength Header specifying the amount of |
||
196 | * data to read. |
||
197 | * |
||
198 | * @return StreamInterface |
||
199 | * @throws \RuntimeException when the sink option is invalid. |
||
200 | */ |
||
201 | private function drain( |
||
202 | StreamInterface $source, |
||
203 | StreamInterface $sink, |
||
204 | $contentLength |
||
205 | ) { |
||
206 | // If a content-length header is provided, then stop reading once |
||
207 | // that number of bytes has been read. This can prevent infinitely |
||
208 | // reading from a stream when dealing with servers that do not honor |
||
209 | // Connection: Close headers. |
||
210 | Psr7\copy_to_stream( |
||
211 | $source, |
||
212 | $sink, |
||
213 | (strlen($contentLength) > 0 && (int) $contentLength > 0) ? (int) $contentLength : -1 |
||
214 | ); |
||
215 | |||
216 | $sink->seek(0); |
||
217 | $source->close(); |
||
218 | |||
219 | return $sink; |
||
220 | } |
||
221 | |||
222 | /** |
||
223 | * Create a resource and check to ensure it was created successfully |
||
224 | * |
||
225 | * @param callable $callback Callable that returns stream resource |
||
226 | * |
||
227 | * @return resource |
||
228 | * @throws \RuntimeException on error |
||
229 | */ |
||
230 | private function createResource(callable $callback) |
||
231 | { |
||
232 | $errors = null; |
||
233 | set_error_handler(function ($_, $msg, $file, $line) use (&$errors) { |
||
234 | $errors[] = [ |
||
235 | 'message' => $msg, |
||
236 | 'file' => $file, |
||
237 | 'line' => $line |
||
238 | ]; |
||
239 | return true; |
||
240 | }); |
||
241 | |||
242 | $resource = $callback(); |
||
243 | restore_error_handler(); |
||
244 | |||
245 | if (!$resource) { |
||
246 | $message = 'Error creating resource: '; |
||
247 | foreach ($errors as $err) { |
||
248 | foreach ($err as $key => $value) { |
||
249 | $message .= "[$key] $value" . PHP_EOL; |
||
250 | } |
||
251 | } |
||
252 | throw new \RuntimeException(trim($message)); |
||
253 | } |
||
254 | |||
255 | return $resource; |
||
256 | } |
||
257 | |||
258 | private function createStream(RequestInterface $request, array $options) |
||
259 | { |
||
260 | static $methods; |
||
261 | if (!$methods) { |
||
262 | $methods = array_flip(get_class_methods(__CLASS__)); |
||
263 | } |
||
264 | |||
265 | // HTTP/1.1 streams using the PHP stream wrapper require a |
||
266 | // Connection: close header |
||
267 | if ($request->getProtocolVersion() == '1.1' |
||
268 | && !$request->hasHeader('Connection') |
||
269 | ) { |
||
270 | $request = $request->withHeader('Connection', 'close'); |
||
271 | } |
||
272 | |||
273 | // Ensure SSL is verified by default |
||
274 | if (!isset($options['verify'])) { |
||
275 | $options['verify'] = true; |
||
276 | } |
||
277 | |||
278 | $params = []; |
||
279 | $context = $this->getDefaultContext($request, $options); |
||
280 | |||
281 | if (isset($options['on_headers']) && !is_callable($options['on_headers'])) { |
||
282 | throw new \InvalidArgumentException('on_headers must be callable'); |
||
283 | } |
||
284 | |||
285 | if (!empty($options)) { |
||
286 | foreach ($options as $key => $value) { |
||
287 | $method = "add_{$key}"; |
||
288 | if (isset($methods[$method])) { |
||
289 | $this->{$method}($request, $context, $value, $params); |
||
290 | } |
||
291 | } |
||
292 | } |
||
293 | |||
294 | if (isset($options['stream_context'])) { |
||
295 | if (!is_array($options['stream_context'])) { |
||
296 | throw new \InvalidArgumentException('stream_context must be an array'); |
||
297 | } |
||
298 | $context = array_replace_recursive( |
||
299 | $context, |
||
300 | $options['stream_context'] |
||
301 | ); |
||
302 | } |
||
303 | |||
304 | // Microsoft NTLM authentication only supported with curl handler |
||
305 | if (isset($options['auth']) |
||
306 | && is_array($options['auth']) |
||
307 | && isset($options['auth'][2]) |
||
308 | && 'ntlm' == $options['auth'][2] |
||
309 | ) { |
||
310 | |||
311 | throw new \InvalidArgumentException('Microsoft NTLM authentication only supported with curl handler'); |
||
312 | } |
||
313 | |||
314 | $uri = $this->resolveHost($request, $options); |
||
315 | |||
316 | $context = $this->createResource( |
||
317 | function () use ($context, $params) { |
||
318 | return stream_context_create($context, $params); |
||
319 | } |
||
320 | ); |
||
321 | |||
322 | return $this->createResource( |
||
323 | function () use ($uri, &$http_response_header, $context, $options) { |
||
324 | $resource = fopen((string) $uri, 'r', null, $context); |
||
325 | $this->lastHeaders = $http_response_header; |
||
326 | |||
327 | if (isset($options['read_timeout'])) { |
||
328 | $readTimeout = $options['read_timeout']; |
||
329 | $sec = (int) $readTimeout; |
||
330 | $usec = ($readTimeout - $sec) * 100000; |
||
331 | stream_set_timeout($resource, $sec, $usec); |
||
332 | } |
||
333 | |||
334 | return $resource; |
||
335 | } |
||
336 | ); |
||
337 | } |
||
338 | |||
339 | private function resolveHost(RequestInterface $request, array $options) |
||
340 | { |
||
341 | $uri = $request->getUri(); |
||
342 | |||
343 | if (isset($options['force_ip_resolve']) && !filter_var($uri->getHost(), FILTER_VALIDATE_IP)) { |
||
344 | if ('v4' === $options['force_ip_resolve']) { |
||
345 | $records = dns_get_record($uri->getHost(), DNS_A); |
||
346 | if (!isset($records[0]['ip'])) { |
||
347 | throw new ConnectException(sprintf("Could not resolve IPv4 address for host '%s'", $uri->getHost()), $request); |
||
348 | } |
||
349 | $uri = $uri->withHost($records[0]['ip']); |
||
350 | } elseif ('v6' === $options['force_ip_resolve']) { |
||
351 | $records = dns_get_record($uri->getHost(), DNS_AAAA); |
||
352 | if (!isset($records[0]['ipv6'])) { |
||
353 | throw new ConnectException(sprintf("Could not resolve IPv6 address for host '%s'", $uri->getHost()), $request); |
||
354 | } |
||
355 | $uri = $uri->withHost('[' . $records[0]['ipv6'] . ']'); |
||
356 | } |
||
357 | } |
||
358 | |||
359 | return $uri; |
||
360 | } |
||
361 | |||
362 | private function getDefaultContext(RequestInterface $request) |
||
363 | { |
||
364 | $headers = ''; |
||
365 | foreach ($request->getHeaders() as $name => $value) { |
||
366 | foreach ($value as $val) { |
||
367 | $headers .= "$name: $val\r\n"; |
||
368 | } |
||
369 | } |
||
370 | |||
371 | $context = [ |
||
372 | 'http' => [ |
||
373 | 'method' => $request->getMethod(), |
||
374 | 'header' => $headers, |
||
375 | 'protocol_version' => $request->getProtocolVersion(), |
||
376 | 'ignore_errors' => true, |
||
377 | 'follow_location' => 0, |
||
378 | ], |
||
379 | ]; |
||
380 | |||
381 | $body = (string) $request->getBody(); |
||
382 | |||
383 | if (!empty($body)) { |
||
384 | $context['http']['content'] = $body; |
||
385 | // Prevent the HTTP handler from adding a Content-Type header. |
||
386 | if (!$request->hasHeader('Content-Type')) { |
||
387 | $context['http']['header'] .= "Content-Type:\r\n"; |
||
388 | } |
||
389 | } |
||
390 | |||
391 | $context['http']['header'] = rtrim($context['http']['header']); |
||
392 | |||
393 | return $context; |
||
394 | } |
||
395 | |||
396 | private function add_proxy(RequestInterface $request, &$options, $value, &$params) |
||
397 | { |
||
398 | if (!is_array($value)) { |
||
399 | $options['http']['proxy'] = $value; |
||
400 | } else { |
||
401 | $scheme = $request->getUri()->getScheme(); |
||
402 | if (isset($value[$scheme])) { |
||
403 | if (!isset($value['no']) |
||
404 | || !\GuzzleHttp\is_host_in_noproxy( |
||
405 | $request->getUri()->getHost(), |
||
406 | $value['no'] |
||
407 | ) |
||
408 | ) { |
||
409 | $options['http']['proxy'] = $value[$scheme]; |
||
410 | } |
||
411 | } |
||
412 | } |
||
413 | } |
||
414 | |||
415 | private function add_timeout(RequestInterface $request, &$options, $value, &$params) |
||
416 | { |
||
417 | if ($value > 0) { |
||
418 | $options['http']['timeout'] = $value; |
||
419 | } |
||
420 | } |
||
421 | |||
422 | private function add_verify(RequestInterface $request, &$options, $value, &$params) |
||
446 | } |
||
447 | |||
448 | private function add_cert(RequestInterface $request, &$options, $value, &$params) |
||
449 | { |
||
450 | if (is_array($value)) { |
||
451 | $options['ssl']['passphrase'] = $value[1]; |
||
452 | $value = $value[0]; |
||
453 | } |
||
454 | |||
455 | if (!file_exists($value)) { |
||
456 | throw new \RuntimeException("SSL certificate not found: {$value}"); |
||
457 | } |
||
458 | |||
459 | $options['ssl']['local_cert'] = $value; |
||
460 | } |
||
461 | |||
462 | private function add_progress(RequestInterface $request, &$options, $value, &$params) |
||
463 | { |
||
464 | $this->addNotification( |
||
465 | $params, |
||
466 | function ($code, $a, $b, $c, $transferred, $total) use ($value) { |
||
467 | if ($code == STREAM_NOTIFY_PROGRESS) { |
||
468 | $value($total, $transferred, null, null); |
||
469 | } |
||
470 | } |
||
471 | ); |
||
472 | } |
||
473 | |||
474 | private function add_debug(RequestInterface $request, &$options, $value, &$params) |
||
475 | { |
||
476 | if ($value === false) { |
||
477 | return; |
||
478 | } |
||
479 | |||
480 | static $map = [ |
||
481 | STREAM_NOTIFY_CONNECT => 'CONNECT', |
||
482 | STREAM_NOTIFY_AUTH_REQUIRED => 'AUTH_REQUIRED', |
||
483 | STREAM_NOTIFY_AUTH_RESULT => 'AUTH_RESULT', |
||
484 | STREAM_NOTIFY_MIME_TYPE_IS => 'MIME_TYPE_IS', |
||
485 | STREAM_NOTIFY_FILE_SIZE_IS => 'FILE_SIZE_IS', |
||
486 | STREAM_NOTIFY_REDIRECTED => 'REDIRECTED', |
||
487 | STREAM_NOTIFY_PROGRESS => 'PROGRESS', |
||
488 | STREAM_NOTIFY_FAILURE => 'FAILURE', |
||
489 | STREAM_NOTIFY_COMPLETED => 'COMPLETED', |
||
490 | STREAM_NOTIFY_RESOLVE => 'RESOLVE', |
||
491 | ]; |
||
492 | static $args = ['severity', 'message', 'message_code', |
||
493 | 'bytes_transferred', 'bytes_max']; |
||
494 | |||
495 | $value = \GuzzleHttp\debug_resource($value); |
||
496 | $ident = $request->getMethod() . ' ' . $request->getUri()->withFragment(''); |
||
497 | $this->addNotification( |
||
498 | $params, |
||
499 | function () use ($ident, $value, $map, $args) { |
||
500 | $passed = func_get_args(); |
||
501 | $code = array_shift($passed); |
||
502 | fprintf($value, '<%s> [%s] ', $ident, $map[$code]); |
||
503 | foreach (array_filter($passed) as $i => $v) { |
||
504 | fwrite($value, $args[$i] . ': "' . $v . '" '); |
||
505 | } |
||
506 | fwrite($value, "\n"); |
||
507 | } |
||
508 | ); |
||
509 | } |
||
510 | |||
511 | private function addNotification(array &$params, callable $notify) |
||
512 | { |
||
513 | // Wrap the existing function if needed. |
||
514 | if (!isset($params['notification'])) { |
||
515 | $params['notification'] = $notify; |
||
516 | } else { |
||
517 | $params['notification'] = $this->callArray([ |
||
518 | $params['notification'], |
||
519 | $notify |
||
520 | ]); |
||
521 | } |
||
522 | } |
||
523 | |||
524 | private function callArray(array $functions) |
||
530 | } |
||
531 | }; |
||
532 | } |
||
533 | } |
||
534 |
This function has been deprecated. The supplier of the function has supplied an explanatory message.
The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.