Completed
Push — master ( 6ae66b...02d37f )
by Jitendra
12s queued 10s
created

Interactor::prompt()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 22
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 1
Metric Value
cc 5
eloc 13
c 2
b 0
f 1
nc 4
nop 4
dl 0
loc 22
rs 9.5222
1
<?php
2
3
/*
4
 * This file is part of the PHP-CLI package.
5
 *
6
 * (c) Jitendra Adhikari <[email protected]>
7
 *     <https://github.com/adhocore>
8
 *
9
 * Licensed under MIT license.
10
 */
11
12
namespace Ahc\Cli\IO;
13
14
use Ahc\Cli\Input\Reader;
15
use Ahc\Cli\Output\Writer;
16
17
/**
18
 * Cli Interactor.
19
 *
20
 * @author  Jitendra Adhikari <[email protected]>
21
 * @license MIT
22
 *
23
 * @link    https://github.com/adhocore/cli
24
 *
25
 * @method Writer bgBlack($text, $eol = false)
26
 * @method Writer bgBlue($text, $eol = false)
27
 * @method Writer bgCyan($text, $eol = false)
28
 * @method Writer bgGreen($text, $eol = false)
29
 * @method Writer bgPurple($text, $eol = false)
30
 * @method Writer bgRed($text, $eol = false)
31
 * @method Writer bgWhite($text, $eol = false)
32
 * @method Writer bgYellow($text, $eol = false)
33
 * @method Writer black($text, $eol = false)
34
 * @method Writer blackBgBlue($text, $eol = false)
35
 * @method Writer blackBgCyan($text, $eol = false)
36
 * @method Writer blackBgGreen($text, $eol = false)
37
 * @method Writer blackBgPurple($text, $eol = false)
38
 * @method Writer blackBgRed($text, $eol = false)
39
 * @method Writer blackBgWhite($text, $eol = false)
40
 * @method Writer blackBgYellow($text, $eol = false)
41
 * @method Writer blue($text, $eol = false)
42
 * @method Writer blueBgBlack($text, $eol = false)
43
 * @method Writer blueBgCyan($text, $eol = false)
44
 * @method Writer blueBgGreen($text, $eol = false)
45
 * @method Writer blueBgPurple($text, $eol = false)
46
 * @method Writer blueBgRed($text, $eol = false)
47
 * @method Writer blueBgWhite($text, $eol = false)
48
 * @method Writer blueBgYellow($text, $eol = false)
49
 * @method Writer bold($text, $eol = false)
50
 * @method Writer boldBlack($text, $eol = false)
51
 * @method Writer boldBlackBgBlue($text, $eol = false)
52
 * @method Writer boldBlackBgCyan($text, $eol = false)
53
 * @method Writer boldBlackBgGreen($text, $eol = false)
54
 * @method Writer boldBlackBgPurple($text, $eol = false)
55
 * @method Writer boldBlackBgRed($text, $eol = false)
56
 * @method Writer boldBlackBgWhite($text, $eol = false)
57
 * @method Writer boldBlackBgYellow($text, $eol = false)
58
 * @method Writer boldBlue($text, $eol = false)
59
 * @method Writer boldBlueBgBlack($text, $eol = false)
60
 * @method Writer boldBlueBgCyan($text, $eol = false)
61
 * @method Writer boldBlueBgGreen($text, $eol = false)
62
 * @method Writer boldBlueBgPurple($text, $eol = false)
63
 * @method Writer boldBlueBgRed($text, $eol = false)
64
 * @method Writer boldBlueBgWhite($text, $eol = false)
65
 * @method Writer boldBlueBgYellow($text, $eol = false)
66
 * @method Writer boldCyan($text, $eol = false)
67
 * @method Writer boldCyanBgBlack($text, $eol = false)
68
 * @method Writer boldCyanBgBlue($text, $eol = false)
69
 * @method Writer boldCyanBgGreen($text, $eol = false)
70
 * @method Writer boldCyanBgPurple($text, $eol = false)
71
 * @method Writer boldCyanBgRed($text, $eol = false)
72
 * @method Writer boldCyanBgWhite($text, $eol = false)
73
 * @method Writer boldCyanBgYellow($text, $eol = false)
74
 * @method Writer boldGreen($text, $eol = false)
75
 * @method Writer boldGreenBgBlack($text, $eol = false)
76
 * @method Writer boldGreenBgBlue($text, $eol = false)
77
 * @method Writer boldGreenBgCyan($text, $eol = false)
78
 * @method Writer boldGreenBgPurple($text, $eol = false)
79
 * @method Writer boldGreenBgRed($text, $eol = false)
80
 * @method Writer boldGreenBgWhite($text, $eol = false)
81
 * @method Writer boldGreenBgYellow($text, $eol = false)
82
 * @method Writer boldPurple($text, $eol = false)
83
 * @method Writer boldPurpleBgBlack($text, $eol = false)
84
 * @method Writer boldPurpleBgBlue($text, $eol = false)
85
 * @method Writer boldPurpleBgCyan($text, $eol = false)
86
 * @method Writer boldPurpleBgGreen($text, $eol = false)
87
 * @method Writer boldPurpleBgRed($text, $eol = false)
88
 * @method Writer boldPurpleBgWhite($text, $eol = false)
89
 * @method Writer boldPurpleBgYellow($text, $eol = false)
90
 * @method Writer boldRed($text, $eol = false)
91
 * @method Writer boldRedBgBlack($text, $eol = false)
92
 * @method Writer boldRedBgBlue($text, $eol = false)
93
 * @method Writer boldRedBgCyan($text, $eol = false)
94
 * @method Writer boldRedBgGreen($text, $eol = false)
95
 * @method Writer boldRedBgPurple($text, $eol = false)
96
 * @method Writer boldRedBgWhite($text, $eol = false)
97
 * @method Writer boldRedBgYellow($text, $eol = false)
98
 * @method Writer boldWhite($text, $eol = false)
99
 * @method Writer boldWhiteBgBlack($text, $eol = false)
100
 * @method Writer boldWhiteBgBlue($text, $eol = false)
101
 * @method Writer boldWhiteBgCyan($text, $eol = false)
102
 * @method Writer boldWhiteBgGreen($text, $eol = false)
103
 * @method Writer boldWhiteBgPurple($text, $eol = false)
104
 * @method Writer boldWhiteBgRed($text, $eol = false)
105
 * @method Writer boldWhiteBgYellow($text, $eol = false)
106
 * @method Writer boldYellow($text, $eol = false)
107
 * @method Writer boldYellowBgBlack($text, $eol = false)
108
 * @method Writer boldYellowBgBlue($text, $eol = false)
109
 * @method Writer boldYellowBgCyan($text, $eol = false)
110
 * @method Writer boldYellowBgGreen($text, $eol = false)
111
 * @method Writer boldYellowBgPurple($text, $eol = false)
112
 * @method Writer boldYellowBgRed($text, $eol = false)
113
 * @method Writer boldYellowBgWhite($text, $eol = false)
114
 * @method Writer colors($text)
115
 * @method Writer comment($text, $eol = false)
116
 * @method Writer cyan($text, $eol = false)
117
 * @method Writer cyanBgBlack($text, $eol = false)
118
 * @method Writer cyanBgBlue($text, $eol = false)
119
 * @method Writer cyanBgGreen($text, $eol = false)
120
 * @method Writer cyanBgPurple($text, $eol = false)
121
 * @method Writer cyanBgRed($text, $eol = false)
122
 * @method Writer cyanBgWhite($text, $eol = false)
123
 * @method Writer cyanBgYellow($text, $eol = false)
124
 * @method Writer eol(int $n = 1)
125
 * @method Writer error($text, $eol = false)
126
 * @method Writer green($text, $eol = false)
127
 * @method Writer greenBgBlack($text, $eol = false)
128
 * @method Writer greenBgBlue($text, $eol = false)
129
 * @method Writer greenBgCyan($text, $eol = false)
130
 * @method Writer greenBgPurple($text, $eol = false)
131
 * @method Writer greenBgRed($text, $eol = false)
132
 * @method Writer greenBgWhite($text, $eol = false)
133
 * @method Writer greenBgYellow($text, $eol = false)
134
 * @method Writer info($text, $eol = false)
135
 * @method Writer ok($text, $eol = false)
136
 * @method Writer purple($text, $eol = false)
137
 * @method Writer purpleBgBlack($text, $eol = false)
138
 * @method Writer purpleBgBlue($text, $eol = false)
139
 * @method Writer purpleBgCyan($text, $eol = false)
140
 * @method Writer purpleBgGreen($text, $eol = false)
141
 * @method Writer purpleBgRed($text, $eol = false)
142
 * @method Writer purpleBgWhite($text, $eol = false)
143
 * @method Writer purpleBgYellow($text, $eol = false)
144
 * @method Writer red($text, $eol = false)
145
 * @method Writer redBgBlack($text, $eol = false)
146
 * @method Writer redBgBlue($text, $eol = false)
147
 * @method Writer redBgCyan($text, $eol = false)
148
 * @method Writer redBgGreen($text, $eol = false)
149
 * @method Writer redBgPurple($text, $eol = false)
150
 * @method Writer redBgWhite($text, $eol = false)
151
 * @method Writer redBgYellow($text, $eol = false)
152
 * @method Writer table(array $rows, array $styles = [])
153
 * @method Writer warn($text, $eol = false)
154
 * @method Writer white($text, $eol = false)
155
 * @method Writer yellow($text, $eol = false)
156
 * @method Writer yellowBgBlack($text, $eol = false)
157
 * @method Writer yellowBgBlue($text, $eol = false)
158
 * @method Writer yellowBgCyan($text, $eol = false)
159
 * @method Writer yellowBgGreen($text, $eol = false)
160
 * @method Writer yellowBgPurple($text, $eol = false)
161
 * @method Writer yellowBgRed($text, $eol = false)
162
 * @method Writer yellowBgWhite($text, $eol = false)
163
 */
