Completed
Branch master (6f5620)
by Anton
02:08
created

Response   B

Complexity

Total Complexity 46

Size/Duplication

Total Lines 461
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 10

Test Coverage

Coverage 58.27%

Importance

Changes 0
Metric Value
dl 0
loc 461
ccs 74
cts 127
cp 0.5827
rs 8.3999
c 0
b 0
f 0
wmc 46
lcom 1
cbo 10

23 Methods

Rating   Name   Duplication   Size   Complexity  
A setHeader() 0 4 1
B switchType() 0 16 5
A getProtocolVersion() 0 4 1
A getStatusCode() 0 4 1
C setCookie() 0 38 7
C send() 0 58 10
A setStatusCode() 0 4 1
A getReasonPhrase() 0 4 1
A setReasonPhrase() 0 4 1
A getHeader() 0 7 2
A getHeaderAsArray() 0 7 2
A hasHeader() 0 4 1
A addHeader() 0 8 2
A removeHeader() 0 4 1
A getHeaders() 0 4 1
A setHeaders() 0 4 1
A addHeaders() 0 4 1
A removeHeaders() 0 4 1
A setBody() 0 4 1
A getBody() 0 4 1
A clearBody() 0 4 1
A getCookie() 0 4 1
A sendCookies() 0 6 2

How to fix   Complexity   

Complex Class

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
 * Bluz Framework Component
4
 *
5
 * @copyright Bluz PHP Team
6
 * @link      https://github.com/bluzphp/framework
7
 */
8
9
declare(strict_types=1);
10
11
namespace Bluz\Response;
12
13
use Bluz\Application\Exception\NotAcceptableException;
14
use Bluz\Cli\CliResponse;
15
use Bluz\Common\Options;
16
use Bluz\Controller\Controller;
17
use Bluz\Http\StatusCode;
18
use Bluz\Layout\Layout;
19
use Bluz\Proxy\Messages;
20
use Zend\Diactoros\Response\EmptyResponse;
21
use Zend\Diactoros\Response\HtmlResponse;
22
use Zend\Diactoros\Response\JsonResponse;
23
use Zend\Diactoros\Response\RedirectResponse;
24
use Zend\Diactoros\Response\SapiEmitter;
25
26
/**
27
 * Response Container
28
 *
29
 * @package  Bluz\Response
30
 * @author   Anton Shevchuk
31
 * @link     https://github.com/bluzphp/framework/wiki/Response
32
 */
