HttpClient::sendHeaders()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 12
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
eloc 8
nc 3
nop 1
dl 0
loc 12
ccs 9
cts 9
cp 1
crap 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Simply\Application;
4
5
use Psr\Http\Message\ResponseInterface;
6
7
/**
8
 * The http client that facilitates sending responses to the browser client.
9
 * @author Riikka Kalliomäki <[email protected]>
10
 * @copyright Copyright (c) 2018 Riikka Kalliomäki
11
 * @license http://opensource.org/licenses/mit-license.php MIT License
12
 */
13
class HttpClient
14
{
15
    /** @var bool Whether to omit the body of the response or not */
16
    private $omitBody;
17
18
    /** @var int Size of the chunks in bytes sent to the browser */
19
    private $chunkSize;
20
21
    /** @var ServerApi The api used to actually communicate to the browser */
22
    private $serverApi;
23
24
    /** @var bool Whether to automatically detect content length */
25
    private $contentLength;
26
27
    /**
28
     * HttpClient constructor.
29
     */
30 18
    public function __construct()
31
    {
32 18
        $this->omitBody = false;
33 18
        $this->chunkSize = 8 * 1024;
34 18
        $this->serverApi = new ServerApi();
35 18
        $this->contentLength = false;
36 18
    }
37
38
    /**
39
     * Sets the api used to communicate to the client.
40
     * @param ServerApi $api The api used to communicate to the client
41
     */
42 16
    public function setServerApi(ServerApi $api): void
43
    {
44 16
        $this->serverApi = $api;
45 16
    }
46
47
    /**
48
     * Sets the size of the chunks in bytes that are sent to the browser.
49
     * @param int $bytes Size of chunks in bytes
50
     */
51 4
    public function setResponseChunkSize(int $bytes): void
52
    {
53 4
        if ($bytes < 1) {
54 1
            throw new \InvalidArgumentException('The response chunk size must be at least 1');
55
        }
56
57 3
        $this->chunkSize = $bytes;
58 3
    }
59
60
    /**
61
     * Enables automatic detection of content length and adding the appropriate header.
62
     * @param bool $enable True to enable, false to disable
63
     */
64 2
    public function enableContentLength(bool $enable = true): void
65
    {
66 2
        $this->contentLength = $enable;
67 2
    }
68
69
    /**
70
     * Tells the client to omit the body from the response.
71
     * @param bool $omit True to omit the body, false to include it
72
     */
73 1
    public function omitBody(bool $omit = true): void
74
    {
75 1
        $this->omitBody = $omit;
76 1
    }
77
78
    /**
79
     * Sends the given response to the browser.
80
     * @param ResponseInterface $response The response to send
81
     * @return int The number of bytes outputted
82
     */
83 17
    public function send(ResponseInterface $response): int
84
    {
85 17
        $length = 0;
86
87 17
        if ($this->hasResponseBody($response)) {
88 13
            $length = $this->detectLength($response);
89
        }
90
91 17
        if ($length !== null) {
92 6
            $response = $response->withHeader('Content-Length', (string) $length);
93
        }
94
95 17
        if (!$this->serverApi->isHeadersSent()) {
96 15
            $this->sendHeaders($response);
97
        }
98
99 17
        if ($length === null || $length > 0) {
100 13
            return $this->sendBody($response, $length);
101
        }
102
103 4
        return 0;
104
    }
105
106
    /**
107
     * Tells if we are supposed to output the body of the response.
108
     * @param ResponseInterface $response The response to send
109
     * @return bool True if we should send the body, false otherwise
110
     */
111 17
    private function hasResponseBody(ResponseInterface $response): bool
112
    {
113 17
        return !$this->omitBody && !\in_array($response->getStatusCode(), [204, 205, 304], true);
114
    }
115
116
    /**
117
     * Attempts to detect the length of the response for the content-length header.
118
     * @param ResponseInterface $response The response used to detect the length
119
     * @return int|null Length of the response in bytes or null if it cannot be determined
120
     */
121 13
    private function detectLength(ResponseInterface $response): ?int
122
    {
123 13
        if (! $this->contentLength) {
124 11
            return null;
125
        }
126
127 2
        if ($response->hasHeader('Content-Length')) {
128 1
            $lengthHeader = $response->getHeader('Content-Length');
129 1
            return array_shift($lengthHeader);
130
        }
131
132 1
        return $response->getBody()->getSize();
133
    }
134
135
    /**
136
     * Sends the headers for the given response.
137
     * @param ResponseInterface $response The response to send
138
     */
139 15
    private function sendHeaders(ResponseInterface $response): void
140
    {
141 15
        $this->serverApi->sendHeaderLine(sprintf(
142 15
            'HTTP/%s %d %s',
143 15
            $response->getProtocolVersion(),
144 15
            $response->getStatusCode(),
145 15
            $response->getReasonPhrase()
146
        ));
147
148 15
        foreach ($response->getHeaders() as $name => $values) {
149 15
            foreach ($values as $value) {
150 15
                $this->serverApi->sendHeaderLine(sprintf('%s: %s', $name, $value));
151
            }
152
        }
153 15
    }
154
155
    /**
156
     * Sends the body for the given response.
157
     * @param ResponseInterface $response The response to send
158
     * @param int|null $length Length of body or null if unknown
159
     * @return int The number of bytes sent to the browser
160
     */
161 13
    private function sendBody(ResponseInterface $response, ?int $length): int
162
    {
163 13
        $body = $response->getBody();
164
165 13
        if ($body->isSeekable()) {
166 13
            $body->rewind();
167
        }
168
169 13
        $bytes = 0;
170
171 13
        while (!$body->eof()) {
172 13
            $read = $length === null ? $this->chunkSize : min($this->chunkSize, $length - $bytes);
173 13
            $output = $body->read($read);
174
175 13
            if ($output !== '') {
176 12
                $this->serverApi->output($output);
177
            }
178
179 13
            $bytes += \strlen($output);
180
181 13
            if ($this->serverApi->isConnectionAborted()) {
182 1
                break;
183
            }
184
185 12
            if ($length !== null && $bytes >= $length) {
186 2
                break;
187
            }
188
        }
189
190 13
        return $bytes;
191
    }
192
}
193