Completed
Push — master ( fbdbe0...f7dc06 )
by Mr
13s queued 11s
created

Client   B

Complexity

Total Complexity 49

Size/Duplication

Total Lines 371
Duplicated Lines 1.62 %

Coupling/Cohesion

Components 1
Dependencies 12

Test Coverage

Coverage 91.3%

Importance

Changes 0
Metric Value
wmc 49
lcom 1
cbo 12
dl 6
loc 371
ccs 105
cts 115
cp 0.913
rs 8.48
c 0
b 0
f 0

13 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 20 4
A config() 0 4 1
A getConfig() 0 4 1
A setConfig() 0 4 1
A write() 0 23 5
B read() 0 36 7
A readAsIterator() 0 4 1
A rosario() 0 31 5
A pregResponse() 0 4 1
B login() 0 39 6
A isLegacy() 0 4 3
A connect() 0 31 4
B parseResponse() 6 36 10

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

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

1
<?php
2
3
namespace RouterOS;
4
5
use RouterOS\Exceptions\ClientException;
6
use RouterOS\Exceptions\ConfigException;
7
use RouterOS\Exceptions\QueryException;
8
use RouterOS\Helpers\ArrayHelper;
9
use RouterOS\Interfaces\ClientInterface;
10
11
/**
12
 * Class Client for RouterOS management
13
 *
14
 * @package RouterOS
15
 * @since   0.1
16
 */
