Response   B
last analyzed

Complexity

Total Complexity 50

Size/Duplication

Total Lines 391
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 15

Importance

Changes 0
Metric Value
wmc 50
lcom 1
cbo 15
dl 0
loc 391
rs 7.2559
c 0
b 0
f 0

19 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 8 1
A prepare() 0 20 4
A setChunkedTransferEncoding() 0 8 1
A setNotModified() 0 22 2
A isNoCache() 0 8 4
A send() 0 21 4
A sendHeaders() 0 19 2
A isChunked() 0 6 2
A __toString() 0 12 1
A computeContentType() 0 12 2
B computeContent() 0 24 4
A computeCacheControl() 0 17 3
A fixContentType() 0 12 3
A fixContentLength() 0 14 4
A fixExpire() 0 13 4
A fixEncoding() 0 11 2
A createHeaders() 0 8 3
A createCookieHeaders() 0 6 2
A sendChunkContent() 0 13 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
 * This file is part of the Borobudur-Http package.
4
 *
5
 * (c) Hexacodelabs <http://hexacodelabs.com>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 */
10
11
namespace Borobudur\Http;
12
13
use Borobudur\Http\Exception\InvalidArgumentException;
14
use Borobudur\Http\Header\Accept\AcceptHeader;
15
use Borobudur\Http\Header\CacheControlHeader;
16
use Borobudur\Http\Header\Content\ContentLengthHeader;
17
use Borobudur\Http\Header\Content\ContentTypeHeader;
18
use Borobudur\Http\Header\ExpiresHeader;
19
use Borobudur\Http\Header\PragmaHeader;
20
use Borobudur\Http\Header\TransferEncodingHeader;
21
use DateTime;
22
23
/**
24
 * @author      Iqbal Maulana <[email protected]>
25
 * @created     7/30/15
26
 */
27
class Response extends AbstractResponse
28
{
29
    use HttpStatusTrait;
30
31
    /**
32
     * @const string
33
     */
34
    const HTTP_VERSION_10 = '1.0';
35
    const HTTP_VERSION_11 = '1.1';
36
37
    /**
38
     * @var SetCookieHeaderBag
39
     */
40
    public $cookies;
41
42
    /**
43
     * @var int
44
     */
45
    protected $chunkLength;
46
47
    /**
48
     * @var int
49
     */
50
    protected $chunkDelayResponse;
51
52
    /**
53
     * Constructor.
54
     *
55
     * @param string $content
56
     * @param int    $statusCode
57
     * @param array  $headers
58
     * @param array  $cookies
59
     */
60
    public function __construct($content = '', $statusCode = 200, array $headers = array(), array $cookies = array())
61
    {
62
        $this->headers = new HeaderBag($headers);
63
        $this->cookies = new SetCookieHeaderBag($cookies);
64
        $this->setStatusCode($statusCode);
65
        $this->setContent($content);
66
        $this->setProtocolVersion(Response::HTTP_VERSION_11);
67
    }
68
69
    /**
70
     * Prepare Response base on Request.
71
     *
72
     * @param Request $request
73
     *
74
     * @return $this
75
     */
76
    public function prepare(Request $request)
77
    {
78
        if ($this->isEmpty() || $this->isInformational()) {
79
            $this->setContent(null);
80
            $this->headers->remove('Content-Type');
81
            $this->headers->remove('Content-Length');
82
        } else {
83
            $this->fixContentType($request);
84
            $this->fixContentLength($request);
85
        }
86
87
        // Fix protocol
88
        if ('HTTP/1.1' !== $request->getServerBag()->get('SERVER_PROTOCOL')) {
89
            $this->setProtocolVersion('1.0');
90
        }
91
92
        $this->fixExpire();
93
94
        return $this;
95
    }
96
97
    /**
98
     * Set response as chunked.
99
     *
100
     * @param int $length Content split length.
101
     * @param int $delay  Delay response in microseconds.
102
     *
103
     * @return $this
104
     */
105
    public function setChunkedTransferEncoding($length = 76, $delay = 1000000)
106
    {
107
        $this->headers->set(new TransferEncodingHeader('chunked'), true);
108
        $this->chunkLength = $length;
109
        $this->chunkDelayResponse = $delay;
110
111
        return $this;
112
    }
113
114
    /**
115
     * Set response as not modified.
116
     *
117
     * @return $this
118
     */
119
    public function setNotModified()
120
    {
121
        $this->setStatusCode(HttpStatus::HTTP_NOT_MODIFIED);
122
        $this->setContent(null);
123
124
        foreach (
125
            array(
126
                'Allow',
127
                'Content-Encoding',
128
                'Content-Language',
129
                'Content-Length',
130
                'Content-MD5',
131
                'Content-Type',
132
                'Transfer-Encoding',
133
                'Last-Modified',
134
            ) as $header
135
        ) {
136
            $this->headers->remove($header);
137
        }
138
139
        return $this;
140
    }
141
142
    /**
143
     * Check if response is no cache.
144
     *
145
     * @return bool
146
     */
147
    public function isNoCache()
148
    {
149
        return
150
            !$this->headers->has('Cache-Control')
151
            && !$this->headers->has('ETag')
152
            && !$this->headers->has('Last-Modified')
153
            && !$this->headers->has('Expires');
154
    }
155
156
    /**
157
     * Send response to client.
158
     *
159
     * @return $this
160
     */
161
    public function send()
162
    {
163
        $content = $this->computeContent();
164
        $this->sendHeaders();
165
166
        if ($content) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $content of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
167
            if ($this->isChunked()) {
168
                $this->sendChunkContent($content);
169
            } else {
170
                echo $content;
171
            }
172
173
            if (function_exists('fastcgi_finish_request')) {
174
                fastcgi_finish_request();
175
            } else {
176
                ob_end_flush();
177
            }
178
        }
179
180
        return $this;
181
    }
