Interactor::prompt()   A
last analyzed

Complexity

Conditions 6
Paths 8

Size

Total Lines 32
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 6
eloc 16
c 2
b 0
f 0
nc 8
nop 5
dl 0
loc 32
rs 9.1111
1
<?php
2
3
/**
4
 * Platine Console
5
 *
6
 * Platine Console is a powerful library with support of custom
7
 * style to build command line interface applications
8
 *
9
 * This content is released under the MIT License (MIT)
10
 *
11
 * Copyright (c) 2020 Platine Console
12
 * Copyright (c) 2017-2020 Jitendra Adhikari
13
 *
14
 * Permission is hereby granted, free of charge, to any person obtaining a copy
15
 * of this software and associated documentation files (the "Software"), to deal
16
 * in the Software without restriction, including without limitation the rights
17
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
18
 * copies of the Software, and to permit persons to whom the Software is
19
 * furnished to do so, subject to the following conditions:
20
 *
21
 * The above copyright notice and this permission notice shall be included in all
22
 * copies or substantial portions of the Software.
23
 *
24
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
25
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
26
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
27
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
28
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
29
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
30
 * SOFTWARE.
31
 */
32
33
/**
34
 *  @file Interactor.php
35
 *
36
 *  The Input/Output interaction class
37
 *
38
 *  @package    Platine\Console\IO
39
 *  @author Platine Developers Team
40
 *  @copyright  Copyright (c) 2020
41
 *  @license    http://opensource.org/licenses/MIT  MIT License
42
 *  @link   https://www.platine-php.com
43
 *  @version 1.0.0
44
 *  @filesource
45
 */
46
47
declare(strict_types=1);
48
49
namespace Platine\Console\IO;
50
51
use Platine\Console\Input\Reader;
52
use Platine\Console\Output\Writer;
53
use Platine\Console\Util\Helper;
54
use Throwable;
55
56
/**
57
 * @class Interactor
58
 * @package Platine\Console\IO
59
 */
