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.