diff --git a/src/System/Integrate/Console/MakeCommand.php b/src/System/Integrate/Console/MakeCommand.php index 7ed0e96e..147f083b 100644 --- a/src/System/Integrate/Console/MakeCommand.php +++ b/src/System/Integrate/Console/MakeCommand.php @@ -4,25 +4,39 @@ namespace System\Integrate\Console; +use System\Collection\Collection; use System\Console\Command; -use System\Console\Traits\CommandTrait; +use System\Console\Prompt; +use System\Console\Style\Style; +use System\Console\Traits\PrintHelpTrait; +use System\Database\MyQuery; +use System\Database\MySchema\Table\Create; use System\Support\Facades\DB; -use System\Template\Generate; -use System\Template\Property; +use System\Support\Facades\PDO; +use System\Support\Facades\Schema; use function System\Console\fail; use function System\Console\info; use function System\Console\ok; -use function System\Console\text; +use function System\Console\style; use function System\Console\warn; /** - * @property bool $update - * @property bool $force + * @property ?int $take + * @property ?int $batch + * @property bool $force + * @property string|bool $seed */ -class MakeCommand extends Command +class MigrationCommand extends Command { - use CommandTrait; + use PrintHelpTrait; + + /** + * Register vendor migration path. + * + * @var string[] + */ + public static array $vendor_paths = []; /** * Register command. @@ -31,23 +45,35 @@ class MakeCommand extends Command */ public static array $command = [ [ - 'pattern' => 'make:controller', - 'fn' => [MakeCommand::class, 'make_controller'], + 'pattern' => 'migrate', + 'fn' => [self::class, 'main'], + ], [ + 'pattern' => 'migrate:fresh', + 'fn' => [self::class, 'fresh'], + ], [ + 'pattern' => 'migrate:reset', + 'fn' => [self::class, 'reset'], + ], [ + 'pattern' => 'migrate:refresh', + 'fn' => [self::class, 'refresh'], ], [ - 'pattern' => 'make:view', - 'fn' => [MakeCommand::class, 'make_view'], + 'pattern' => 'migrate:rollback', + 'fn' => [self::class, 'rollback'], ], [ - 'pattern' => 'make:services', - 'fn' => [MakeCommand::class, 'make_services'], + 'pattern' => ['database:create', 'db:create'], + 'fn' => [self::class, 'databaseCreate'], ], [ - 'pattern' => 'make:model', - 'fn' => [MakeCommand::class, 'make_model'], + 'pattern' => ['database:drop', 'db:drop'], + 'fn' => [self::class, 'databaseDrop'], ], [ - 'pattern' => 'make:command', - 'fn' => [MakeCommand::class, 'make_command'], + 'pattern' => ['database:show', 'db:show'], + 'fn' => [self::class, 'databaseShow'], ], [ - 'pattern' => 'make:migration', - 'fn' => [MakeCommand::class, 'make_migration'], + 'pattern' => 'migrate:status', + 'fn' => [self::class, 'status'], + ], [ + 'pattern' => 'migrate:init', + 'fn' => [self::class, 'initializeMigration'], ], ]; @@ -58,239 +84,613 @@ public function printHelp() { return [ 'commands' => [ - 'make:controller' => 'Generate new controller', - 'make:view' => 'Generate new view', - 'make:service' => 'Generate new service', - 'make:model' => 'Generate new model', - 'make:command' => 'Generate new command', - 'make:migration' => 'Generate new migration file', + 'migrate' => 'Run migration (up)', + 'migrate:fresh' => 'Drop database and run migrations', + 'migrate:reset' => 'Rolling back all migrations (down)', + 'migrate:refresh' => 'Rolling back and run migration all', + 'migrate:rollback' => 'Rolling back last migrations (down)', + 'migrate:init' => 'Initialize migartion table', + 'migrate:status' => 'Show migartion status.', + 'database:create' => 'Create database', + 'database:drop' => 'Drop database', + 'database:show' => 'Show database table', ], 'options' => [ - '--table-name' => 'Set table column when creating model.', - '--update' => 'Generate migration file with alter (update).', - '--force' => 'Force to creating template.', + '--take' => 'Number of migrations to be run.', + '--batch' => 'Batch migration excution.', + '--dry-run' => 'Excute migration but olny get query output.', + '--force' => 'Force runing migration/database query in production.', + '--seed' => 'Run seeder after migration.', + '--seed-namespace' => 'Run seeder after migration using class namespace.', + '--yes' => 'Accept it without having it ask any questions', ], 'relation' => [ - 'make:controller' => ['[controller_name]'], - 'make:view' => ['[view_name]'], - 'make:service' => ['[service_name]'], - 'make:model' => ['[model_name]', '--table-name', '--force'], - 'make:command' => ['[command_name]'], - 'make:migration' => ['[table_name]', '--update'], - ], + 'migrate' => ['--take', '--seed', '--dry-run', '--force'], + 'migrate:fresh' => ['--seed', '--dry-run', '--force'], + 'migrate:reset' => ['--dry-run', '--force'], + 'migrate:refresh' => ['--seed', '--dry-run', '--force'], + 'migrate:rollback' => ['--batch', '--take', '--dry-run', '--force'], + 'database:create' => ['--force'], + 'database:drop' => ['--force'], ], ]; } - public function make_controller(): int + private function DbName(): string { - info('Making controller file...')->out(false); + return app()->get('dsn.sql')['database_name']; + } - $success = $this->makeTemplate($this->OPTION[0], [ - 'template_location' => __DIR__ . '/stubs/controller', - 'save_location' => controllers_path(), - 'pattern' => '__controller__', - 'surfix' => 'Controller.php', - ]); + private function runInDev(): bool + { + if (app()->isDev() || $this->force) { + return true; + } - if ($success) { - ok('Finish created controller')->out(); + /* @var bool */ + return (new Prompt(style('Runing migration/database in production?')->textRed(), [ + 'yes' => fn () => true, + 'no' => fn () => false, + ], 'no')) + ->selection([ + style('yes')->textDim(), + ' no', + ]) + ->option(); + } - return 0; + /** + * @param string|Style $message + */ + private function confirmation($message): bool + { + if ($this->option('yes', false)) { + return true; } - fail('Failed Create controller')->out(); - - return 1; + /* @var bool */ + return (new Prompt($message, [ + 'yes' => fn () => true, + 'no' => fn () => false, + ], 'no')) + ->selection([ + style('yes')->textDim(), + ' no', + ]) + ->option(); } - public function make_view(): int + /** + * Get migration list. + * + * @param int|false $batch + * + * @return Collection> + */ + public function baseMigrate(&$batch = false): Collection { - info('Making view file...')->out(false); + $migartion_batch = $this->getMigrationTable(); + $hights = $migartion_batch->lenght() > 0 + ? $migartion_batch->max() + 1 + : 0; + $batch = false === $batch ? $hights : $batch; + + $paths = [migration_path(), ...static::$vendor_paths]; + $migrate = new Collection([]); + foreach ($paths as $dir) { + foreach (new \DirectoryIterator($dir) as $file) { + if ($file->isDot() | $file->isDir()) { + continue; + } - $success = $this->makeTemplate($this->OPTION[0], [ - 'template_location' => __DIR__ . '/stubs/view', - 'save_location' => view_path(), - 'pattern' => '__view__', - 'surfix' => '.template.php', - ]); + $migration_name = pathinfo($file->getBasename(), PATHINFO_FILENAME); + $hasMigration = $migartion_batch->has($migration_name); - if ($success) { - ok('Finish created view file')->out(); + if (false == $batch && $hasMigration) { + if ($migartion_batch->get($migration_name) <= $hights - 1) { + $migrate->set($migration_name, [ + 'file_name' => $dir . $file->getFilename(), + 'batch' => $migartion_batch->get($migration_name), + ]); + continue; + } + } - return 0; + if (false === $hasMigration) { + $migrate->set($migration_name, [ + 'file_name' => $dir . $file->getFilename(), + 'batch' => $hights, + ]); + $this->insertMigrationTable([ + 'migration' => $migration_name, + 'batch' => $hights, + ]); + continue; + } + + if ($migartion_batch->get($migration_name) <= $batch) { + $migrate->set($migration_name, [ + 'file_name' => $dir . $file->getFilename(), + 'batch' => $migartion_batch->get($migration_name), + ]); + continue; + } + } } - fail('Failed Create view file')->out(); + return $migrate; + } - return 1; + public function main(): int + { + return $this->migration(); } - public function make_services(): int + public function migration(bool $silent = false): int { - info('Making service file...')->out(false); + if (false === $this->runInDev() && false === $silent) { + return 2; + } - $success = $this->makeTemplate($this->OPTION[0], [ - 'template_location' => __DIR__ . '/stubs/service', - 'save_location' => services_path(), - 'pattern' => '__service__', - 'surfix' => 'Service.php', - ]); + $print = new Style(); + $width = $this->getWidth(40, 60); + $batch = false; + $migrate = $this->baseMigrate($batch); + $migrate + ->filter(static fn ($value): bool => $value['batch'] == $batch) + ->sort(); - if ($success) { - ok('Finish created services file')->out(); + $print->tap(info('Running migration')); - return 0; + foreach ($migrate as $key => $val) { + $schema = require_once $val['file_name']; + $up = new Collection($schema['up'] ?? []); + + if ($this->option('dry-run')) { + $up->each(function ($item) use ($print) { + $print->push($item->__toString())->textDim()->newLines(2); + + return true; + }); + continue; + } + + $print->push($key)->textDim(); + $print->repeat('.', $width - strlen($key))->textDim(); + + try { + $success = $up->every(fn ($item) => $item->execute()); + } catch (\Throwable $th) { + $success = false; + fail($th->getMessage())->out(false); + } + + if ($success) { + $print->push('DONE')->textGreen()->newLines(); + continue; + } + + $print->push('FAIL')->textRed()->newLines(); } - fail('Failed Create services file')->out(); + $print->out(); - return 1; + return $this->seed(); } - public function make_model(): int + public function fresh(bool $silent = false): int { - info('Making model file...')->out(false); - $name = ucfirst($this->OPTION[0]); - $model_location = model_path() . $name . '.php'; + // drop and recreate database + if (($drop = $this->databaseDrop($silent)) > 0) { + return $drop; + } + if (($create = $this->databaseCreate(true)) > 0) { + return $create; + } - if (file_exists($model_location) && false === $this->option('force', false)) { - warn('File already exist')->out(false); - fail('Failed Create model file')->out(); + // run migration - return 1; - } + $print = new Style(); + $migrate = $this->baseMigrate()->sort(); + $width = $this->getWidth(40, 60); + + $print->tap(info('Running migration')); + + foreach ($migrate as $key => $val) { + $schema = require_once $val['file_name']; + $up = new Collection($schema['up'] ?? []); + + if ($this->option('dry-run')) { + $up->each(function ($item) use ($print) { + $print->push($item->__toString())->textDim()->newLines(2); + + return true; + }); + continue; + } + + $print->push($key)->textDim(); + $print->repeat('.', $width - strlen($key))->textDim(); - info('Creating Model class in ' . $model_location)->out(false); - - $class = new Generate($name); - $class->customizeTemplate("tabSize(4); - $class->tabIndent(' '); - $class->setEndWithNewLine(); - $class->namespace('App\\Models'); - $class->uses(['System\Database\MyModel\Model']); - $class->extend('Model'); - - $primery_key = 'id'; - $table_name = $this->OPTION[0]; - if ($this->option('table-name', false)) { - $table_name = $this->option('table-name'); - info("Getting Information from table {$table_name}.")->out(false); try { - foreach (DB::table($table_name)->info() as $column) { - $class->addComment('@property mixed $' . $column['COLUMN_NAME']); - if ('PRI' === $column['COLUMN_KEY']) { - $primery_key = $column['COLUMN_NAME']; - } - } + $success = $up->every(fn ($item) => $item->execute()); } catch (\Throwable $th) { - warn($th->getMessage())->out(false); + $success = false; + fail($th->getMessage())->out(false); + } + + if ($success) { + $print->push('DONE')->textGreen()->newLines(); + continue; } + + $print->push('FAIL')->textRed()->newLines(); } - $class->addProperty('table_name')->visibility(Property::PROTECTED_)->dataType('string')->expecting(" = '{$table_name}'"); - $class->addProperty('primery_key')->visibility(Property::PROTECTED_)->dataType('string')->expecting("= '{$primery_key}'"); + $print->out(); - if (false === file_put_contents($model_location, $class->generate())) { - fail('Failed Create model file')->out(); + return $this->seed(); + } - return 1; + public function reset(bool $silent = false): int + { + if (false === $this->runInDev() && false === $silent) { + return 2; } + info('Rolling back all migrations')->out(false); + $rollback = $this->rollbacks(false, 0); - ok("Finish created model file `App\\Models\\{$name}`")->out(); + return $rollback; + } + + public function refresh(): int + { + if (false === $this->runInDev()) { + return 2; + } + + if (($reset = $this->reset(true)) > 0) { + return $reset; + } + if (($migration = $this->migration(true)) > 0) { + return $migration; + } return 0; } + public function rollback(): int + { + if (false === ($batch = $this->option('batch', false))) { + fail('batch is required.')->out(); + + return 1; + } + $take = $this->take; + $message = "Rolling {$take} back migrations."; + if ($take < 0) { + $take = 0; + $message = 'Rolling back migrations.'; + } + info($message)->out(false); + + return $this->rollbacks((int) $batch, (int) $take); + } + /** - * Replece template to new class/resoure. + * Rolling backs migartion. * - * @param string $argument Name of Class/file - * @param array $make_option Configuration to replace template - * @param string $folder Create folder for save location - * - * @return bool True if templete success copie + * @param int|false $batch */ - private function makeTemplate(string $argument, array $make_option, string $folder = ''): bool + public function rollbacks($batch, int $take): int { - $folder = ucfirst($folder); - if (file_exists($file_name = $make_option['save_location'] . $folder . $argument . $make_option['surfix'])) { - warn('File already exist')->out(false); + $print = new Style(); + $width = $this->getWidth(40, 60); - return false; - } + $migrate = false === $batch + ? $this->baseMigrate($batch) + : $this->baseMigrate($batch)->filter(static fn ($value): bool => $value['batch'] >= $batch - $take); + + foreach ($migrate->sortDesc() as $key => $val) { + $schema = require_once $val['file_name']; + $down = new Collection($schema['down'] ?? []); + + if ($this->option('dry-run')) { + $down->each(function ($item) use ($print) { + $print->push($item->__toString())->textDim()->newLines(2); + + return true; + }); + continue; + } - if ('' !== $folder && !is_dir($make_option['save_location'] . $folder)) { - mkdir($make_option['save_location'] . $folder); + $print->push($key)->textDim(); + $print->repeat('.', $width - strlen($key))->textDim(); + + try { + $success = $down->every(fn ($item) => $item->execute()); + } catch (\Throwable $th) { + $success = false; + fail($th->getMessage())->out(false); + } + + if ($success) { + $print->push('DONE')->textGreen()->newLines(); + continue; + } + + $print->push('FAIL')->textRed()->newLines(); } - $get_template = file_get_contents($make_option['template_location']); - $get_template = str_replace($make_option['pattern'], ucfirst($argument), $get_template); - $get_template = preg_replace('/^.+\n/', '', $get_template); - $isCopied = file_put_contents($file_name, $get_template); + $print->out(); - return $isCopied === false ? false : true; + return 0; } - public function make_command(): int + public function databaseCreate(bool $silent=false): int { - info('Making command file...')->out(false); - $name = $this->OPTION[0]; - $success = $this->makeTemplate($name, [ - 'template_location' => __DIR__ . '/stubs/command', - 'save_location' => commands_path(), - 'pattern' => '__command__', - 'surfix' => 'Command.php', - ]); + $db_name = $this->DbName(); + $message = style("Do you want to create database `{$db_name}`?")->textBlue(); + + if (false === $silent && (!$this->runInDev() || !$this->confirmation($message))) { + return 2; + } + + info("creating database `{$db_name}`")->out(false); + + $success = Schema::create()->database($db_name)->ifNotExists()->execute(); if ($success) { - $geContent = file_get_contents(config_path() . 'command.config.php'); - $geContent = str_replace( - '// more command here', - "// {$name} \n\t" . 'App\\Commands\\' . $name . 'Command::$' . "command\n\t// more command here", - $geContent - ); + ok("success create database `{$db_name}`")->out(false); - file_put_contents(config_path() . 'command.config.php', $geContent); + $this->initializeMigration(); - ok('Finish created command file')->out(); + return 0; + } + + fail("cant created database `{$db_name}`")->out(false); + + return 1; + } + + public function databaseDrop(bool $silent = false): int + { + $db_name = $this->DbName(); + $message = style("Do you want to drop database `{$db_name}`?")->textRed(); + + if (false === $silent && (!$this->runInDev() || !$this->confirmation($message))) { + return 2; + } + + info("try to drop database `{$db_name}`")->out(false); + + $success = Schema::drop()->database($db_name)->ifExists(true)->execute(); + + if ($success) { + ok("success drop database `{$db_name}`")->out(false); return 0; } - fail("\nFailed Create command file")->out(); + fail("cant drop database `{$db_name}`")->out(false); return 1; } - public function make_migration(): int + public function databaseShow(): int { - info('Making migration')->out(false); - - $name = $this->OPTION[0] ?? false; - if (false === $name) { - warn('Table name cant be empty.')->out(false); - do { - $name = text('Fill the table name?', static fn ($text) => $text); - } while ($name === '' || $name === false); + if ($this->option('table-name')) { + return $this->tableShow($this->option('table-name', null)); } - $name = strtolower($name); - $path_to_file = migration_path(); - $bath = now()->format('Y_m_d_His'); - $file_name = "{$path_to_file}{$bath}_{$name}.php"; + $db_name = $this->DbName(); + $width = $this->getWidth(40, 60); + info('showing database')->out(false); - $use = $this->update ? 'migration_update' : 'migration'; - $template = file_get_contents(__DIR__ . DIRECTORY_SEPARATOR . 'stubs' . DIRECTORY_SEPARATOR . $use); - $template = str_replace('__table__', $name, $template); + $tables = PDO::instance() + ->query('SHOW DATABASES') + ->query(' + SELECT table_name, create_time, ROUND((DATA_LENGTH + INDEX_LENGTH) / 1024 / 1024) AS `size` + FROM information_schema.tables + WHERE table_schema = :db_name') + ->bind(':db_name', $db_name) + ->resultset(); - if (false === file_exists($path_to_file) || false === file_put_contents($file_name, $template)) { - fail('Can\'t create migration file.')->out(); + if (0 === count($tables)) { + warn('table is empty try to run migration')->out(); - return 1; + return 2; + } + + foreach ($tables as $table) { + $name = $table['table_name']; + $time = $table['create_time']; + $size = $table['size']; + $lenght = strlen($name) + strlen($time) + strlen($size); + + style($name) + ->push(' ' . $size . ' Mb ')->textDim() + ->repeat('.', $width - $lenght)->textDim() + ->push(' ' . $time) + ->out(); + } + + return 0; + } + + public function tableShow(string $table): int + { + $table = (new MyQuery(PDO::instance()))->table($table)->info(); + $print = new Style("\n"); + $width = $this->getWidth(40, 60); + + $print->push('column')->textYellow()->bold()->resetDecorate()->newLines(); + foreach ($table as $column) { + $will_print = []; + + if ($column['IS_NULLABLE'] === 'YES') { + $will_print[] = 'nullable'; + } + if ($column['COLUMN_KEY'] === 'PRI') { + $will_print[] = 'primary'; + } + + $info = implode(', ', $will_print); + $lenght = strlen($column['COLUMN_NAME']) + strlen($column['COLUMN_TYPE']) + strlen($info); + + $print->push($column['COLUMN_NAME'])->bold()->resetDecorate(); + $print->push(' ' . $info . ' ')->textDim(); + $print->repeat('.', $width - $lenght)->textDim(); + $print->push(' ' . $column['COLUMN_TYPE']); + $print->newLines(); + } + + $print->out(); + + return 0; + } + + public function status(): int + { + $print = new Style(); + $print->tap(info('show migration status')); + $width = $this->getWidth(40, 60); + foreach ($this->getMigrationTable() as $migration_name => $batch) { + $lenght = strlen($migration_name) + strlen((string) $batch); + $print + ->push($migration_name) + ->push(' ') + ->repeat('.', $width - $lenght)->textDim() + ->push(' ') + ->push($batch) + ->newLines(); + } + + $print->out(); + + return 0; + } + + /** + * Integrate seeder during run migration. + */ + private function seed(): int + { + if ($this->option('dry-run', false)) { + return 0; + } + if ($this->seed) { + $seed = true === $this->seed ? null : $this->seed; + + return (new SeedCommand([], ['class' => $seed]))->main(); + } + + $namespace = $this->option('seed-namespace', false); + if ($namespace) { + $namespace = true === $namespace ? null : $namespace; + + return (new SeedCommand([], ['name-space' => $namespace]))->main(); } - ok('Success create migration file.')->out(); return 0; } + + /** + * Check for migration table exist or not in this current database. + */ + private function hasMigrationTable(): bool + { + $result = PDO::instance()->query( + "SELECT COUNT(table_name) as total + FROM information_schema.tables + WHERE table_schema = :dbname + AND table_name = 'migration'" + )->bind(':dbname', $this->DbName()) + ->single(); + + if ($result) { + return $result['total'] > 0; + } + + return false; + } + + /** + * Create migarion table schema. + */ + private function createMigrationTable(): bool + { + return Schema::table('migration', function (Create $column) { + $column('migration')->varchar(100)->notNull(); + $column('batch')->int(4)->notNull(); + + $column->unique('migration'); + })->execute(); + } + + /** + * Get migration batch file in migation table. + * + * @return Collection + */ + private function getMigrationTable(): Collection + { + /** @var Collection */ + $pair = DB::table('migration') + ->select() + ->get() + ->assocBy(static fn ($item) => [$item['migration'] => (int) $item['batch']]); + + return $pair; + } + + /** + * Save insert migration file with batch to migration table. + * + * @param array $migration + */ + private function insertMigrationTable($migration): bool + { + return DB::table('migration') + ->insert() + ->values($migration) + ->execute() + ; + } + + public function initializeMigration(): int + { + $has_migration_table = $this->hasMigrationTable(); + + if ($has_migration_table) { + info('Migration table alredy exist on your database table.')->out(false); + + return 0; + } + + if ($this->createMigrationTable()) { + ok('Success create migration table.')->out(false); + + return 0; + } + + fail('Migration table cant be create.')->out(false); + + return 1; + } + + /** + * Add migration from vendor path. + */ + public static function addVendorMigrationPath(string $path): void + { + static::$vendor_paths[] = $path; + } + + /** + * Flush migration vendor ptahs. + */ + public static function flushVendorMigrationPaths(): void + { + static::$vendor_paths = []; + } }