Completed
Push — master ( b3af43...6273ca )
by Todd
19:01
created

Response   C

Complexity

Total Complexity 78

Size/Duplication

Total Lines 616
Duplicated Lines 3.9 %

Coupling/Cohesion

Components 2
Dependencies 2

Test Coverage

Coverage 44.16%

Importance

Changes 8
Bugs 3 Features 1
Metric Value
wmc 78
c 8
b 3
f 1
lcom 2
cbo 2
dl 24
loc 616
ccs 87
cts 197
cp 0.4416
rs 5.389

18 Methods

Rating   Name   Duplication   Size   Complexity  
A contentType() 0 7 2
A statusMessage() 0 9 2
A cookies() 0 8 2
A flush() 0 5 1
A current() 0 9 3
C create() 0 54 10
A contentAsset() 0 8 2
B contentTypeFromAccept() 0 20 6
A globalCookies() 0 16 4
A meta() 12 12 3
A data() 12 12 3
B headers() 0 16 5
C globalHeaders() 0 26 7
C splitHeaders() 0 24 7
A normalizeHeader() 0 14 2
A status() 0 10 3
D flushHeaders() 0 45 9
C jsonSerialize() 0 28 7

How to fix   Duplicated Code    Complexity   

Duplicated Code

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 Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

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
2
/**
3
 * @author Todd Burry <[email protected]>
4
 * @copyright 2009-2014 Vanilla Forums Inc.
5
 * @license MIT
6
 * @since 1.0
7
 */
8
9
namespace Garden;
10
11
use JsonSerializable;
12
13
/**
14
 * A class that contains the information in an http response.
15
 */
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) {
227 44
        if ($value === null) {
228 44
            return $this->headers('Content-Type');
229
        }
230
231 44
        return $this->headers('Content-Type', $value);
232
    }
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) {
284
        $message = val($statusCode, self::$messages, 'Unknown');
285
286
        if ($header) {
287
            return "HTTP/1.1 $statusCode $message";
288
        } else {
289
            return $message;
290
        }
291
    }
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) {
307
        if ($value === false) {
308
            return val($name, $this->cookies);
309
        }
310
311
        $this->cookies[$name] = [$value, $expires, $path, $domain, $secure, $httponly];
312
        return $this;
313
    }
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) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
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) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
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() {
540
        $this->flushHeaders();
541
542
        echo json_encode($this->data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
543
    }
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);
557
            $headers = array_replace(static::globalHeaders(), $this->headers);
558
        } else {
559
            $cookies = $this->cookies;
560
            $headers = $this->headers;
561
        }
562
563
        // Set the cookies first.
564
        foreach ($cookies as $name => $value) {
565
            setcookie(
566
                $name,
567
                $value[0],
568
                $value[1],
569
                $value[2] !== null ? $value[2] : $this->defaultCookiePath,
570
                $value[3] !== null ? $value[3] : $this->defaultCookieDomain,
571
                $value[4],
572
                $value[5]
573
            );
574
        }
575
576
        // Set the response code.
577
        header(static::statusMessage($this->status, true), true, $this->status);
578
579
        $headers = array_filter($headers);
580
581
        // The content type is a special case.
582
        if (isset($headers['Content-Type'])) {
583
            $contentType = (array)$headers['Content-Type'];
584
            header('Content-Type: '.reset($contentType).'; charset=utf8', true);
585
            unset($headers['Content-Type']);
586
        }
587
588
        // Flush the rest of the headers.
589
        foreach ($headers as $name => $value) {
590
            foreach ((array)$value as $hvalue) {
591
                header("$name: $hvalue", false);
592
            }
593
        }
594
    }
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() {
604 23
        $asset = (string)$this->contentAsset();
605
606 23
        if ($asset) {
607
            // A specific asset was specified.
608 23
            if (strpos($asset, '.') !== false) {
609 15
                list($group, $key) = explode('.', $asset, 2);
610
                switch ($group) {
611 15
                    case 'meta':
612 15
                        return val($key, $this->meta);
613
                    case 'data':
614
                        return val($key, $this->data);
615
                    default:
616
                        return null;
617
                }
618
            } else {
619
                switch ($asset) {
620 8
                    case 'data':
621 8
                        return $this->data;
622
                    case 'meta':
623
                        return $this->meta;
624
                    default:
625
                        return null;
626
                }
627
            }
628
        }
629
        return $this->data;
630
    }
631
}
632