From ab7dd621cdbbbe68e8380cb44838c63fd0cfd287 Mon Sep 17 00:00:00 2001 From: Dan Nita Date: Fri, 8 Nov 2024 14:40:19 +0000 Subject: [PATCH 1/2] create conversions --- .../Commands/ConversionStatusCommand.php | 50 +++++ .../Commands/MakeConversionCommand.php | 169 ++++++++++++++++ .../Commands/RunConversionsCommand.php | 183 ++++++++++++++++++ composer.json | 3 +- .../conversions/2024_11_08_135915_example.php | 25 +++ ..._11_08_124034_create_conversions_table.php | 28 +++ 6 files changed, 457 insertions(+), 1 deletion(-) create mode 100644 app/Console/Commands/ConversionStatusCommand.php create mode 100644 app/Console/Commands/MakeConversionCommand.php create mode 100644 app/Console/Commands/RunConversionsCommand.php create mode 100644 database/conversions/2024_11_08_135915_example.php create mode 100644 database/migrations/2024_11_08_124034_create_conversions_table.php diff --git a/app/Console/Commands/ConversionStatusCommand.php b/app/Console/Commands/ConversionStatusCommand.php new file mode 100644 index 000000000..b0e3a557b --- /dev/null +++ b/app/Console/Commands/ConversionStatusCommand.php @@ -0,0 +1,50 @@ +ensureConversionsTableExists(); + + $executed = DB::table('conversions')->pluck('conversion')->toArray(); + $files = File::glob(app_path('Conversions') . '/*.php'); + + $statuses = []; + + foreach ($files as $file) { + $class = $this->getClassName($file); + $status = in_array($class, $executed) ? 'Executed' : 'Pending'; + $statuses[] = ['Conversion' => $class, 'Status' => $status]; + } + + $this->table(['Conversion', 'Status'], $statuses); + } + + protected function ensureConversionsTableExists() + { + if (!DB::schema()->hasTable('conversions')) { + $this->error('Conversions table does not exist. Run "php artisan conversion" first.'); + exit; + } + } + + protected function getClassName($filePath) + { + $content = file_get_contents($filePath); + + $namespace = Str::between($content, 'namespace ', ';'); + $class = Str::between($content, 'class ', ' '); + + return "{$namespace}\\{$class}"; + } +} diff --git a/app/Console/Commands/MakeConversionCommand.php b/app/Console/Commands/MakeConversionCommand.php new file mode 100644 index 000000000..2f98682c5 --- /dev/null +++ b/app/Console/Commands/MakeConversionCommand.php @@ -0,0 +1,169 @@ +files = $files; + $this->composer = $composer; + } + + /** + * Execute the console command. + * + * @return int + */ + public function handle() + { + // Get the name of the conversion + $name = trim($this->argument('name')); + + // Generate the class name + $className = $this->getClassName($name); + + // Generate the file path + $filePath = $this->getFilePath($name); + + // Build the class content + $classContent = $this->buildClass($className); + + // Create the conversion file + if ($this->files->exists($filePath)) { + $this->error("Conversion {$className} already exists!"); + return 1; + } + + $this->makeDirectory($filePath); + + $this->files->put($filePath, $classContent); + + $this->info("Created Conversion: " . basename($filePath)); + + // Dump the autoloads + $this->composer->dumpAutoloads(); + + return 0; + } + + /** + * Get the class name from the conversion name. + * + * @param string $name + * @return string + */ + protected function getClassName($name) + { + return Str::studly($name); + } + + /** + * Get the file path for the conversion. + * + * @param string $name + * @return string + */ + protected function getFilePath($name) + { + $timestamp = date('Y_m_d_His'); + $fileName = $timestamp . '_' . $name . '.php'; + return database_path('conversions') . '/' . $fileName; + } + + /** + * Build the class content. + * + * @param string $className + * @return string + */ + protected function buildClass($className) + { + return <<files->isDirectory($directory)) { + $this->files->makeDirectory($directory, 0755, true, true); + } + } +} diff --git a/app/Console/Commands/RunConversionsCommand.php b/app/Console/Commands/RunConversionsCommand.php new file mode 100644 index 000000000..3839c4bbb --- /dev/null +++ b/app/Console/Commands/RunConversionsCommand.php @@ -0,0 +1,183 @@ +ensureConversionsTableExists(); + + if ($this->option('rollback')) { + $steps = (int) $this->option('step'); + $this->rollbackConversions($steps); + } else { + $this->runConversions(); + } + + return 0; + } + + /** + * Ensure that the conversions table exists. + * + * @return void + */ + protected function ensureConversionsTableExists() + { + if (!DB::connection()->getSchemaBuilder()->hasTable('conversions')) { + DB::connection()->getSchemaBuilder()->create('conversions', function ($table) { + $table->increments('id'); + $table->string('conversion'); + $table->integer('batch'); + $table->timestamps(); + }); + $this->info('Created conversions table.'); + } + } + + /** + * Run all pending conversions. + * + * @return void + */ + protected function runConversions() + { + $executed = DB::table('conversions')->pluck('conversion')->toArray(); + $lastBatch = DB::table('conversions')->max('batch'); + $batch = $lastBatch ? $lastBatch + 1 : 1; + + $files = File::glob(database_path('conversions') . '/*.php'); + + // Sort files to ensure they run in order + sort($files); + + foreach ($files as $file) { + $fileName = basename($file, '.php'); + + if (!in_array($fileName, $executed)) { + $this->line("Running: {$fileName}"); + + try { + $instance = include $file; + + if (!is_object($instance)) { + $this->error("The file {$fileName}.php did not return a class instance."); + continue; + } + + if (!method_exists($instance, 'up')) { + $this->error("Method 'up' does not exist in the conversion returned by {$fileName}.php"); + continue; + } + + $instance->up(); + + // If no exception occurred, consider it successful + DB::table('conversions')->insert([ + 'conversion' => $fileName, + 'batch' => $batch, + 'created_at' => now(), + 'updated_at' => now(), + ]); + + $this->info("Completed: {$fileName}"); + } catch (\Exception $e) { + $this->error("Failed to run {$fileName}: {$e->getMessage()}"); + } + } + } + } + + /** + * Rollback the specified number of batches. + * + * @param int $steps + * @return void + */ + protected function rollbackConversions($steps) + { + $lastBatch = DB::table('conversions')->max('batch'); + + if ($lastBatch === null) { + $this->info('No conversions to rollback.'); + return; + } + + $batches = DB::table('conversions') + ->select('batch') + ->distinct() + ->orderBy('batch', 'desc') + ->limit($steps) + ->pluck('batch') + ->toArray(); + + if (empty($batches)) { + $this->info('No conversions to rollback.'); + return; + } + + foreach ($batches as $batch) { + $conversions = DB::table('conversions') + ->where('batch', $batch) + ->orderBy('id', 'desc') + ->get(); + + foreach ($conversions as $conversion) { + $fileName = $conversion->conversion; + $file = database_path('conversions') . '/' . $fileName . '.php'; + + if (File::exists($file)) { + $this->line("Rolling back: {$fileName}"); + + try { + $instance = include $file; + + if (!is_object($instance)) { + $this->error("The file {$fileName}.php did not return a class instance."); + continue; + } + + if (!method_exists($instance, 'down')) { + $this->error("Method 'down' does not exist in the conversion returned by {$fileName}.php"); + continue; + } + + $instance->down(); + + DB::table('conversions')->where('id', $conversion->id)->delete(); + + $this->info("Rolled back: {$fileName}"); + } catch (\Exception $e) { + $this->error("Failed to rollback {$fileName}: {$e->getMessage()}"); + } + } else { + $this->error("Conversion file {$fileName}.php does not exist."); + } + } + } + } +} diff --git a/composer.json b/composer.json index 93bc6fdfa..814376e9f 100644 --- a/composer.json +++ b/composer.json @@ -70,7 +70,8 @@ "Database\\Seeders\\": "database/seeders/", "Database\\Seeders\\Omop\\": "database/seeders/omop", "Database\\Migrations\\": "database/migrations/", - "Database\\Beta\\": "database/beta/" + "Database\\Beta\\": "database/beta/", + "Database\\Conversions\\": "database/conversions/" } }, "autoload-dev": { diff --git a/database/conversions/2024_11_08_135915_example.php b/database/conversions/2024_11_08_135915_example.php new file mode 100644 index 000000000..0198a9620 --- /dev/null +++ b/database/conversions/2024_11_08_135915_example.php @@ -0,0 +1,25 @@ +id(); + $table->string('conversion')->unique(); + $table->integer('batch'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('conversions'); + } +}; From 2292bacea1f307073cada3dfe8d3773787055617 Mon Sep 17 00:00:00 2001 From: Dan Nita Date: Fri, 8 Nov 2024 14:48:57 +0000 Subject: [PATCH 2/2] update --- .../Commands/ConversionStatusCommand.php | 98 +++++++++++++++---- 1 file changed, 77 insertions(+), 21 deletions(-) diff --git a/app/Console/Commands/ConversionStatusCommand.php b/app/Console/Commands/ConversionStatusCommand.php index b0e3a557b..9c555cbf3 100644 --- a/app/Console/Commands/ConversionStatusCommand.php +++ b/app/Console/Commands/ConversionStatusCommand.php @@ -5,46 +5,102 @@ use Illuminate\Console\Command; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\File; -use Illuminate\Support\Str; class ConversionStatusCommand extends Command { + /** + * The name and signature of the console command. + * + * @var string + */ protected $signature = 'conversion:status'; - protected $description = 'Show the status of each conversion'; + /** + * The console command description. + * + * @var string + */ + protected $description = 'Show the status of each conversion file'; + + /** + * Execute the console command. + * + * @return int + */ public function handle() { $this->ensureConversionsTableExists(); - $executed = DB::table('conversions')->pluck('conversion')->toArray(); - $files = File::glob(app_path('Conversions') . '/*.php'); - - $statuses = []; - - foreach ($files as $file) { - $class = $this->getClassName($file); - $status = in_array($class, $executed) ? 'Executed' : 'Pending'; - $statuses[] = ['Conversion' => $class, 'Status' => $status]; - } + $this->showConversionStatus(); - $this->table(['Conversion', 'Status'], $statuses); + return 0; } + /** + * Ensure that the conversions table exists. + * + * @return void + */ protected function ensureConversionsTableExists() { - if (!DB::schema()->hasTable('conversions')) { - $this->error('Conversions table does not exist. Run "php artisan conversion" first.'); - exit; + if (!DB::connection()->getSchemaBuilder()->hasTable('conversions')) { + DB::connection()->getSchemaBuilder()->create('conversions', function ($table) { + $table->increments('id'); + $table->string('conversion')->unique(); + $table->integer('batch'); + $table->timestamps(); + }); + $this->info('Created conversions table.'); } } - protected function getClassName($filePath) + /** + * Display the status of each conversion file. + * + * @return void + */ + protected function showConversionStatus() { - $content = file_get_contents($filePath); + // Retrieve all executed conversions with their batch and timestamps + $executedConversions = DB::table('conversions')->get(); + + // Create a map of executed conversions for quick lookup + $executedConversionsMap = $executedConversions->keyBy('conversion'); + + $files = File::glob(database_path('conversions') . '/*.php'); + + // Sort files for consistent display + sort($files); - $namespace = Str::between($content, 'namespace ', ';'); - $class = Str::between($content, 'class ', ' '); + $statusData = []; + + foreach ($files as $file) { + $fileName = basename($file, '.php'); + + if ($executedConversionsMap->has($fileName)) { + $status = 'Yes'; + $batchNumber = $executedConversionsMap->get($fileName)->batch; + $executedAt = $executedConversionsMap->get($fileName)->created_at; + } else { + $status = 'No'; + $batchNumber = 'N/A'; + $executedAt = 'N/A'; + } + + $statusData[] = [ + 'Ran?' => $status, + 'Conversion' => $fileName, + 'Batch' => $batchNumber, + 'Executed At' => $executedAt, + ]; + } + + if (empty($statusData)) { + $this->info('No conversion files found.'); + return; + } - return "{$namespace}\\{$class}"; + // Display the table with the additional columns + $this->table(['Ran?', 'Conversion', 'Batch', 'Executed At'], $statusData); } }