164
class Interactor
165
{
166
    protected $reader;
167
    protected $writer;
168
169
    /**
170
     * Constructor.
171
     *
172
     * @param string|null $input  Input stream path.
173
     * @param string|null $output Output steam path.
174
     */
175
    public function __construct(string $input = null, string $output = null)
176
    {
177
        $this->reader = new Reader($input);
178
        $this->writer = new Writer($output);
179
    }
180
181
    /**
182
     * Get reader.
183
     *
184
     * @return Reader
185
     */
186
    public function reader(): Reader
187
    {
188
        return $this->reader;
189
    }
190
191
    /**
192
     * Get writer.
193
     *
194
     * @return Writer
195
     */
196
    public function writer(): Writer
197
    {
198
        return $this->writer;
199
    }
200
201
    /**
202
     * Confirms if user agrees to prompt as indicated by given text.
203
     *
204
     * @param string $text    Eg: `Are you sure?`
205
     * @param string $default One of `y|n`
206
     *
207
     * @return bool
208
     */
209
    public function confirm(string $text, string $default = 'y'): bool
210
    {
211
        $choice = $this->choice($text, ['y', 'n'], $default, false);
212
213
        return \strtolower($choice[0] ?? $default) === 'y';
214
    }
215
216
    /**
217
     * Let user make a choice out of available choices.
218
     *
219
     * @param string $text    Prompt text.
220
     * @param array  $choices Possible choices for user.
221
     * @param mixed  $default Default value- if not chosen or invalid.
222
     * @param bool   $case    If user input should be case sensitive.
223
     *
224
     * @return mixed User input or default.
225
     */
226
    public function choice(string $text, array $choices, $default = null, bool $case = false)
227
    {
228
        $this->writer->yellow($text);
229
230
        $this->listOptions($choices, $default, false);
231
232
        $choice = $this->reader->read($default);
233
234
        return $this->isValidChoice($choice, $choices, $case) ? $choice : $default;
235
    }
236
237
    /**
238
     * Let user make multiple choices out of available choices.
239
     *
240
     * @param string $text    Prompt text.
241
     * @param array  $choices Possible choices for user.
242
     * @param mixed  $default Default value- if not chosen or invalid.
243
     * @param bool   $case    If user input should be case sensitive.
244
     *
245
     * @return mixed User input or default.
246
     */
247
    public function choices(string $text, array $choices, $default = null, bool $case = false)
248
    {
249
        $this->writer->yellow($text);
250
251
        $this->listOptions($choices, $default, true);
252
253
        $choice = $this->reader->read($default);
254
255
        if (\is_string($choice)) {
256
            $choice = \explode(',', \str_replace(' ', '', $choice));
257
        }
258
259
        $valid = [];
260
261
        foreach ($choice as $option) {
262
            if ($this->isValidChoice($option, $choices, $case)) {
263
                $valid[] = $option;
264
            }
265
        }
266
267
        return $valid ?: (array) $default;
268
    }
269
270
    /**
271
     * Prompt user for free input.
272
     *
273
     * @param string        $text    Prompt text.
274
     * @param mixed         $default
275
     * @param callable|null $fn      The sanitizer/validator for user input
276
     *                               Any exception message is printed and prompted again.
277
     * @param int           $retry   How many more times to retry on failure.
278
     *
279
     * @return mixed
280
     */
281
    public function prompt(string $text, $default = null, callable $fn = null, int $retry = 3)
282
    {
283
        $error  = 'Invalid value. Please try again!';
284
        $hidden = \func_get_args()[4] ?? false;
285
        $readFn = ['read', 'readHidden'][(int) $hidden];
286
287
        $this->writer->yellow($text)->comment(null !== $default ? " [$default]: " : ': ');
288
289
        try {
290
            $input = $this->reader->{$readFn}($default, $fn);
291
        } catch (\Throwable $e) {
292
            $input = '';
293
            $error = $e->getMessage();
294
        }
295
296
        if ($retry > 0 && $input === '') {
297
            $this->writer->bgRed($error, true);
298
299
            return $this->prompt($text, $default, $fn, $retry - 1, $hidden);
300
        }
301
302
        return $input ?? $default;
303
    }
304
305
    /**
306
     * Prompt user for secret input like password. Currently for unix only.
307
     *
308
     * @param string        $text  Prompt text.
309
     * @param callable|null $fn    The sanitizer/validator for user input
310
     *                             Any exception message is printed as error.
311
     * @param int           $retry How many more times to retry on failure.
312
     *
313
     * @return mixed
314
     */
315
    public function promptHidden(string $text, callable $fn = null, int $retry = 3)
316
    {
317
        return $this->prompt($text, null, $fn, $retry, true);
318
    }
319
320
    /**
321
     * Show choices list.
322
     *
323
     * @param array $choices Available choices.
324
     * @param mixed $default
325
     * @param bool  $multi   Indicates multiple choices.
326
     *
327
     * @return self
328
     */
329
    protected function listOptions(array $choices, $default = null, bool $multi = false): self
330
    {
331
        if (!$this->isAssocChoice($choices)) {
332
            return $this->promptOptions($choices, $default);
333
        }
334
335
        $maxLen = \max(\array_map('strlen', \array_keys($choices)));
336
337
        foreach ($choices as $choice => $desc) {
338
            $this->writer->eol()->cyan(\str_pad("  [$choice]", $maxLen + 6))->comment($desc);
339
        }
340
341
        $label = $multi ? 'Choices (comma separated)' : 'Choice';
342
343
        $this->writer->eol()->yellow($label);
344
345
        return $this->promptOptions(\array_keys($choices), $default);
346
    }
347
348
    /**
349
     * Show prompt with possible options.
350
     *
351
     * @param array $choices
352
     * @param mixed $default
353
     *
354
     * @return self
355
     */
356
    protected function promptOptions(array $choices, $default): self
357
    {
358
        $options = '';
359
360
        foreach ($choices as $choice) {
361
            $style    = \in_array($choice, (array) $default) ? 'boldCyan' : 'cyan';
362
            $options .= "/<$style>$choice</end>";
363
        }
364
365
        $options = \ltrim($options, '/');
366
367
        $this->writer->colors(" ($options): ");
368
369
        return $this;
370
    }
371
372
    /**
373
     * Check if user choice is one of possible choices.
374
     *
375
     * @param string $choice  User choice.
376
     * @param array  $choices Possible choices.
377
     * @param bool   $case    If input is case sensitive.
378
     *
379
     * @return bool
380
     */
381
    protected function isValidChoice($choice, array $choices, bool $case)
382
    {
383
        if ($this->isAssocChoice($choices)) {
384
            $choices = \array_keys($choices);
385
        }
386
387
        $fn = ['\strcasecmp', '\strcmp'][(int) $case];
388
389
        foreach ($choices as $option) {
390
            if ($fn($choice, $option) == 0) {
391
                return true;
392
            }
393
        }
394
395
        return false;
396
    }
397
398
    /**
399
     * Check if the choices array is associative.
400
     *
401
     * @param array $array Choices
402
     *
403
     * @return bool
404
     */
405
    protected function isAssocChoice(array $array)
406
    {
407
        return !empty($array) && \array_keys($array) != \range(0, \count($array) - 1);
408
    }
409
410
    /**
411
     * Channel method calls to reader/writer.
412
     *
413
     * @param string $method
414
     * @param array  $arguments
415
     *
416
     * @return mixed
417
     */
418
    public function __call(string $method, array $arguments)
419
    {
420
        if (\method_exists($this->reader, $method)) {
421
            return $this->reader->{$method}(...$arguments);
422
        }
423
424
        return $this->writer->{$method}(...$arguments);
425
    }
426
}
427