33
class Response
34
{
35
    use Options;
36
37
    /**
38
     * @var string HTTP protocol version
39
     */
40
    protected $protocol = '1.1';
41
42
    /**
43
     * @var integer response code equal to HTTP status codes
44
     */
45
    protected $code = StatusCode::OK;
46
47
    /**
48
     * @var string|null HTTP Phrase
49
     */
50
    protected $phrase;
51
52
    /**
53
     * @var array list of headers
54
     */
55
    protected $headers = [];
56
57
    /**
58
     * @var array list of cookies
59
     */
60
    protected $cookies = [];
61
62
    /**
63
     * @var Controller
64
     */
65
    protected $body;
66
67
    /**
68
     * @var string CLI|HTML|JSON|FILE
69
     */
70
    protected $type = 'HTML';
71
72
    /**
73
     * send
74
     *
75
     * @throws NotAcceptableException
76
     * @throws \InvalidArgumentException
77
     */
78
    public function send()
79
    {
80
        $body = $this->getBody();
81
82
        $this->sendCookies();
83
84
        switch (true) {
85
            case 'CLI' === $this->type:
86
                // no CLI response
87
                return;
88
            case is_null($body):
89
            case StatusCode::NO_CONTENT === $this->getStatusCode():
90
                $response = new EmptyResponse($this->getStatusCode(), $this->getHeaders());
91
                break;
92
            case StatusCode::MOVED_PERMANENTLY === $this->getStatusCode():
93
            case StatusCode::FOUND === $this->getStatusCode():
94
                $response = new RedirectResponse(
95
                    $this->getHeader('Location'),
96
                    $this->getStatusCode(),
97
                    $this->getHeaders()
98
                );
99
                break;
100
            case 'JSON' === $this->type:
101
                // JSON response
102
                // setup messages
103
                if (Messages::count()) {
104
                    $this->setHeader('Bluz-Notify', json_encode(Messages::popAll()));
105
                }
106
107
                // encode body data to JSON
108
                $response = new JsonResponse(
109
                    $body,
110
                    $this->getStatusCode(),
111
                    $this->getHeaders()
112
                );
113
                break;
114
            case 'FILE' === $this->type:
115
                // File attachment
116
                $response = new AttachmentResponse(
117
                    $this->body->getData()->get('FILE'),
118
                    $this->getStatusCode(),
119
                    $this->getHeaders()
120
                );
121
                break;
122
            case 'HTML' === $this->type:
123
            default:
124
                // HTML response
125
                $response = new HtmlResponse(
126
                    (string)$body,
127
                    $this->getStatusCode(),
128
                    $this->getHeaders()
129
                );
130
                break;
131
        }
132
133
        $emitter = new SapiEmitter();
134
        $emitter->emit($response);
135
    }
136
137
    /**
138
     * Set Response Type, one of JSON, HTML or CLI
139
     *
140
     * @param $type
141
     */
142
    public function switchType($type)
143
    {
144
        // switch statement by content type
145
        switch ($type) {
146
            case 'JSON':
147
                $this->setHeader('Content-Type', 'application/json');
148
                break;
149
            case 'CLI':
150
            case 'FILE':
151
            case 'HTML':
152
            default:
153
                break;
154
        }
155
156
        $this->type = $type;
157
    }
158
159
    /**
160
     * Gets the HTTP protocol version as a string
161
     *
162
     * The string MUST contain only the HTTP version number (e.g., "1.1", "1.0").
163
     *
164
     * @return string HTTP protocol version.
165
     */
166 1
    public function getProtocolVersion()
167
    {
168 1
        return $this->protocol;
169
    }
170
171
    /**
172
     * Gets the response Status-Code
173
     *
174
     * The Status-Code is a 3-digit integer result code of the server's attempt
175
     * to understand and satisfy the request.
176
     *
177
     * @return integer status code.
178
     */
179 4
    public function getStatusCode()
180
    {
181 4
        return $this->code;
182
    }
183
184
    /**
185
     * Sets the status code of this response
186
     *
187
     * @param  integer $code the 3-digit integer result code to set.
188
     *
189
     * @return void
190
     */
191 5
    public function setStatusCode($code)
192
    {
193 5
        $this->code = (int)$code;
194 5
    }
195
196
    /**
197
     * Gets the response Reason-Phrase, a short textual description of the Status-Code
198
     *
199
     * Because a Reason-Phrase is not a required element in response
200
     * Status-Line, the Reason-Phrase value MAY be null. Implementations MAY
201
     * choose to return the default RFC 2616 recommended reason phrase for the
202
     * response's Status-Code.
203
     *
204
     * @return string|null reason phrase, or null if unknown.
205
     */
206 2
    public function getReasonPhrase()
207
    {
208 2
        return $this->phrase;
209
    }
210
211
    /**
212
     * Sets the Reason-Phrase of the response
213
     *
214
     * If no Reason-Phrase is specified, implementations MAY choose to default
215
     * to the RFC 2616 recommended reason phrase for the response's Status-Code.
216
     *
217
     * @param string $phrase the Reason-Phrase to set.
218
     */
219 1
    public function setReasonPhrase($phrase)
220
    {
221 1
        $this->phrase = $phrase;
222 1
    }
223
224
    /**
225
     * Retrieve a header by the given case-insensitive name as a string
226
     *
227
     * This method returns all of the header values of the given
228
     * case-insensitive header name as a string concatenated together using
229
     * a comma.
230
     *
231
     * @param  string $header case-insensitive header name.
232
     *
233
     * @return string
234
     */
235 15
    public function getHeader($header)
236
    {
237 15
        if ($this->hasHeader($header)) {
238 13
            return implode(', ', $this->headers[$header]);
239
        }
240 6
        return '';
241
    }
242
243
    /**
244
     * Retrieves a header by the given case-insensitive name as an array of strings
245
     *
246
     * @param  string $header Case-insensitive header name.
247
     *
248
     * @return string[]
249
     */
250 1
    public function getHeaderAsArray($header)
251
    {
252 1
        if ($this->hasHeader($header)) {
253 1
            return $this->headers[$header];
254
        }
255 1
        return [];
256
    }
257
258
    /**
259
     * Checks if a header exists by the given case-insensitive name
260
     *
261
     * @param  string $header case-insensitive header name.
262
     *
263
     * @return bool returns true if any header names match the given header
264
     *              name using a case-insensitive string comparison. Returns false if
265
     *              no matching header name is found in the message.
266
     */
267 14
    public function hasHeader($header)
268
    {
269 14
        return isset($this->headers[$header]);
270
    }
271
272
    /**
273
     * Sets a header, replacing any existing values of any headers with the
274
     * same case-insensitive name
275
     *
276
     * The header name is case-insensitive. The header values MUST be a string
277
     * or an array of strings.
278
     *
279
     * @param  string          $header header name
280
     * @param  string|string[] $value  header value(s)
281
     *
282
     * @return void
283
     */
284 13
    public function setHeader($header, $value)
285
    {
286 13
        $this->headers[$header] = (array)$value;
287 13
    }
288
289
    /**
290
     * Appends a header value for the specified header
291
     *
292
     * Existing values for the specified header will be maintained. The new
293
     * value will be appended to the existing list.
294
     *
295
     * @param  string $header header name to add
296
     * @param  string $value  value of the header
297
     *
298
     * @return void
299
     */
300 1
    public function addHeader($header, $value)
301
    {
302 1
        if ($this->hasHeader($header)) {
303 1
            $this->headers[$header][] = $value;
304
        } else {
305 1
            $this->setHeader($header, $value);
306
        }
307 1
    }
308
309
    /**
310
     * Remove a specific header by case-insensitive name.
311
     *
312
     * @param  string $header HTTP header to remove
313
     *
314
     * @return void
315
     */
316 1
    public function removeHeader($header)
317
    {
318 1
        unset($this->headers[$header]);
319 1
    }
320
321
    /**
322
     * Gets all message headers
323
     *
324
     * The keys represent the header name as it will be sent over the wire, and
325
     * each value is an array of strings associated with the header.
326
     *
327
     *     // Represent the headers as a string
328
     *     foreach ($message->getHeaders() as $name => $values) {
329
     *         echo $name . ": " . implode(", ", $values);
330
     *     }
331
     *
332
     * @return array returns an associative array of the message's headers.
333
     */
334 1
    public function getHeaders()
335
    {
336 1
        return $this->headers;
337
    }
338
339
    /**
340
     * Sets headers, replacing any headers that have already been set on the message
341
     *
342
     * The array keys MUST be a string. The array values must be either a
343
     * string or an array of strings.
344
     *
345
     * @param  array $headers Headers to set.
346
     *
347
     * @return void
348
     */
349 1
    public function setHeaders(array $headers)
350
    {
351 1
        $this->headers = $headers;
352 1
    }
353
354
    /**
355
     * Merges in an associative array of headers.
356
     *
357
     * Each array key MUST be a string representing the case-insensitive name
358
     * of a header. Each value MUST be either a string or an array of strings.
359
     * For each value, the value is appended to any existing header of the same
360
     * name, or, if a header does not already exist by the given name, then the
361
     * header is added.
362
     *
363
     * @param  array $headers Associative array of headers to add to the message
364
     *
365
     * @return void
366
     */
367 1
    public function addHeaders(array $headers)
368
    {
369 1
        $this->headers = array_merge_recursive($this->headers, $headers);
370 1
    }
371
372
    /**
373
     * Remove all headers
374
     *
375
     * @return void
376
     */
377 6
    public function removeHeaders()
378
    {
379 6
        $this->headers = [];
380 6
    }
381
382
    /**
383
     * Set response body
384
     *
385
     * @param  mixed $body
386
     *
387
     * @return void
388
     */
389 6
    public function setBody($body)
390
    {
391 6
        $this->body = $body;
392 6
    }
393
394
    /**
395
     * Get response body
396
     *
397
     * @return Controller|Layout
398
     */
399 2
    public function getBody()
400
    {
401 2
        return $this->body;
402
    }
403
404
    /**
405
     * Clear response body
406
     *
407
     * @return void
408
     */
409 5
    public function clearBody()
410
    {
411 5
        $this->body = null;
412 5
    }
413
414
    /**
415
     * Set Cookie
416
     *
417
     * @param  string               $name
418
     * @param  string               $value
419
     * @param  int|string|\DateTime $expire
420
     * @param  string               $path
421
     * @param  string               $domain
422
     * @param  bool                 $secure
423
     * @param  bool                 $httpOnly
424
     *
425
     * @return void
426
     */
427 5
    public function setCookie(
428
        $name,
429
        $value = '',
430
        $expire = 0,
431
        $path = '/',
432
        $domain = '',
433
        $secure = false,
434
        $httpOnly = false
435
    ) {
436
        // from PHP source code
437 5
        if (preg_match("/[=,; \t\r\n\013\014]/", $name)) {
438 1
            throw new \InvalidArgumentException('The cookie name contains invalid characters.');
439
        }
440
441 4
        if (empty($name)) {
442 1
            throw new \InvalidArgumentException('The cookie name cannot be empty.');
443
        }
444
445
        // convert expiration time to a Unix timestamp
446 3
        if ($expire instanceof \DateTime) {
447 1
            $expire = $expire->format('U');
448 2
        } elseif (!is_numeric($expire)) {
449 1
            $expire = strtotime($expire);
450 1
            if (false === $expire || -1 === $expire) {
451 1
                throw new \InvalidArgumentException('The cookie expiration time is not valid.');
452
            }
453
        }
454
455 2
        $this->cookies[$name] = [
456 2
            'name' => $name,
457 2
            'value' => $value,
458 2
            'expire' => $expire,
459 2
            'path' => $path,
460 2
            'domain' => $domain,
461 2
            'secure' => (bool)$secure,
462 2
            'httpOnly' => (bool)$httpOnly
463
        ];
464 2
    }
465
466
    /**
467
     * Get Cookie by name
468
     *
469
     * @param  string $name
470
     *
471
     * @return array|null
472
     */
473 2
    public function getCookie($name)
474
    {
475 2
        return $this->cookies[$name] ?? null;
476
    }
477
478
    /**
479
     * Process Cookies to Header
480
     *
481
     *   Set-Cookie: <name>=<value>[; <name>=<value>]...
482
     *   [; expires=<date>][; domain=<domain_name>]
483
     *   [; path=<some_path>][; secure][; httponly]
484
     *
485
     * @return void
486
     */
487
    protected function sendCookies()
488
    {
489
        foreach ($this->cookies as $cookie) {
490
            setcookie(...array_values($cookie));
491
        }
492
    }
493
}
494