144 lines
5.3 KiB
PHP
144 lines
5.3 KiB
PHP
<?php
|
||
|
||
namespace App\Console\Commands;
|
||
|
||
use App\Services\LegacySmileyMapper;
|
||
use Illuminate\Console\Command;
|
||
use Illuminate\Support\Facades\DB;
|
||
use Illuminate\Support\Facades\Log;
|
||
|
||
/**
|
||
* php artisan skinbase:migrate-smileys
|
||
*
|
||
* Scans artworks.description, artwork_comments.content, and forum_posts.content,
|
||
* replaces legacy smiley codes (:beer, :lol, etc.) with Unicode emoji.
|
||
*
|
||
* Options:
|
||
* --dry-run Show what would change without writing to DB
|
||
* --chunk=200 Rows processed per batch (default 200)
|
||
* --table=artworks Limit scan to one table
|
||
*/
|
||
class MigrateSmileys extends Command
|
||
{
|
||
protected $signature = 'skinbase:migrate-smileys
|
||
{--dry-run : Preview changes without writing to the database}
|
||
{--chunk=200 : Number of rows to process per batch}
|
||
{--table= : Limit scan to a single table (artworks|artwork_comments|forum_posts)}';
|
||
|
||
protected $description = 'Convert legacy :smiley: codes to Unicode emoji in content fields.';
|
||
|
||
/** Tables and their content columns to scan. */
|
||
private const TARGETS = [
|
||
'artworks' => '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 <info>{$table}.{$column}</info>…");
|
||
|
||
[$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];
|
||
}
|
||
}
|