Completed
Push — master ( d292ba...467d47 )
by Anton
15s
created

Response   B

Complexity

Total Complexity 47

Size/Duplication

Total Lines 473
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 10

Test Coverage

Coverage 82.95%

Importance

Changes 0
Metric Value
dl 0
loc 473
ccs 107
cts 129
cp 0.8295
rs 8.439
c 0
b 0
f 0
wmc 47
lcom 1
cbo 10

24 Methods

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