182
183
    /**
184
     * Send response header.
185
     *
186
     * @return $this
187
     */
188
    public function sendHeaders()
189
    {
190
        // check is headers already sent.
191
        if (headers_sent()) {
192
            return $this;
193
        }
194
195
        $this->computeContentType();
196
        $this->headers->set($this->computeCacheControl(), true);
197
198
        // send status header
199
        header(sprintf('HTTP/%s %s %s', $this->version, $this->statusCode, $this->statusText), true, $this->statusCode);
200
        // send headers
201
        $this->createHeaders();
202
        // send cookies header
203
        $this->createCookieHeaders();
204
205
        return $this;
206
    }
207
208
    /**
209
     * Check if current response is chunked.
210
     *
211
     * @return bool
212
     */
213
    public function isChunked()
214
    {
215
        return
216
            $this->headers->has('Transfer-Encoding')
217
            && 'chunked' === $this->headers->first('Transfer-Encoding')->getFieldValue();
218
    }
219
220
    /**
221
     * Cast Response as string representation.
222
     *
223
     * @return string
224
     */
225
    public function __toString()
226
    {
227
        $content = $this->computeContent();
228
        $this->computeContentType();
229
        $this->headers->set($this->computeCacheControl(), true);
230
231
        return
232
            sprintf(sprintf('HTTP/%s %s %s', $this->version, $this->statusCode, $this->statusText)) . "\r\n" .
233
            $this->headers . "\r\n" .
234
            $this->cookies . "\r\n" .
235
            $content;
236
    }
237
238
    /**
239
     * Add charset if content type exist.
240
     */
241
    protected function computeContentType()
242
    {
243
        if ($this->headers->has('Content-Type')) {
244
            /**
245
             * @var ContentTypeHeader $contentType
246
             */
247
            $contentType = $this->headers->first('Content-Type');
248
            $contentType->setParameter('charset', $this->getCharset());
249
250
            $this->headers->set($contentType, true);
251
        }
252
    }
253
254
    /**
255
     * Get computed content with specific encoding.
256
     *
257
     * @return string
258
     */
259
    protected function computeContent()
260
    {
261
        if ($this->isNotModified()) {
262
            $this->setNotModified();
263
264
            return null;
265
        }
266
267
        $encoding = $this->fixEncoding();
268
        $this->headers->set(new ContentLengthHeader($encoding->encode()->getLength()), true);
269
270
        if ($this->isChunked()) {
271
            if ('1.0' === $this->getProtocolVersion()) {
272
                throw new InvalidArgumentException(sprintf(
273
                    'Transfer-Encoding chunked need HTTP Protocol version 1.1, "%s" given.',
274
                    $this->getProtocolVersion()
275
                ));
276
            }
277
278
            return chunk_split($encoding->getContent(), $this->chunkLength);
279
        }
280
281
        return $encoding->getContent();
282
    }
