option('dry-run'); $force = $this->option('force'); $skipErrors = $this->option('skip-errors'); $excludeIds = $this->parseIds($this->option('exclude')); $onlyIds = $this->parseIds($this->option('only')); if (!empty($excludeIds) && !empty($onlyIds)) { $this->error('--exclude and --only cannot be used together.'); return 1; } if ($dryRun) { $this->info('DRY RUN MODE - No changes will be made'); $this->newLine(); } // Fetch all active organizations, filtered by --only or --exclude $query = Organization::whereNull('deleted_at'); if (!empty($onlyIds)) { $query->whereIn('id', $onlyIds); } elseif (!empty($excludeIds)) { $query->whereNotIn('id', $excludeIds); } $organizations = $query->orderBy('id')->get(['id', 'name', 'email']); $totalActive = Organization::whereNull('deleted_at')->count(); $excluded = $totalActive - $organizations->count(); $this->info("Active organizations: {$totalActive}"); $this->info("Excluded: {$excluded}"); $this->info("To convert: {$organizations->count()}"); $this->newLine(); if ($organizations->isEmpty()) { $this->info('No organizations to convert.'); return 0; } // List the organizations that will be converted $this->table( ['ID', 'Name', 'Email'], $organizations->map(fn($o) => [$o->id, $o->name, $o->email])->toArray() ); $this->newLine(); if (!empty($onlyIds)) { $this->line('Only IDs: ' . implode(', ', $onlyIds)); $this->newLine(); } elseif (!empty($excludeIds)) { $this->line('Excluded IDs: ' . implode(', ', $excludeIds)); $this->newLine(); } if (!$dryRun && !$force) { if (!$this->confirm("Convert all {$organizations->count()} organization(s) to user profiles? This cannot be easily undone.", false)) { $this->info('Bulk migration cancelled.'); return 0; } $this->newLine(); } // Process each organization $results = []; $successCount = 0; $failCount = 0; foreach ($organizations as $org) { $this->line("Processing: [{$org->id}] {$org->name}"); $args = [ 'organization_id' => $org->id, '--force' => true, ]; if ($dryRun) { $args['--dry-run'] = true; } try { $exitCode = Artisan::call('migrate:organization-to-user', $args); $output = Artisan::output(); if ($exitCode === 0) { $results[] = [$org->id, $org->name, $dryRun ? 'preview ok' : 'converted']; $successCount++; // Indent the sub-command output foreach (explode("\n", trim($output)) as $line) { if ($line !== '') { $this->line(' ' . $line); } } } else { $results[] = [$org->id, $org->name, 'FAILED']; $failCount++; $this->error(" Failed (exit code {$exitCode}):"); foreach (explode("\n", trim($output)) as $line) { if ($line !== '') { $this->error(' ' . $line); } } if (!$skipErrors) { $this->newLine(); $this->error('Stopping due to failure. Use --skip-errors to continue past failures.'); break; } } } catch (\Exception $e) { $results[] = [$org->id, $org->name, 'EXCEPTION']; $failCount++; $this->error(" Exception: " . $e->getMessage()); if (!$skipErrors) { $this->newLine(); $this->error('Stopping due to exception. Use --skip-errors to continue past failures.'); break; } } $this->newLine(); } // Final summary $this->line('─────────────────────────────────────────'); $this->info('Summary:'); $this->table( ['ID', 'Name', 'Result'], $results ); $this->newLine(); if ($dryRun) { $this->info("Dry run complete. {$successCount} organization(s) previewed, {$failCount} would fail."); $this->info('Run without --dry-run to execute the migrations.'); } else { $this->info("Bulk migration complete. {$successCount} converted, {$failCount} failed."); } return $failCount > 0 ? 1 : 0; } /** * Parse an ID list option, supporting both repeated flags and comma-separated values. * e.g. --exclude=1 --exclude=2 OR --exclude=1,2,3 */ private function parseIds(array $rawExclude): array { $ids = []; foreach ($rawExclude as $value) { foreach (explode(',', $value) as $part) { $part = trim($part); if (is_numeric($part) && (int) $part > 0) { $ids[] = (int) $part; } } } return array_unique($ids); } }