Completed
Pull Request — master (#14)
by
unknown
04:08
created

Client   B

Complexity

Total Complexity 49

Size/Duplication

Total Lines 371
Duplicated Lines 9.7 %

Coupling/Cohesion

Components 1
Dependencies 12

Test Coverage

Coverage 91.3%

Importance

Changes 0
Metric Value
wmc 49
lcom 1
cbo 12
dl 36
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
B parseResponse() 36 36 10
A pregResponse() 0 4 1
B login() 0 39 6
A isLegacy() 0 4 3
A connect() 0 31 4

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 Iterators\ResponseIterator
183
	 */
184
	public function readAsIterator() 
185
	{
186
		return new Iterators\ResponseIterator($this->read(false));
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 View Code Duplication
	protected function parseResponse(array $response): array
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
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
						if (isset($matches[1][0], $matches[2][0])) {
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
				if (isset($matches[1][0], $matches[2][0])) {
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();
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