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 Response 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 Response, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
16 | class Response implements JsonSerializable { |
||
17 | /// Properties /// |
||
18 | |||
19 | /** |
||
20 | * An array of cookie sets. This array is in the form: |
||
21 | * |
||
22 | * ``` |
||
23 | * array ( |
||
24 | * 'name' => [args for setcookie()] |
||
25 | * ) |
||
26 | * ``` |
||
27 | * |
||
28 | * @var array An array of cookies sets. |
||
29 | */ |
||
30 | protected $cookies = []; |
||
31 | |||
32 | /** |
||
33 | * An array of global cookie sets. |
||
34 | * |
||
35 | * This array is for code the queue up cookie changes before the response has been created. |
||
36 | * |
||
37 | * @var array An array of cookies. |
||
38 | */ |
||
39 | protected static $globalCookies; |
||
40 | |||
41 | /** |
||
42 | * @var Response The current response. |
||
43 | */ |
||
44 | protected static $current; |
||
45 | |||
46 | /** |
||
47 | * @var array An array of meta data that is not related to the response data. |
||
48 | */ |
||
49 | protected $meta = []; |
||
50 | |||
51 | /** |
||
52 | * @var array An array of response data. |
||
53 | */ |
||
54 | protected $data = []; |
||
55 | |||
56 | /** |
||
57 | * @var string The asset that should be rendered. |
||
58 | */ |
||
59 | protected $contentAsset; |
||
60 | |||
61 | /** |
||
62 | * @var string The default cookie domain. |
||
63 | */ |
||
64 | public $defaultCookieDomain; |
||
65 | |||
66 | /** |
||
67 | * @var string The default cookie path. |
||
68 | */ |
||
69 | public $defaultCookiePath; |
||
70 | |||
71 | /** |
||
72 | * @var array An array of http headers. |
||
73 | */ |
||
74 | protected $headers = array(); |
||
75 | |||
76 | /** |
||
77 | * @var array An array of global http headers. |
||
78 | */ |
||
79 | protected static $globalHeaders; |
||
80 | |||
81 | /** |
||
82 | * @var int HTTP status code |
||
83 | */ |
||
84 | protected $status = 200; |
||
85 | |||
86 | /** |
||
87 | * @var array HTTP response codes and messages. |
||
88 | */ |
||
89 | protected static $messages = array( |
||
90 | // Informational 1xx |
||
91 | 100 => 'Continue', |
||
92 | 101 => 'Switching Protocols', |
||
93 | // Successful 2xx |
||
94 | 200 => 'OK', |
||
95 | 201 => 'Created', |
||
96 | 202 => 'Accepted', |
||
97 | 203 => 'Non-Authoritative Information', |
||
98 | 204 => 'No Content', |
||
99 | 205 => 'Reset Content', |
||
100 | 206 => 'Partial Content', |
||
101 | // Redirection 3xx |
||
102 | 300 => 'Multiple Choices', |
||
103 | 301 => 'Moved Permanently', |
||
104 | 302 => 'Found', |
||
105 | 303 => 'See Other', |
||
106 | 304 => 'Not Modified', |
||
107 | 305 => 'Use Proxy', |
||
108 | 306 => '(Unused)', |
||
109 | 307 => 'Temporary Redirect', |
||
110 | // Client Error 4xx |
||
111 | 400 => 'Bad Request', |
||
112 | 401 => 'Unauthorized', |
||
113 | 402 => 'Payment Required', |
||
114 | 403 => 'Forbidden', |
||
115 | 404 => 'Not Found', |
||
116 | 405 => 'Method Not Allowed', |
||
117 | 406 => 'Not Acceptable', |
||
118 | 407 => 'Proxy Authentication Required', |
||
119 | 408 => 'Request Timeout', |
||
120 | 409 => 'Conflict', |
||
121 | 410 => 'Gone', |
||
122 | 411 => 'Length Required', |
||
123 | 412 => 'Precondition Failed', |
||
124 | 413 => 'Request Entity Too Large', |
||
125 | 414 => 'Request-URI Too Long', |
||
126 | 415 => 'Unsupported Media Type', |
||
127 | 416 => 'Requested Range Not Satisfiable', |
||
128 | 417 => 'Expectation Failed', |
||
129 | 418 => 'I\'m a teapot', |
||
130 | 422 => 'Unprocessable Entity', |
||
131 | 423 => 'Locked', |
||
132 | // Server Error 5xx |
||
133 | 500 => 'Internal Server Error', |
||
134 | 501 => 'Not Implemented', |
||
135 | 502 => 'Bad Gateway', |
||
136 | 503 => 'Service Unavailable', |
||
137 | 504 => 'Gateway Timeout', |
||
138 | 505 => 'HTTP Version Not Supported' |
||
139 | ); |
||
140 | |||
141 | /// Methods /// |
||
142 | |||
143 | /** |
||
144 | * Gets or sets the response that is currently being processed. |
||
145 | * |
||
146 | * @param Response|null $response Set a new response or pass null to get the current response. |
||
147 | * @return Response Returns the current response. |
||
148 | */ |
||
149 | public static function current(Response $response = null) { |
||
150 | if ($response !== null) { |
||
151 | self::$current = $response; |
||
152 | } elseif (self::$current === null) { |
||
153 | self::$current = new Response(); |
||
154 | } |
||
155 | |||
156 | return self::$current; |
||
157 | } |
||
158 | |||
159 | /** |
||
160 | * Create a Response from a variety of data. |
||
161 | * |
||
162 | * @param mixed $result The result to create the response from. |
||
163 | * @return Response Returns a {@link Response} object. |
||
164 | */ |
||
165 | 44 | public static function create($result) { |
|
166 | 44 | if ($result instanceof Response) { |
|
167 | return $result; |
||
168 | 23 | } elseif ($result instanceof Exception\ResponseException) { |
|
169 | /* @var Exception\ResponseException $result */ |
||
170 | return $result->getResponse(); |
||
171 | } |
||
172 | |||
173 | 44 | $response = new Response(); |
|
174 | |||
175 | 44 | if ($result instanceof Exception\ClientException) { |
|
176 | /* @var Exception\ClientException $cex */ |
||
177 | 23 | $cex = $result; |
|
178 | 23 | $response->status($cex->getCode()); |
|
179 | 23 | $response->headers($cex->getHeaders()); |
|
180 | 23 | $response->data($cex->jsonSerialize()); |
|
181 | } elseif ($result instanceof \Exception) { |
||
182 | /* @var \Exception $ex */ |
||
183 | $ex = $result; |
||
184 | $response->status($ex->getCode()); |
||
185 | $response->data([ |
||
186 | 'exception' => $ex->getMessage(), |
||
187 | 'code' => $ex->getCode() |
||
188 | ]); |
||
189 | 23 | } elseif (is_array($result)) { |
|
190 | 23 | if (count($result) === 3 && isset($result[0], $result[1], $result[2])) { |
|
191 | // This is a rack style response in the form [code, headers, body]. |
||
192 | $response->status($result[0]); |
||
193 | $response->headers($result[1]); |
||
194 | $response->data($result[2]); |
||
195 | 23 | } elseif (array_key_exists('response', $result)) { |
|
196 | 23 | $resultResponse = $result['response']; |
|
197 | 23 | if (!$resultResponse) { |
|
198 | 15 | $response->data($result['body']); |
|
199 | } else { |
||
200 | // This is a dispatched response. |
||
201 | 8 | $response = static::create($resultResponse); |
|
202 | } |
||
203 | |||
204 | // Set the rest of the result to the response context. |
||
205 | 23 | unset($result['response']); |
|
206 | 23 | $response->meta($result, true); |
|
207 | } else { |
||
208 | 23 | $response->data($result); |
|
209 | } |
||
210 | } else { |
||
211 | $response->status(422); |
||
212 | $response->data([ |
||
213 | 'exception' => "Unknown result type for response.", |
||
214 | 'code' => $response->status() |
||
215 | ]); |
||
216 | } |
||
217 | 44 | return $response; |
|
218 | } |
||
219 | |||
220 | /** |
||
221 | * Gets or sets the content type. |
||
222 | * |
||
223 | * @param string|null $value The new content type or null to get the current content type. |
||
224 | * @return Response|string Returns the current content type or `$this` for fluent calls. |
||
225 | */ |
||
226 | 44 | public function contentType($value = null) { |
|
233 | |||
234 | /** |
||
235 | * Gets or sets the asset that will be rendered in the response. |
||
236 | * |
||
237 | * @param string $asset Set a new value or pass `null` to get the current value. |
||
238 | * @return Response|string Returns the current content asset or `$this` when settings. |
||
239 | */ |
||
240 | 44 | public function contentAsset($asset = null) { |
|
241 | 44 | if ($asset !== null) { |
|
242 | 44 | $this->contentAsset = $asset; |
|
243 | 44 | return $this; |
|
244 | } |
||
245 | |||
246 | 23 | return $this->contentAsset; |
|
247 | } |
||
248 | |||
249 | /** |
||
250 | * Set the content type from an accept header. |
||
251 | * |
||
252 | * @param string $accept The value of the accept header. |
||
253 | * @return Response $this Returns `$this` for fluent calls. |
||
254 | */ |
||
255 | 44 | public function contentTypeFromAccept($accept) { |
|
256 | 44 | if (!empty($this->headers['Content-Type'])) { |
|
257 | return; |
||
258 | } |
||
259 | |||
260 | 44 | $accept = strtolower($accept); |
|
261 | 44 | if (strpos($accept, ',') === false) { |
|
262 | 44 | list($contentType) = explode(';', $accept); |
|
263 | } elseif (strpos($accept, 'text/html') !== false) { |
||
264 | $contentType = 'text/html'; |
||
265 | } elseif (strpos($accept, 'application/rss+xml' !== false)) { |
||
266 | $contentType = 'application/rss+xml'; |
||
267 | } elseif (strpos($accept, 'text/plain')) { |
||
268 | $contentType = 'text/plain'; |
||
269 | } else { |
||
270 | $contentType = 'text/html'; |
||
271 | } |
||
272 | 44 | $this->contentType($contentType); |
|
273 | 44 | return $this; |
|
274 | } |
||
275 | |||
276 | /** |
||
277 | * Translate an http code to its corresponding status message. |
||
278 | * |
||
279 | * @param int $statusCode The http status code. |
||
280 | * @param bool $header Whether or not the result should be in a form that can be passed to {@link header}. |
||
281 | * @return string Returns the status message corresponding to {@link $code}. |
||
282 | */ |
||
283 | public static function statusMessage($statusCode, $header = false) { |
||
292 | |||
293 | /** |
||
294 | * Gets or sets a cookie. |
||
295 | * |
||
296 | * @param string $name The name of the cookie. |
||
297 | * @param bool $value The value of the cookie. This value is stored on the clients computer; do not store sensitive information. |
||
298 | * @param int $expires The time the cookie expires. This is a Unix timestamp so is in number of seconds since the epoch. |
||
299 | * @param string $path The path on the server in which the cookie will be available on. |
||
300 | * If set to '/', the cookie will be available within the entire {@link $domain}. |
||
301 | * @param string $domain The domain that the cookie is available to. |
||
302 | * @param bool $secure Indicates that the cookie should only be transmitted over a secure HTTPS connection from the client. |
||
303 | * @param bool $httponly When TRUE the cookie will be made accessible only through the HTTP protocol. |
||
304 | * @return $this|mixed Returns the cookie settings at {@link $name} or `$this` when setting a cookie for fluent calls. |
||
305 | */ |
||
306 | public function cookies($name, $value = false, $expires = 0, $path = null, $domain = null, $secure = false, $httponly = false) { |
||
314 | |||
315 | /** |
||
316 | * Gets or sets a global cookie. |
||
317 | * |
||
318 | * Global cookies are used when you want to set a cookie, but a {@link Response} has not been created yet. |
||
319 | * |
||
320 | * @param string $name The name of the cookie. |
||
321 | * @param bool $value The value of the cookie. This value is stored on the clients computer; do not store sensitive information. |
||
322 | * @param int $expires The time the cookie expires. This is a Unix timestamp so is in number of seconds since the epoch. |
||
323 | * @param string $path The path on the server in which the cookie will be available on. |
||
324 | * If set to '/', the cookie will be available within the entire {@link $domain}. |
||
325 | * @param string $domain The domain that the cookie is available to. |
||
326 | * @param bool $secure Indicates that the cookie should only be transmitted over a secure HTTPS connection from the client. |
||
327 | * @param bool $httponly When TRUE the cookie will be made accessible only through the HTTP protocol. |
||
328 | * @return mixed|null Returns the cookie settings at {@link $name} or `null` when setting a cookie. |
||
329 | */ |
||
330 | public static function globalCookies($name = null, $value = false, $expires = 0, $path = null, $domain = null, $secure = false, $httponly = false) { |
||
331 | if (self::$globalCookies === null) { |
||
332 | self::$globalCookies = []; |
||
333 | } |
||
334 | |||
335 | if ($name === null) { |
||
336 | return self::$globalCookies; |
||
337 | } |
||
338 | |||
339 | if ($value === false) { |
||
340 | return val($name, self::$globalCookies); |
||
341 | } |
||
342 | |||
343 | self::$globalCookies[$name] = [$value, $expires, $path, $domain, $secure, $httponly]; |
||
344 | return null; |
||
345 | } |
||
346 | |||
347 | /** |
||
348 | * Get or set the meta data for the response. |
||
349 | * |
||
350 | * The meta is an array of data that is unrelated to the response data. |
||
351 | * |
||
352 | * @param array|null $meta Pass a new meta data value or `null` to get the current meta array. |
||
353 | * @param bool $merge Whether or not to merge new data with the current data when setting. |
||
354 | * @return $this|array Returns either the meta or `$this` when setting the meta data. |
||
355 | */ |
||
356 | 44 | View Code Duplication | public function meta($meta = null, $merge = false) { |
|
|||
357 | 44 | if ($meta !== null) { |
|
358 | 44 | if ($merge) { |
|
359 | 44 | $this->meta = array_merge($this->meta, $meta); |
|
360 | } else { |
||
361 | $this->meta = $meta; |
||
362 | } |
||
363 | 44 | return $this; |
|
364 | } else { |
||
365 | return $this->meta; |
||
366 | } |
||
367 | } |
||
368 | |||
369 | /** |
||
370 | * Get or set the data for the response. |
||
371 | * |
||
372 | * @param array|null $data Pass a new data value or `null` to get the current data array. |
||
373 | * @param bool $merge Whether or not to merge new data with the current data when setting. |
||
374 | * @return Response|array Returns either the data or `$this` when setting the data. |
||
375 | */ |
||
376 | 44 | View Code Duplication | public function data($data = null, $merge = false) { |
377 | 44 | if ($data !== null) { |
|
378 | 44 | if ($merge) { |
|
379 | $this->data = array_merge($this->data, $data); |
||
380 | } else { |
||
381 | 44 | $this->data = $data; |
|
382 | } |
||
383 | 44 | return $this; |
|
384 | } else { |
||
385 | return $this->data; |
||
386 | } |
||
387 | } |
||
388 | |||
389 | /** |
||
390 | * Gets or sets headers. |
||
391 | * |
||
392 | * @param string|array $name The name of the header or an array of headers. |
||
393 | * @param string|null $value A new value for the header or null to get the current header. |
||
394 | * @param bool $replace Whether or not to replace the current header or append. |
||
395 | * @return Response|string Returns the value of the header or `$this` for fluent calls. |
||
396 | */ |
||
397 | 44 | public function headers($name, $value = null, $replace = true) { |
|
398 | 44 | $headers = static::splitHeaders($name, $value); |
|
399 | |||
400 | 44 | if (is_string($headers)) { |
|
401 | 44 | return val($headers, $this->headers); |
|
402 | } |
||
403 | |||
404 | 44 | foreach ($headers as $name => $value) { |
|
405 | 44 | if ($replace || !isset($this->headers[$name])) { |
|
406 | 44 | $this->headers[$name] = $value; |
|
407 | } else { |
||
408 | 44 | $this->headers[$name] = array_merge((array)$this->headers, [$value]); |
|
409 | } |
||
410 | } |
||
411 | 44 | return $this; |
|
412 | } |
||
413 | |||
414 | /** |
||
415 | * Gets or sets global headers. |
||
416 | * |
||
417 | * The global headers exist to allow code to queue up headers before the response has been constructed. |
||
418 | * |
||
419 | * @param string|array|null $name The name of the header or an array of headers. |
||
420 | * @param string|null $value A new value for the header or null to get the current header. |
||
421 | * @param bool $replace Whether or not to replace the current header or append. |
||
422 | * @return string|array Returns one of the following: |
||
423 | * - string|array: Returns the current value of the header at {@link $name}. |
||
424 | * - array: Returns the entire global headers array when {@link $name} is not passed. |
||
425 | * - null: Returns `null` when setting a global header. |
||
426 | */ |
||
427 | public static function globalHeaders($name = null, $value = null, $replace = true) { |
||
428 | if (self::$globalHeaders === null) { |
||
429 | self::$globalHeaders = [ |
||
430 | 'P3P' => 'CP="CAO PSA OUR"' |
||
431 | ]; |
||
432 | } |
||
433 | |||
434 | if ($name === null) { |
||
435 | return self::$globalHeaders; |
||
436 | } |
||
437 | |||
438 | $headers = static::splitHeaders($name, $value); |
||
439 | |||
440 | if (is_string($headers)) { |
||
441 | return val($headers, self::$globalHeaders); |
||
442 | } |
||
443 | |||
444 | foreach ($headers as $name => $value) { |
||
445 | if ($replace || !isset(self::$globalHeaders[$name])) { |
||
446 | self::$globalHeaders[$name] = $value; |
||
447 | } else { |
||
448 | self::$globalHeaders[$name] = array_merge((array)self::$globalHeaders, [$value]); |
||
449 | } |
||
450 | } |
||
451 | return null; |
||
452 | } |
||
453 | |||
454 | /** |
||
455 | * Split and normalize headers into a form appropriate for {@link $headers} or {@link $globalHeaders}. |
||
456 | * |
||
457 | * @param string|array $name The name of the header or an array of headers. |
||
458 | * @param string|null $value The header value if {@link $name} is a string. |
||
459 | * @return array|string Returns one of the following: |
||
460 | * - array: An array of headers. |
||
461 | * - string: The header name if just a name was passed. |
||
462 | * @throws \InvalidArgumentException Throws an exception if {@link $name} is not a valid string or array. |
||
463 | */ |
||
464 | 44 | protected static function splitHeaders($name, $value = null) { |
|
465 | 44 | if (is_string($name)) { |
|
466 | 44 | if (strpos($name, ':') !== false) { |
|
467 | // The name is in the form Header: value. |
||
468 | list($name, $value) = explode(':', $name, 2); |
||
469 | return [static::normalizeHeader(trim($name)) => trim($value)]; |
||
470 | 44 | } elseif ($value !== null) { |
|
471 | 44 | return [static::normalizeHeader($name) => $value]; |
|
472 | } else { |
||
473 | 44 | return static::normalizeHeader($name); |
|
474 | } |
||
475 | 23 | } elseif (is_array($name)) { |
|
476 | 23 | $result = []; |
|
477 | 23 | foreach ($name as $key => $value) { |
|
478 | 7 | if (is_numeric($key)) { |
|
479 | // $value should be a header in the form Header: value. |
||
480 | list($key, $value) = explode(':', $value, 2); |
||
481 | } |
||
482 | 7 | $result[static::normalizeHeader(trim($key))] = trim($value); |
|
483 | } |
||
484 | 23 | return $result; |
|
485 | } |
||
486 | throw new \InvalidArgumentException("Argument #1 to splitHeaders() was not valid.", 422); |
||
487 | } |
||
488 | |||
489 | /** |
||
490 | * Normalize a header key to the proper casing. |
||
491 | * |
||
492 | * Example: |
||
493 | * |
||
494 | * ``` |
||
495 | * echo static::normalizeHeader('CONTENT_TYPE'); |
||
496 | * |
||
497 | * // Content-Type |
||
498 | * ``` |
||
499 | * |
||
500 | * @param string $name The name of the header. |
||
501 | * @return string Returns the normalized header name. |
||
502 | */ |
||
503 | 44 | public static function normalizeHeader($name) { |
|
504 | 44 | static $special = [ |
|
505 | 'etag' => 'ETag', 'p3p' => 'P3P', 'www-authenticate' => 'WWW-Authenticate', |
||
506 | 'x-ua-compatible' => 'X-UA-Compatible' |
||
507 | ]; |
||
508 | |||
509 | 44 | $name = str_replace(['-', '_'], ' ', strtolower($name)); |
|
510 | 44 | if (isset($special[$name])) { |
|
511 | $name = $special[$name]; |
||
512 | } else { |
||
513 | 44 | $name = str_replace(' ', '-', ucwords($name)); |
|
514 | } |
||
515 | 44 | return $name; |
|
516 | } |
||
517 | |||
518 | /** |
||
519 | * Gets/sets the http status code. |
||
520 | * |
||
521 | * @param int $value The new value if setting the http status code. |
||
522 | * @return int The current http status code. |
||
523 | * @throws \InvalidArgumentException The new status is not a valid http status number. |
||
524 | */ |
||
525 | 23 | public function status($value = null) { |
|
526 | 23 | if ($value !== null) { |
|
527 | 23 | if (!isset(self::$messages[$value])) { |
|
528 | $this->headers('X-Original-Status', $value); |
||
529 | $value = 500; |
||
530 | } |
||
531 | 23 | $this->status = (int)$value; |
|
532 | } |
||
533 | 23 | return $this->status; |
|
534 | } |
||
535 | |||
536 | /** |
||
537 | * Flush the response to the client. |
||
538 | */ |
||
539 | public function flush() { |
||
544 | |||
545 | /** |
||
546 | * Flush the headers to the browser. |
||
547 | * |
||
548 | * @param bool $global Whether or not to merge the global headers with this response. |
||
549 | */ |
||
550 | public function flushHeaders($global = true) { |
||
551 | if (headers_sent()) { |
||
552 | return; |
||
553 | } |
||
554 | |||
555 | if ($global) { |
||
556 | $cookies = array_replace(static::globalCookies(), $this->cookies); |
||
595 | |||
596 | /** |
||
597 | * Specify data which should be serialized to JSON. |
||
598 | * |
||
599 | * @link http://php.net/manual/en/jsonserializable.jsonserialize.php |
||
600 | * @return mixed data which can be serialized by <b>json_encode</b>, |
||
601 | * which is a value of any type other than a resource. |
||
602 | */ |
||
603 | 23 | public function jsonSerialize() { |
|
631 | } |
||
632 |
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.