Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
94.00% covered (success)
94.00%
47 / 50
60.00% covered (warning)
60.00%
3 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
Prompt
94.00% covered (success)
94.00%
47 / 50
60.00% covered (warning)
60.00%
3 / 5
25.14
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 prompt
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
10
 promptMulti
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
7
 confirm
66.67% covered (warning)
66.67%
4 / 6
0.00% covered (danger)
0.00%
0 / 1
4.59
 getPromptInput
83.33% covered (success)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
3.04
1<?php
2declare(strict_types=1);
3/**
4 * Pop PHP Framework (https://www.popphp.org/)
5 *
6 * @link       https://github.com/popphp/popphp-framework
7 * @author     Nick Sagona, III <nick@popphp.org>
8 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
9 * @license    https://www.popphp.org/license     New BSD License
10 */
11
12/**
13 * @namespace
14 */
15namespace Pop\Console;
16
17/**
18 * Console prompt class
19 *
20 * @category   Pop
21 * @package    Pop\Console
22 * @author     Nick Sagona, III <nick@popphp.org>
23 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
24 * @license    https://www.popphp.org/license     New BSD License
25 * @version    5.0.0
26 */
27class Prompt
28{
29
30    /**
31     * Instantiate the prompt object
32     *
33     * @param string  $indent
34     * @param ?string $formattedHeader
35     * @param mixed   $inputStream
36     */
37    public function __construct(
38        protected string $indent = '', protected ?string $formattedHeader = null, protected mixed $inputStream = null
39    ) { }
40
41    /**
42     * Get input from the prompt
43     *
44     * @param  string $prompt
45     * @param  ?array $options
46     * @param  bool   $caseSensitive
47     * @param  int    $length
48     * @return string
49     */
50    public function prompt(string $prompt, ?array $options = null, bool $caseSensitive = false, int $length = 500): string
51    {
52        if ($this->formattedHeader !== null) {
53            echo $this->formattedHeader . $this->indent . $prompt;
54        } else {
55            echo $this->indent . $prompt;
56        }
57
58        $input = null;
59
60        if ($options !== null) {
61            $length = 0;
62            foreach ($options as $key => $value) {
63                $options[$key] = ($caseSensitive) ? $value : strtolower((string)$value);
64                if (strlen((string)$value) > $length) {
65                    $length = strlen((string)$value);
66                }
67            }
68
69            while (!in_array($input, $options)) {
70                if ($input !== null) {
71                    echo $this->indent . $prompt;
72                }
73                $input = $this->getPromptInput($prompt, $length, $caseSensitive);
74
75                // Empty input is what a closed/exhausted input stream produces on every
76                // read, so treat it the same as promptMulti() does: stop retrying and
77                // return it rather than spin forever re-reading a stream that can't
78                // ever satisfy $options.
79                if ($input === '') {
80                    return $input;
81                }
82            }
83        } else {
84            while ($input === null) {
85                $input = $this->getPromptInput($prompt, $length, $caseSensitive);
86            }
87        }
88
89        return $input;
90    }
91
92    /**
93     * Display a prompt that accepts one or more comma-separated selections
94     *
95     * @param  string $prompt
96     * @param  array  $options
97     * @param  bool   $caseSensitive
98     * @param  int    $length
99     * @return array
100     */
101    public function promptMulti(string $prompt, array $options, bool $caseSensitive = false, int $length = 500): array
102    {
103        foreach ($options as $key => $value) {
104            $options[$key] = ($caseSensitive) ? (string)$value : strtolower((string)$value);
105        }
106
107        if ($this->formattedHeader !== null) {
108            echo $this->formattedHeader . $this->indent . $prompt;
109        } else {
110            echo $this->indent . $prompt;
111        }
112
113        $selected = null;
114
115        while ($selected === null) {
116            $input  = $this->getPromptInput($prompt, $length, $caseSensitive);
117            $tokens = array_values(array_filter(
118                array_map('trim', explode(',', $input)),
119                fn($token) => $token !== ''
120            ));
121
122            // Empty input is a valid "no selection" answer, and is also what a
123            // closed input stream produces. Returning here is what keeps EOF
124            // from spinning the retry loop forever.
125            if (empty($tokens)) {
126                return [];
127            }
128
129            if (empty(array_diff($tokens, $options))) {
130                $selected = array_values(array_unique($tokens));
131            } else {
132                echo $this->indent . $prompt;
133            }
134        }
135
136        return $selected;
137    }
138
139    /**
140     * Display confirm message prompt
141     *
142     * @param  string $message
143     * @param  array  $options
144     * @param  bool   $caseSensitive
145     * @param  int    $length
146     * @param  bool   $exit
147     * @return string
148     */
149    public function confirm(
150        string $message = 'Are you sure?', array $options = ['Y', 'N'], bool $caseSensitive = false,
151        int $length = 500, bool $exit = true
152    ): string
153    {
154        $message .= ' [' . implode('/', $options) . '] ';
155        $response = $this->prompt($message, $options, $caseSensitive, $length);
156
157        if (($exit) && ((strtolower($response) == 'n') || (strtolower($response) == 'no'))) {
158            echo PHP_EOL;
159            exit(127);
160        }
161
162        return $response;
163    }
164
165    /**
166     * Get prompt input
167     *
168     * @param  string $prompt
169     * @param  int    $length
170     * @param  bool   $caseSensitive
171     * @return string
172     */
173    protected function getPromptInput(string $prompt, int $length = 500, bool $caseSensitive = false): string
174    {
175        $stream = $this->inputStream ?? fopen('php://stdin', 'r');
176        $input  = fgets($stream, strlen((string)$prompt) + $length);
177        $input  = ($caseSensitive) ? rtrim((string)$input) : strtolower(rtrim((string)$input));
178
179        if ($this->inputStream === null) {
180            fclose($stream);
181        }
182
183        return $input;
184    }
185
186}