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 |
||
30 | class Client implements Interfaces\ClientInterface |
||
31 | { |
||
32 | use SocketTrait, ShortsTrait; |
||
33 | |||
34 | /** |
||
35 | * Configuration of connection |
||
36 | * |
||
37 | * @var \RouterOS\Config |
||
38 | */ |
||
39 | private $config; |
||
40 | |||
41 | /** |
||
42 | * API communication object |
||
43 | * |
||
44 | * @var \RouterOS\APIConnector |
||
45 | */ |
||
46 | private $connector; |
||
47 | |||
48 | /** |
||
49 | * Some strings with custom output |
||
50 | * |
||
51 | * @var string |
||
52 | */ |
||
53 | private $customOutput; |
||
54 | |||
55 | /** |
||
56 | * Client constructor. |
||
57 | * |
||
58 | * @param array|\RouterOS\Interfaces\ConfigInterface $config Array with configuration or Config object |
||
59 | * @param bool $autoConnect If false it will skip auto-connect stage if not need to instantiate connection |
||
60 | * |
||
61 | * @throws \RouterOS\Exceptions\ClientException |
||
62 | * @throws \RouterOS\Exceptions\ConnectException |
||
63 | * @throws \RouterOS\Exceptions\BadCredentialsException |
||
64 | * @throws \RouterOS\Exceptions\ConfigException |
||
65 | * @throws \RouterOS\Exceptions\QueryException |
||
66 | */ |
||
67 | 27 | public function __construct($config, bool $autoConnect = true) |
|
68 | { |
||
69 | // If array then need create object |
||
70 | 27 | if (is_array($config)) { |
|
71 | 26 | $config = new Config($config); |
|
72 | } |
||
73 | |||
74 | // Check for important keys |
||
75 | 27 | if (true !== $key = ArrayHelper::checkIfKeysNotExist(['host', 'user', 'pass'], $config->getParameters())) { |
|
76 | 1 | throw new ConfigException("One or few parameters '$key' of Config is not set or empty"); |
|
77 | } |
||
78 | |||
79 | // Save config if everything is okay |
||
80 | 27 | $this->config = $config; |
|
81 | |||
82 | // Skip next step if not need to instantiate connection |
||
83 | 27 | if (false === $autoConnect) { |
|
84 | 1 | return; |
|
85 | } |
||
86 | |||
87 | // Throw error if cannot to connect |
||
88 | 26 | if (false === $this->connect()) { |
|
89 | 1 | throw new ConnectException('Unable to connect to ' . $config->get('host') . ':' . $config->get('port')); |
|
90 | } |
||
91 | 26 | } |
|
92 | |||
93 | /** |
||
94 | * Get some parameter from config |
||
95 | * |
||
96 | * @param string $parameter Name of required parameter |
||
97 | * |
||
98 | * @return mixed |
||
99 | * @throws \RouterOS\Exceptions\ConfigException |
||
100 | */ |
||
101 | 26 | private function config(string $parameter) |
|
102 | { |
||
103 | 26 | return $this->config->get($parameter); |
|
104 | } |
||
105 | |||
106 | /** |
||
107 | * Send write query to RouterOS (modern version of write) |
||
108 | * |
||
109 | * @param array|string|\RouterOS\Interfaces\QueryInterface $endpoint Path of API query or Query object |
||
110 | * @param array|null $where List of where filters |
||
111 | * @param string|null $operations Some operations which need make on response |
||
112 | * @param string|null $tag Mark query with tag |
||
113 | * |
||
114 | * @return \RouterOS\Interfaces\ClientInterface |
||
115 | * @throws \RouterOS\Exceptions\QueryException |
||
116 | * @throws \RouterOS\Exceptions\ClientException |
||
117 | * @throws \RouterOS\Exceptions\ConfigException |
||
118 | * @since 1.0.0 |
||
119 | */ |
||
120 | 26 | public function query($endpoint, array $where = null, string $operations = null, string $tag = null): ClientInterface |
|
121 | { |
||
122 | // If endpoint is string then build Query object |
||
123 | 26 | $query = ($endpoint instanceof Query) |
|
124 | 26 | ? $endpoint |
|
125 | 26 | : new Query($endpoint); |
|
126 | |||
127 | // Parse where array |
||
128 | 26 | if (!empty($where)) { |
|
129 | |||
130 | // If array is multidimensional, then parse each line |
||
131 | 6 | if (is_array($where[0])) { |
|
132 | 5 | foreach ($where as $item) { |
|
133 | 5 | $query = $this->preQuery($item, $query); |
|
134 | } |
||
135 | } else { |
||
136 | 2 | $query = $this->preQuery($where, $query); |
|
137 | } |
||
138 | |||
139 | } |
||
140 | |||
141 | // Append operations if set |
||
142 | 26 | if (!empty($operations)) { |
|
143 | 1 | $query->operations($operations); |
|
144 | } |
||
145 | |||
146 | // Append tag if set |
||
147 | 26 | if (!empty($tag)) { |
|
148 | 1 | $query->tag($tag); |
|
149 | } |
||
150 | |||
151 | // Submit query to RouterOS |
||
152 | 26 | return $this->writeRAW($query); |
|
153 | } |
||
154 | |||
155 | /** |
||
156 | * Query helper |
||
157 | * |
||
158 | * @param array $item |
||
159 | * @param \RouterOS\Interfaces\QueryInterface $query |
||
160 | * |
||
161 | * @return \RouterOS\Query |
||
162 | * @throws \RouterOS\Exceptions\QueryException |
||
163 | * @throws \RouterOS\Exceptions\ClientException |
||
164 | */ |
||
165 | 6 | private function preQuery(array $item, QueryInterface $query): QueryInterface |
|
166 | { |
||
167 | // Null by default |
||
168 | 6 | $key = null; |
|
169 | 6 | $operator = null; |
|
170 | 6 | $value = null; |
|
171 | |||
172 | 6 | switch (count($item)) { |
|
173 | 6 | case 1: |
|
174 | 1 | [$key] = $item; |
|
175 | 1 | break; |
|
176 | 6 | case 2: |
|
177 | 1 | [$key, $operator] = $item; |
|
178 | 1 | break; |
|
179 | 6 | case 3: |
|
180 | 1 | [$key, $operator, $value] = $item; |
|
181 | 1 | break; |
|
182 | default: |
||
183 | 5 | throw new ClientException('From 1 to 3 parameters of "where" condition is allowed'); |
|
184 | } |
||
185 | |||
186 | 1 | return $query->where($key, $operator, $value); |
|
187 | } |
||
188 | |||
189 | /** |
||
190 | * Send write query object to RouterOS |
||
191 | * |
||
192 | * @param \RouterOS\Interfaces\QueryInterface $query |
||
193 | * |
||
194 | * @return \RouterOS\Interfaces\ClientInterface |
||
195 | * @throws \RouterOS\Exceptions\QueryException |
||
196 | * @throws \RouterOS\Exceptions\ConfigException |
||
197 | * @since 1.0.0 |
||
198 | */ |
||
199 | 26 | private function writeRAW(QueryInterface $query): ClientInterface |
|
200 | { |
||
201 | 26 | $commands = $query->getQuery(); |
|
202 | |||
203 | // Check if first command is export |
||
204 | 26 | if (0 === strpos($commands[0], '/export')) { |
|
205 | |||
206 | // Convert export command with all arguments to valid SSH command |
||
207 | $arguments = explode('/', $commands[0]); |
||
208 | unset($arguments[1]); |
||
209 | $arguments = implode(' ', $arguments); |
||
210 | |||
211 | // Call the router via ssh and store output of export |
||
212 | $this->customOutput = $this->export($arguments); |
||
213 | |||
214 | // Return current object |
||
215 | return $this; |
||
216 | } |
||
217 | |||
218 | // Send commands via loop to router |
||
219 | 26 | foreach ($commands as $command) { |
|
220 | 26 | $this->connector->writeWord(trim($command)); |
|
221 | } |
||
222 | |||
223 | // Write zero-terminator (empty string) |
||
224 | 26 | $this->connector->writeWord(''); |
|
225 | |||
226 | // Return current object |
||
227 | 26 | return $this; |
|
228 | } |
||
229 | |||
230 | /** |
||
231 | * Read RAW response from RouterOS, it can be /export command results also, not only array from API |
||
232 | * |
||
233 | * @param array $options Additional options |
||
234 | * |
||
235 | * @return array|string |
||
236 | * @since 1.0.0 |
||
237 | */ |
||
238 | 26 | public function readRAW(array $options = []) |
|
239 | { |
||
240 | // By default response is empty |
||
241 | 26 | $response = []; |
|
242 | // We have to wait a !done or !fatal |
||
243 | 26 | $lastReply = false; |
|
244 | // Count !re in response |
||
245 | 26 | $countResponse = 0; |
|
246 | |||
247 | // Convert strings to array and return results |
||
248 | 26 | if ($this->isCustomOutput()) { |
|
249 | // Return RAW configuration |
||
250 | return $this->customOutput; |
||
251 | } |
||
252 | |||
253 | // Read answer from socket in loop |
||
254 | 26 | while (true) { |
|
255 | 26 | $word = $this->connector->readWord(); |
|
256 | |||
257 | //Limit response number to finish the read |
||
258 | 26 | if (isset($options['count']) && $countResponse >= (int) $options['count']) { |
|
259 | 1 | $lastReply = true; |
|
260 | } |
||
261 | |||
262 | 26 | if ('' === $word) { |
|
263 | 26 | if ($lastReply) { |
|
264 | // We received a !done or !fatal message in a precedent loop |
||
265 | // response is complete |
||
266 | 26 | break; |
|
267 | } |
||
268 | // We did not receive the !done or !fatal message |
||
269 | // This 0 length message is the end of a reply !re or !trap |
||
270 | // We have to wait the router to send a !done or !fatal reply followed by optionals values and a 0 length message |
||
271 | 5 | continue; |
|
272 | } |
||
273 | |||
274 | // Save output line to response array |
||
275 | 26 | $response[] = $word; |
|
276 | |||
277 | // If we get a !done or !fatal line in response, we are now ready to finish the read |
||
278 | // but we need to wait a 0 length message, switch the flag |
||
279 | 26 | if ('!done' === $word || '!fatal' === $word) { |
|
280 | 26 | $lastReply = true; |
|
281 | } |
||
282 | |||
283 | // If we get a !re line in response, we increment the variable |
||
284 | 26 | if ('!re' === $word) { |
|
285 | 3 | $countResponse++; |
|
286 | } |
||
287 | } |
||
288 | |||
289 | // Parse results and return |
||
290 | 26 | return $response; |
|
291 | } |
||
292 | |||
293 | /** |
||
294 | * Read answer from server after query was executed |
||
295 | * |
||
296 | * A Mikrotik reply is formed of blocks |
||
297 | * Each block starts with a word, one of ('!re', '!trap', '!done', '!fatal') |
||
298 | * Each block end with an zero byte (empty line) |
||
299 | * Reply ends with a complete !done or !fatal block (ended with 'empty line') |
||
300 | * A !fatal block precedes TCP connexion close |
||
301 | * |
||
302 | * @param bool $parse If need parse output to array |
||
303 | * @param array $options Additional options |
||
304 | * |
||
305 | * @return mixed |
||
306 | */ |
||
307 | 26 | public function read(bool $parse = true, array $options = []) |
|
308 | { |
||
309 | // Read RAW response |
||
310 | 26 | $response = $this->readRAW($options); |
|
311 | |||
312 | // Return RAW configuration if custom output is set |
||
313 | 26 | if ($this->isCustomOutput()) { |
|
314 | $this->customOutput = null; |
||
315 | return $response; |
||
316 | } |
||
317 | |||
318 | // Parse results and return |
||
319 | 26 | return $parse ? $this->rosario($response) : $response; |
|
|
|||
320 | } |
||
321 | |||
322 | /** |
||
323 | * Read using Iterators to improve performance on large dataset |
||
324 | * |
||
325 | * @param array $options Additional options |
||
326 | * |
||
327 | * @return \RouterOS\ResponseIterator |
||
328 | * @since 1.0.0 |
||
329 | */ |
||
330 | 3 | public function readAsIterator(array $options = []): ResponseIterator |
|
331 | { |
||
332 | 3 | return new ResponseIterator($this, $options); |
|
333 | } |
||
334 | |||
335 | /** |
||
336 | * This method was created by memory save reasons, it convert response |
||
337 | * from RouterOS to readable array in safe way. |
||
338 | * |
||
339 | * @param array $raw Array RAW response from server |
||
340 | * |
||
341 | * @return mixed |
||
342 | * |
||
343 | * Based on RouterOSResponseArray solution by @arily |
||
344 | * |
||
345 | * @see https://github.com/arily/RouterOSResponseArray |
||
346 | * @since 1.0.0 |
||
347 | */ |
||
348 | 4 | private function rosario(array $raw): array |
|
379 | |||
380 | /** |
||
381 | * Parse response from Router OS |
||
382 | * |
||
383 | * @param array $response Response data |
||
384 | * |
||
385 | * @return array Array with parsed data |
||
386 | */ |
||
387 | 5 | public function parseResponse(array $response): array |
|
388 | { |
||
389 | 5 | $result = []; |
|
390 | 5 | $i = -1; |
|
391 | 5 | $lines = count($response); |
|
392 | 5 | foreach ($response as $key => $value) { |
|
393 | 5 | switch ($value) { |
|
394 | 5 | case '!re': |
|
395 | 2 | $i++; |
|
396 | 2 | break; |
|
397 | 5 | case '!fatal': |
|
398 | 1 | $result = $response; |
|
399 | 1 | break 2; |
|
400 | 4 | case '!trap': |
|
401 | 4 | case '!done': |
|
402 | // Check for =ret=, .tag and any other following messages |
||
403 | 4 | for ($j = $key + 1; $j <= $lines; $j++) { |
|
404 | // If we have lines after current one |
||
405 | 4 | if (isset($response[$j])) { |
|
406 | 3 | $this->preParseResponse($response[$j], $result, $matches); |
|
407 | } |
||
408 | } |
||
409 | 4 | break 2; |
|
410 | default: |
||
411 | 2 | $this->preParseResponse($value, $result, $matches, $i); |
|
412 | 2 | break; |
|
413 | } |
||
414 | } |
||
415 | 5 | return $result; |
|
416 | } |
||
417 | |||
418 | /** |
||
419 | * Response helper |
||
420 | * |
||
421 | * @param string $value Value which should be parsed |
||
422 | * @param array $result Array with parsed response |
||
423 | * @param array|null $matches Matched words |
||
424 | * @param string|int $iterator Type of iterations or number of item |
||
425 | */ |
||
426 | 4 | private function preParseResponse(string $value, array &$result, ?array &$matches, $iterator = 'after'): void |
|
427 | { |
||
428 | 4 | $this->pregResponse($value, $matches); |
|
429 | 4 | if (isset($matches[1][0], $matches[2][0])) { |
|
430 | 4 | $result[$iterator][$matches[1][0]] = $matches[2][0]; |
|
431 | } |
||
432 | 4 | } |
|
433 | |||
434 | /** |
||
435 | * Parse result from RouterOS by regular expression |
||
436 | * |
||
437 | * @param string $value |
||
438 | * @param array|null $matches |
||
439 | */ |
||
440 | 9 | protected function pregResponse(string $value, ?array &$matches): void |
|
441 | { |
||
442 | 9 | preg_match_all('/^[=|.]([.\w-]+)=(.*)/', $value, $matches); |
|
443 | 9 | } |
|
444 | |||
445 | /** |
||
446 | * Authorization logic |
||
447 | * |
||
448 | * @param bool $legacyRetry Retry login if we detect legacy version of RouterOS |
||
449 | * |
||
450 | * @return bool |
||
451 | * @throws \RouterOS\Exceptions\ClientException |
||
452 | * @throws \RouterOS\Exceptions\BadCredentialsException |
||
453 | * @throws \RouterOS\Exceptions\ConfigException |
||
454 | * @throws \RouterOS\Exceptions\QueryException |
||
455 | */ |
||
456 | 26 | private function login(bool $legacyRetry = false): bool |
|
457 | { |
||
458 | // If legacy login scheme is enabled |
||
459 | 26 | if ($this->config('legacy')) { |
|
460 | // For the first we need get hash with salt |
||
461 | 2 | $response = $this->query('/login')->read(); |
|
462 | |||
463 | // Now need use this hash for authorization |
||
464 | 2 | $query = new Query('/login', [ |
|
465 | 2 | '=name=' . $this->config('user'), |
|
466 | 2 | '=response=00' . md5(chr(0) . $this->config('pass') . pack('H*', $response['after']['ret'])), |
|
467 | ]); |
||
468 | } else { |
||
469 | // Just login with our credentials |
||
470 | 26 | $query = new Query('/login', [ |
|
471 | 26 | '=name=' . $this->config('user'), |
|
472 | 26 | '=password=' . $this->config('pass'), |
|
473 | ]); |
||
474 | |||
475 | // If we set modern auth scheme but router with legacy firmware then need to retry query, |
||
476 | // but need to prevent endless loop |
||
477 | 26 | $legacyRetry = true; |
|
478 | } |
||
479 | |||
480 | // Execute query and get response |
||
481 | 26 | $response = $this->query($query)->read(false); |
|
482 | |||
483 | // if: |
||
484 | // - we have more than one response |
||
485 | // - response is '!done' |
||
486 | // => problem with legacy version, swap it and retry |
||
487 | // Only tested with ROS pre 6.43, will test with post 6.43 => this could make legacy parameter obsolete? |
||
488 | 26 | if ($legacyRetry && $this->isLegacy($response)) { |
|
489 | 1 | $this->config->set('legacy', true); |
|
490 | 1 | return $this->login(); |
|
491 | } |
||
492 | |||
493 | // If RouterOS answered with invalid credentials then throw error |
||
494 | 26 | if (!empty($response[0]) && '!trap' === $response[0]) { |
|
495 | 1 | throw new BadCredentialsException('Invalid user name or password'); |
|
496 | } |
||
497 | |||
498 | // Return true if we have only one line from server and this line is !done |
||
499 | 26 | return (1 === count($response)) && isset($response[0]) && ('!done' === $response[0]); |
|
500 | } |
||
501 | |||
502 | /** |
||
503 | * Detect by login request if firmware is legacy |
||
504 | * |
||
505 | * @param array $response |
||
506 | * |
||
507 | * @return bool |
||
508 | * @throws \RouterOS\Exceptions\ConfigException |
||
509 | */ |
||
510 | 26 | private function isLegacy(array $response): bool |
|
514 | |||
515 | /** |
||
516 | * Connect to socket server |
||
517 | * |
||
518 | * @return bool |
||
519 | * @throws \RouterOS\Exceptions\ClientException |
||
520 | * @throws \RouterOS\Exceptions\ConfigException |
||
521 | * @throws \RouterOS\Exceptions\QueryException |
||
522 | */ |
||
523 | 26 | public function connect(): bool |
|
524 | { |
||
525 | // By default we not connected |
||
526 | 26 | $connected = false; |
|
554 | |||
555 | /** |
||
556 | * Check if custom output is not empty |
||
557 | * |
||
558 | * @return bool |
||
559 | */ |
||
560 | 26 | private function isCustomOutput(): bool |
|
564 | |||
565 | /** |
||
566 | * Execute export command on remote host, it also will be used |
||
567 | * if "/export" command passed to query. |
||
568 | * |
||
569 | * @param string|null $arguments String with arguments which should be passed to export command |
||
570 | * |
||
571 | * @return string |
||
572 | * @throws \RouterOS\Exceptions\ConfigException |
||
573 | * @since 1.3.0 |
||
574 | */ |
||
575 | public function export(string $arguments = null): string |
||
593 | } |
||
594 |
If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:
If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.