Passed
Push — develop ( 424f0e...acec73 )
by nguereza
02:07
created

Interactor::confirm()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 2
nc 1
nop 2
dl 0
loc 5
rs 10
c 0
b 0
f 0
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   http://www.iacademy.cf
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\Color;
53
use Platine\Console\Output\Writer;
54
use Platine\Console\Util\Helper;
55
use Throwable;
56
57
/**
58
 * Class Interactor
59
 * @package Platine\Console\IO
60
 */
61
class Interactor
62
{
0 ignored issues
show
Coding Style introduced by
Opening brace must not be followed by a blank line
Loading history...
63
64
    /**
65
     * Stream reader instance
66
     * @var Reader
67
     */
68
    protected Reader $reader;
69
70
    /**
71
     * Stream writer instance
72
     * @var Writer
73
     */
74
    protected Writer $writer;
75
76
    /**
77
     * Create new instance
78
     * @param string|null $input
79
     * @param string|null $output
80
     */
81
    public function __construct(?string $input = null, ?string $output = null)
82
    {
83
        $this->reader = new Reader($input);
84
        $this->writer = new Writer($output);
85
    }
86
87
    /**
88
     * Return the reader instance
89
     * @return Reader
90
     */
91
    public function reader(): Reader
92
    {
93
        return $this->reader;
94
    }
95
96
    /**
97
     * Return the writer instance
98
     * @return Writer
99
     */
100
    public function writer(): Writer
101
    {
102
        return $this->writer;
103
    }
104
105
    /**
106
     * Confirms if user agrees to prompt as indicated by given text.
107
     * @param string $text
108
     * @param string $default
109
     * @return bool
110
     */
111
    public function confirm(string $text, string $default = 'y'): bool
112
    {
113
        $choice = $this->choice($text, ['y', 'n'], $default, false);
114
115
        return strtolower(isset($choice[0]) ? $choice[0] : $default) === 'y';
116
    }
117
118
    /**
119
     * Let user make a choice out of available choices.
120
     * @param string $text
121
     * @param array<int|string, string> $choices
122
     * @param mixed $default
123
     * @param bool $case
124
     *
125
     * @return mixed
126
     */
127
    public function choice(string $text, array $choices, $default = null, bool $case = false)
128
    {
129
        $this->writer->yellow($text);
130
131
        $this->listOptions($choices, $default, false);
132
133
        $choice = $this->reader->read($default);
134
135
        return $this->isValidChoice($choice, $choices, $case)
136
                ? $choice
137
                : $default;
138
    }
139
140
    /**
141
     * Let user make multiple choice out of available choices.
142
     * @param string $text
143
     * @param array<int|string, string> $choices
144
     * @param mixed $default
145
     * @param bool $case
146
     *
147
     * @return mixed
148
     */
149
    public function choices(string $text, array $choices, $default = null, bool $case = false)
150
    {
151
        $this->writer->yellow($text);
152
153
        $this->listOptions($choices, $default, true);
154
155
        $choice = $this->reader->read($default);
156
        $values = [];
157
        if (is_string($choice)) {
158
            $values = explode(',', str_replace(' ', '', $choice));
159
        }
160
161
        $valid = [];
162
163
        foreach ($values as $option) {
164
            if ($this->isValidChoice($option, $choices, $case)) {
165
                $valid[] = $option;
166
            }
167
        }
168
169
        return !empty($valid) ? $valid : (array) $default;
170
    }
171
172
    /**
173
     * Prompt user for free input.
174
     * @param string $text
175
     * @param mixed $default
176
     * @param callable|null $callback
177
     * @param bool $required
178
     * @param bool $hidden
179
     * @return mixed
180
     */
181
    public function prompt(
182
        string $text,
183
        $default = null,
184
        ?callable $callback = null,
185
        bool $required = true,
186
        bool $hidden = false
187
    ) {
188
        $error = 'Invalid value, please try again';
189
        $readFunct = ['read', 'readHidden'][(int) $hidden];
190
191
        $this->writer->yellow($text);
192
193
        $this->writer->dim(
194
            $default !== null
195
                            ? ' [' . $default . ']: '
196
                            : ': '
197
        );
198
199
        try {
200
            $input = $this->reader->{$readFunct}($default, $callback);
201
        } catch (Throwable $ex) {
202
            $input = '';
203
            $error = $ex->getMessage();
204
        }
205
206
        while ($required && $input === '') {
207
            $this->writer->bgRed($error, true);
208
209
            $input = $this->prompt($text, $default, $callback, $required, $hidden);
210
        }
211
212
        return $input ? $input : $default;
213
    }
214
215
    /**
216
     * Prompt user for secret input like password.
217
     * Currently for Unix only.
218
     * @param string $text
219
     * @param callable|null $callback
220
     * @param bool $required
221
     * @return mixed
222
     */
223
    public function promptHidden(
224
        string $text,
225
        ?callable $callback = null,
226
        bool $required = true
227
    ) {
228
        return $this->prompt($text, null, $callback, $required, true);
229
    }
230
231
    /**
232
     * Show choices list.
233
     * @param array<int|string, string> $choices
234
     * @param mixed $default
235
     * @param bool $isMutliple
236
     * @return $this
237
     */
238
    protected function listOptions(
239
        array $choices,
240
        $default = null,
241
        bool $isMutliple = false
242
    ): self {
243
        if (!Helper::isAssocArray($choices)) {
244
            return $this->promptOptions($choices, $default);
245
        }
246
247
        $maxLength = max(array_map('strlen', array_keys($choices)));
248
249
        foreach ($choices as $choice => $desc) {
250
            $this->writer->eol()
251
                         ->cyan(
252
                             str_pad(' [' . $choice . ']', $maxLength + 6)
253
                         );
254
255
            $this->writer->dim($desc);
256
        }
257
258
        $label = $isMutliple ? 'Choices (comma separated)' : 'Choice';
259
260
        $this->writer->eol()->yellow($label);
261
262
        /** @var array<string> $keys */
263
        $keys = array_keys($choices);
264
265
        return $this->promptOptions($keys, $default);
266
    }
267
268
    /**
269
     * Show prompt with possible options.
270
     * @param array<int|string, string> $choices
271
     * @param mixed $default
272
     * @return $this
273
     */
274
    protected function promptOptions(array $choices, $default): self
275
    {
276
        $options = '';
277
278
        foreach ($choices as $choice) {
279
            $style = in_array($choice, (array) $default) ? 'info' : 'ok';
280
            $options .= '/<' . $style . '>' . $choice . '</end>';
281
        }
282
283
        $finalOptions = ltrim($options, '/');
284
285
        $this->writer()->colors(' (' . $finalOptions . '): ');
286
287
        return $this;
288
    }
289
290
    /**
291
     * Check if user choice is one of possible choices.
292
     * @param string $choice
293
     * @param array<int|string, string> $choices
294
     * @param bool $case
295
     * @return bool
296
     */
297
    protected function isValidChoice(string $choice, array $choices, bool $case): bool
298
    {
299
        if (Helper::isAssocArray($choices)) {
300
            /** @var array<string> $choices */
301
            $choices = array_keys($choices);
302
        }
303
304
        $func = ['strcasecmp', 'strcmp'][(int) $case];
305
        foreach ($choices as $option) {
306
            //Don't use === here
307
            if ($func($choice, (string) $option) == 0) {
308
                return true;
309
            }
310
        }
311
312
        return false;
313
    }
314
}
315