200 lines
7.2 KiB
PHP
200 lines
7.2 KiB
PHP
<?php
|
|
|
|
namespace App\Console\Commands;
|
|
|
|
use App\Models\Organization;
|
|
use Illuminate\Console\Command;
|
|
use Illuminate\Support\Facades\Artisan;
|
|
|
|
/**
|
|
* Bulk Migrate Organizations to Users Command
|
|
*
|
|
* Converts all active (non-deleted) organization profiles to user profiles,
|
|
* excluding any organization IDs specified via --exclude.
|
|
*
|
|
* Delegates each individual migration to MigrateOrganizationToUserCommand,
|
|
* reusing all its validation, relationship migration, and safety logic.
|
|
*
|
|
* Usage examples:
|
|
* php artisan migrate:organizations-to-users --dry-run
|
|
* php artisan migrate:organizations-to-users --exclude=5 --exclude=12
|
|
* php artisan migrate:organizations-to-users --exclude=5,12,34 --force
|
|
* php artisan migrate:organizations-to-users --only=3,7,9
|
|
* php artisan migrate:organizations-to-users --only=3 --only=7 --only=9
|
|
* php artisan migrate:organizations-to-users --skip-errors
|
|
*/
|
|
class BulkMigrateOrganizationsToUsersCommand extends Command
|
|
{
|
|
protected $signature = 'migrate:organizations-to-users
|
|
{--exclude=* : Organization IDs to skip (use --exclude=1 --exclude=2 or --exclude=1,2,3)}
|
|
{--only=* : Only convert these organization IDs, ignoring all others (use --only=1 --only=2 or --only=1,2,3)}
|
|
{--dry-run : Preview all conversions without executing them}
|
|
{--force : Skip confirmation prompts}
|
|
{--skip-errors : Continue to the next organization if one fails instead of stopping}';
|
|
|
|
protected $description = 'Bulk convert all active organization profiles to user profiles, with optional exclusions';
|
|
|
|
public function handle(): int
|
|
{
|
|
$dryRun = $this->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);
|
|
}
|
|
}
|