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
45 changes: 34 additions & 11 deletions system/CLI/AbstractGeneratorCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ abstract class AbstractGeneratorCommand extends AbstractCommand implements Promp

protected ?string $templatePath = null;

/**
* The answer to the `CodeIgniter` namespace confirmation for the current `generateClass()`, or `null` if not yet asked.
*/
private ?bool $confirmedCodeIgniterNamespace = null;

/**
* @throws LogicException
*/
Expand Down Expand Up @@ -137,6 +142,8 @@ protected function execute(array $arguments, array $options): int
*/
protected function generateClass(): int
{
$this->confirmedCodeIgniterNamespace = null;

return $this->generate($this->qualifyClassName());
}

Expand All @@ -150,6 +157,14 @@ protected function generateView(string $view): int
return $this->generate($view);
}

/**
* Whether the `CodeIgniter` namespace confirmation was declined since the last `generateClass()`.
*/
protected function isCodeIgniterNamespaceDeclined(): bool
{
return $this->confirmedCodeIgniterNamespace === false;
}

/**
* Additional template placeholder replacements, which take precedence over the core `{namespace}` / `{class}` pairs.
*
Expand Down Expand Up @@ -327,17 +342,8 @@ private function generate(string $class): int
*/
private function generateFile(string $target, string $content): int
{
if (str_starts_with($target, SYSTEMPATH)) {
CLI::write(lang('CLI.generator.usingCINamespace'), 'yellow');

if (
$this->isInteractive()
&& CLI::prompt(lang('CLI.generator.confirmContinue'), ['y', 'n'], 'required') === 'n'
) {
CLI::write(lang('CLI.generator.cancelOperation'), 'yellow');

return EXIT_SUCCESS;
}
if (str_starts_with($target, SYSTEMPATH) && ! $this->confirmCodeIgniterNamespaceOnce()) {
return EXIT_SUCCESS;
}

$isFile = is_file($target);
Expand Down Expand Up @@ -374,6 +380,23 @@ private function generateFile(string $target, string $content): int
return EXIT_SUCCESS;
}

private function confirmCodeIgniterNamespaceOnce(): bool
{
if ($this->confirmedCodeIgniterNamespace !== null) {
return $this->confirmedCodeIgniterNamespace;
}

CLI::write(lang('CLI.generator.usingCINamespace'), 'yellow');

if ($this->isInteractive() && CLI::prompt(lang('CLI.generator.confirmContinue'), ['y', 'n'], 'required') === 'n') {
CLI::write(lang('CLI.generator.cancelOperation'), 'yellow');

return $this->confirmedCodeIgniterNamespace = false;
}

return $this->confirmedCodeIgniterNamespace = true;
}

/**
* Performs the placeholder replacements on the rendered template.
*
Expand Down
9 changes: 7 additions & 2 deletions system/Commands/Generators/ModelGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ protected function execute(array $arguments, array $options): int

$exitCode = $this->generateClass();

if ($exitCode !== EXIT_SUCCESS || $return !== 'entity') {
if ($exitCode !== EXIT_SUCCESS || $return !== 'entity' || $this->isCodeIgniterNamespaceDeclined()) {
return $exitCode;
}

Expand All @@ -92,7 +92,12 @@ protected function execute(array $arguments, array $options): int
$entityOptions['force'] = null;
}

return $this->call('make:entity', [$this->getEntityClass($this->qualifyClassName())], $entityOptions);
return $this->call(
'make:entity',
[$this->getEntityClass($this->qualifyClassName())],
$entityOptions,
noInteractionOverride: true,
);
}

protected function getReplacements(string $class): array
Expand Down
85 changes: 85 additions & 0 deletions tests/system/CLI/AbstractGeneratorCommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@

namespace CodeIgniter\CLI;

use CodeIgniter\CLI\Attributes\Command;
use CodeIgniter\CLI\Attributes\GeneratorCommand;
use CodeIgniter\Exceptions\LogicException;
use CodeIgniter\Test\CIUnitTestCase;
use CodeIgniter\Test\Mock\MockInputOutput;
Expand Down Expand Up @@ -413,6 +415,89 @@ public function testAcceptingCodeIgniterNamespacePromptProceeds(): void
$this->assertStringContainsString('File exists: ', $io->getOutput());
}

public function testNonInteractiveCodeIgniterNamespaceWarnsOnceAndProceeds(): void
{
$io = new MockInputOutput();
CLI::setInputOutput($io);

$command = $this->createClassAndViewGenerator();
$command->setInteractive(false);

$this->assertSame(EXIT_ERROR, $command->run([CLI::class], ['namespace' => 'CodeIgniter']));
$this->assertSame(
<<<'EOT'

Warning: Using the "CodeIgniter" namespace will generate the file in the system directory.
File exists: "SYSTEMPATH/CLI/CLI.php"
File exists: "SYSTEMPATH/CLI/Commands.php"

EOT,
preg_replace('/\e\[[^m]+m/', '', $io->getOutput()),
);
}

public function testAcceptedCodeIgniterNamespaceIsReusedForTheView(): void
{
$io = new MockInputOutput();
$io->setInputs(['y']);
CLI::setInputOutput($io);

$command = $this->createClassAndViewGenerator();
$command->setInteractive(true);

$this->assertSame(EXIT_ERROR, $command->run([CLI::class], ['namespace' => 'CodeIgniter']));
$this->assertSame(
<<<'EOT'

Warning: Using the "CodeIgniter" namespace will generate the file in the system directory.
Are you sure you want to continue? [y, n]: y
File exists: "SYSTEMPATH/CLI/CLI.php"
File exists: "SYSTEMPATH/CLI/Commands.php"

EOT,
preg_replace('/\e\[[^m]+m/', '', $io->getOutput()),
);
}

public function testCodeIgniterNamespaceIsConfirmedAgainOnTheNextRun(): void
{
$io = new MockInputOutput();
$io->setInputs(['y', 'n']);
CLI::setInputOutput($io);

$command = $this->createClassAndViewGenerator();
$command->setInteractive(true);

$this->assertSame(EXIT_ERROR, $command->run([CLI::class], ['namespace' => 'CodeIgniter']));
$this->assertSame(EXIT_SUCCESS, $command->run([CLI::class], ['namespace' => 'CodeIgniter']));
$this->assertSame(
<<<'EOT'

Warning: Using the "CodeIgniter" namespace will generate the file in the system directory.
Are you sure you want to continue? [y, n]: y
File exists: "SYSTEMPATH/CLI/CLI.php"
File exists: "SYSTEMPATH/CLI/Commands.php"
Warning: Using the "CodeIgniter" namespace will generate the file in the system directory.
Are you sure you want to continue? [y, n]: n
Operation has been cancelled.

EOT,
preg_replace('/\e\[[^m]+m/', '', $io->getOutput()),
);
}

private function createClassAndViewGenerator(): AbstractGeneratorCommand
{
return new #[Command(name: 'make:classandview', description: 'Generates a class and a view.', group: 'Fixtures')]
#[GeneratorCommand(component: 'Widget', template: 'config.tpl.php')]
class (new Commands()) extends AbstractGeneratorCommand {
protected function execute(array $arguments, array $options): int
{
return $this->generateClass() | $this->generateView(Commands::class);
}
};
}

public function testImportsAreSortedInGeneratedContent(): void
{
$command = new ImportSortingGeneratorCommand(new Commands());
Expand Down
38 changes: 32 additions & 6 deletions tests/system/Commands/Generators/CellGeneratorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@
namespace CodeIgniter\Commands\Generators;

use CodeIgniter\CLI\CLI;
use CodeIgniter\CLI\Commands;
use CodeIgniter\Test\CIUnitTestCase;
use CodeIgniter\Test\Mock\MockInputOutput;
use CodeIgniter\Test\StreamFilterTrait;
use PHPUnit\Framework\Attributes\Group;

Expand All @@ -39,12 +41,12 @@ protected function tearDown(): void

CLI::reset();

$dir = APPPATH . 'Cells';

if (is_dir($dir)) {
helper('filesystem');
delete_files($dir, true, false, true);
rmdir($dir);
foreach ([APPPATH . 'Cells', SYSTEMPATH . 'Cells'] as $dir) {
if (is_dir($dir)) {
helper('filesystem');
delete_files($dir, true, false, true);
rmdir($dir);
}
}
}

Expand Down Expand Up @@ -134,4 +136,28 @@ public function testForceOverwritesBothFiles(): void
$this->getUndecoratedBuffer(),
);
}

public function testDecliningCodeIgniterNamespaceAsksOnceForBothFiles(): void
{
$io = new MockInputOutput();
$io->setInputs(['n']);
CLI::setInputOutput($io);

$command = new CellGenerator(new Commands());
$command->setInteractive(true);

$this->assertSame(EXIT_SUCCESS, $command->run(['foo'], ['namespace' => 'CodeIgniter']));
$this->assertSame(
<<<'EOT'

Warning: Using the "CodeIgniter" namespace will generate the file in the system directory.
Are you sure you want to continue? [y, n]: n
Operation has been cancelled.

EOT,
preg_replace('/\e\[[^m]+m/', '', $io->getOutput()),
);
$this->assertFileDoesNotExist(SYSTEMPATH . 'Cells' . DIRECTORY_SEPARATOR . 'FooCell.php');
$this->assertFileDoesNotExist(SYSTEMPATH . 'Cells' . DIRECTORY_SEPARATOR . 'foo.php');
}
}
50 changes: 49 additions & 1 deletion tests/system/Commands/Generators/ModelGeneratorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ protected function tearDown(): void

helper('filesystem');

foreach ([APPPATH . 'Models/Admin', APPPATH . 'Entities'] as $dir) {
foreach ([APPPATH . 'Models/Admin', APPPATH . 'Entities', SYSTEMPATH . 'Models', SYSTEMPATH . 'Entities'] as $dir) {
if (is_dir($dir)) {
delete_files($dir, true, false, true);
rmdir($dir);
Expand Down Expand Up @@ -216,4 +216,52 @@ public function testInvalidReturnTypePromptsWhenInteractive(): void
$this->assertStringContainsString('Return type', $io->getOutput());
$this->assertStringContainsString("protected \$returnType = 'object';", $this->getContents('Models/User.php'));
}

public function testDecliningCodeIgniterNamespaceSkipsTheEntity(): void
{
$io = new MockInputOutput();
$io->setInputs(['n']);
CLI::setInputOutput($io);

$command = new ModelGenerator(new Commands());
$command->setInteractive(true);

$this->assertSame(EXIT_SUCCESS, $command->run(['user'], ['namespace' => 'CodeIgniter', 'return' => 'entity']));
$this->assertSame(
<<<'EOT'

Warning: Using the "CodeIgniter" namespace will generate the file in the system directory.
Are you sure you want to continue? [y, n]: n
Operation has been cancelled.

EOT,
preg_replace('/\e\[[^m]+m/', '', $io->getOutput()),
);
$this->assertFileDoesNotExist(SYSTEMPATH . 'Models' . DIRECTORY_SEPARATOR . 'User.php');
$this->assertFileDoesNotExist(SYSTEMPATH . 'Entities' . DIRECTORY_SEPARATOR . 'User.php');
}

public function testAcceptingCodeIgniterNamespaceDoesNotAskAgainForTheEntity(): void
{
$io = new MockInputOutput();
$io->setInputs(['y']);
CLI::setInputOutput($io);

$command = new ModelGenerator(new Commands());
$command->setInteractive(true);

$this->assertSame(EXIT_SUCCESS, $command->run(['user'], ['namespace' => 'CodeIgniter', 'return' => 'entity']));
$this->assertSame(
<<<'EOT'

Warning: Using the "CodeIgniter" namespace will generate the file in the system directory.
Are you sure you want to continue? [y, n]: y
File created: SYSTEMPATH/Models/User.php
Warning: Using the "CodeIgniter" namespace will generate the file in the system directory.
File created: SYSTEMPATH/Entities/User.php

EOT,
preg_replace('/\e\[[^m]+m/', '', $io->getOutput()),
);
}
}
2 changes: 1 addition & 1 deletion user_guide_src/source/changelogs/v4.8.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ Behavior Changes
behaviour may need to re-implement against the modern API (``configure()`` + ``execute()`` and the ``#[Command]`` attribute) once the class it extends is migrated, or, preferably, compose instead of extending. Invocations on the command line are unaffected.
- **Commands:** Generator commands migrated to ``AbstractGeneratorCommand`` now return ``EXIT_ERROR`` (previously ``EXIT_SUCCESS``) when the target file exists
without ``--force``, when the namespace is not defined, or when the name does not form a valid class name. The ``CodeIgniter`` namespace confirmation is no
longer prompted on non-interactive runs.
longer prompted on non-interactive runs, and ``make:cell`` and ``make:model --return entity`` now ask it only once.
- **Commands:** The ``make:command`` command now generates modern commands: a ``basic`` command extends ``AbstractCommand`` with the ``#[Command]`` attribute,
and a ``generator`` command extends ``AbstractGeneratorCommand`` with the ``#[GeneratorCommand]`` attribute.
- **Commands:** The ``make:migration`` command now rejects ``--force`` as an unknown option. It was previously accepted but ignored, since the
Expand Down
13 changes: 11 additions & 2 deletions user_guide_src/source/cli/cli_modern_generators.rst
Original file line number Diff line number Diff line change
Expand Up @@ -262,8 +262,10 @@ Behavioural changes we need to be aware of when migrating:
generator reported ``EXIT_SUCCESS`` even when the target file already existed. The base class
returns ``EXIT_ERROR`` for an existing file without ``--force``, a write failure, and an undefined
namespace. Declining the ``CodeIgniter`` namespace confirmation still exits with ``EXIT_SUCCESS``.
- **The** ``CodeIgniter`` **namespace confirmation only prompts on interactive runs.** Non-interactive
runs print the warning and proceed instead of blocking on input that will never arrive.
- **The** ``CodeIgniter`` **namespace confirmation is asked once, and only on interactive runs.** The
trait asked for every file. The base class asks once per ``generateClass()`` and reuses the answer for
the views generated after it. Non-interactive runs print the warning and proceed instead of blocking
on input that will never arrive.
- **The** ``CodeIgniter`` **namespace confirmation keys off the target path.** The trait compared
the raw ``--namespace`` option, so an attribute-pinned ``CodeIgniter`` namespace or a spelling like
``--namespace CodeIgniter/`` did not warn. The base class warns exactly when the file would be written
Expand Down Expand Up @@ -294,6 +296,13 @@ AbstractGeneratorCommand
Like :php:meth:`generateClass`, but for non-class artifacts. The name is
used as-is, without qualification or suffix handling.

.. php:method:: isCodeIgniterNamespaceDeclined(): bool

Whether the ``CodeIgniter`` namespace confirmation was declined since the
last :php:meth:`generateClass` call. A declined run still returns
``EXIT_SUCCESS``, so check this before generating anything that depends
on the declined file.

.. php:method:: getReplacements(string $class): array

:param string $class: The namespaced class (or view) being generated.
Expand Down
Loading