'description', 'artwork_comments' => 'content', 'forum_posts' => 'content', ]; public function handle(): int { $dryRun = (bool) $this->option('dry-run'); $chunk = max(1, (int) $this->option('chunk')); $tableOpt = $this->option('table'); $targets = self::TARGETS; if ($tableOpt) { if (! isset($targets[$tableOpt])) { $this->error("Unknown table: {$tableOpt}. Allowed: " . implode(', ', array_keys($targets))); return self::FAILURE; } $targets = [$tableOpt => $targets[$tableOpt]]; } if ($dryRun) { $this->warn('DRY-RUN mode — no changes will be written.'); } $totalChanged = 0; $totalRows = 0; foreach ($targets as $table => $column) { $this->line("Scanning {$table}.{$column}…"); [$changed, $rows] = $this->processTable($table, $column, $chunk, $dryRun); $totalChanged += $changed; $totalRows += $rows; $this->line(" → {$rows} rows scanned, {$changed} updated."); } $this->newLine(); $this->info("Summary: {$totalRows} rows scanned, {$totalChanged} rows " . ($dryRun ? 'would be ' : '') . 'updated.'); return self::SUCCESS; } private function processTable( string $table, string $column, int $chunk, bool $dryRun ): array { $totalChanged = 0; $totalRows = 0; DB::table($table) ->whereNotNull($column) ->orderBy('id') ->chunk($chunk, function ($rows) use ($table, $column, $dryRun, &$totalChanged, &$totalRows) { foreach ($rows as $row) { $original = $row->$column ?? ''; $converted = LegacySmileyMapper::convert($original); // Collapse emoji flood runs BEFORE size/DB checks so that // rows like ":beer :beer :beer …" (×500) don't exceed MEDIUMTEXT. $collapsed = LegacySmileyMapper::collapseFlood($converted); if ($collapsed !== $converted) { $beforeBytes = mb_strlen($converted, '8bit'); $afterBytes = mb_strlen($collapsed, '8bit'); $floodMsg = "[{$table}#{$row->id}] Emoji flood collapsed " . "({$beforeBytes} bytes \u{2192} {$afterBytes} bytes)."; $this->warn(" {$floodMsg}"); Log::warning($floodMsg); $converted = $collapsed; } $totalRows++; if ($converted === $original) { continue; } $totalChanged++; $codes = LegacySmileyMapper::detect($original); $msg = "[{$table}#{$row->id}] Converting: " . implode(', ', $codes); $this->line(" {$msg}"); Log::info($msg); if (! $dryRun) { // Guard: MEDIUMTEXT max is 16,777,215 bytes. if (mb_strlen($converted, '8bit') > 16_777_215) { $warn = "[{$table}#{$row->id}] SKIP — converted content exceeds MEDIUMTEXT limit (" . mb_strlen($converted, '8bit') . " bytes). Row left unchanged."; $this->warn(" {$warn}"); Log::warning($warn); continue; } try { DB::table($table) ->where('id', $row->id) ->update([$column => $converted]); } catch (\Throwable $e) { $err = "[{$table}#{$row->id}] DB error: {$e->getMessage()}"; $this->warn(" {$err}"); Log::error($err); } } } }); return [$totalChanged, $totalRows]; } }