Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions system/CLI/Console.php
Original file line number Diff line number Diff line change
Expand Up @@ -285,11 +285,13 @@ private function chooseAlternative(array $alternatives): ?string
CLI::error(lang('CLI.commandNotFound', [$this->command]));
CLI::newLine();

$prompt = CLI::getPrompt();

if (count($alternatives) === 1) {
return CLI::prompt(lang('CLI.altCommandRun', [$alternatives[0]]), ['y', 'n']) === 'y' ? $alternatives[0] : null;
return $prompt->ask(lang('CLI.altCommandRun', [$alternatives[0]]), ['y', 'n']) === 'y' ? $alternatives[0] : null;
}

$chosen = (int) CLI::promptByKey(lang('CLI.altCommandSelect'), [...$alternatives, lang('CLI.altCommandNone')]);
$chosen = $prompt->select(lang('CLI.altCommandSelect'), [...$alternatives, lang('CLI.altCommandNone')]);

return $alternatives[$chosen] ?? null;
}
Expand Down
16 changes: 11 additions & 5 deletions system/CLI/Prompt.php
Original file line number Diff line number Diff line change
Expand Up @@ -166,8 +166,12 @@ public function askByMultipleKeys(string $text, array $options): array
/**
* Lets the user pick one option with the arrow keys and returns its key, asking for its number where keys cannot be read.
*
* @param array<array-key, string> $options
* @param int|string|null $default The key highlighted at the start, or `null` for the first one
* @template TKey of array-key
*
* @param array<TKey, string> $options
* @param TKey|null $default The key highlighted at the start, or `null` for the first one
*
* @return TKey
*
* @throws InvalidArgumentException
*/
Expand Down Expand Up @@ -209,10 +213,12 @@ public function select(string $question, array $options, int|string|null $defaul
/**
* Lets the user tick options with the arrow keys and the space bar, asking for their numbers where keys cannot be read.
*
* @param array<array-key, string> $options
* @param list<array-key> $default The keys ticked at the start
* @template TKey of array-key
*
* @param array<TKey, string> $options
* @param list<TKey> $default The keys ticked at the start
*
* @return array<array-key, string> The ticked options, in the order of `$options`
* @return array<TKey, string> The ticked options, in the order of `$options`
*
* @throws CLIException When a required answer has no default on an output that cannot ask
* @throws InvalidArgumentException
Expand Down
5 changes: 4 additions & 1 deletion system/Commands/Database/CreateDatabase.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,10 @@ protected function interact(array &$arguments, array &$options): void
$ext = $this->getUnboundOption('ext', $options);

if (is_string($ext) && ! in_array($ext, self::VALID_EXTENSIONS, true)) {
$options['ext'] = CLI::prompt('Please choose a valid file extension', self::VALID_EXTENSIONS, 'required');
$options['ext'] = $this->getPrompt()->select(
'Please choose a valid file extension',
array_combine(self::VALID_EXTENSIONS, self::VALID_EXTENSIONS),
);
}
}

Expand Down
11 changes: 2 additions & 9 deletions system/Commands/Database/ShowTableInfo.php
Original file line number Diff line number Diff line change
Expand Up @@ -108,15 +108,8 @@ protected function interact(array &$arguments, array &$options): void
return;
}

while (! in_array($arguments[0] ?? '', $tables, true)) {
$tableKey = CLI::promptByKey(
['Here is the list of your database tables:', 'Which table do you want to see?'],
$tables,
'required',
);
CLI::newLine();

$arguments[0] = $tables[$tableKey] ?? '';
if (! in_array($arguments[0] ?? '', $tables, true)) {
$arguments[0] = $tables[$this->getPrompt()->select('Which table do you want to see?', $tables)];
}
}

Expand Down
5 changes: 4 additions & 1 deletion system/Commands/Encryption/GenerateKey.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,10 @@ protected function interact(array &$arguments, array &$options): void
$prefix = $this->getUnboundOption('prefix', $options);

if (is_string($prefix) && ! in_array($prefix, self::VALID_PREFIXES, true)) {
$options['prefix'] = CLI::prompt('Please provide a valid prefix to use.', self::VALID_PREFIXES, 'required');
$options['prefix'] = $this->getPrompt()->select(
'Please provide a valid prefix to use.',
array_combine(self::VALID_PREFIXES, self::VALID_PREFIXES),
);
}

if ($this->hasUnboundOption('show', $options)) {
Expand Down
5 changes: 4 additions & 1 deletion system/Commands/Encryption/RotateKey.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,10 @@ protected function interact(array &$arguments, array &$options): void
$prefix = $this->getUnboundOption('prefix', $options);

if (is_string($prefix) && ! in_array($prefix, self::VALID_PREFIXES, true)) {
$options['prefix'] = CLI::prompt('Please provide a valid prefix to use.', self::VALID_PREFIXES, 'required');
$options['prefix'] = $this->getPrompt()->select(
'Please provide a valid prefix to use.',
array_combine(self::VALID_PREFIXES, self::VALID_PREFIXES),
);
}

if ($this->hasUnboundOption('force', $options)) {
Expand Down
5 changes: 4 additions & 1 deletion system/Commands/Generators/CommandGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,10 @@ protected function interact(array &$arguments, array &$options): void
return;
}

$options['type'] = CLI::prompt(lang('CLI.generator.commandType'), ['basic', 'generator'], 'required');
$options['type'] = $this->getPrompt()->select(
lang('CLI.generator.commandType'),
['basic' => 'basic', 'generator' => 'generator'],
);
}

protected function execute(array $arguments, array $options): int
Expand Down
5 changes: 4 additions & 1 deletion system/Commands/Generators/ControllerGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,10 @@ protected function interact(array &$arguments, array &$options): void
return;
}

$options['restful'] = CLI::prompt(lang('CLI.generator.parentClass'), ['controller', 'presenter'], 'required');
$options['restful'] = $this->getPrompt()->select(
lang('CLI.generator.parentClass'),
['controller' => 'controller', 'presenter' => 'presenter'],
);
}

protected function execute(array $arguments, array $options): int
Expand Down
5 changes: 4 additions & 1 deletion system/Commands/Generators/ModelGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,10 @@ protected function interact(array &$arguments, array &$options): void
return;
}

$options['return'] = CLI::prompt(lang('CLI.generator.returnType'), self::RETURN_TYPES, 'required');
$options['return'] = $this->getPrompt()->select(
lang('CLI.generator.returnType'),
array_combine(self::RETURN_TYPES, self::RETURN_TYPES),
);
}

protected function execute(array $arguments, array $options): int
Expand Down
26 changes: 12 additions & 14 deletions tests/system/CLI/ConsoleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ public function testUnknownCommandDeclinedSuggestionExitsWithError(): void
public function testUnknownCommandRunsSelectedSuggestion(): void
{
$this->initializeConsole('clear', '--no-header');
$io = $this->useInputs('0');
$io = $this->useInputs('1');

$console = new Console();
$exitCode = $console->run();
Expand All @@ -283,12 +283,11 @@ public function testUnknownCommandRunsSelectedSuggestion(): void
Command "clear" not found.

Select a command to run instead:
[0] cache:clear
[1] debugbar:clear
[2] logs:clear
[3] none of these

[0, 1, 2, 3]: 0
[1] cache:clear
[2] debugbar:clear
[3] logs:clear
[4] none of these
Number [1]: 1

Cache cleared using the "file" driver.

Expand All @@ -300,7 +299,7 @@ public function testUnknownCommandRunsSelectedSuggestion(): void
public function testUnknownCommandSelectingNoneExitsWithError(): void
{
$this->initializeConsole('clear', '--no-header');
$io = $this->useInputs('3');
$io = $this->useInputs('4');

$console = new Console();
$exitCode = $console->run();
Expand All @@ -313,12 +312,11 @@ public function testUnknownCommandSelectingNoneExitsWithError(): void
Command "clear" not found.

Select a command to run instead:
[0] cache:clear
[1] debugbar:clear
[2] logs:clear
[3] none of these

[0, 1, 2, 3]: 3
[1] cache:clear
[2] debugbar:clear
[3] logs:clear
[4] none of these
Number [1]: 4

EOT,
$this->getUndecoratedIoOutput($io),
Expand Down
8 changes: 6 additions & 2 deletions tests/system/Commands/CreateDatabaseTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -176,14 +176,18 @@ public function testSqlitePromptsForInvalidExtension(): void
}

$io = new MockInputOutput();
$io->setInputs(['db']);
$io->setInputs(['1']);
CLI::setInputOutput($io);

command('db:create database --ext txt');

$this->assertSame(
<<<'EOT'
Please choose a valid file extension [db, sqlite]: db

Please choose a valid file extension
[1] db
[2] sqlite
Number [1]: 1

Database "database.db" successfully created.

Expand Down
17 changes: 4 additions & 13 deletions tests/system/Commands/Database/ShowTableInfoMockIOTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,24 +63,15 @@ public function testDbTableWithInputs(): void
CLI::setInputOutput($io);

// User will input "a" (invalid value) and then select db_migrations.
$io->setInputs(['a', (string) $tableIndex]);
$io->setInputs(['a', (string) ($tableIndex + 1)]);

command('db:table');

$result = $io->getOutput();

$this->assertMatchesRegularExpression(
'/Which table do you want to see\? \[[\d,\s]+\]\: a/',
$result,
);
$this->assertMatchesRegularExpression(
'/The "Which table do you want to see\?" field must be one of: [\d,\s]+./',
$result,
);
$this->assertMatchesRegularExpression(
'/Which table do you want to see\? \[[\d,\s]+\]\: ' . $tableIndex . '/',
$result,
);
$this->assertStringContainsString('Which table do you want to see?', $result);
$this->assertMatchesRegularExpression('/Number \[\S+\]: a\n.*Please select correctly\./', $result);
$this->assertMatchesRegularExpression('/Number \[\S+\]: ' . ($tableIndex + 1) . '\n/', $result);
$this->assertMatchesRegularExpression(
'/Data of "db_migrations" table:/',
$result,
Expand Down
2 changes: 1 addition & 1 deletion tests/system/Commands/Encryption/GenerateKeyTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,7 @@ public function testKeyGenerateErrorsOnInvalidPrefixNonInteractively(): void
public function testKeyGeneratePromptsForInvalidPrefix(): void
{
$io = new MockInputOutput();
$io->setInputs(['hex2bin']);
$io->setInputs(['1']);
CLI::setInputOutput($io);

command('key:generate --prefix invalid --show');
Expand Down
12 changes: 10 additions & 2 deletions tests/system/Commands/Encryption/RotateKeyTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -332,13 +332,21 @@ public function testRotateInteractRePromptsForInvalidPrefix(): void

$io = new MockInputOutput();
// First input answers the invalid-prefix recovery prompt; second answers the rotate confirmation.
$io->setInputs(['base64', 'y']);
$io->setInputs(['2', 'y']);
CLI::setInputOutput($io);

command('key:rotate --prefix invalid');

$output = self::getUndecoratedIoOutput($io);
$this->assertStringContainsString('Please provide a valid prefix to use. [hex2bin, base64]: base64', $output);
$this->assertStringContainsString(
<<<'EOT'
Please provide a valid prefix to use.
[1] hex2bin
[2] base64
Number [1]: 2
EOT,
$output,
);
$this->assertStringContainsString('Encryption key rotated. 1 previous key retained for decryption fallback.', $output);

$contents = (string) file_get_contents($this->envPath);
Expand Down
2 changes: 1 addition & 1 deletion tests/system/Commands/Generators/CommandGeneratorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ public function testInvalidTypeIsRejectedWhenNotInteractive(): void
public function testInvalidTypePromptsWhenInteractive(): void
{
$io = new MockInputOutput();
$io->setInputs(['generator']);
$io->setInputs(['2']);
CLI::setInputOutput($io);

$command = new CommandGenerator(new Commands());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ public function testInvalidRestfulTypeIsRejectedWhenNotInteractive(): void
public function testInvalidRestfulTypePromptsWhenInteractive(): void
{
$io = new MockInputOutput();
$io->setInputs(['presenter']);
$io->setInputs(['2']);
CLI::setInputOutput($io);

$command = new ControllerGenerator(new Commands());
Expand Down
2 changes: 1 addition & 1 deletion tests/system/Commands/Generators/ModelGeneratorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ public function testInvalidReturnTypeIsRejectedWhenNotInteractive(): void
public function testInvalidReturnTypePromptsWhenInteractive(): void
{
$io = new MockInputOutput();
$io->setInputs(['object']);
$io->setInputs(['2']);
CLI::setInputOutput($io);

$command = new ModelGenerator(new Commands());
Expand Down
1 change: 1 addition & 0 deletions user_guide_src/source/changelogs/v4.8.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ Behavior Changes
- **Commands:** The ``db:table`` output was reworded: the data and metadata column headers are now capitalized (e.g. ``Id``, ``Created_at``), the connection summary headers read ``Hostname``, ``Database``, ``Username``, ``DB Driver``, ``DB Prefix``, and ``Port``, and the section titles changed (e.g. ``Data of "users" table:``). Scripts that grep the previous output will need updating.
- **Commands:** The ``worker:uninstall`` command now returns ``EXIT_SUCCESS`` (previously ``EXIT_ERROR``) when the user declines the interactive confirmation prompt, since user-initiated cancellation is not a failure. In non-interactive mode without ``--force`` it now aborts with ``EXIT_ERROR`` instead of prompting, and the redundant ``Uninstalling FrankenPHP Worker Mode`` header was dropped. Scripts that branch on the previous exit code or wording will need updating.
- **Commands:** The ``phpini:check`` command's invalid-argument error now prints only ``You must specify a correct argument.``. The usage example and the argument listing moved to ``phpini:check --help``, where the argument is now named ``section``. Scripts that grep the previous output will need updating.
- **Commands:** The prompts that pick from fixed options (``db:table``, ``db:create``, ``key:generate``, ``key:rotate``, ``make:command``, ``make:controller``, and ``make:model``) now use ``Prompt::select()``, so where the arrow-key menu is unavailable, as on Windows, the answer is the option's number instead of its value. Option values such as ``--prefix`` still take the value.
- **Config:** ``CodeIgniter\Config\BaseService`` is now ``abstract``. It is intended to be extended (as ``Config\Services`` does) and accessed statically; code that instantiated it directly with ``new BaseService()`` will now throw an ``Error``.
- **Database:** The Postgre driver's ``$db->error()['code']`` previously always returned ``''``. It now returns the 5-character SQLSTATE string for query and transaction failures (e.g., ``'42P01'``), or ``'08006'`` for connection-level failures. Code that relied on ``$db->error()['code'] === ''`` will need updating.
- **Debug:** ``CodeIgniter\Debug\Toolbar\Collectors\BaseCollector`` is now ``abstract``. Custom toolbar collectors must extend it; code that instantiated it directly with ``new BaseCollector()`` will now throw an ``Error``.
Expand Down
8 changes: 5 additions & 3 deletions user_guide_src/source/cli/spark_commands.rst
Original file line number Diff line number Diff line change
Expand Up @@ -153,9 +153,10 @@ Correcting a Mistyped Command

When the command name is not found, **spark** lists the closest matches. On an interactive
run it also offers to run one of them: a single match asks for a ``y``/``n`` confirmation, and
several matches present a numbered list with a "none of these" entry. Pressing Enter picks the
highlighted default. Non-interactive runs (``--no-interaction`` / ``-N``, or piped input) only
print the suggestions and exit with an error, as before.
several matches present a menu with a "none of these" entry, picked with the arrow keys (see
:ref:`cli-library-select`). Pressing Enter picks the highlighted entry. Non-interactive runs
(``--no-interaction`` / ``-N``, or piped input) only print the suggestions and exit with
an error, as before.

.. code-block:: console

Expand All @@ -164,6 +165,7 @@ print the suggestions and exit with an error, as before.
Command "cache:clea" not found.

Run "cache:clear" instead? [y, n]: y

Cache cleared using the "file" driver.

Calling Commands
Expand Down
10 changes: 3 additions & 7 deletions user_guide_src/source/testing/cli/001.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,25 +13,21 @@ final class DbTableTest extends CIUnitTestCase

public function testDbTable(): void
{
// Set MockInputOutput to CLI.
$io = new MockInputOutput();
CLI::setInputOutput($io);

// User will input "a" (invalid value) and "0".
$io->setInputs(['a', '0']);
$io->setInputs(['a', '1']);

command('db:table');

// Get the whole output string.
$output = $io->getOutput();

$expected = 'Which table do you want to see? [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]: a';
$expected = 'Please select correctly.';
$this->assertStringContainsString($expected, $output);

$expected = 'Data of Table "db_migrations":';
$expected = 'Data of "db_migrations" table:';
$this->assertStringContainsString($expected, $output);

// Remove MockInputOutput.
CLI::resetInputOutput();
}
}
Loading