diff --git a/system/Commands/Database/CreateDatabase.php b/system/Commands/Database/CreateDatabase.php index a586eddf55cc..ce1cf3159365 100644 --- a/system/Commands/Database/CreateDatabase.php +++ b/system/Commands/Database/CreateDatabase.php @@ -13,75 +13,60 @@ namespace CodeIgniter\Commands\Database; -use CodeIgniter\CLI\BaseCommand; +use CodeIgniter\CLI\AbstractCommand; +use CodeIgniter\CLI\Attributes\Command; use CodeIgniter\CLI\CLI; +use CodeIgniter\CLI\Input\Argument; +use CodeIgniter\CLI\Input\Option; use CodeIgniter\Config\Factories; -use CodeIgniter\Database\SQLite3\Connection; +use CodeIgniter\Database\SQLite3\Connection as SQLite3Connection; use Config\Database; use Throwable; /** * Creates a new database. */ -class CreateDatabase extends BaseCommand +#[Command(name: 'db:create', description: 'Create a new database schema.', group: 'Database')] +class CreateDatabase extends AbstractCommand { /** - * The group the command is lumped under - * when listing commands. - * - * @var string + * @var list */ - protected $group = 'Database'; + private const VALID_EXTENSIONS = ['db', 'sqlite']; - /** - * The Command's name - * - * @var string - */ - protected $name = 'db:create'; + protected function configure(): void + { + $this + ->addArgument(new Argument( + name: 'db_name', + description: 'The database name to use.', + required: true, + )) + ->addOption(new Option( + name: 'ext', + description: 'File extension of the database file for SQLite3. Can be `db` or `sqlite`.', + requiresValue: true, + default: 'db', + )); + } - /** - * the Command's short description - * - * @var string - */ - protected $description = 'Create a new database schema.'; + protected function interact(array &$arguments, array &$options): void + { + if ($arguments === []) { + $arguments[] = CLI::prompt('Database name', null, 'required'); + } - /** - * the Command's usage - * - * @var string - */ - protected $usage = 'db:create [options]'; + $ext = $this->getUnboundOption('ext', $options); - /** - * The Command's arguments - * - * @var array - */ - protected $arguments = [ - 'db_name' => 'The database name to use', - ]; - - /** - * The Command's options - * - * @var array - */ - protected $options = [ - '--ext' => 'File extension of the database file for SQLite3. Can be `db` or `sqlite`. Defaults to `db`.', - ]; + 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'); + } + } - /** - * Creates a new database. - */ - public function run(array $params) + protected function execute(array $arguments, array $options): int { - $name = array_shift($params); - - if (empty($name)) { - $name = CLI::prompt('Database name', null, 'required'); // @codeCoverageIgnore - } + $name = $arguments['db_name']; + assert(is_string($name)); try { $config = config(Database::class); @@ -93,12 +78,14 @@ public function run(array $params) $db = Database::connect(); - // Special SQLite3 handling - if ($db instanceof Connection) { - $ext = $params['ext'] ?? CLI::getOption('ext') ?? 'db'; + if ($db instanceof SQLite3Connection) { + $ext = $options['ext']; + assert(is_string($ext)); - if (! in_array($ext, ['db', 'sqlite'], true)) { - $ext = CLI::prompt('Please choose a valid file extension', ['db', 'sqlite']); // @codeCoverageIgnore + if (! in_array($ext, self::VALID_EXTENSIONS, true)) { + CLI::error(sprintf('Invalid file extension "%s". Use either `db` or `sqlite`.', $ext), 'light_gray', 'red'); + + return EXIT_ERROR; } if ($name !== ':memory:') { @@ -113,7 +100,6 @@ public function run(array $params) if (is_file($dbName)) { CLI::error("Database \"{$dbName}\" already exists.", 'light_gray', 'red'); - CLI::newLine(); return EXIT_ERROR; } @@ -128,26 +114,21 @@ public function run(array $params) if (! is_file($db->getDatabase()) && $name !== ':memory:') { // @codeCoverageIgnoreStart CLI::error('Database creation failed.', 'light_gray', 'red'); - CLI::newLine(); return EXIT_ERROR; // @codeCoverageIgnoreEnd } } elseif (! Database::forge()->createDatabase($name)) { - // @codeCoverageIgnoreStart CLI::error('Database creation failed.', 'light_gray', 'red'); - CLI::newLine(); return EXIT_ERROR; - // @codeCoverageIgnoreEnd } CLI::write("Database \"{$name}\" successfully created.", 'green'); - CLI::newLine(); return EXIT_SUCCESS; } catch (Throwable $e) { - $this->showError($e); + $this->renderThrowable($e); return EXIT_ERROR; } finally { diff --git a/tests/system/Commands/CreateDatabaseTest.php b/tests/system/Commands/CreateDatabaseTest.php index 8bae8282f931..d45ff4efe6b6 100644 --- a/tests/system/Commands/CreateDatabaseTest.php +++ b/tests/system/Commands/CreateDatabaseTest.php @@ -13,10 +13,12 @@ namespace CodeIgniter\Commands; +use CodeIgniter\CLI\CLI; use CodeIgniter\Database\BaseConnection; use CodeIgniter\Database\OCI8\Connection as OCI8Connection; use CodeIgniter\Database\SQLite3\Connection as SQLite3Connection; use CodeIgniter\Test\CIUnitTestCase; +use CodeIgniter\Test\Mock\MockInputOutput; use CodeIgniter\Test\StreamFilterTrait; use Config\Database; use PHPUnit\Framework\Attributes\Group; @@ -39,6 +41,7 @@ protected function setUp(): void parent::setUp(); + CLI::resetLastWrite(); $this->dropDatabase(); } @@ -46,6 +49,7 @@ protected function tearDown(): void { parent::tearDown(); + CLI::reset(); $this->dropDatabase(); } @@ -81,9 +85,9 @@ private function closeDatabaseConnections(): void $this->setPrivateProperty(Database::class, 'instances', []); } - protected function getBuffer(): string + private function getUndecoratedBuffer(): string { - return $this->getStreamFilterBuffer(); + return preg_replace('/\e\[[^m]+m/', '', $this->getStreamFilterBuffer()) ?? ''; } public function testCreateDatabase(): void @@ -92,8 +96,17 @@ public function testCreateDatabase(): void $this->markTestSkipped('Needs to run on non-OCI8 drivers.'); } + $name = $this->connection instanceof SQLite3Connection ? 'database.db' : 'database'; + command('db:create database'); - $this->assertStringContainsString('successfully created.', $this->getBuffer()); + $this->assertSame( + <<getUndecoratedBuffer(), + ); } public function testSqliteDatabaseDuplicated(): void @@ -104,9 +117,19 @@ public function testSqliteDatabaseDuplicated(): void command('db:create database'); $this->resetStreamFilterBuffer(); + CLI::resetLastWrite(); + + $database = WRITEPATH . 'database.db'; command('db:create database --ext db'); - $this->assertStringContainsString('already exists.', $this->getBuffer()); + $this->assertSame( + <<getUndecoratedBuffer(), + ); } public function testOtherDriverDuplicatedDatabase(): void @@ -119,6 +142,69 @@ public function testOtherDriverDuplicatedDatabase(): void $this->resetStreamFilterBuffer(); command('db:create database'); - $this->assertStringContainsString('Unable to create the specified database.', $this->getBuffer()); + $this->assertStringContainsString('Unable to create the specified database.', $this->getUndecoratedBuffer()); + } + + public function testOtherDriverCreationFailsWithoutDebug(): void + { + if ($this->connection instanceof SQLite3Connection || $this->connection instanceof OCI8Connection) { + $this->markTestSkipped('Needs to run on non-SQLite3 and non-OCI8 drivers.'); + } + + command('db:create database'); + $this->resetStreamFilterBuffer(); + + $this->connection = Database::connect(); + $this->setPrivateProperty($this->connection, 'DBDebug', false); + + CLI::resetLastWrite(); + command('db:create database'); + $this->assertSame( + <<<'EOT' + + Database creation failed. + + EOT, + $this->getUndecoratedBuffer(), + ); + } + + public function testSqlitePromptsForInvalidExtension(): void + { + if (! $this->connection instanceof SQLite3Connection) { + $this->markTestSkipped('Needs to run on SQLite3.'); + } + + $io = new MockInputOutput(); + $io->setInputs(['db']); + CLI::setInputOutput($io); + + command('db:create database --ext txt'); + + $this->assertSame( + <<<'EOT' + Please choose a valid file extension [db, sqlite]: db + Database "database.db" successfully created. + + EOT, + preg_replace('/\e\[[^m]+m/', '', $io->getOutput()) ?? '', + ); + } + + public function testSqliteRejectsInvalidExtensionWhenNonInteractive(): void + { + if (! $this->connection instanceof SQLite3Connection) { + $this->markTestSkipped('Needs to run on SQLite3.'); + } + + command('db:create database --ext txt --no-interaction'); + $this->assertSame( + <<<'EOT' + + Invalid file extension "txt". Use either `db` or `sqlite`. + + EOT, + $this->getUndecoratedBuffer(), + ); } } diff --git a/utils/phpstan-baseline/empty.notAllowed.neon b/utils/phpstan-baseline/empty.notAllowed.neon index 97d0b83db012..61c7d2319bca 100644 --- a/utils/phpstan-baseline/empty.notAllowed.neon +++ b/utils/phpstan-baseline/empty.notAllowed.neon @@ -1,12 +1,7 @@ -# total 210 errors +# total 209 errors parameters: ignoreErrors: - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - count: 1 - path: ../../system/Commands/Database/CreateDatabase.php - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' count: 1