From a760bea536339669de6f7913e8fa360c220f5b26 Mon Sep 17 00:00:00 2001 From: "John Paul E. Balandan, CPA" Date: Sun, 6 Sep 2026 19:12:28 +0800 Subject: [PATCH 1/2] refactor: migrate the simple `make:*` generators to `AbstractGeneratorCommand` --- app/Config/Generators.php | 21 +++-- system/CLI/AbstractGeneratorCommand.php | 2 +- .../Commands/Generators/EntityGenerator.php | 83 +++-------------- .../Commands/Generators/FilterGenerator.php | 83 +++-------------- .../Generators/FormRequestGenerator.php | 83 +++-------------- .../Commands/Generators/SeederGenerator.php | 83 +++-------------- .../Generators/TransformerGenerator.php | 91 +++---------------- .../Generators/ValidationGenerator.php | 83 +++-------------- .../Generators/EntityGeneratorTest.php | 39 ++++++-- .../Generators/FilterGeneratorTest.php | 35 +++++-- .../Generators/FormRequestGeneratorTest.php | 43 ++++++--- .../Generators/SeederGeneratorTest.php | 35 +++++-- .../Generators/TransformerGeneratorTest.php | 85 ++++++----------- .../Generators/ValidationGeneratorTest.php | 39 +++++--- tests/system/Commands/HelpCommandTest.php | 4 +- user_guide_src/source/changelogs/v4.8.0.rst | 6 +- user_guide_src/source/cli/cli_generators.rst | 54 ++++++++--- .../source/installation/upgrade_480.rst | 2 + 18 files changed, 305 insertions(+), 566 deletions(-) diff --git a/app/Config/Generators.php b/app/Config/Generators.php index 2600f0cdea08..e418647f00fe 100644 --- a/app/Config/Generators.php +++ b/app/Config/Generators.php @@ -30,14 +30,17 @@ class Generators extends BaseConfig 'class' => 'CodeIgniter\Commands\Generators\Views\cell.tpl.php', 'view' => 'CodeIgniter\Commands\Generators\Views\cell_view.tpl.php', ], - 'make:command' => 'CodeIgniter\Commands\Generators\Views\command.tpl.php', - 'make:config' => 'CodeIgniter\Commands\Generators\Views\config.tpl.php', - 'make:controller' => 'CodeIgniter\Commands\Generators\Views\controller.tpl.php', - 'make:entity' => 'CodeIgniter\Commands\Generators\Views\entity.tpl.php', - 'make:filter' => 'CodeIgniter\Commands\Generators\Views\filter.tpl.php', - 'make:migration' => 'CodeIgniter\Commands\Generators\Views\migration.tpl.php', - 'make:model' => 'CodeIgniter\Commands\Generators\Views\model.tpl.php', - 'make:seeder' => 'CodeIgniter\Commands\Generators\Views\seeder.tpl.php', - 'make:validation' => 'CodeIgniter\Commands\Generators\Views\validation.tpl.php', + 'make:command' => 'CodeIgniter\Commands\Generators\Views\command.tpl.php', + 'make:config' => 'CodeIgniter\Commands\Generators\Views\config.tpl.php', + 'make:controller' => 'CodeIgniter\Commands\Generators\Views\controller.tpl.php', + 'make:entity' => 'CodeIgniter\Commands\Generators\Views\entity.tpl.php', + 'make:filter' => 'CodeIgniter\Commands\Generators\Views\filter.tpl.php', + 'make:migration' => 'CodeIgniter\Commands\Generators\Views\migration.tpl.php', + 'make:model' => 'CodeIgniter\Commands\Generators\Views\model.tpl.php', + 'make:request' => 'CodeIgniter\Commands\Generators\Views\formrequest.tpl.php', + 'make:seeder' => 'CodeIgniter\Commands\Generators\Views\seeder.tpl.php', + 'make:test' => 'CodeIgniter\Commands\Generators\Views\test.tpl.php', + 'make:transformer' => 'CodeIgniter\Commands\Generators\Views\transformer.tpl.php', + 'make:validation' => 'CodeIgniter\Commands\Generators\Views\validation.tpl.php', ]; } diff --git a/system/CLI/AbstractGeneratorCommand.php b/system/CLI/AbstractGeneratorCommand.php index 8c91ad7e7614..38235a764d22 100644 --- a/system/CLI/AbstractGeneratorCommand.php +++ b/system/CLI/AbstractGeneratorCommand.php @@ -73,7 +73,7 @@ protected function configure(): void { $this->addArgument(new Argument( name: 'name', - description: 'The name of the class to generate.', + description: sprintf('The %s class name.', lcfirst($this->component)), required: true, )); } diff --git a/system/Commands/Generators/EntityGenerator.php b/system/Commands/Generators/EntityGenerator.php index 1040b33bbcff..1687d2d189c8 100644 --- a/system/Commands/Generators/EntityGenerator.php +++ b/system/Commands/Generators/EntityGenerator.php @@ -13,76 +13,17 @@ namespace CodeIgniter\Commands\Generators; -use CodeIgniter\CLI\BaseCommand; -use CodeIgniter\CLI\GeneratorTrait; - -/** - * Generates a skeleton Entity file. - */ -class EntityGenerator extends BaseCommand +use CodeIgniter\CLI\AbstractGeneratorCommand; +use CodeIgniter\CLI\Attributes\Command; +use CodeIgniter\CLI\Attributes\GeneratorCommand; + +#[Command(name: 'make:entity', description: 'Generates a new entity file.', group: 'Generators')] +#[GeneratorCommand( + component: 'Entity', + template: 'entity.tpl.php', + directory: 'Entities', + classNameLang: 'CLI.generator.className.entity', +)] +class EntityGenerator extends AbstractGeneratorCommand { - use GeneratorTrait; - - /** - * The Command's Group - * - * @var string - */ - protected $group = 'Generators'; - - /** - * The Command's Name - * - * @var string - */ - protected $name = 'make:entity'; - - /** - * The Command's Description - * - * @var string - */ - protected $description = 'Generates a new entity file.'; - - /** - * The Command's Usage - * - * @var string - */ - protected $usage = 'make:entity [options]'; - - /** - * The Command's Arguments - * - * @var array - */ - protected $arguments = [ - 'name' => 'The entity class name.', - ]; - - /** - * The Command's Options - * - * @var array - */ - protected $options = [ - '--namespace' => 'Set root namespace. Default: "APP_NAMESPACE".', - '--suffix' => 'Append the component title to the class name (e.g. User => UserEntity).', - '--force' => 'Force overwrite existing file.', - ]; - - /** - * Actually execute a command. - */ - public function run(array $params) - { - $this->component = 'Entity'; - $this->directory = 'Entities'; - $this->template = 'entity.tpl.php'; - - $this->classNameLang = 'CLI.generator.className.entity'; - $this->generateClass($params); - - return EXIT_SUCCESS; - } } diff --git a/system/Commands/Generators/FilterGenerator.php b/system/Commands/Generators/FilterGenerator.php index d13c0feff45a..ba8500b990f5 100644 --- a/system/Commands/Generators/FilterGenerator.php +++ b/system/Commands/Generators/FilterGenerator.php @@ -13,76 +13,17 @@ namespace CodeIgniter\Commands\Generators; -use CodeIgniter\CLI\BaseCommand; -use CodeIgniter\CLI\GeneratorTrait; - -/** - * Generates a skeleton Filter file. - */ -class FilterGenerator extends BaseCommand +use CodeIgniter\CLI\AbstractGeneratorCommand; +use CodeIgniter\CLI\Attributes\Command; +use CodeIgniter\CLI\Attributes\GeneratorCommand; + +#[Command(name: 'make:filter', description: 'Generates a new filter file.', group: 'Generators')] +#[GeneratorCommand( + component: 'Filter', + template: 'filter.tpl.php', + directory: 'Filters', + classNameLang: 'CLI.generator.className.filter', +)] +class FilterGenerator extends AbstractGeneratorCommand { - use GeneratorTrait; - - /** - * The Command's Group - * - * @var string - */ - protected $group = 'Generators'; - - /** - * The Command's Name - * - * @var string - */ - protected $name = 'make:filter'; - - /** - * The Command's Description - * - * @var string - */ - protected $description = 'Generates a new filter file.'; - - /** - * The Command's Usage - * - * @var string - */ - protected $usage = 'make:filter [options]'; - - /** - * The Command's Arguments - * - * @var array - */ - protected $arguments = [ - 'name' => 'The filter class name.', - ]; - - /** - * The Command's Options - * - * @var array - */ - protected $options = [ - '--namespace' => 'Set root namespace. Default: "APP_NAMESPACE".', - '--suffix' => 'Append the component title to the class name (e.g. User => UserFilter).', - '--force' => 'Force overwrite existing file.', - ]; - - /** - * Actually execute a command. - */ - public function run(array $params) - { - $this->component = 'Filter'; - $this->directory = 'Filters'; - $this->template = 'filter.tpl.php'; - - $this->classNameLang = 'CLI.generator.className.filter'; - $this->generateClass($params); - - return EXIT_SUCCESS; - } } diff --git a/system/Commands/Generators/FormRequestGenerator.php b/system/Commands/Generators/FormRequestGenerator.php index 65e3be3d6754..ce6743c0a477 100644 --- a/system/Commands/Generators/FormRequestGenerator.php +++ b/system/Commands/Generators/FormRequestGenerator.php @@ -13,76 +13,17 @@ namespace CodeIgniter\Commands\Generators; -use CodeIgniter\CLI\BaseCommand; -use CodeIgniter\CLI\GeneratorTrait; - -/** - * Generates a skeleton FormRequest file. - */ -class FormRequestGenerator extends BaseCommand +use CodeIgniter\CLI\AbstractGeneratorCommand; +use CodeIgniter\CLI\Attributes\Command; +use CodeIgniter\CLI\Attributes\GeneratorCommand; + +#[Command(name: 'make:request', description: 'Generates a new FormRequest file.', group: 'Generators')] +#[GeneratorCommand( + component: 'Request', + template: 'formrequest.tpl.php', + directory: 'Requests', + classNameLang: 'CLI.generator.className.request', +)] +class FormRequestGenerator extends AbstractGeneratorCommand { - use GeneratorTrait; - - /** - * The Command's Group - * - * @var string - */ - protected $group = 'Generators'; - - /** - * The Command's Name - * - * @var string - */ - protected $name = 'make:request'; - - /** - * The Command's Description - * - * @var string - */ - protected $description = 'Generates a new FormRequest file.'; - - /** - * The Command's Usage - * - * @var string - */ - protected $usage = 'make:request [options]'; - - /** - * The Command's Arguments - * - * @var array - */ - protected $arguments = [ - 'name' => 'The FormRequest class name.', - ]; - - /** - * The Command's Options - * - * @var array - */ - protected $options = [ - '--namespace' => 'Set root namespace. Default: "APP_NAMESPACE".', - '--suffix' => 'Append the component title to the class name (e.g. User => UserRequest).', - '--force' => 'Force overwrite existing file.', - ]; - - /** - * Actually execute a command. - */ - public function run(array $params) - { - $this->component = 'Request'; - $this->directory = 'Requests'; - $this->template = 'formrequest.tpl.php'; - - $this->classNameLang = 'CLI.generator.className.request'; - $this->generateClass($params); - - return EXIT_SUCCESS; - } } diff --git a/system/Commands/Generators/SeederGenerator.php b/system/Commands/Generators/SeederGenerator.php index 605bc2c16ead..452ad8279eb6 100644 --- a/system/Commands/Generators/SeederGenerator.php +++ b/system/Commands/Generators/SeederGenerator.php @@ -13,76 +13,17 @@ namespace CodeIgniter\Commands\Generators; -use CodeIgniter\CLI\BaseCommand; -use CodeIgniter\CLI\GeneratorTrait; - -/** - * Generates a skeleton seeder file. - */ -class SeederGenerator extends BaseCommand +use CodeIgniter\CLI\AbstractGeneratorCommand; +use CodeIgniter\CLI\Attributes\Command; +use CodeIgniter\CLI\Attributes\GeneratorCommand; + +#[Command(name: 'make:seeder', description: 'Generates a new seeder file.', group: 'Generators')] +#[GeneratorCommand( + component: 'Seeder', + template: 'seeder.tpl.php', + directory: 'Database\\Seeds', + classNameLang: 'CLI.generator.className.seeder', +)] +class SeederGenerator extends AbstractGeneratorCommand { - use GeneratorTrait; - - /** - * The Command's Group - * - * @var string - */ - protected $group = 'Generators'; - - /** - * The Command's Name - * - * @var string - */ - protected $name = 'make:seeder'; - - /** - * The Command's Description - * - * @var string - */ - protected $description = 'Generates a new seeder file.'; - - /** - * The Command's Usage - * - * @var string - */ - protected $usage = 'make:seeder [options]'; - - /** - * The Command's Arguments - * - * @var array - */ - protected $arguments = [ - 'name' => 'The seeder class name.', - ]; - - /** - * The Command's Options - * - * @var array - */ - protected $options = [ - '--namespace' => 'Set root namespace. Default: "APP_NAMESPACE".', - '--suffix' => 'Append the component title to the class name (e.g. User => UserSeeder).', - '--force' => 'Force overwrite existing file.', - ]; - - /** - * Actually execute a command. - */ - public function run(array $params) - { - $this->component = 'Seeder'; - $this->directory = 'Database\Seeds'; - $this->template = 'seeder.tpl.php'; - - $this->classNameLang = 'CLI.generator.className.seeder'; - $this->generateClass($params); - - return EXIT_SUCCESS; - } } diff --git a/system/Commands/Generators/TransformerGenerator.php b/system/Commands/Generators/TransformerGenerator.php index 82837b6c1d87..e2137ebd5f1f 100644 --- a/system/Commands/Generators/TransformerGenerator.php +++ b/system/Commands/Generators/TransformerGenerator.php @@ -13,84 +13,17 @@ namespace CodeIgniter\Commands\Generators; -use CodeIgniter\CLI\BaseCommand; -use CodeIgniter\CLI\GeneratorTrait; - -/** - * Generates a skeleton transformer file. - */ -class TransformerGenerator extends BaseCommand +use CodeIgniter\CLI\AbstractGeneratorCommand; +use CodeIgniter\CLI\Attributes\Command; +use CodeIgniter\CLI\Attributes\GeneratorCommand; + +#[Command(name: 'make:transformer', description: 'Generates a new transformer file.', group: 'Generators')] +#[GeneratorCommand( + component: 'Transformer', + template: 'transformer.tpl.php', + directory: 'Transformers', + classNameLang: 'CLI.generator.className.transformer', +)] +class TransformerGenerator extends AbstractGeneratorCommand { - use GeneratorTrait; - - /** - * The Command's Group - * - * @var string - */ - protected $group = 'Generators'; - - /** - * The Command's Name - * - * @var string - */ - protected $name = 'make:transformer'; - - /** - * The Command's Description - * - * @var string - */ - protected $description = 'Generates a new transformer file.'; - - /** - * The Command's Usage - * - * @var string - */ - protected $usage = 'make:transformer [options]'; - - /** - * The Command's Arguments - * - * @var array - */ - protected $arguments = [ - 'name' => 'The transformer class name.', - ]; - - /** - * The Command's Options - * - * @var array - */ - protected $options = [ - '--namespace' => 'Set root namespace. Default: "APP_NAMESPACE".', - '--suffix' => 'Append the component title to the class name (e.g. User => UserTransformer).', - '--force' => 'Force overwrite existing file.', - ]; - - /** - * Actually execute a command. - */ - public function run(array $params) - { - $this->component = 'Transformer'; - $this->directory = 'Transformers'; - $this->template = 'transformer.tpl.php'; - - $this->classNameLang = 'CLI.generator.className.transformer'; - $this->generateClass($params); - - return EXIT_SUCCESS; - } - - /** - * Prepare options and do the necessary replacements. - */ - protected function prepare(string $class): string - { - return $this->parseTemplate($class); - } } diff --git a/system/Commands/Generators/ValidationGenerator.php b/system/Commands/Generators/ValidationGenerator.php index 64e0c4ff9a34..ae6eaffc1b2c 100644 --- a/system/Commands/Generators/ValidationGenerator.php +++ b/system/Commands/Generators/ValidationGenerator.php @@ -13,76 +13,17 @@ namespace CodeIgniter\Commands\Generators; -use CodeIgniter\CLI\BaseCommand; -use CodeIgniter\CLI\GeneratorTrait; - -/** - * Generates a skeleton Validation file. - */ -class ValidationGenerator extends BaseCommand +use CodeIgniter\CLI\AbstractGeneratorCommand; +use CodeIgniter\CLI\Attributes\Command; +use CodeIgniter\CLI\Attributes\GeneratorCommand; + +#[Command(name: 'make:validation', description: 'Generates a new validation file.', group: 'Generators')] +#[GeneratorCommand( + component: 'Validation', + template: 'validation.tpl.php', + directory: 'Validation', + classNameLang: 'CLI.generator.className.validation', +)] +class ValidationGenerator extends AbstractGeneratorCommand { - use GeneratorTrait; - - /** - * The Command's Group - * - * @var string - */ - protected $group = 'Generators'; - - /** - * The Command's Name - * - * @var string - */ - protected $name = 'make:validation'; - - /** - * The Command's Description - * - * @var string - */ - protected $description = 'Generates a new validation file.'; - - /** - * The Command's Usage - * - * @var string - */ - protected $usage = 'make:validation [options]'; - - /** - * The Command's Arguments - * - * @var array - */ - protected $arguments = [ - 'name' => 'The validation class name.', - ]; - - /** - * The Command's Options - * - * @var array - */ - protected $options = [ - '--namespace' => 'Set root namespace. Default: "APP_NAMESPACE".', - '--suffix' => 'Append the component title to the class name (e.g. User => UserValidation).', - '--force' => 'Force overwrite existing file.', - ]; - - /** - * Actually execute a command. - */ - public function run(array $params) - { - $this->component = 'Validation'; - $this->directory = 'Validation'; - $this->template = 'validation.tpl.php'; - - $this->classNameLang = 'CLI.generator.className.validation'; - $this->generateClass($params); - - return EXIT_SUCCESS; - } } diff --git a/tests/system/Commands/Generators/EntityGeneratorTest.php b/tests/system/Commands/Generators/EntityGeneratorTest.php index f8dce1c17608..9ed096919e9c 100644 --- a/tests/system/Commands/Generators/EntityGeneratorTest.php +++ b/tests/system/Commands/Generators/EntityGeneratorTest.php @@ -13,6 +13,7 @@ namespace CodeIgniter\Commands\Generators; +use CodeIgniter\CLI\CLI; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\StreamFilterTrait; use PHPUnit\Framework\Attributes\Group; @@ -25,28 +26,46 @@ final class EntityGeneratorTest extends CIUnitTestCase { use StreamFilterTrait; + private function getUndecoratedBuffer(): string + { + return preg_replace('/\e\[[^m]+m/', '', $this->getStreamFilterBuffer()) ?? ''; + } + + protected function setUp(): void + { + parent::setUp(); + + CLI::reset(); + } + protected function tearDown(): void { - $result = str_replace(["\033[0;32m", "\033[0m", "\n"], '', $this->getStreamFilterBuffer()); - $file = str_replace('APPPATH' . DIRECTORY_SEPARATOR, APPPATH, trim(substr($result, 14))); - $dir = dirname($file); - if (is_file($file)) { - unlink($file); - } - if (is_dir($dir)) { - rmdir($dir); + parent::tearDown(); + + CLI::reset(); + + if (is_dir(APPPATH . 'Entities')) { + helper('filesystem'); + delete_files(APPPATH . 'Entities', true); + rmdir(APPPATH . 'Entities'); } } public function testGenerateEntity(): void { command('make:entity user'); + + $this->assertSame( + PHP_EOL . 'File created: ' . clean_path(APPPATH . 'Entities/User.php') . PHP_EOL, + $this->getUndecoratedBuffer(), + ); $this->assertFileExists(APPPATH . 'Entities/User.php'); } - public function testGenerateEntityWithOptionSuffix(): void + public function testGenerateEntityWithSuffix(): void { - command('make:entity user -suffix'); + command('make:entity user --suffix'); + $this->assertFileExists(APPPATH . 'Entities/UserEntity.php'); } } diff --git a/tests/system/Commands/Generators/FilterGeneratorTest.php b/tests/system/Commands/Generators/FilterGeneratorTest.php index a1144c1bc31d..508762247bb4 100644 --- a/tests/system/Commands/Generators/FilterGeneratorTest.php +++ b/tests/system/Commands/Generators/FilterGeneratorTest.php @@ -13,6 +13,7 @@ namespace CodeIgniter\Commands\Generators; +use CodeIgniter\CLI\CLI; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\StreamFilterTrait; use PHPUnit\Framework\Attributes\Group; @@ -25,24 +26,46 @@ final class FilterGeneratorTest extends CIUnitTestCase { use StreamFilterTrait; + private function getUndecoratedBuffer(): string + { + return preg_replace('/\e\[[^m]+m/', '', $this->getStreamFilterBuffer()) ?? ''; + } + + protected function setUp(): void + { + parent::setUp(); + + CLI::reset(); + } + protected function tearDown(): void { - $result = str_replace(["\033[0;32m", "\033[0m", "\n"], '', $this->getStreamFilterBuffer()); - $file = str_replace('APPPATH' . DIRECTORY_SEPARATOR, APPPATH, trim(substr($result, 14))); - if (is_file($file)) { - unlink($file); + parent::tearDown(); + + CLI::reset(); + + foreach (['Admin.php', 'AdminFilter.php'] as $file) { + if (is_file(APPPATH . 'Filters/' . $file)) { + unlink(APPPATH . 'Filters/' . $file); + } } } public function testGenerateFilter(): void { command('make:filter admin'); + + $this->assertSame( + PHP_EOL . 'File created: ' . clean_path(APPPATH . 'Filters/Admin.php') . PHP_EOL, + $this->getUndecoratedBuffer(), + ); $this->assertFileExists(APPPATH . 'Filters/Admin.php'); } - public function testGenerateFilterWithOptionSuffix(): void + public function testGenerateFilterWithSuffix(): void { - command('make:filter admin -suffix'); + command('make:filter admin --suffix'); + $this->assertFileExists(APPPATH . 'Filters/AdminFilter.php'); } } diff --git a/tests/system/Commands/Generators/FormRequestGeneratorTest.php b/tests/system/Commands/Generators/FormRequestGeneratorTest.php index c2acc73ec4ca..f82561dc19dc 100644 --- a/tests/system/Commands/Generators/FormRequestGeneratorTest.php +++ b/tests/system/Commands/Generators/FormRequestGeneratorTest.php @@ -13,6 +13,7 @@ namespace CodeIgniter\Commands\Generators; +use CodeIgniter\CLI\CLI; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\StreamFilterTrait; use PHPUnit\Framework\Attributes\Group; @@ -25,15 +26,28 @@ final class FormRequestGeneratorTest extends CIUnitTestCase { use StreamFilterTrait; + private function getUndecoratedBuffer(): string + { + return preg_replace('/\e\[[^m]+m/', '', $this->getStreamFilterBuffer()) ?? ''; + } + + protected function setUp(): void + { + parent::setUp(); + + CLI::reset(); + } + protected function tearDown(): void { parent::tearDown(); - $result = str_replace(["\033[0;32m", "\033[0m", "\n"], '', $this->getStreamFilterBuffer()); - $file = str_replace('APPPATH' . DIRECTORY_SEPARATOR, APPPATH, trim(substr($result, 14))); + CLI::reset(); - if (is_file($file)) { - unlink($file); + if (is_dir(APPPATH . 'Requests')) { + helper('filesystem'); + delete_files(APPPATH . 'Requests', true); + rmdir(APPPATH . 'Requests'); } } @@ -41,18 +55,21 @@ public function testGenerateFormRequest(): void { command('make:request user'); - $file = APPPATH . 'Requests/User.php'; - - $this->assertFileExists($file); - $this->assertStringContainsString( - 'Defaults to true in FormRequest. Override only when authorization', - (string) file_get_contents($file), + $this->assertSame( + PHP_EOL . 'File created: ' . clean_path(APPPATH . 'Requests/User.php') . PHP_EOL, + $this->getUndecoratedBuffer(), ); + $this->assertFileExists(APPPATH . 'Requests/User.php'); + + $content = file_get_contents(APPPATH . 'Requests/User.php'); + $this->assertIsString($content); + $this->assertStringContainsString('Defaults to true in FormRequest. Override only when authorization', $content); } - public function testGenerateFormRequestWithOptionSuffix(): void + public function testGenerateFormRequestWithSuffix(): void { - command('make:request admin -suffix'); - $this->assertFileExists(APPPATH . 'Requests/AdminRequest.php'); + command('make:request user --suffix'); + + $this->assertFileExists(APPPATH . 'Requests/UserRequest.php'); } } diff --git a/tests/system/Commands/Generators/SeederGeneratorTest.php b/tests/system/Commands/Generators/SeederGeneratorTest.php index c455576876bb..43aa6911982e 100644 --- a/tests/system/Commands/Generators/SeederGeneratorTest.php +++ b/tests/system/Commands/Generators/SeederGeneratorTest.php @@ -13,6 +13,7 @@ namespace CodeIgniter\Commands\Generators; +use CodeIgniter\CLI\CLI; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\StreamFilterTrait; use PHPUnit\Framework\Attributes\Group; @@ -25,28 +26,46 @@ final class SeederGeneratorTest extends CIUnitTestCase { use StreamFilterTrait; + private function getUndecoratedBuffer(): string + { + return preg_replace('/\e\[[^m]+m/', '', $this->getStreamFilterBuffer()) ?? ''; + } + + protected function setUp(): void + { + parent::setUp(); + + CLI::reset(); + } + protected function tearDown(): void { parent::tearDown(); - $result = str_replace(["\033[0;32m", "\033[0m", "\n"], '', $this->getStreamFilterBuffer()); - $file = str_replace('APPPATH' . DIRECTORY_SEPARATOR, APPPATH, trim(substr($result, 14))); - if (is_file($file)) { - unlink($file); + CLI::reset(); + + foreach (['Cars.php', 'CarsSeeder.php'] as $file) { + if (is_file(APPPATH . 'Database/Seeds/' . $file)) { + unlink(APPPATH . 'Database/Seeds/' . $file); + } } } public function testGenerateSeeder(): void { command('make:seeder cars'); - $this->assertStringContainsString('File created: ', $this->getStreamFilterBuffer()); + + $this->assertSame( + PHP_EOL . 'File created: ' . clean_path(APPPATH . 'Database/Seeds/Cars.php') . PHP_EOL, + $this->getUndecoratedBuffer(), + ); $this->assertFileExists(APPPATH . 'Database/Seeds/Cars.php'); } - public function testGenerateSeederWithOptionSuffix(): void + public function testGenerateSeederWithSuffix(): void { - command('make:seeder cars -suffix'); - $this->assertStringContainsString('File created: ', $this->getStreamFilterBuffer()); + command('make:seeder cars --suffix'); + $this->assertFileExists(APPPATH . 'Database/Seeds/CarsSeeder.php'); } } diff --git a/tests/system/Commands/Generators/TransformerGeneratorTest.php b/tests/system/Commands/Generators/TransformerGeneratorTest.php index 2b007d590aaa..d1f5eef2cc89 100644 --- a/tests/system/Commands/Generators/TransformerGeneratorTest.php +++ b/tests/system/Commands/Generators/TransformerGeneratorTest.php @@ -13,6 +13,7 @@ namespace CodeIgniter\Commands\Generators; +use CodeIgniter\CLI\CLI; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\StreamFilterTrait; use PHPUnit\Framework\Attributes\Group; @@ -25,78 +26,50 @@ final class TransformerGeneratorTest extends CIUnitTestCase { use StreamFilterTrait; - protected function tearDown(): void + private function getUndecoratedBuffer(): string { - $result = str_replace(["\033[0;33m", "\033[0;32m", "\033[0m", "\n"], '', $this->getStreamFilterBuffer()); - preg_match('/APPPATH(\/[^\s"]+\.php)/', $result, $matches); - $file = isset($matches[0]) ? str_replace('APPPATH' . DIRECTORY_SEPARATOR, APPPATH, $matches[0]) : ''; + return preg_replace('/\e\[[^m]+m/', '', $this->getStreamFilterBuffer()) ?? ''; + } - if (is_file($file)) { - unlink($file); - } + protected function setUp(): void + { + parent::setUp(); + + CLI::reset(); } - protected function getFileContents(string $filepath): string + protected function tearDown(): void { - if (! is_file($filepath)) { - return ''; - } + parent::tearDown(); - $contents = file_get_contents($filepath); + CLI::reset(); - return $contents !== false ? $contents : ''; + if (is_dir(APPPATH . 'Transformers')) { + helper('filesystem'); + delete_files(APPPATH . 'Transformers', true); + rmdir(APPPATH . 'Transformers'); + } } public function testGenerateTransformer(): void { command('make:transformer user'); - $this->assertStringContainsString('File created: ', $this->getStreamFilterBuffer()); - $file = APPPATH . 'Transformers/User.php'; - $this->assertFileExists($file); - $contents = $this->getFileContents($file); - $this->assertStringContainsString('extends BaseTransformer', $contents); - $this->assertStringContainsString('namespace App\Transformers', $contents); - $this->assertStringContainsString('public function toArray(mixed $resource): array', $contents); - } - public function testGenerateTransformerWithSubdirectory(): void - { - command('make:transformer api/v1/product'); - $this->assertStringContainsString('File created: ', $this->getStreamFilterBuffer()); - $file = APPPATH . 'Transformers/Api/V1/Product.php'; - $this->assertFileExists($file); - $contents = $this->getFileContents($file); - $this->assertStringContainsString('namespace App\Transformers\Api\V1', $contents); - $this->assertStringContainsString('class Product extends BaseTransformer', $contents); - } + $this->assertSame( + PHP_EOL . 'File created: ' . clean_path(APPPATH . 'Transformers/User.php') . PHP_EOL, + $this->getUndecoratedBuffer(), + ); + $this->assertFileExists(APPPATH . 'Transformers/User.php'); - public function testGenerateTransformerWithOptionSuffix(): void - { - command('make:transformer order -suffix'); - $this->assertStringContainsString('File created: ', $this->getStreamFilterBuffer()); - $file = APPPATH . 'Transformers/OrderTransformer.php'; - $this->assertFileExists($file); - $contents = $this->getFileContents($file); - $this->assertStringContainsString('class OrderTransformer extends BaseTransformer', $contents); + $content = file_get_contents(APPPATH . 'Transformers/User.php'); + $this->assertIsString($content); + $this->assertStringContainsString('extends BaseTransformer', $content); } - public function testGenerateTransformerWithOptionForce(): void + public function testGenerateTransformerWithSuffix(): void { - // Create the file first - command('make:transformer customer'); - $this->assertStringContainsString('File created: ', $this->getStreamFilterBuffer()); - $file = APPPATH . 'Transformers/Customer.php'; - $this->assertFileExists($file); - - // Try to overwrite without force - $this->resetStreamFilterBuffer(); - command('make:transformer customer'); - $this->assertStringContainsString('File exists: ', $this->getStreamFilterBuffer()); - - // Now overwrite with force - $this->resetStreamFilterBuffer(); - command('make:transformer customer -force'); - $this->assertStringContainsString('File overwritten: ', $this->getStreamFilterBuffer()); - $this->assertFileExists($file); + command('make:transformer user --suffix'); + + $this->assertFileExists(APPPATH . 'Transformers/UserTransformer.php'); } } diff --git a/tests/system/Commands/Generators/ValidationGeneratorTest.php b/tests/system/Commands/Generators/ValidationGeneratorTest.php index 6779e9a6d4d5..c5a7f7eb0d5f 100644 --- a/tests/system/Commands/Generators/ValidationGeneratorTest.php +++ b/tests/system/Commands/Generators/ValidationGeneratorTest.php @@ -13,6 +13,7 @@ namespace CodeIgniter\Commands\Generators; +use CodeIgniter\CLI\CLI; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\StreamFilterTrait; use PHPUnit\Framework\Attributes\Group; @@ -25,30 +26,46 @@ final class ValidationGeneratorTest extends CIUnitTestCase { use StreamFilterTrait; + private function getUndecoratedBuffer(): string + { + return preg_replace('/\e\[[^m]+m/', '', $this->getStreamFilterBuffer()) ?? ''; + } + + protected function setUp(): void + { + parent::setUp(); + + CLI::reset(); + } + protected function tearDown(): void { parent::tearDown(); - $result = str_replace(["\033[0;32m", "\033[0m", "\n"], '', $this->getStreamFilterBuffer()); - $file = str_replace('APPPATH' . DIRECTORY_SEPARATOR, APPPATH, trim(substr($result, 14))); - $dir = dirname($file); - if (is_file($file)) { - unlink($file); - } - if (is_dir($dir)) { - rmdir($dir); + CLI::reset(); + + if (is_dir(APPPATH . 'Validation')) { + helper('filesystem'); + delete_files(APPPATH . 'Validation', true); + rmdir(APPPATH . 'Validation'); } } public function testGenerateValidation(): void { command('make:validation user'); + + $this->assertSame( + PHP_EOL . 'File created: ' . clean_path(APPPATH . 'Validation/User.php') . PHP_EOL, + $this->getUndecoratedBuffer(), + ); $this->assertFileExists(APPPATH . 'Validation/User.php'); } - public function testGenerateValidationWithOptionSuffix(): void + public function testGenerateValidationWithSuffix(): void { - command('make:validation admin -suffix'); - $this->assertFileExists(APPPATH . 'Validation/AdminValidation.php'); + command('make:validation user --suffix'); + + $this->assertFileExists(APPPATH . 'Validation/UserValidation.php'); } } diff --git a/tests/system/Commands/HelpCommandTest.php b/tests/system/Commands/HelpCommandTest.php index d42c94ad40cf..f39ef72efde5 100644 --- a/tests/system/Commands/HelpCommandTest.php +++ b/tests/system/Commands/HelpCommandTest.php @@ -220,7 +220,7 @@ public function testDescribeGeneratorCommand(): void Fixture generator command. Arguments: - name The name of the class to generate. + name The widget class name. Options: -h, --help Display help for the given command. @@ -249,7 +249,7 @@ public function testDescribeGeneratorCommandWithTrimmedOptions(): void Fixture generator command with trimmed options and forced suffixing. Arguments: - name The name of the class to generate. + name The widget class name. Options: -h, --help Display help for the given command. diff --git a/user_guide_src/source/changelogs/v4.8.0.rst b/user_guide_src/source/changelogs/v4.8.0.rst index 31346849cf8f..f784316d88e6 100644 --- a/user_guide_src/source/changelogs/v4.8.0.rst +++ b/user_guide_src/source/changelogs/v4.8.0.rst @@ -30,6 +30,9 @@ Behavior Changes - **Commands:** The ``filter:check`` command now requires the HTTP method argument to be uppercase (e.g., ``spark filter:check GET /`` instead of ``spark filter:check get /``). - **Commands:** Several built-in commands have been migrated from ``BaseCommand`` to the modern ``AbstractCommand`` style. Applications that extend a built-in command to override 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. - **Commands:** The success and error messages from ``debugbar:clear``, ``cache:clear``, and ``cache:info`` now include the affected path or cache driver/handler so the user can see which resource was acted on (or rejected). Scripts asserting on the prior literal text will need to be updated. - **Commands:** Declining the ``key:generate`` overwrite prompt interactively now returns ``EXIT_SUCCESS`` instead of ``EXIT_ERROR``. Output messages were also reworded; CI/automation that branches on the exit code or greps the previous wording will need updating. - **Commands:** The ``migrate:rollback`` command no longer accepts the undocumented ``-g`` (database group) option. It never had any effect, since ``MigrationRunner::regress()`` ignores the group, and the modern command pipeline now rejects unknown options. Remove ``-g`` from any ``migrate:rollback`` invocation. @@ -227,7 +230,8 @@ Commands - Added ``key:rotate`` command to demote the current ``encryption.key`` to ``encryption.previousKeys`` in **.env** and generate a new key. See :ref:`spark-key-rotate`. - Added ``AbstractCommand::callSilently()`` to invoke another command with its output discarded, restoring the prior IO afterwards. See :ref:`modern-commands-call-silently`. - Added :php:class:`AbstractGeneratorCommand ` and the ``#[GeneratorCommand]`` attribute, the modern - counterpart of ``GeneratorTrait`` for commands that generate files from templates. See :doc:`../cli/cli_modern_generators`. + counterpart of ``GeneratorTrait`` for commands that generate files from templates. Generator commands built on it accept the ``-n``, ``-s``, + and ``-f`` shortcuts for ``--namespace``, ``--suffix``, and ``--force``. See :doc:`../cli/cli_modern_generators`. - Modern commands can now opt in to prompting for missing required arguments on interactive runs by implementing the new ``PromptsForMissingInputInterface`` marker interface. Prompt labels can be customized via ``getArgumentPromptLabels()``, and an ``afterPrompting()`` hook runs when prompting occurred. Non-interactive runs keep failing fast with the missing-arguments error. diff --git a/user_guide_src/source/cli/cli_generators.rst b/user_guide_src/source/cli/cli_generators.rst index 3372a97f6b49..80634f8867dd 100644 --- a/user_guide_src/source/cli/cli_generators.rst +++ b/user_guide_src/source/cli/cli_generators.rst @@ -19,9 +19,12 @@ To view the full description and usage information on a particular generator, us .. code-block:: console php spark help + php spark --help where ```` will be replaced with the command to check. +.. note:: The single-letter option shortcuts listed below (``-n``, ``-s``, ``-f``) are available since v4.8.0. + .. note:: Do you need to have the generated code in a subfolder? Let's say if you want to create a controller class to reside in the ``Admin`` subfolder of the main ``Controllers`` folder, you will just need to prepend the subfolder to the class name, like this: ``php spark make:controller admin/login``. This @@ -156,9 +159,9 @@ Argument: Options: ======== -* ``--namespace``: Set the root namespace. Defaults to value of ``APP_NAMESPACE``. -* ``--suffix``: Append the component suffix to the generated class name. -* ``--force``: Set this flag to overwrite existing files on destination. +* ``--namespace`` (``-n``): Set the root namespace. Defaults to value of ``APP_NAMESPACE``. +* ``--suffix`` (``-s``): Append the component suffix to the generated class name. +* ``--force`` (``-f``): Set this flag to overwrite existing files on destination. make:filter ----------- @@ -177,9 +180,9 @@ Argument: Options: ======== -* ``--namespace``: Set the root namespace. Defaults to value of ``APP_NAMESPACE``. -* ``--suffix``: Append the component suffix to the generated class name. -* ``--force``: Set this flag to overwrite existing files on destination. +* ``--namespace`` (``-n``): Set the root namespace. Defaults to value of ``APP_NAMESPACE``. +* ``--suffix`` (``-s``): Append the component suffix to the generated class name. +* ``--force`` (``-f``): Set this flag to overwrite existing files on destination. make:model ---------- @@ -205,6 +208,27 @@ Options: * ``--suffix``: Append the component suffix to the generated class name. * ``--force``: Set this flag to overwrite existing files on destination. +make:request +------------ + +Creates a new FormRequest file. + +Usage: +====== +:: + + make:request [options] + +Argument: +========= +* ``name``: The name of the FormRequest class. **[REQUIRED]** + +Options: +======== +* ``--namespace`` (``-n``): Set the root namespace. Defaults to value of ``APP_NAMESPACE``. +* ``--suffix`` (``-s``): Append the component suffix to the generated class name. +* ``--force`` (``-f``): Set this flag to overwrite existing files on destination. + make:seeder ----------- @@ -222,9 +246,9 @@ Argument: Options: ======== -* ``--namespace``: Set the root namespace. Defaults to value of ``APP_NAMESPACE``. -* ``--suffix``: Append the component suffix to the generated class name. -* ``--force``: Set this flag to overwrite existing files on destination. +* ``--namespace`` (``-n``): Set the root namespace. Defaults to value of ``APP_NAMESPACE``. +* ``--suffix`` (``-s``): Append the component suffix to the generated class name. +* ``--force`` (``-f``): Set this flag to overwrite existing files on destination. .. _cli-generators-make-test: @@ -268,9 +292,9 @@ Argument: Options: ======== -* ``--namespace``: Set the root namespace. Defaults to value of ``APP_NAMESPACE``. -* ``--suffix``: Append the component suffix to the generated class name. -* ``--force``: Set this flag to overwrite existing files on destination. +* ``--namespace`` (``-n``): Set the root namespace. Defaults to value of ``APP_NAMESPACE``. +* ``--suffix`` (``-s``): Append the component suffix to the generated class name. +* ``--force`` (``-f``): Set this flag to overwrite existing files on destination. make:migration -------------- @@ -313,9 +337,9 @@ Argument: Options: ======== -* ``--namespace``: Set the root namespace. Defaults to value of ``APP_NAMESPACE``. -* ``--suffix``: Append the component suffix to the generated class name. -* ``--force``: Set this flag to overwrite existing files on destination. +* ``--namespace`` (``-n``): Set the root namespace. Defaults to value of ``APP_NAMESPACE``. +* ``--suffix`` (``-s``): Append the component suffix to the generated class name. +* ``--force`` (``-f``): Set this flag to overwrite existing files on destination. **************************************** Scaffolding a Complete Set of Stock Code diff --git a/user_guide_src/source/installation/upgrade_480.rst b/user_guide_src/source/installation/upgrade_480.rst index 873aec497c10..da80ada7dd73 100644 --- a/user_guide_src/source/installation/upgrade_480.rst +++ b/user_guide_src/source/installation/upgrade_480.rst @@ -161,6 +161,8 @@ Config - app/Config/Filters.php - Added a new filter named ``requestid`` that adds a unique request ID to each request in the application's context. +- app/Config/Generators.php + - ``Config\Generators::$views`` added entries for ``make:request``, ``make:test``, and ``make:transformer``, and dropped the stale ``session:migration`` entry. - app/Config/Mimes.php - ``Config\Mimes::$mimes`` added a new key ``md`` for Markdown files. - app/Config/Routing.php From 7076099921718495860f83d01daabee7c2323366 Mon Sep 17 00:00:00 2001 From: "John Paul E. Balandan, CPA" Date: Sun, 6 Sep 2026 19:35:09 +0800 Subject: [PATCH 2/2] allow `BaseCommand::call()` to also invoke modern commands --- system/CLI/BaseCommand.php | 20 ++++++++++++++++++- system/Commands/Generators/ModelGenerator.php | 4 +++- tests/system/CLI/BaseCommandTest.php | 16 +++++++++++++++ user_guide_src/source/changelogs/v4.8.0.rst | 2 ++ .../source/cli/cli_modern_commands.rst | 5 +++++ 5 files changed, 45 insertions(+), 2 deletions(-) diff --git a/system/CLI/BaseCommand.php b/system/CLI/BaseCommand.php index d047170c9e16..33022145ad2c 100644 --- a/system/CLI/BaseCommand.php +++ b/system/CLI/BaseCommand.php @@ -108,6 +108,8 @@ abstract public function run(array $params); /** * Can be used by a command to run other commands. * + * For a modern command, integer-keyed params are passed as arguments and string-keyed params as options. + * * @param array $params * * @return int|null @@ -116,7 +118,23 @@ abstract public function run(array $params); */ protected function call(string $command, array $params = []) { - return $this->commands->runLegacy($command, $params); + if ($this->commands->hasLegacyCommand($command) || ! $this->commands->hasModernCommand($command)) { + return $this->commands->runLegacy($command, $params); + } + + $arguments = []; + $options = []; + + foreach ($params as $key => $value) { + if (is_int($key)) { + assert(is_string($value)); + $arguments[] = $value; + } else { + $options[$key] = $value; + } + } + + return $this->commands->runCommand($command, $arguments, $options); } /** diff --git a/system/Commands/Generators/ModelGenerator.php b/system/Commands/Generators/ModelGenerator.php index 84b20c3a23c3..55f2cec2ae4c 100644 --- a/system/Commands/Generators/ModelGenerator.php +++ b/system/Commands/Generators/ModelGenerator.php @@ -130,7 +130,9 @@ protected function prepare(string $class): string // Call the entity generator with the fully-qualified class name so // it ends up under the correct sub-namespace/folder (eg. Admin). - $this->call('make:entity', array_merge([trim($entityClass, '\\')], $this->params)); + $entityOptions = array_intersect_key($this->params, array_flip(['namespace', 'suffix', 'force'])); + + $this->call('make:entity', array_merge([trim($entityClass, '\\')], $entityOptions)); $return = '\\' . trim($entityClass, '\\') . '::class'; } else { diff --git a/tests/system/CLI/BaseCommandTest.php b/tests/system/CLI/BaseCommandTest.php index 7923f7667ed2..b107966bb01b 100644 --- a/tests/system/CLI/BaseCommandTest.php +++ b/tests/system/CLI/BaseCommandTest.php @@ -60,6 +60,22 @@ public function testCallingOtherCommands(): void $this->assertStringContainsString('Displays basic usage information.', $this->getStreamFilterBuffer()); } + public function testCallingModernCommand(): void + { + $command = new class (single_service('logger'), single_service('commands')) extends BaseCommand { + protected $group = 'Fixtures'; + protected $name = 'legacy:bridge'; + + public function run(array $params) + { + return $this->call('app:about', ['hello', 'foo' => 'provided', 'quux' => null]); + } + }; + + $this->assertSame(EXIT_SUCCESS, $command->run([])); + $this->assertStringContainsString('CodeIgniter Version:', $this->getStreamFilterBuffer()); + } + public function testShowError(): void { $command = new AppInfo(single_service('logger'), single_service('commands')); diff --git a/user_guide_src/source/changelogs/v4.8.0.rst b/user_guide_src/source/changelogs/v4.8.0.rst index f784316d88e6..d296985cca1d 100644 --- a/user_guide_src/source/changelogs/v4.8.0.rst +++ b/user_guide_src/source/changelogs/v4.8.0.rst @@ -232,6 +232,8 @@ Commands - Added :php:class:`AbstractGeneratorCommand ` and the ``#[GeneratorCommand]`` attribute, the modern counterpart of ``GeneratorTrait`` for commands that generate files from templates. Generator commands built on it accept the ``-n``, ``-s``, and ``-f`` shortcuts for ``--namespace``, ``--suffix``, and ``--force``. See :doc:`../cli/cli_modern_generators`. +- Legacy ``BaseCommand::call()`` can now invoke modern commands: integer-keyed params are passed as arguments and string-keyed params as + options, which the target command validates like any other input. - Modern commands can now opt in to prompting for missing required arguments on interactive runs by implementing the new ``PromptsForMissingInputInterface`` marker interface. Prompt labels can be customized via ``getArgumentPromptLabels()``, and an ``afterPrompting()`` hook runs when prompting occurred. Non-interactive runs keep failing fast with the missing-arguments error. diff --git a/user_guide_src/source/cli/cli_modern_commands.rst b/user_guide_src/source/cli/cli_modern_commands.rst index ec21403842a7..9a7c73729907 100644 --- a/user_guide_src/source/cli/cli_modern_commands.rst +++ b/user_guide_src/source/cli/cli_modern_commands.rst @@ -465,6 +465,11 @@ The ``help`` command understands both styles — it delegates to the legacy ``showHelp()`` method for legacy commands and renders a structured view for modern ones. +A legacy command can call a modern one through ``$this->call()``: integer-keyed +params become positional arguments and string-keyed params become options, and +the modern command validates them like any other input. Unknown options or extra +arguments that a legacy target would have ignored are rejected. + .. note:: Legacy commands remain supported while the framework's own built-in