Snapshot of main at 9f39a6e1e (2026-11-02)
This commit is contained in:
commit
6ccf1792d7
2580 files changed
+1040202
No files matched your search
@@ -0,0 +1,199 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user