Files
SkinbaseNova/app/Console/Commands/RepairTemporaryUsernamesCommand.php

136 lines
4.4 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Console\Commands;
use App\Support\UsernamePolicy;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
class RepairTemporaryUsernamesCommand extends Command
{
protected $signature = 'skinbase:repair-temp-usernames
{--chunk=500 : Number of users to process per batch}
{--dry-run : Preview username changes without writing them}';
protected $description = 'Replace current users.username values like tmpu% using the users.name field';
public function handle(): int
{
$chunk = max(1, (int) $this->option('chunk'));
$dryRun = (bool) $this->option('dry-run');
if ($dryRun) {
$this->warn('[DRY RUN] No changes will be written.');
}
$total = (int) DB::table('users')
->where('username', 'like', 'tmpu%')
->count();
if ($total === 0) {
$this->info('No users with temporary tmpu% usernames were found.');
return self::SUCCESS;
}
$this->info("Found {$total} users with temporary tmpu% usernames.");
$processed = 0;
$updated = 0;
$skipped = 0;
DB::table('users')
->select(['id', 'name', 'username'])
->where('username', 'like', 'tmpu%')
->chunkById($chunk, function ($rows) use (&$processed, &$updated, &$skipped, $dryRun) {
foreach ($rows as $row) {
$processed++;
$sourceName = trim((string) ($row->name ?? ''));
if ($sourceName === '') {
$skipped++;
$this->warn("Skipping user id={$row->id}: name is empty.");
continue;
}
$candidate = $this->resolveCandidate($sourceName, (int) $row->id);
if ($candidate === null || strcasecmp($candidate, (string) $row->username) === 0) {
$skipped++;
$this->warn("Skipping user id={$row->id}: unable to resolve a better username from name='{$sourceName}'.");
continue;
}
if ($dryRun) {
$this->line("[dry] Would update user id={$row->id} username '{$row->username}' => '{$candidate}'");
$updated++;
continue;
}
$affected = DB::table('users')
->where('id', (int) $row->id)
->where('username', 'like', 'tmpu%')
->update([
'username' => $candidate,
'username_changed_at' => now(),
'updated_at' => now(),
]);
if ($affected > 0) {
$updated += $affected;
$this->line("[update] user id={$row->id} username '{$row->username}' => '{$candidate}'");
}
}
}, 'id');
$this->info(sprintf('Finished. processed=%d updated=%d skipped=%d', $processed, $updated, $skipped));
return self::SUCCESS;
}
private function resolveCandidate(string $sourceName, int $userId): ?string
{
$base = UsernamePolicy::sanitizeLegacy($sourceName);
$min = UsernamePolicy::min();
$max = UsernamePolicy::max();
if ($base === '') {
return null;
}
if (preg_match('/^tmpu\d+$/i', $base) === 1) {
$base = 'user' . $userId;
}
if (strlen($base) < $min) {
$base = substr($base . $userId, 0, $max);
}
if ($base === '' || $base === 'user') {
$base = 'user' . $userId;
}
$candidate = substr($base, 0, $max);
$suffix = 1;
while ($this->usernameExists($candidate, $userId) || UsernamePolicy::isReserved($candidate)) {
$suffixValue = (string) $suffix;
$prefixLen = max(1, $max - strlen($suffixValue));
$candidate = substr($base, 0, $prefixLen) . $suffixValue;
$suffix++;
}
return $candidate;
}
private function usernameExists(string $username, int $ignoreUserId): bool
{
return DB::table('users')
->whereRaw('LOWER(username) = ?', [strtolower($username)])
->where('id', '!=', $ignoreUserId)
->exists();
}
}