17
class Client implements Interfaces\ClientInterface
18
{
19
    use SocketTrait, ShortsTrait;
20
21
    /**
22
     * Configuration of connection
23
     *
24
     * @var \RouterOS\Config
25
     */
26
    private $_config;
27
28
    /**
29
     * API communication object
30
     *
31
     * @var \RouterOS\APIConnector
32
     */
33
34
    private $_connector;
35
36
    /**
37
     * Client constructor.
38
     *
39
     * @param array|\RouterOS\Config $config
40
     * @throws \RouterOS\Exceptions\ClientException
41
     * @throws \RouterOS\Exceptions\ConfigException
42
     * @throws \RouterOS\Exceptions\QueryException
43
     */
44 14
    public function __construct($config)
45
    {
46
        // If array then need create object
47 14
        if (\is_array($config)) {
48 3
            $config = new Config($config);
49
        }
50
51
        // Check for important keys
52 14
        if (true !== $key = ArrayHelper::checkIfKeysNotExist(['host', 'user', 'pass'], $config->getParameters())) {
53 1
            throw new ConfigException("One or few parameters '$key' of Config is not set or empty");
54
        }
55
56
        // Save config if everything is okay
57 13
        $this->setConfig($config);
58
59
        // Throw error if cannot to connect
60 13
        if (false === $this->connect()) {
61 1
            throw new ClientException('Unable to connect to ' . $config->get('host') . ':' . $config->get('port'));
62
        }
63 11
    }
64
65
    /**
66
     * Get some parameter from config
67
     *
68
     * @param string $parameter Name of required parameter
69
     * @return mixed
70
     * @throws \RouterOS\Exceptions\ConfigException
71
     */
72 13
    private function config(string $parameter)
73
    {
74 13
        return $this->_config->get($parameter);
75
    }
76
77
    /**
78
     * Return socket resource if is exist
79
     *
80
     * @return \RouterOS\Config
81
     * @since 0.6
82
     */
83 1
    public function getConfig(): Config
84
    {
85 1
        return $this->_config;
86
    }
87
88
    /**
89
     * Set configuration of client
90
     *
91
     * @param \RouterOS\Config $config
92
     * @since 0.7
93
     */
94 13
    public function setConfig(Config $config)
95
    {
96 13
        $this->_config = $config;
97 13
    }
98
99
    /**
100
     * Send write query to RouterOS (with or without tag)
101
     *
102
     * @param string|array|\RouterOS\Query $query
103
     * @return \RouterOS\Interfaces\ClientInterface
104
     * @throws \RouterOS\Exceptions\QueryException
105
     */
106 12
    public function write($query): ClientInterface
107
    {
108 12
        if (\is_string($query)) {
109 2
            $query = new Query($query);
110 12
        } elseif (\is_array($query)) {
111 1
            $endpoint = array_shift($query);
112 1
            $query    = new Query($endpoint, $query);
113
        }
114
115 12
        if (!$query instanceof Query) {
116 1
            throw new QueryException('Parameters cannot be processed');
117
        }
118
119
        // Send commands via loop to router
120 12
        foreach ($query->getQuery() as $command) {
121 12
            $this->_connector->writeWord(trim($command));
122
        }
123
124
        // Write zero-terminator (empty string)
125 12
        $this->_connector->writeWord('');
126
127 12
        return $this;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this; (RouterOS\Client) is incompatible with the return type declared by the interface RouterOS\Interfaces\ClientInterface::write of type self.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
128
    }
129
130
    /**
131
     * Read answer from server after query was executed
132
     *
133
     * A Mikrotik reply is formed of blocks
134
     * Each block starts with a word, one of ('!re', '!trap', '!done', '!fatal')
135
     * Each block end with an zero byte (empty line)
136
     * Reply ends with a complete !done or !fatal block (ended with 'empty line')
137
     * A !fatal block precedes TCP connexion close
138
     *
139
     * @param bool $parse
140
     * @return mixed
141
     */
142 12
    public function read(bool $parse = true)
143
    {
144
        // By default response is empty
145 12
        $response = [];
146
        // We have to wait a !done or !fatal
147 12
        $lastReply = false;
148
149
        // Read answer from socket in loop
150 12
        while (true) {
151 12
            $word = $this->_connector->readWord();
152
153 12
            if ('' === $word) {
154 12
                if ($lastReply) {
155
                    // We received a !done or !fatal message in a precedent loop
156
                    // response is complete
157 12
                    break;
158
                }
159
                // We did not receive the !done or !fatal message
160
                // This 0 length message is the end of a reply !re or !trap
161
                // We have to wait the router to send a !done or !fatal reply followed by optionals values and a 0 length message
162 4
                continue;
163
            }
164
165
            // Save output line to response array
166 12
            $response[] = $word;
167
168
            // If we get a !done or !fatal line in response, we are now ready to finish the read
169
            // but we need to wait a 0 length message, switch the flag
170 12
            if ('!done' === $word || '!fatal' === $word) {
171 12
                $lastReply = true;
172
            }
173
        }
174
175
        // Parse results and return
176 12
        return $parse ? $this->rosario($response) : $response;
177
    }
178
179
    /**
180
     * Read using Iterators to improve performance on large dataset
181
     *
182
     * @return \RouterOS\ResponseIterator
183
     */
184
    public function readAsIterator(): ResponseIterator
185
    {
186
        return new ResponseIterator($this);
187
    }
188
189
    /**
190
     * This method was created by memory save reasons, it convert response
191
     * from RouterOS to readable array in safe way.
192
     *
193
     * @param array $raw Array RAW response from server
194
     * @return mixed
195
     *
196
     * Based on RouterOSResponseArray solution by @arily
197
     *
198
     * @link    https://github.com/arily/RouterOSResponseArray
199
     * @since   0.10
200
     */
201 4
    private function rosario(array $raw): array
202
    {
203
        // This RAW should't be an error
204 4
        $positions = array_keys($raw, '!re');
205 4
        $count     = count($raw);
206 4
        $result    = [];
207
208 4
        if (isset($positions[1])) {
209
210
            foreach ($positions as $key => $position) {
211
                // Get length of future block
212
                $length = isset($positions[$key + 1])
213
                    ? $positions[$key + 1] - $position + 1
214
                    : $count - $position;
215
216
                // Convert array to simple items
217
                $item = [];
218
                for ($i = 1; $i < $length; $i++) {
219
                    $item[] = array_shift($raw);
220
                }
221
222
                // Save as result
223
                $result[] = $this->parseResponse($item)[0];
224
            }
225
226
        } else {
227 4
            $result = $this->parseResponse($raw);
228
        }
229
230 4
        return $result;
231
    }
232
233
    /**
234
     * Parse response from Router OS
235
     *
236
     * @param array $response Response data
237
     * @return array Array with parsed data
238
     */
239 4
    public function parseResponse(array $response): array
240
    {
241 4
        $result = [];
242 4
        $i      = -1;
243 4
        $lines  = \count($response);
244 4
        foreach ($response as $key => $value) {
245
            switch ($value) {
246 4
                case '!re':
247 1
                    $i++;
248 1
                    break;
249 4
                case '!fatal':
250 1
                    $result = $response;
251 1
                    break 2;
252 3
                case '!trap':
253 3
                case '!done':
254
                    // Check for =ret=, .tag and any other following messages
255 3
                    for ($j = $key + 1; $j <= $lines; $j++) {
256
                        // If we have lines after current one
257 3
                        if (isset($response[$j])) {
258 2
                            $this->pregResponse($response[$j], $matches);
259 2 View Code Duplication
                            if (isset($matches[1][0], $matches[2][0])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
260 2
                                $result['after'][$matches[1][0]] = $matches[2][0];
261
                            }
262
                        }
263
                    }
264 3
                    break 2;
265
                default:
266 1
                    $this->pregResponse($value, $matches);
267 1 View Code Duplication
                    if (isset($matches[1][0], $matches[2][0])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
268 1
                        $result[$i][$matches[1][0]] = $matches[2][0];
269
                    }
270 1
                    break;
271
            }
272
        }
273 4
        return $result;
274
    }
275
276
    /**
277
     * Parse result from RouterOS by regular expression
278
     *
279
     * @param string $value
280
     * @param array  $matches
281
     */
282 3
    private function pregResponse(string $value, &$matches)
283
    {
284 3
        preg_match_all('/^[=|\.](.*)=(.*)/', $value, $matches);
285 3
    }
286
287
    /**
288
     * Authorization logic
289
     *
290
     * @param bool $legacyRetry Retry login if we detect legacy version of RouterOS
291
     * @return bool
292
     * @throws \RouterOS\Exceptions\ClientException
293
     * @throws \RouterOS\Exceptions\ConfigException
294
     * @throws \RouterOS\Exceptions\QueryException
295
     */
296 12
    private function login(bool $legacyRetry = false): bool
297
    {
298
        // If legacy login scheme is enabled
299 12
        if ($this->config('legacy')) {
300
            // For the first we need get hash with salt
301 2
            $query    = new Query('/login');
302 2
            $response = $this->write($query)->read();
0 ignored issues
show
Bug introduced by
The call to read() misses a required argument $parse.

This check looks for function calls that miss required arguments.

Loading history...
303
304
            // Now need use this hash for authorization
305 2
            $query = (new Query('/login'))
306 2
                ->add('=name=' . $this->config('user'))
307 2
                ->add('=response=00' . md5(\chr(0) . $this->config('pass') . pack('H*', $response['after']['ret'])));
308
        } else {
309
            // Just login with our credentials
310 11
            $query = (new Query('/login'))
311 11
                ->add('=name=' . $this->config('user'))
312 11
                ->add('=password=' . $this->config('pass'));
313
314
            // If we set modern auth scheme but router with legacy firmware then need to retry query,
315
            // but need to prevent endless loop
316 11
            $legacyRetry = true;
317
        }
318
319
        // Execute query and get response
320 12
        $response = $this->write($query)->read(false);
321
322
        // if:
323
        //  - we have more than one response
324
        //  - response is '!done'
325
        // => problem with legacy version, swap it and retry
326
        // Only tested with ROS pre 6.43, will test with post 6.43 => this could make legacy parameter obsolete?
327 12
        if ($legacyRetry && $this->isLegacy($response)) {
328 1
            $this->_config->set('legacy', true);
329 1
            return $this->login();
330
        }
331
332
        // Return true if we have only one line from server and this line is !done
333 12
        return (1 === count($response)) && isset($response[0]) && ($response[0] === '!done');
334
    }
335
336
    /**
337
     * Detect by login request if firmware is legacy
338
     *
339
     * @param array $response
340
     * @return bool
341
     * @throws ConfigException
342
     */
343 11
    private function isLegacy(array &$response): bool
344
    {
345 11
        return \count($response) > 1 && $response[0] === '!done' && !$this->config('legacy');
346
    }
347
348
    /**
349
     * Connect to socket server
350
     *
351
     * @return bool
352
     * @throws \RouterOS\Exceptions\ClientException
353
     * @throws \RouterOS\Exceptions\ConfigException
354
     * @throws \RouterOS\Exceptions\QueryException
355
     */
356 13
    private function connect(): bool
357
    {
358
        // By default we not connected
359 13
        $connected = false;
360
361
        // Few attempts in loop
362 13
        for ($attempt = 1; $attempt <= $this->config('attempts'); $attempt++) {
363
364
            // Initiate socket session
365 13
            $this->openSocket();
366
367
            // If socket is active
368 12
            if (null !== $this->getSocket()) {
369 12
                $this->_connector = new APIConnector(new Streams\ResourceStream($this->getSocket()));
370
                // If we logged in then exit from loop
371 12
                if (true === $this->login()) {
372 11
                    $connected = true;
373 11
                    break;
374
                }
375
376
                // Else close socket and start from begin
377 1
                $this->closeSocket();
378
            }
379
380
            // Sleep some time between tries
381 1
            sleep($this->config('delay'));
382
        }
383
384
        // Return status of connection
385 12
        return $connected;
386
    }
387
}
388