option('dry-run'); $logPath = storage_path('logs/username_migration.log'); @file_put_contents($logPath, '['.now()."] enforce-usernames dry_run=".($dryRun ? '1' : '0')."\n", FILE_APPEND); $used = User::query()->whereNotNull('username')->pluck('id', 'username')->mapWithKeys(fn ($id, $username) => [strtolower((string) $username) => (int) $id])->all(); $updated = 0; User::query()->orderBy('id')->chunkById(500, function ($users) use (&$used, &$updated, $dryRun, $logPath): void { foreach ($users as $user) { $current = strtolower(trim((string) ($user->username ?? ''))); $base = UsernamePolicy::sanitizeLegacy($current !== '' ? $current : ('user'.$user->id)); if (UsernamePolicy::isReserved($base) || UsernamePolicy::similarReserved($base) !== null) { $base = 'user'.$user->id; } $candidate = substr($base, 0, UsernamePolicy::max()); $suffix = 1; while ((isset($used[$candidate]) && (int) $used[$candidate] !== (int) $user->id) || UsernamePolicy::isReserved($candidate) || UsernamePolicy::similarReserved($candidate) !== null) { $suffixStr = (string) $suffix; $prefixLen = max(1, UsernamePolicy::max() - strlen($suffixStr)); $candidate = substr($base, 0, $prefixLen) . $suffixStr; $suffix++; } $needsUpdate = $candidate !== $current; if (! $needsUpdate) { $used[$candidate] = (int) $user->id; continue; } @file_put_contents($logPath, sprintf("[%s] user_id=%d old=%s new=%s\n", now()->toDateTimeString(), (int) $user->id, $current, $candidate), FILE_APPEND); if (! $dryRun) { DB::transaction(function () use ($user, $current, $candidate): void { if ($current !== '' && Schema::hasTable('username_history')) { DB::table('username_history')->insert([ 'user_id' => (int) $user->id, 'old_username' => $current, 'changed_at' => now(), 'created_at' => now(), 'updated_at' => now(), ]); } if ($current !== '' && Schema::hasTable('username_redirects')) { DB::table('username_redirects')->updateOrInsert( ['old_username' => $current], [ 'new_username' => $candidate, 'user_id' => (int) $user->id, 'created_at' => now(), 'updated_at' => now(), ] ); } DB::table('users')->where('id', (int) $user->id)->update([ 'username' => $candidate, 'username_changed_at' => now(), 'updated_at' => now(), ]); }); } $used[$candidate] = (int) $user->id; $updated++; } }); $this->info("Username policy enforcement complete. Updated: {$updated}" . ($dryRun ? ' (dry run)' : '')); return self::SUCCESS; } }