283
284
    /**
285
     * Get calculated cache control header.
286
     *
287
     * @return CacheControlHeader
288
     */
289
    protected function computeCacheControl()
290
    {
291
        if ($this->isNoCache()) {
292
            return (new CacheControlHeader())->setNoCache();
293
        }
294
295
        if (!$this->headers->has('Cache-Control')) {
296
            return (new CacheControlHeader())->setPrivate()->setMustReValidate();
297
        }
298
299
        /**
300
         * @var CacheControlHeader $header
301
         */
302
        $header = $this->headers->first('Cache-Control', new CacheControlHeader());
303
304
        return $header->setAutoPrivilege();
305
    }
306
307
    /**
308
     * Add content type header from request if not defined.
309
     *
310
     * @param Request $request
311
     */
312
    private function fixContentType(Request $request)
313
    {
314
        // Add content type from request if not defined.
315
        if (false === $this->headers->has('Content-Type') && true === $request->getHeaderBag()->has('Accept')) {
316
            /**
317
             * @var AcceptHeader $acceptHeader
318
             */
319
            $acceptHeader = $request->getHeaderBag()->first('Accept');
320
            // get first accept content type=
321
            $this->setContentType($acceptHeader->first()->getValue());
322
        }
323
    }
324
325
    /**
326
     * Fix content length header.
327
     *
328
     * @param Request $request
329
     */
330
    private function fixContentLength(Request $request)
331
    {
332
        if ($this->headers->has('Transfer-Encoding')) {
333
            $this->headers->remove('Content-Length');
334
        }
335
336
        if ($request->isMethod(Request::HTTP_METHOD_HEAD)) {
337
            $contentLengthHeader = $this->headers->first('Content-Length');
338
            $this->setContent(null);
339
            if ($length = $contentLengthHeader->getFieldValue()) {
340
                $this->headers->set(new ContentLengthHeader($length), true);
341
            }
342
        }
343
    }
344
345
    /**
346
     * Send extra expire info if needed.
347
     */
348
    private function fixExpire()
349
    {
350
        if ('1.0' === $this->getProtocolVersion() && $this->headers->has('Cache-Control')) {
351
            /**
352
             * @var CacheControlHeader $cacheControl
353
             */
354
            $cacheControl = $this->headers->first('Cache-Control');
355
            if (true === $cacheControl->isNoCache()) {
356
                $this->headers->set(new PragmaHeader('no-cache'), true);
357
                $this->headers->set(new ExpiresHeader(new DateTime('-1 year')), true);
358
            }
359
        }
360
    }
361
362
    /**
363
     * Fix http encoding header.
364
     */
365
    private function fixEncoding()
366
    {
367
        $encoding = new HttpEncoding($this->content, HttpEncoding::NONE);
368
369
        if ($this->headers->has('Content-Encoding')) {
370
            $contentEncoding = $this->headers->first('Content-Encoding');
371
            $encoding->setEncoding($contentEncoding->getFieldValue());
372
        }
373
374
        return $encoding;
375
    }
376
377
    /**
378
     * Send http headers.
379
     */
380
    private function createHeaders()
381
    {
382
        foreach ($this->headers as $headers) {
383
            foreach ($headers as $header) {
384
                header((string) $header, false, $this->statusCode);
385
            }
386
        }
387
    }
388
389
    /**
390
     * Send http cookie headers.
391
     */
392
    private function createCookieHeaders()
393
    {
394
        foreach ($this->cookies as $cookie) {
395
            header((string) $cookie, false, $this->statusCode);
396
        }
397
    }
398
399
    /**
400
     * Send chunk content.
401
     *
402
     * @param string $content
403
     */
404
    private function sendChunkContent($content)
405
    {
406
        flush();
407
        ob_flush();
408
409
        foreach (explode("\r\n", $content) as $chunk) {
410
            echo sprintf("%x\r\n", strlen($chunk));
411
            echo $chunk . "\r\n";
412
            flush();
413
            ob_flush();
414
            usleep($this->chunkDelayResponse);
415
        }
416
    }
417
}
418