You've already forked Atomcms-edit
353 lines
12 KiB
PHP
Executable File
353 lines
12 KiB
PHP
Executable File
<?php
|
|
|
|
namespace App\Console\Commands;
|
|
|
|
use Illuminate\Console\Command;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\File;
|
|
use Illuminate\Support\Facades\Schema;
|
|
|
|
class CheckMigrationsCommand extends Command
|
|
{
|
|
#[\Override]
|
|
protected $signature = 'atom:check-migrations
|
|
{--orphaned : Check for orphaned migrations only}
|
|
{--superseded : Check for superseded migrations only}
|
|
{--duplicates : Check for duplicate table creations only}
|
|
{--pending : Check pending migrations only}
|
|
{--delete : Delete problematic migration files}
|
|
{--force : Skip confirmation prompt when deleting}';
|
|
|
|
#[\Override]
|
|
protected $description = 'Analyze and clean up orphaned, superseded, duplicate, or pending migrations';
|
|
|
|
private int $errors = 0;
|
|
|
|
private int $warnings = 0;
|
|
|
|
/** @var list<string> */
|
|
private array $filesToDelete = [];
|
|
|
|
public function handle(): int
|
|
{
|
|
$mode = $this->option('orphaned') ? 'orphaned'
|
|
: ($this->option('superseded') ? 'superseded'
|
|
: ($this->option('duplicates') ? 'duplicates'
|
|
: ($this->option('pending') ? 'pending'
|
|
: 'all')));
|
|
|
|
if (! Schema::hasTable('migrations')) {
|
|
$this->error('The migrations table does not exist. Run `php artisan migrate` first.');
|
|
|
|
return self::FAILURE;
|
|
}
|
|
|
|
$ranMigrations = DB::table('migrations')->pluck('migration')->toArray();
|
|
|
|
if ($mode === 'pending' || $mode === 'all') {
|
|
$this->checkPending($ranMigrations);
|
|
}
|
|
if ($mode === 'orphaned' || $mode === 'all') {
|
|
$this->checkOrphaned();
|
|
}
|
|
if ($mode === 'superseded' || $mode === 'all') {
|
|
$this->checkSuperseded();
|
|
}
|
|
if ($mode === 'duplicates' || $mode === 'all') {
|
|
$this->checkDuplicates();
|
|
}
|
|
|
|
$this->newLine();
|
|
|
|
if ($this->option('delete') && $this->filesToDelete !== []) {
|
|
$this->deleteFiles();
|
|
}
|
|
|
|
if ($this->errors > 0) {
|
|
$this->error("Found {$this->errors} error(s) and {$this->warnings} warning(s).");
|
|
|
|
return self::FAILURE;
|
|
}
|
|
if ($this->warnings > 0) {
|
|
$this->warn("Found {$this->warnings} warning(s), no errors.");
|
|
|
|
return self::SUCCESS;
|
|
}
|
|
$this->info('No issues found.');
|
|
|
|
return self::SUCCESS;
|
|
}
|
|
|
|
private function checkPending(array $ran): void
|
|
{
|
|
$files = File::files(database_path('migrations'));
|
|
$pending = 0;
|
|
|
|
foreach ($files as $f) {
|
|
$name = str_replace('.php', '', $f->getFilename());
|
|
if (! in_array($name, $ran)) {
|
|
$pending++;
|
|
}
|
|
}
|
|
|
|
if ($pending > 0) {
|
|
$this->warn("{$pending} pending migration(s) found. Run `php artisan migrate`.");
|
|
$this->errors++;
|
|
} else {
|
|
$this->info('All migrations have been run.');
|
|
}
|
|
}
|
|
|
|
private function checkOrphaned(): void
|
|
{
|
|
$migrationsPath = database_path('migrations');
|
|
$files = File::files($migrationsPath);
|
|
|
|
foreach ($files as $file) {
|
|
$content = File::get($file->getPathname());
|
|
|
|
$createdTables = $this->extractCreatedTables($content);
|
|
|
|
foreach ($createdTables as $table) {
|
|
if (! Schema::hasTable($table)) {
|
|
$this->warn("[Orphaned] {$file->getFilename()} creates table `{$table}` which no longer exists.");
|
|
$this->warnings++;
|
|
$this->addFileToDelete($file->getFilename());
|
|
}
|
|
}
|
|
|
|
$modifiedTables = $this->extractModifiedTables($content);
|
|
|
|
foreach ($modifiedTables as $table => $columns) {
|
|
if (! Schema::hasTable($table)) {
|
|
$this->warn("[Orphaned] {$file->getFilename()} modifies table `{$table}` which no longer exists.");
|
|
$this->warnings++;
|
|
$this->addFileToDelete($file->getFilename());
|
|
continue;
|
|
}
|
|
|
|
foreach ($columns as $column) {
|
|
if (! Schema::hasColumn($table, $column)) {
|
|
$this->warn("[Orphaned] {$file->getFilename()} adds column `{$column}` to `{$table}` but column no longer exists.");
|
|
$this->warnings++;
|
|
$this->addFileToDelete($file->getFilename());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private function checkSuperseded(): void
|
|
{
|
|
$migrationsPath = database_path('migrations');
|
|
$files = File::files($migrationsPath);
|
|
|
|
$createdIn = [];
|
|
$droppedIn = [];
|
|
|
|
foreach ($files as $file) {
|
|
$content = File::get($file->getPathname());
|
|
$name = $file->getFilename();
|
|
|
|
$upContent = $this->extractUpMethod($content);
|
|
|
|
$createdTables = $this->extractCreatedTables($upContent);
|
|
$droppedTablesInUp = $this->extractDroppedTables($upContent);
|
|
|
|
foreach ($createdTables as $table) {
|
|
$createdIn[$table][] = $name;
|
|
}
|
|
|
|
foreach ($droppedTablesInUp as $table) {
|
|
$droppedIn[$table][] = $name;
|
|
}
|
|
}
|
|
|
|
foreach ($createdIn as $table => $creators) {
|
|
if (isset($droppedIn[$table]) && $droppedIn[$table] !== []) {
|
|
$droppers = $droppedIn[$table];
|
|
$stillExists = Schema::hasTable($table);
|
|
|
|
if (! $stillExists) {
|
|
$this->warn('[Superseded] Table `' . $table . '` created in ' . implode(', ', $creators) . ' and dropped in ' . implode(', ', $droppers) . '.');
|
|
$this->warnings++;
|
|
foreach (array_merge($creators, $droppers) as $f) {
|
|
$this->addFileToDelete($f);
|
|
}
|
|
}
|
|
|
|
if (count($creators) > 1) {
|
|
$this->warn('[Duplicate] Table `' . $table . '` was created multiple times across migrations.');
|
|
$this->warnings++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private function extractUpMethod(string $content): string
|
|
{
|
|
if (preg_match('/function\s+up\s*\(\s*\)\s*(?::\s*\w+\s*)?\s*\{.*?\n\s*\}/s', $content, $m)) {
|
|
return $m[0];
|
|
}
|
|
|
|
return $content;
|
|
}
|
|
|
|
private function extractDownMethod(string $content): string
|
|
{
|
|
if (preg_match('/function\s+down\s*\(\s*\)\s*(?::\s*\w+\s*)?\s*\{.*?\n\s*\}/s', $content, $m)) {
|
|
return $m[0];
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
private function checkDuplicates(): void
|
|
{
|
|
$migrationsPath = database_path('migrations');
|
|
$files = File::files($migrationsPath);
|
|
|
|
$tableCreations = [];
|
|
|
|
foreach ($files as $file) {
|
|
$content = File::get($file->getPathname());
|
|
$createdTables = $this->extractCreatedTables($content);
|
|
|
|
foreach ($createdTables as $table) {
|
|
if (! isset($tableCreations[$table])) {
|
|
$tableCreations[$table] = [];
|
|
}
|
|
$tableCreations[$table][] = $file->getFilename();
|
|
}
|
|
}
|
|
|
|
foreach ($tableCreations as $table => $creators) {
|
|
if (count($creators) > 1) {
|
|
$this->warn('[Duplicate] Table `' . $table . '` created in: ' . implode(', ', $creators));
|
|
$this->warnings++;
|
|
|
|
$existing = Schema::hasTable($table);
|
|
|
|
if ($existing) {
|
|
$toDelete = array_slice($creators, 0, -1);
|
|
foreach ($toDelete as $f) {
|
|
$this->addFileToDelete($f);
|
|
}
|
|
} else {
|
|
foreach ($creators as $f) {
|
|
$this->addFileToDelete($f);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private function extractCreatedTables(string $content): array
|
|
{
|
|
preg_match_all('/Schema::create\([\'"]([^\'"]+)[\'"]/', $content, $matches);
|
|
|
|
return array_unique($matches[1]);
|
|
}
|
|
|
|
private function extractModifiedTables(string $content): array
|
|
{
|
|
$tables = [];
|
|
|
|
preg_match_all('/Schema::table\([\'"]([^\'"]+)[\'"]/', $content, $tableMatches);
|
|
|
|
if (empty($tableMatches[1])) {
|
|
return $tables;
|
|
}
|
|
|
|
preg_match_all('/\$table->(' . implode('|', $this->columnMethods()) . ')\([\'"]([^\'"]+)[\'"]\)/', $content, $colMatches);
|
|
|
|
foreach ($colMatches[1] as $i => $method) {
|
|
$table = $tableMatches[1][0];
|
|
$column = $colMatches[2][$i];
|
|
if (! isset($tables[$table]) || ! in_array($column, $tables[$table])) {
|
|
$tables[$table][] = $column;
|
|
}
|
|
}
|
|
|
|
return $tables;
|
|
}
|
|
|
|
private function extractDroppedTables(string $content): array
|
|
{
|
|
$tables = [];
|
|
|
|
if (preg_match('/Schema::dropIfExists\([\'"]([^\'"]+)[\'"]/', $content, $m)) {
|
|
$tables[] = $m[1];
|
|
}
|
|
if (preg_match('/Schema::drop\([\'"]([^\'"]+)[\'"]/', $content, $m)) {
|
|
$tables[] = $m[1];
|
|
}
|
|
|
|
return $tables;
|
|
}
|
|
|
|
private function columnMethods(): array
|
|
{
|
|
return [
|
|
'bigInteger', 'binary', 'boolean', 'char', 'date', 'dateTime', 'datetime',
|
|
'decimal', 'double', 'enum', 'float', 'foreignId', 'foreignIdFor',
|
|
'foreignUlid', 'foreignUuid', 'geometry', 'geometryCollection',
|
|
'id', 'integer', 'json', 'jsonb', 'lineString', 'longText',
|
|
'macAddress', 'mediumInteger', 'mediumText', 'morphs',
|
|
'nullableMorphs', 'nullableUuidMorphs', 'nullableTimestamps',
|
|
'point', 'polygon', 'rememberToken', 'set', 'smallInteger',
|
|
'softDeletes', 'softDeletesTz', 'string', 'text', 'time',
|
|
'timestamp', 'timestamps', 'timestampsTz', 'tinyInteger',
|
|
'tinyText', 'unsignedBigInteger', 'unsignedInteger',
|
|
'unsignedMediumInteger', 'unsignedSmallInteger',
|
|
'unsignedTinyInteger', 'uuid', 'year',
|
|
];
|
|
}
|
|
|
|
private function addFileToDelete(string $filename): void
|
|
{
|
|
if (! in_array($filename, $this->filesToDelete)) {
|
|
$this->filesToDelete[] = $filename;
|
|
}
|
|
}
|
|
|
|
private function deleteFiles(): void
|
|
{
|
|
$this->newLine();
|
|
$this->warn('The following migration files will be deleted:');
|
|
foreach ($this->filesToDelete as $f) {
|
|
$this->line(" - {$f}");
|
|
}
|
|
$this->newLine();
|
|
|
|
if (! $this->option('force')) {
|
|
$this->line('Records will also be removed from the `migrations` database table.');
|
|
if (! $this->confirm('Are you sure you want to delete these migration files?', false)) {
|
|
$this->info('Deletion cancelled.');
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
$deleted = 0;
|
|
$migrationsPath = database_path('migrations');
|
|
$names = array_map(fn ($f) => str_replace('.php', '', $f), $this->filesToDelete);
|
|
|
|
foreach ($this->filesToDelete as $filename) {
|
|
$path = $migrationsPath . '/' . $filename;
|
|
|
|
if (File::exists($path)) {
|
|
File::delete($path);
|
|
$this->line("[Deleted] {$filename}");
|
|
$deleted++;
|
|
}
|
|
}
|
|
|
|
DB::table('migrations')->whereIn('migration', $names)->delete();
|
|
$this->line('[Cleanup] Removed ' . count($names) . ' record(s) from migrations table.');
|
|
|
|
$this->newLine();
|
|
$this->info("Deleted {$deleted} migration file(s) and cleaned up the migrations table.");
|
|
}
|
|
}
|