Completed
Push — master ( f66a6f...5578a1 )
by Robin
02:04
created

Connection   B

Complexity

Total Complexity 40

Size/Duplication

Total Lines 264
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 7

Test Coverage

Coverage 22.13%

Importance

Changes 29
Bugs 7 Features 2
Metric Value
wmc 40
c 29
b 7
f 2
lcom 1
cbo 7
dl 0
loc 264
ccs 27
cts 122
cp 0.2213
rs 8.2608

9 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 8 2
A connect() 0 20 3
A disconnect() 0 8 2
B sendArticle() 0 31 5
A getResponse() 0 19 4
B getMultiLineResponse() 0 27 5
B getCompressedResponse() 0 36 6
A getSocketUrl() 0 9 2
C sendCommand() 0 44 11

How to fix   Complexity   

Complex Class

Complex classes like Connection 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 Connection, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/*
4
 * This file is part of the NNTP library.
5
 *
6
 * (c) Robin van der Vleuten <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Rvdv\Nntp\Connection;
13
14
use Rvdv\Nntp\Command\CommandInterface;
15
use Rvdv\Nntp\Exception\InvalidArgumentException;
16
use Rvdv\Nntp\Exception\RuntimeException;
17
use Rvdv\Nntp\Response\MultiLineResponse;
18
use Rvdv\Nntp\Response\Response;
19
use Socket\Raw\Exception;
20
use Socket\Raw\Factory;
21
use Socket\Raw\Socket;
22
23
/**
24
 * @author Robin van der Vleuten <[email protected]>
25
 */
