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 classes like DialogHelper 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 DialogHelper, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
25 | class DialogHelper extends InputAwareHelper |
||
26 | { |
||
27 | private $inputStream; |
||
28 | private static $shell; |
||
29 | private static $stty; |
||
30 | |||
31 | /** |
||
32 | * Asks the user to select a value. |
||
33 | * |
||
34 | * @param OutputInterface $output An Output instance |
||
35 | * @param string|array $question The question to ask |
||
36 | * @param array $choices List of choices to pick from |
||
37 | * @param bool|string $default The default answer if the user enters nothing |
||
38 | * @param bool|int $attempts Max number of times to ask before giving up (false by default, which means infinite) |
||
39 | * @param string $errorMessage Message which will be shown if invalid value from choice list would be picked |
||
40 | * @param bool $multiselect Select more than one value separated by comma |
||
41 | * |
||
42 | * @return int|string|array The selected value or values (the key of the choices array) |
||
43 | * |
||
44 | * @throws \InvalidArgumentException |
||
45 | */ |
||
46 | public function select(OutputInterface $output, $question, $choices, $default = null, $attempts = false, $errorMessage = 'Value "%s" is invalid', $multiselect = false) |
||
47 | { |
||
48 | $width = max(array_map('strlen', array_keys($choices))); |
||
49 | |||
50 | $messages = (array) $question; |
||
51 | foreach ($choices as $key => $value) { |
||
52 | $messages[] = sprintf(" [<info>%-${width}s</info>] %s", $key, $value); |
||
53 | } |
||
54 | |||
55 | $output->writeln($messages); |
||
56 | |||
57 | View Code Duplication | $result = $this->askAndValidate($output, '> ', function ($picked) use ($choices, $errorMessage, $multiselect) { |
|
58 | // Collapse all spaces. |
||
59 | $selectedChoices = str_replace(' ', '', $picked); |
||
60 | |||
61 | if ($multiselect) { |
||
62 | // Check for a separated comma values |
||
63 | if (!preg_match('/^[a-zA-Z0-9_-]+(?:,[a-zA-Z0-9_-]+)*$/', $selectedChoices, $matches)) { |
||
64 | throw new \InvalidArgumentException(sprintf($errorMessage, $picked)); |
||
65 | } |
||
66 | $selectedChoices = explode(',', $selectedChoices); |
||
67 | } else { |
||
68 | $selectedChoices = array($picked); |
||
69 | } |
||
70 | |||
71 | $multiselectChoices = array(); |
||
72 | |||
73 | foreach ($selectedChoices as $value) { |
||
74 | if (empty($choices[$value])) { |
||
75 | throw new \InvalidArgumentException(sprintf($errorMessage, $value)); |
||
76 | } |
||
77 | array_push($multiselectChoices, $value); |
||
78 | } |
||
79 | |||
80 | if ($multiselect) { |
||
81 | return $multiselectChoices; |
||
82 | } |
||
83 | |||
84 | return $picked; |
||
85 | }, $attempts, $default); |
||
86 | |||
87 | return $result; |
||
88 | } |
||
89 | |||
90 | /** |
||
91 | * Asks a question to the user. |
||
92 | * |
||
93 | * @param OutputInterface $output An Output instance |
||
94 | * @param string|array $question The question to ask |
||
95 | * @param string $default The default answer if none is given by the user |
||
96 | * @param array $autocomplete List of values to autocomplete |
||
97 | * |
||
98 | * @return string The user answer |
||
99 | * |
||
100 | * @throws \RuntimeException If there is no data to read in the input stream |
||
101 | */ |
||
102 | public function ask(OutputInterface $output, $question, $default = null, array $autocomplete = null) |
||
226 | |||
227 | /** |
||
228 | * Asks a confirmation to the user. |
||
229 | * |
||
230 | * The question will be asked until the user answers by nothing, yes, or no. |
||
231 | * |
||
232 | * @param OutputInterface $output An Output instance |
||
233 | * @param string|array $question The question to ask |
||
234 | * @param bool $default The default answer if the user enters nothing |
||
235 | * |
||
236 | * @return bool true if the user has confirmed, false otherwise |
||
237 | */ |
||
238 | public function askConfirmation(OutputInterface $output, $question, $default = true) |
||
251 | |||
252 | /** |
||
253 | * Asks a question to the user, the response is hidden. |
||
254 | * |
||
255 | * @param OutputInterface $output An Output instance |
||
256 | * @param string|array $question The question |
||
257 | * @param bool $fallback In case the response can not be hidden, whether to fallback on non-hidden question or not |
||
258 | * |
||
259 | * @return string The answer |
||
260 | * |
||
261 | * @throws \RuntimeException In case the fallback is deactivated and the response can not be hidden |
||
262 | */ |
||
263 | public function askHiddenResponse(OutputInterface $output, $question, $fallback = true) |
||
264 | { |
||
265 | View Code Duplication | if ('\\' === DIRECTORY_SEPARATOR) { |
|
266 | $exe = __DIR__.'/../Resources/bin/hiddeninput.exe'; |
||
267 | |||
268 | // handle code running from a phar |
||
269 | if ('phar:' === substr(__FILE__, 0, 5)) { |
||
270 | $tmpExe = sys_get_temp_dir().'/hiddeninput.exe'; |
||
271 | copy($exe, $tmpExe); |
||
272 | $exe = $tmpExe; |
||
273 | } |
||
274 | |||
275 | $output->write($question); |
||
276 | $value = rtrim(shell_exec($exe)); |
||
277 | $output->writeln(''); |
||
278 | |||
279 | if (isset($tmpExe)) { |
||
280 | unlink($tmpExe); |
||
281 | } |
||
282 | |||
283 | return $value; |
||
284 | } |
||
285 | |||
286 | View Code Duplication | if ($this->hasSttyAvailable()) { |
|
287 | $output->write($question); |
||
288 | |||
289 | $sttyMode = shell_exec('stty -g'); |
||
290 | |||
291 | shell_exec('stty -echo'); |
||
292 | $value = fgets($this->inputStream ?: STDIN, 4096); |
||
293 | shell_exec(sprintf('stty %s', $sttyMode)); |
||
294 | |||
295 | if (false === $value) { |
||
296 | throw new \RuntimeException('Aborted'); |
||
297 | } |
||
298 | |||
299 | $value = trim($value); |
||
300 | $output->writeln(''); |
||
301 | |||
302 | return $value; |
||
303 | } |
||
304 | |||
305 | View Code Duplication | if (false !== $shell = $this->getShell()) { |
|
306 | $output->write($question); |
||
307 | $readCmd = $shell === 'csh' ? 'set mypassword = $<' : 'read -r mypassword'; |
||
308 | $command = sprintf("/usr/bin/env %s -c 'stty -echo; %s; stty echo; echo \$mypassword'", $shell, $readCmd); |
||
309 | $value = rtrim(shell_exec($command)); |
||
310 | $output->writeln(''); |
||
311 | |||
312 | return $value; |
||
313 | } |
||
314 | |||
315 | if ($fallback) { |
||
316 | return $this->ask($output, $question); |
||
317 | } |
||
318 | |||
319 | throw new \RuntimeException('Unable to hide the response'); |
||
320 | } |
||
321 | |||
322 | /** |
||
323 | * Asks for a value and validates the response. |
||
324 | * |
||
325 | * The validator receives the data to validate. It must return the |
||
326 | * validated data when the data is valid and throw an exception |
||
327 | * otherwise. |
||
328 | * |
||
329 | * @param OutputInterface $output An Output instance |
||
330 | * @param string|array $question The question to ask |
||
331 | * @param callable $validator A PHP callback |
||
332 | * @param int|false $attempts Max number of times to ask before giving up (false by default, which means infinite) |
||
333 | * @param string $default The default answer if none is given by the user |
||
334 | * @param array $autocomplete List of values to autocomplete |
||
335 | * |
||
336 | * @return mixed |
||
337 | * |
||
338 | * @throws \Exception When any of the validators return an error |
||
339 | */ |
||
340 | View Code Duplication | public function askAndValidate(OutputInterface $output, $question, $validator, $attempts = false, $default = null, array $autocomplete = null) |
|
350 | |||
351 | /** |
||
352 | * Asks for a value, hide and validates the response. |
||
353 | * |
||
354 | * The validator receives the data to validate. It must return the |
||
355 | * validated data when the data is valid and throw an exception |
||
356 | * otherwise. |
||
357 | * |
||
358 | * @param OutputInterface $output An Output instance |
||
359 | * @param string|array $question The question to ask |
||
360 | * @param callable $validator A PHP callback |
||
361 | * @param int|false $attempts Max number of times to ask before giving up (false by default, which means infinite) |
||
362 | * @param bool $fallback In case the response can not be hidden, whether to fallback on non-hidden question or not |
||
363 | * |
||
364 | * @return string The response |
||
365 | * |
||
366 | * @throws \Exception When any of the validators return an error |
||
367 | * @throws \RuntimeException In case the fallback is deactivated and the response can not be hidden |
||
368 | */ |
||
369 | View Code Duplication | public function askHiddenResponseAndValidate(OutputInterface $output, $question, $validator, $attempts = false, $fallback = true) |
|
370 | { |
||
371 | $that = $this; |
||
372 | |||
373 | $interviewer = function () use ($output, $question, $fallback, $that) { |
||
374 | return $that->askHiddenResponse($output, $question, $fallback); |
||
375 | }; |
||
376 | |||
377 | return $this->validateAttempts($interviewer, $output, $validator, $attempts); |
||
378 | } |
||
379 | |||
380 | /** |
||
381 | * Sets the input stream to read from when interacting with the user. |
||
382 | * |
||
383 | * This is mainly useful for testing purpose. |
||
384 | * |
||
385 | * @param resource $stream The input stream |
||
386 | */ |
||
387 | public function setInputStream($stream) |
||
388 | { |
||
389 | $this->inputStream = $stream; |
||
390 | } |
||
391 | |||
392 | /** |
||
393 | * Returns the helper's input stream. |
||
394 | * |
||
395 | * @return string |
||
396 | */ |
||
397 | public function getInputStream() |
||
398 | { |
||
399 | return $this->inputStream; |
||
400 | } |
||
401 | |||
402 | /** |
||
403 | * {@inheritdoc} |
||
404 | */ |
||
405 | public function getName() |
||
409 | |||
410 | /** |
||
411 | * Return a valid Unix shell. |
||
412 | * |
||
413 | * @return string|bool The valid shell name, false in case no valid shell is found |
||
414 | */ |
||
415 | View Code Duplication | private function getShell() |
|
416 | { |
||
417 | if (null !== self::$shell) { |
||
418 | return self::$shell; |
||
419 | } |
||
420 | |||
421 | self::$shell = false; |
||
422 | |||
423 | if (file_exists('/usr/bin/env')) { |
||
424 | // handle other OSs with bash/zsh/ksh/csh if available to hide the answer |
||
425 | $test = "/usr/bin/env %s -c 'echo OK' 2> /dev/null"; |
||
426 | foreach (array('bash', 'zsh', 'ksh', 'csh') as $sh) { |
||
427 | if ('OK' === rtrim(shell_exec(sprintf($test, $sh)))) { |
||
428 | self::$shell = $sh; |
||
429 | break; |
||
430 | } |
||
431 | } |
||
432 | } |
||
433 | |||
434 | return self::$shell; |
||
435 | } |
||
436 | |||
437 | View Code Duplication | private function hasSttyAvailable() |
|
438 | { |
||
439 | if (null !== self::$stty) { |
||
440 | return self::$stty; |
||
441 | } |
||
442 | |||
443 | exec('stty 2>&1', $output, $exitcode); |
||
444 | |||
445 | return self::$stty = $exitcode === 0; |
||
446 | } |
||
447 | |||
448 | /** |
||
449 | * Validate an attempt. |
||
450 | * |
||
451 | * @param callable $interviewer A callable that will ask for a question and return the result |
||
452 | * @param OutputInterface $output An Output instance |
||
453 | * @param callable $validator A PHP callback |
||
454 | * @param int|false $attempts Max number of times to ask before giving up ; false will ask infinitely |
||
455 | * |
||
456 | * @return string The validated response |
||
457 | * |
||
458 | * @throws \Exception In case the max number of attempts has been reached and no valid response has been given |
||
459 | */ |
||
460 | private function validateAttempts($interviewer, OutputInterface $output, $validator, $attempts) |
||
461 | { |
||
462 | $error = null; |
||
463 | while (false === $attempts || $attempts--) { |
||
464 | if (null !== $error) { |
||
465 | $output->writeln($this->getHelperSet()->get('formatter')->formatBlock($error->getMessage(), 'error')); |
||
466 | } |
||
467 | |||
468 | try { |
||
469 | return call_user_func($validator, $interviewer()); |
||
470 | } catch (\Exception $error) { |
||
471 | } |
||
472 | } |
||
473 | |||
474 | throw $error; |
||
475 | } |
||
476 | } |
||
477 |
This check looks at variables that have been passed in as parameters and are passed out again to other methods.
If the outgoing method call has stricter type requirements than the method itself, an issue is raised.
An additional type check may prevent trouble.