Completed
Push — master ( 2b1385...7c6a84 )
by Thomas
07:21
created

DialogHelper   C

Complexity

Total Complexity 74

Size/Duplication

Total Lines 459
Duplicated Lines 41.61 %

Coupling/Cohesion

Components 1
Dependencies 4

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 74
c 1
b 0
f 0
lcom 1
cbo 4
dl 191
loc 459
rs 5.5245

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 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
2
3
/*
4
 * This file is part of the Symfony package.
5
 *
6
 * (c) Fabien Potencier <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Symfony\Component\Console\Helper;
13
14
use Symfony\Component\Console\Output\OutputInterface;
15
use Symfony\Component\Console\Formatter\OutputFormatterStyle;
16
17
/**
18
 * The Dialog class provides helpers to interact with the user.
19
 *
20
 * @author Fabien Potencier <[email protected]>
21
 *
22
 * @deprecated since version 2.5, to be removed in 3.0.
23
 *             Use {@link \Symfony\Component\Console\Helper\QuestionHelper} instead.
24
 */
25
class DialogHelper extends InputAwareHelper
26
{
27
    private $inputStream;
28
    private static $shell;
29
    private static $stty;
30
31
    public function __construct($triggerDeprecationError = true)
32
    {
33
        if ($triggerDeprecationError) {
34
            @trigger_error('"Symfony\Component\Console\Helper\DialogHelper" is deprecated since version 2.5 and will be removed in 3.0. Use "Symfony\Component\Console\Helper\QuestionHelper" instead.', E_USER_DEPRECATED);
35
        }
36
    }
37
38
    /**
39
     * Asks the user to select a value.
40
     *
41
     * @param OutputInterface $output       An Output instance
42
     * @param string|array    $question     The question to ask
43
     * @param array           $choices      List of choices to pick from
44
     * @param bool|string     $default      The default answer if the user enters nothing
45
     * @param bool|int        $attempts     Max number of times to ask before giving up (false by default, which means infinite)
46
     * @param string          $errorMessage Message which will be shown if invalid value from choice list would be picked
47
     * @param bool            $multiselect  Select more than one value separated by comma
48
     *
49
     * @return int|string|array The selected value or values (the key of the choices array)
50
     *
51
     * @throws \InvalidArgumentException
52
     */
53
    public function select(OutputInterface $output, $question, $choices, $default = null, $attempts = false, $errorMessage = 'Value "%s" is invalid', $multiselect = false)
54
    {
55
        $width = max(array_map('strlen', array_keys($choices)));
56
57
        $messages = (array) $question;
58
        foreach ($choices as $key => $value) {
59
            $messages[] = sprintf("  [<info>%-${width}s</info>] %s", $key, $value);
60
        }
61
62
        $output->writeln($messages);
63
64
        $result = $this->askAndValidate($output, '> ', function ($picked) use ($choices, $errorMessage, $multiselect) {
65
            // Collapse all spaces.
66
            $selectedChoices = str_replace(' ', '', $picked);
67
68
            if ($multiselect) {
69
                // Check for a separated comma values
70
                if (!preg_match('/^[a-zA-Z0-9_-]+(?:,[a-zA-Z0-9_-]+)*$/', $selectedChoices, $matches)) {
71
                    throw new \InvalidArgumentException(sprintf($errorMessage, $picked));
72
                }
73
                $selectedChoices = explode(',', $selectedChoices);
74
            } else {
75
                $selectedChoices = array($picked);
76
            }
77
78
            $multiselectChoices = array();
79
80
            foreach ($selectedChoices as $value) {
81
                if (empty($choices[$value])) {
82
                    throw new \InvalidArgumentException(sprintf($errorMessage, $value));
83
                }
84
                $multiselectChoices[] = $value;
85
            }
86
87
            if ($multiselect) {
88
                return $multiselectChoices;
89
            }
90
91
            return $picked;
92
        }, $attempts, $default);
93
94
        return $result;
95
    }
96
97
    /**
98
     * Asks a question to the user.
99
     *
100
     * @param OutputInterface $output       An Output instance
101
     * @param string|array    $question     The question to ask
102
     * @param string          $default      The default answer if none is given by the user
103
     * @param array           $autocomplete List of values to autocomplete
104
     *
105
     * @return string The user answer
106
     *
107
     * @throws \RuntimeException If there is no data to read in the input stream
108
     */
109
    public function ask(OutputInterface $output, $question, $default = null, array $autocomplete = null)
110
    {
111
        if ($this->input && !$this->input->isInteractive()) {
112
            return $default;
113
        }
114
115
        $output->write($question);
116
117
        $inputStream = $this->inputStream ?: STDIN;
118
119
        if (null === $autocomplete || !$this->hasSttyAvailable()) {
120
            $ret = fgets($inputStream, 4096);
121
            if (false === $ret) {
122
                throw new \RuntimeException('Aborted');
123
            }
124
            $ret = trim($ret);
125
        } else {
126
            $ret = '';
127
128
            $i = 0;
129
            $ofs = -1;
130
            $matches = $autocomplete;
131
            $numMatches = count($matches);
132
133
            $sttyMode = shell_exec('stty -g');
134
135
            // Disable icanon (so we can fread each keypress) and echo (we'll do echoing here instead)
136
            shell_exec('stty -icanon -echo');
137
138
            // Add highlighted text style
139
            $output->getFormatter()->setStyle('hl', new OutputFormatterStyle('black', 'white'));
140
141
            // Read a keypress
142
            while (!feof($inputStream)) {
143
                $c = fread($inputStream, 1);
144
145
                // Backspace Character
146
                if ("\177" === $c) {
147
                    if (0 === $numMatches && 0 !== $i) {
148
                        --$i;
149
                        // Move cursor backwards
150
                        $output->write("\033[1D");
151
                    }
152
153
                    if ($i === 0) {
154
                        $ofs = -1;
155
                        $matches = $autocomplete;
156
                        $numMatches = count($matches);
157
                    } else {
158
                        $numMatches = 0;
159
                    }
160
161
                    // Pop the last character off the end of our string
162
                    $ret = substr($ret, 0, $i);
163
                } elseif ("\033" === $c) {
164
                    // Did we read an escape sequence?
165
                    $c .= fread($inputStream, 2);
166
167
                    // A = Up Arrow. B = Down Arrow
168
                    if (isset($c[2]) && ('A' === $c[2] || 'B' === $c[2])) {
169
                        if ('A' === $c[2] && -1 === $ofs) {
170
                            $ofs = 0;
171
                        }
172
173
                        if (0 === $numMatches) {
174
                            continue;
175
                        }
176
177
                        $ofs += ('A' === $c[2]) ? -1 : 1;
178
                        $ofs = ($numMatches + $ofs) % $numMatches;
179
                    }
180
                } elseif (ord($c) < 32) {
181
                    if ("\t" === $c || "\n" === $c) {
182
                        if ($numMatches > 0 && -1 !== $ofs) {
183
                            $ret = $matches[$ofs];
184
                            // Echo out remaining chars for current match
185
                            $output->write(substr($ret, $i));
186
                            $i = strlen($ret);
187
                        }
188
189
                        if ("\n" === $c) {
190
                            $output->write($c);
191
                            break;
192
                        }
193
194
                        $numMatches = 0;
195
                    }
196
197
                    continue;
198
                } else {
199
                    $output->write($c);
200
                    $ret .= $c;
201
                    ++$i;
202
203
                    $numMatches = 0;
204
                    $ofs = 0;
205
206
                    foreach ($autocomplete as $value) {
207
                        // If typed characters match the beginning chunk of value (e.g. [AcmeDe]moBundle)
208
                        if (0 === strpos($value, $ret) && $i !== strlen($value)) {
209
                            $matches[$numMatches++] = $value;
210
                        }
211
                    }
212
                }
213
214
                // Erase characters from cursor to end of line
215
                $output->write("\033[K");
216
217
                if ($numMatches > 0 && -1 !== $ofs) {
218
                    // Save cursor position
219
                    $output->write("\0337");
220
                    // Write highlighted text
221
                    $output->write('<hl>'.substr($matches[$ofs], $i).'</hl>');
222
                    // Restore cursor position
223
                    $output->write("\0338");
224
                }
225
            }
226
227
            // Reset stty so it behaves normally again
228
            shell_exec(sprintf('stty %s', $sttyMode));
229
        }
230
231
        return strlen($ret) > 0 ? $ret : $default;
232
    }
233
234
    /**
235
     * Asks a confirmation to the user.
236
     *
237
     * The question will be asked until the user answers by nothing, yes, or no.
238
     *
239
     * @param OutputInterface $output   An Output instance
240
     * @param string|array    $question The question to ask
241
     * @param bool            $default  The default answer if the user enters nothing
242
     *
243
     * @return bool true if the user has confirmed, false otherwise
244
     */
245
    public function askConfirmation(OutputInterface $output, $question, $default = true)
246
    {
247
        $answer = 'z';
248
        while ($answer && !in_array(strtolower($answer[0]), array('y', 'n'))) {
249
            $answer = $this->ask($output, $question);
250
        }
251
252
        if (false === $default) {
253
            return $answer && 'y' == strtolower($answer[0]);
254
        }
255
256
        return !$answer || 'y' == strtolower($answer[0]);
257
    }
258
259
    /**
260
     * Asks a question to the user, the response is hidden.
261
     *
262
     * @param OutputInterface $output   An Output instance
263
     * @param string|array    $question The question
264
     * @param bool            $fallback In case the response can not be hidden, whether to fallback on non-hidden question or not
265
     *
266
     * @return string The answer
267
     *
268
     * @throws \RuntimeException In case the fallback is deactivated and the response can not be hidden
269
     */
270
    public function askHiddenResponse(OutputInterface $output, $question, $fallback = true)
271
    {
272
        if ('\\' === DIRECTORY_SEPARATOR) {
273
            $exe = __DIR__.'/../Resources/bin/hiddeninput.exe';
274
275
            // handle code running from a phar
276
            if ('phar:' === substr(__FILE__, 0, 5)) {
277
                $tmpExe = sys_get_temp_dir().'/hiddeninput.exe';
278
                copy($exe, $tmpExe);
279
                $exe = $tmpExe;
280
            }
281
282
            $output->write($question);
283
            $value = rtrim(shell_exec($exe));
284
            $output->writeln('');
285
286
            if (isset($tmpExe)) {
287
                unlink($tmpExe);
288
            }
289
290
            return $value;
291
        }
292
293
        if ($this->hasSttyAvailable()) {
294
            $output->write($question);
295
296
            $sttyMode = shell_exec('stty -g');
297
298
            shell_exec('stty -echo');
299
            $value = fgets($this->inputStream ?: STDIN, 4096);
300
            shell_exec(sprintf('stty %s', $sttyMode));
301
302
            if (false === $value) {
303
                throw new \RuntimeException('Aborted');
304
            }
305
306
            $value = trim($value);
307
            $output->writeln('');
308
309
            return $value;
310
        }
311
312
        if (false !== $shell = $this->getShell()) {
313
            $output->write($question);
314
            $readCmd = $shell === 'csh' ? 'set mypassword = $<' : 'read -r mypassword';
315
            $command = sprintf("/usr/bin/env %s -c 'stty -echo; %s; stty echo; echo \$mypassword'", $shell, $readCmd);
316
            $value = rtrim(shell_exec($command));
317
            $output->writeln('');
318
319
            return $value;
320
        }
321
322
        if ($fallback) {
323
            return $this->ask($output, $question);
324
        }
325
326
        throw new \RuntimeException('Unable to hide the response');
327
    }
328
329
    /**
330
     * Asks for a value and validates the response.
331
     *
332
     * The validator receives the data to validate. It must return the
333
     * validated data when the data is valid and throw an exception
334
     * otherwise.
335
     *
336
     * @param OutputInterface $output       An Output instance
337
     * @param string|array    $question     The question to ask
338
     * @param callable        $validator    A PHP callback
339
     * @param int|false       $attempts     Max number of times to ask before giving up (false by default, which means infinite)
340
     * @param string          $default      The default answer if none is given by the user
341
     * @param array           $autocomplete List of values to autocomplete
342
     *
343
     * @return mixed
344
     *
345
     * @throws \Exception When any of the validators return an error
346
     */
347
    public function askAndValidate(OutputInterface $output, $question, $validator, $attempts = false, $default = null, array $autocomplete = null)
348
    {
349
        $that = $this;
350
351
        $interviewer = function () use ($output, $question, $default, $autocomplete, $that) {
352
            return $that->ask($output, $question, $default, $autocomplete);
353
        };
354
355
        return $this->validateAttempts($interviewer, $output, $validator, $attempts);
356
    }
357
358
    /**
359
     * Asks for a value, hide and validates the response.
360
     *
361
     * The validator receives the data to validate. It must return the
362
     * validated data when the data is valid and throw an exception
363
     * otherwise.
364
     *
365
     * @param OutputInterface $output    An Output instance
366
     * @param string|array    $question  The question to ask
367
     * @param callable        $validator A PHP callback
368
     * @param int|false       $attempts  Max number of times to ask before giving up (false by default, which means infinite)
369
     * @param bool            $fallback  In case the response can not be hidden, whether to fallback on non-hidden question or not
370
     *
371
     * @return string The response
372
     *
373
     * @throws \Exception        When any of the validators return an error
374
     * @throws \RuntimeException In case the fallback is deactivated and the response can not be hidden
375
     */
376
    public function askHiddenResponseAndValidate(OutputInterface $output, $question, $validator, $attempts = false, $fallback = true)
377
    {
378
        $that = $this;
379
380
        $interviewer = function () use ($output, $question, $fallback, $that) {
381
            return $that->askHiddenResponse($output, $question, $fallback);
382
        };
383
384
        return $this->validateAttempts($interviewer, $output, $validator, $attempts);
385
    }
386
387
    /**
388
     * Sets the input stream to read from when interacting with the user.
389
     *
390
     * This is mainly useful for testing purpose.
391
     *
392
     * @param resource $stream The input stream
393
     */
394
    public function setInputStream($stream)
395
    {
396
        $this->inputStream = $stream;
397
    }
398
399
    /**
400
     * Returns the helper's input stream.
401
     *
402
     * @return resource|null The input stream or null if the default STDIN is used
403
     */
404
    public function getInputStream()
405
    {
406
        return $this->inputStream;
407
    }
408
409
    /**
410
     * {@inheritdoc}
411
     */
412
    public function getName()
413
    {
414
        return 'dialog';
415
    }
416
417
    /**
418
     * Return a valid Unix shell.
419
     *
420
     * @return string|bool The valid shell name, false in case no valid shell is found
421
     */
422
    private function getShell()
423
    {
424
        if (null !== self::$shell) {
425
            return self::$shell;
426
        }
427
428
        self::$shell = false;
429
430
        if (file_exists('/usr/bin/env')) {
431
            // handle other OSs with bash/zsh/ksh/csh if available to hide the answer
432
            $test = "/usr/bin/env %s -c 'echo OK' 2> /dev/null";
433
            foreach (array('bash', 'zsh', 'ksh', 'csh') as $sh) {
434
                if ('OK' === rtrim(shell_exec(sprintf($test, $sh)))) {
435
                    self::$shell = $sh;
436
                    break;
437
                }
438
            }
439
        }
440
441
        return self::$shell;
442
    }
443
444
    private function hasSttyAvailable()
445
    {
446
        if (null !== self::$stty) {
447
            return self::$stty;
448
        }
449
450
        exec('stty 2>&1', $output, $exitcode);
451
452
        return self::$stty = $exitcode === 0;
453
    }
454
455
    /**
456
     * Validate an attempt.
457
     *
458
     * @param callable        $interviewer A callable that will ask for a question and return the result
459
     * @param OutputInterface $output      An Output instance
460
     * @param callable        $validator   A PHP callback
461
     * @param int|false       $attempts    Max number of times to ask before giving up ; false will ask infinitely
462
     *
463
     * @return string The validated response
464
     *
465
     * @throws \Exception In case the max number of attempts has been reached and no valid response has been given
466
     */
467
    private function validateAttempts($interviewer, OutputInterface $output, $validator, $attempts)
468
    {
469
        $e = null;
470
        while (false === $attempts || $attempts--) {
471
            if (null !== $e) {
472
                $output->writeln($this->getHelperSet()->get('formatter')->formatBlock($e->getMessage(), 'error'));
473
            }
474
475
            try {
476
                return call_user_func($validator, $interviewer());
477
            } catch (\Exception $e) {
478
            }
479
        }
480
481
        throw $e;
482
    }
483
}
484