Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.
Common duplication problems, and corresponding solutions are:
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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.
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 new RejectedPromise($e); |
||
71 | } |
||
72 | } |
||
73 | |||
74 | private function invokeStats( |
||
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 new RejectedPromise($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) |
||
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 | View Code Duplication | 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) |
||
257 | |||
258 | private function createStream(RequestInterface $request, array $options) |
||
259 | { |
||
260 | static $methods; |
||
261 | if (!$methods) { |
||
336 | |||
337 | private function getDefaultContext(RequestInterface $request) |
||
370 | |||
371 | private function add_proxy(RequestInterface $request, &$options, $value, &$params) |
||
389 | |||
390 | private function add_timeout(RequestInterface $request, &$options, $value, &$params) |
||
396 | |||
397 | private function add_verify(RequestInterface $request, &$options, $value, &$params) |
||
422 | |||
423 | private function add_cert(RequestInterface $request, &$options, $value, &$params) |
||
436 | |||
437 | private function add_progress(RequestInterface $request, &$options, $value, &$params) |
||
448 | |||
449 | private function add_debug(RequestInterface $request, &$options, $value, &$params) |
||
485 | |||
486 | private function addNotification(array &$params, callable $notify) |
||
498 | |||
499 | private function callArray(array $functions) |
||
508 | } |
||
509 |
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.