Completed
Push — master ( 4c74f5...2b33f7 )
by Aurimas
04:08
created

Request::writeHead()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 19
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30
Metric Value
dl 0
loc 19
ccs 0
cts 15
cp 0
rs 8.8571
cc 5
eloc 10
nc 5
nop 4
crap 30
1
<?php
2
3
namespace Thruster\Component\HttpServer;
4
5
use Psr\Http\Message\ResponseInterface;
6
use Thruster\Component\Http\RequestParser;
7
use Thruster\Component\EventEmitter\EventEmitterInterface;
8
use Thruster\Component\EventEmitter\EventEmitterTrait;
9
use Thruster\Component\Socket\ConnectionInterface;
10
11
/**
12
 * Class Request
13
 *
14
 * @package Thruster\Component\HttpServer
15
 * @author  Aurimas Niekis <[email protected]>
16
 */
17
class Request implements EventEmitterInterface
18
{
19
    use EventEmitterTrait;
20
21
    /**
22
     * @var ConnectionInterface
23
     */
24
    private $connection;
25
26
    /**
27
     * @var bool
28
     */
29
    private $closed;
30
31
    /**
32
     * @var bool
33
     */
34
    private $writable;
35
36
    /**
37
     * @var bool
38
     */
39
    private $headWritten;
40
41
    /**
42
     * @var bool
43
     */
44
    private $chunkedEncoding;
45
46
    /**
47
     * @var RequestParser
48
     */
49
    private $requestParser;
50
51
    public function __construct(ConnectionInterface $connection, array $options = [])
52
    {
53
        $this->connection = $connection;
54
55
        $this->requestParser = new RequestParser($options);
56
57
        $this->chunkedEncoding = true;
58
        $this->closed          = false;
59
        $this->headWritten     = false;
60
61
        $this->requestParser->on('request', function () {
62
            $this->emit('request', func_get_args());
63
        });
64
65
        $this->requestParser->on('received_head', function () {
66
            $this->emit('received_head', func_get_args());
67
        });
68
69
        $connection->on('end', function () {
70
            $this->close();
71
        });
72
73
        $connection->on('error', function ($error) {
74
            $this->emit('error', [$error, $this]);
75
            $this->close();
76
        });
77
78
        $connection->on('drain', function () {
79
            $this->emit('drain');
80
        });
81
    }
82
83
    public function feed($data)
84
    {
85
        $this->requestParser->onData($data);
86
    }
87
88
    public function isWritable() : bool
89
    {
90
        return $this->writable;
91
    }
92
93
    public function sendResponse(ResponseInterface $response)
94
    {
95
        $headers = [];
96
97
        foreach ($response->getHeaders() as $name => $values) {
98
            $headers[$name] = implode(', ', $values);
99
        }
100
101
102
        $response->getBody()->rewind();
103
        $size = $response->getBody()->getSize();
104
105
        $this->writeHead($size > 0, $response->getStatusCode(), $response->getReasonPhrase(), $headers);
106
107
108
        $this->end($response->getBody()->getContents());
109
    }
110
111
    public function writeHead(bool $hasBody, int $status, string $reasonPhrase, array $headers)
112
    {
113
        if (true === $this->headWritten) {
114
            throw new \Exception('Response head has already been written.');
115
        }
116
117
        if (true === isset($headers['Content-Length']) || false === $hasBody) {
118
            $this->chunkedEncoding = false;
119
        }
120
121
        if (true === $this->chunkedEncoding) {
122
            $headers['Transfer-Encoding'] = 'chunked';
123
        }
124
125
        $data = $this->formatHead($status, $reasonPhrase, $headers);
126
        $this->connection->write($data);
127
128
        $this->headWritten = true;
129
    }
130
131
    private function formatHead($status, $reasonPhrase, array $headers)
132
    {
133
        $status = (int)$status;
134
        $data   = "HTTP/1.1 $status $reasonPhrase\r\n";
135
136
        foreach ($headers as $name => $value) {
137
            $name = str_replace(["\r", "\n"], '', $name);
138
139
            foreach ((array)$value as $val) {
140
                $val = str_replace(["\r", "\n"], '', $val);
141
142
                $data .= "$name: $val\r\n";
143
            }
144
        }
145
146
        $data .= "\r\n";
147
148
        return $data;
149
    }
150
151
    public function write($data)
152
    {
153
        if (false === $this->headWritten) {
154
            throw new \Exception('Response head has not yet been written.');
155
        }
156
157
        if (true === $this->chunkedEncoding) {
158
            $len     = strlen($data);
159
            $chunk   = dechex($len) . "\r\n" . $data . "\r\n";
160
            $flushed = $this->connection->write($chunk);
161
        } else {
162
            $flushed = $this->connection->write($data);
163
        }
164
165
        return $flushed;
166
    }
167
168
    public function end($data = null)
169
    {
170
        if (null !== $data) {
171
            $this->write($data);
172
        }
173
174
        if (true === $this->chunkedEncoding) {
175
            $this->connection->write("0\r\n\r\n");
176
        }
177
178
        $this->emit('end');
179
        $this->removeListeners();
180
        $this->connection->end();
181
    }
182
183
    public function close()
184
    {
185
        if (true === $this->closed) {
186
            return;
187
        }
188
189
        $this->closed   = true;
190
        $this->writable = false;
191
        $this->emit('close');
192
        $this->removeListeners();
193
194
        $this->connection->close();
195
    }
196
}
197