26
class Connection implements ConnectionInterface
27
{
28
    /**
29
     * @var Factory
30
     */
31
    private $factory;
32
33
    /**
34
     * @var string
35
     */
36
    private $host;
37
38
    /**
39
     * @var int
40
     */
41
    private $port;
42
43
    /**
44
     * @var bool
45
     */
46
    private $secure;
47
48
    /**
49
     * @var Socket
50
     */
51
    private $socket;
52
53
    /**
54
     * @var int
55
     */
56
    private $timeout;
57
58
    /**
59
     * Constructor.
60
     *
61
     * @param string  $host    The hostname of the NNTP server.
62
     * @param int     $port    The port of the NNTP server.
63
     * @param bool    $secure  A bool indicating if a secure connection should be established.
64
     * @param int     $timeout The socket timeout in seconds.
65
     * @param Factory $factory The socket client factory.
66
     */
67 2
    public function __construct($host, $port, $secure = false, $timeout = 15, Factory $factory = null)
68
    {
69 2
        $this->host = $host;
70 2
        $this->port = $port;
71 2
        $this->secure = $secure;
72 2
        $this->timeout = $timeout;
73 2
        $this->factory = $factory ?: new Factory();
74 2
    }
75
76
    /**
77
     * {@inheritdoc}
78
     */
79 2
    public function connect()
80
    {
81 2
        $address = gethostbyname($this->host);
82 2
        $url = $this->getSocketUrl($address);
83
84
        try {
85 2
            $this->socket = $this->factory->createFromString($url, $scheme)
86 2
                ->connectTimeout($url, $this->timeout);
87 2
        } catch (Exception $e) {
88 1
            throw new RuntimeException(sprintf('Connection to %s:%d failed: %s', $address, $this->port, $e->getMessage()), 0, $e);
89
        }
90
91 1
        if ($this->secure) {
92
            stream_socket_enable_crypto($this->socket->getResource(), true, STREAM_CRYPTO_METHOD_TLS_CLIENT);
93
        }
94
95 1
        $this->socket->setBlocking(false);
96
97 1
        return $this->getResponse();
98
    }
99
100
    /**
101
     * {@inheritdoc}
102
     */
103
    public function disconnect()
104
    {
105
        try {
106
            $this->socket->shutdown()->close();
107
        } catch (Exception $e) {
108
            throw new RuntimeException(sprintf('Error while disconnecting from NNTP server: %s', $e->getMessage()), 0, $e);
109
        }
110
    }
111
112
    /**
113
     * {@inheritdoc}
114
     */
115
    public function sendCommand(CommandInterface $command)
116
    {
117
        $commandString = $command->execute();
118
119
        // NNTP/RFC977 only allows command up to 512 (-2 \r\n) chars.
120
        if (!strlen($commandString) > 510) {
121
            throw new InvalidArgumentException('Failed to write to socket: command exceeded 510 characters');
122
        }
123
124
        if (!$this->socket->selectWrite() || strlen($commandString."\r\n") !== $this->socket->write($commandString."\r\n")) {
125
            throw new RuntimeException('Failed to write to socket');
126
        }
127
128
        $response = $this->getResponse();
129
130
        if ($command->isMultiLine() && ($response->getStatusCode() >= 200 && $response->getStatusCode() <= 399)) {
131
            $response = $command->isCompressed() ? $this->getCompressedResponse($response) : $this->getMultiLineResponse($response);
132
        }
133
134
        if (in_array($response->getStatusCode(), [Response::COMMAND_UNKNOWN, Response::COMMAND_UNAVAILABLE])) {
135
            throw new RuntimeException('Sent command is either unknown or unavailable on server');
136
        }
137
138
        $expectedResponseCodes = $command->getExpectedResponseCodes();
139
140
        // Check if we received a response expected by the command.
141
        if (!isset($expectedResponseCodes[$response->getStatusCode()])) {
142
            throw new RuntimeException(sprintf(
143
                'Unexpected response received: [%d] %s',
144
                $response->getStatusCode(),
145
                $response->getMessage()
146
            ));
147
        }
148
149
        $expectedResponseHandler = $expectedResponseCodes[$response->getStatusCode()];
150
        if (!is_callable([$command, $expectedResponseHandler])) {
151
            throw new RuntimeException(sprintf('Response handler (%s) is not callable method on given command object', $expectedResponseHandler));
152
        }
153
154
        $command->setResponse($response);
155
        $command->$expectedResponseHandler($response);
156
157
        return $command;
158
    }
159
160
    public function sendArticle(CommandInterface $command)
161
    {
162
        $commandString = $command->execute();
163
164
        if (!$this->socket->selectWrite() || strlen($commandString."\r\n.\r\n") !== $this->socket->write($commandString."\r\n.\r\n")) {
165
            throw new RuntimeException('Failed to write to socket');
166
        }
167
168
        $response = $this->getResponse();
169
170
        $expectedResponseCodes = $command->getExpectedResponseCodes();
171
172
        // Check if we received a response expected by the command.
173
        if (!isset($expectedResponseCodes[$response->getStatusCode()])) {
174
            throw new RuntimeException(sprintf(
175
                'Unexpected response received: [%d] %s',
176
                $response->getStatusCode(),
177
                $response->getMessage()
178
            ));
179
        }
180
181
        $expectedResponseHandler = $expectedResponseCodes[$response->getStatusCode()];
182
        if (!is_callable([$command, $expectedResponseHandler])) {
183
            throw new RuntimeException(sprintf('Response handler (%s) is not callable method on given command object', $expectedResponseHandler));
184
        }
185
186
        $command->setResponse($response);
187
        $command->$expectedResponseHandler($response);
188
189
        return $command;
190
    }
191
192 1
    protected function getResponse()
193
    {
194 1
        $buffer = '';
195
196 1
        while ($this->socket->selectRead($this->timeout)) {
197 1
            $buffer .= $this->socket->read(1024);
198
199 1
            if ("\r\n" === substr($buffer, -2)) {
200 1
                break;
201
            }
202
203
            if ($buffer === false) {
204
                $this->disconnect();
205
                throw new RuntimeException('Incorrect data received from buffer');
206
            }
207
        }
208
209 1
        return Response::createFromString($buffer);
210
    }
211
212
    public function getMultiLineResponse(Response $response)
213
    {
214
        $buffer = '';
215
216
        while ($this->socket->selectRead($this->timeout)) {
217
            $buffer .= $this->socket->read(1024);
218
219
            if ("\n.\r\n" === substr($buffer, -4)) {
220
                break;
221
            }
222
223
            if ($buffer === false) {
224
                $this->disconnect();
225
                throw new RuntimeException('Incorrect data received from buffer');
226
            }
227
        }
228
229
        $lines = explode("\r\n", trim($buffer));
230
        if (end($lines) === '.') {
231
            array_pop($lines);
232
        }
233
234
        $lines = array_filter($lines);
235
        $lines = \SplFixedArray::fromArray($lines);
236
237
        return new MultiLineResponse($response, $lines);
238
    }
239
240
    public function getCompressedResponse(Response $response)
241
    {
242
        // Determine encoding by fetching first line.
243
        $line = $this->socket->read(1024);
244
245
        $uncompressed = '';
246
247
        while ($this->socket->selectRead($this->timeout)) {
248
            $buffer = $this->socket->read(1024);
249
250
            if (strlen($buffer) === 0) {
251
                $uncompressed = @gzuncompress($line);
252
253
                if ($uncompressed !== false) {
254
                    break;
255
                }
256
            }
257
258
            if ($buffer === false) {
259
                $this->disconnect();
260
                throw new RuntimeException('Incorrect data received from buffer');
261
            }
262
263
            $line .= $buffer;
264
        }
265
266
        $lines = explode("\r\n", trim($uncompressed));
267
        if (end($lines) === '.') {
268
            array_pop($lines);
269
        }
270
271
        $lines = array_filter($lines);
272
        $lines = \SplFixedArray::fromArray($lines);
273
274
        return new MultiLineResponse($response, $lines);
275
    }
276
277
    /**
278
     * @param string $address
279
     */
280 2
    protected function getSocketUrl($address)
281
    {
282 2
        if (strpos($address, ':') !== false) {
283
            // enclose IPv6 addresses in square brackets before appending port
284
            $address = '['.$address.']';
285
        }
286
287 2
        return sprintf('tcp://%s:%s', $address, $this->port);
288
    }
289
}
290