60
class Interactor
61
{
62
    /**
63
     * Stream reader instance
64
     * @var Reader
65
     */
66
    protected Reader $reader;
67
68
    /**
69
     * Stream writer instance
70
     * @var Writer
71
     */
72
    protected Writer $writer;
73
74
    /**
75
     * Create new instance
76
     * @param string|null $input
77
     * @param string|null $output
78
     */
79
    public function __construct(?string $input = null, ?string $output = null)
80
    {
81
        $this->reader = new Reader($input);
82
        $this->writer = new Writer($output);
83
    }
84
85
    /**
86
     * Return the reader instance
87
     * @return Reader
88
     */
89
    public function reader(): Reader
90
    {
91
        return $this->reader;
92
    }
93
94
    /**
95
     * Return the writer instance
96
     * @return Writer
97
     */
98
    public function writer(): Writer
99
    {
100
        return $this->writer;
101
    }
102
103
    /**
104
     * Set the reader
105
     * @param Reader $reader
106
     * @return $this
107
     */
108
    public function setReader(Reader $reader): self
109
    {
110
        $this->reader = $reader;
111
        return $this;
112
    }
113
114
    /**
115
     * Set the writer
116
     * @param Writer $writer
117
     * @return $this
118
     */
119
    public function setWriter(Writer $writer): self
120
    {
121
        $this->writer = $writer;
122
        return $this;
123
    }
124
125
126
    /**
127
     * Confirms if user agrees to prompt as indicated by given text.
128
     * @param string $text
129
     * @param string $default
130
     * @return bool
131
     */
132
    public function confirm(string $text, string $default = 'y'): bool
133
    {
134
        $choice = $this->choice($text, ['y', 'n'], $default, false);
135
136
        return strtolower(isset($choice[0]) ? $choice[0] : $default) === 'y';
137
    }
138
139
    /**
140
     * Let user make a choice out of available choices.
141
     * @param string $text
142
     * @param array<int|string, string> $choices
143
     * @param mixed $default
144
     * @param bool $case
145
     *
146
     * @return mixed
147
     */
148
    public function choice(
149
        string $text,
150
        array $choices,
151
        mixed $default = null,
152
        bool $case = false
153
    ): mixed {
154
        $this->writer->yellow($text);
155
156
        $this->listOptions($choices, $default, false);
157
158
        $choice = $this->reader->read($default);
159
160
        return $this->isValidChoice($choice, $choices, $case)
161
                ? $choice
162
                : $default;
163
    }
164
165
    /**
166
     * Let user make multiple choice out of available choices.
167
     * @param string $text
168
     * @param array<int|string, string> $choices
169
     * @param mixed $default
170
     * @param bool $case
171
     *
172
     * @return mixed
173
     */
174
    public function choices(
175
        string $text,
176
        array $choices,
177
        mixed $default = null,
178
        bool $case = false
179
    ): mixed {
180
        $this->writer->yellow($text);
181
182
        $this->listOptions($choices, $default, true);
183
184
        $choice = $this->reader->read($default);
185
        $values = [];
186
        if (is_string($choice)) {
187
            $values = explode(',', str_replace(' ', '', $choice));
188
        }
189
190
        $valid = [];
191
192
        foreach ($values as $option) {
193
            if ($this->isValidChoice($option, $choices, $case)) {
194
                $valid[] = $option;
195
            }
196
        }
197
198
        return !empty($valid) ? $valid : (array) $default;
199
    }
200
201
    /**
202
     * Prompt user for free input.
203
     * @param string $text
204
     * @param mixed $default
205
     * @param callable|null $callback
206
     * @param bool $required
207
     * @param bool $hidden
208
     * @return mixed
209
     */
210
    public function prompt(
211
        string $text,
212
        mixed $default = null,
213
        ?callable $callback = null,
214
        bool $required = true,
215
        bool $hidden = false
216
    ): mixed {
217
        $error = 'Invalid value, please try again';
218
        $readFunct = ['read', 'readHidden'][(int) $hidden];
219
220
        $this->writer->yellow($text);
221
222
        $this->writer->dim(
223
            $default !== null
224
                            ? ' [' . $default . ']: '
225
                            : ': '
226
        );
227
228
        try {
229
            $input = $this->reader->{$readFunct}($default, $callback);
230
        } catch (Throwable $ex) {
231
            $input = '';
232
            $error = $ex->getMessage();
233
        }
234
235
        while ($required && $input === '') {
236
            $this->writer->bgRed($error, true);
237
238
            $input = $this->prompt($text, $default, $callback, $required, $hidden);
239
        }
240
241
        return $input ? $input : $default;
242
    }
243
244
    /**
245
     * Prompt user for secret input like password.
246
     * Currently for Unix only.
247
     * @param string $text
248
     * @param callable|null $callback
249
     * @param bool $required
250
     * @return mixed
251
     */
252
    public function promptHidden(
253
        string $text,
254
        ?callable $callback = null,
255
        bool $required = true
256
    ): mixed {
257
        return $this->prompt($text, null, $callback, $required, true);
258
    }
259
260
    /**
261
     * Show choices list.
262
     * @param array<int|string, string> $choices
263
     * @param mixed $default
264
     * @param bool $isMutliple
265
     * @return $this
266
     */
267
    protected function listOptions(
268
        array $choices,
269
        mixed $default = null,
270
        bool $isMutliple = false
271
    ): self {
272
        if (!Helper::isAssocArray($choices)) {
273
            return $this->promptOptions($choices, $default);
274
        }
275
276
        $maxLength = 0;
277
        $results = array_map(fn($a) => strlen((string) $a), array_keys($choices));
278
        if (count($results) > 0) {
279
            $maxLength = max($results);
280
        }
281
282
        foreach ($choices as $choice => $desc) {
283
            $this->writer->eol()
284
                         ->cyan(
285
                             str_pad(' [' . $choice . ']', $maxLength + 6)
286
                         );
287
288
            $this->writer->dim($desc);
289
        }
290
291
        $label = $isMutliple ? 'Choices (comma separated)' : 'Choice';
292
293
        $this->writer->eol()->yellow($label);
294
295
        /** @var array<string> $keys */
296
        $keys = array_keys($choices);
297
298
        return $this->promptOptions($keys, $default);
299
    }
300
301
    /**
302
     * Show prompt with possible options.
303
     * @param array<int|string, string> $choices
304
     * @param mixed $default
305
     * @return $this
306
     */
307
    protected function promptOptions(array $choices, mixed $default): self
308
    {
309
        $options = '';
310
311
        foreach ($choices as $choice) {
312
            $style = in_array($choice, (array) $default) ? 'info' : 'ok';
313
            $options .= '/<' . $style . '>' . $choice . '</end>';
314
        }
315
316
        $finalOptions = ltrim($options, '/');
317
318
        $this->writer()->colors(' (' . $finalOptions . '): ');
319
320
        return $this;
321
    }
322
323
    /**
324
     * Check if user choice is one of possible choices.
325
     * @param string $choice
326
     * @param array<int|string, string> $choices
327
     * @param bool $case
328
     * @return bool
329
     */
330
    protected function isValidChoice(string $choice, array $choices, bool $case): bool
331
    {
332
        if (Helper::isAssocArray($choices)) {
333
            /** @var array<string> $choices */
334
            $choices = array_keys($choices);
335
        }
336
337
        $func = ['strcasecmp', 'strcmp'][(int) $case];
338
        foreach ($choices as $option) {
339
            //Don't use === here
340
            if ($func($choice, (string) $option) == 0) {
341
                return true;
342
            }
343
        }
344
345
        return false;
346
    }
347
}
348