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

267 lines
9.7 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
/**
* Migrates legacy `artworks_comments` (projekti_old_skinbase) into `artwork_comments`.
*
* Column mapping:
* legacy.comment_id → artwork_comments.legacy_id (idempotency key)
* legacy.artwork_id → artwork_comments.artwork_id
* legacy.user_id → artwork_comments.user_id
* legacy.description → artwork_comments.content
* legacy.date + .time → artwork_comments.created_at / updated_at
*
* Ignored legacy columns: owner, author (username strings), owner_user_id
*
* Usage:
* php artisan comments:import-legacy
* php artisan comments:import-legacy --dry-run
* php artisan comments:import-legacy --chunk=1000
* php artisan comments:import-legacy --allow-guest-user=0 (import rows where user_id maps to 0 / not found, assigning a fallback user_id)
*/
class ImportLegacyComments extends Command
{
protected $signature = 'comments:import-legacy
{--dry-run : Preview only — no writes to DB}
{--chunk=500 : Rows to process per batch}
{--skip-empty : Skip comments with empty/whitespace-only content}';
protected $description = 'Import legacy artworks_comments into artwork_comments';
public function handle(): int
{
$dryRun = (bool) $this->option('dry-run');
$chunk = max(1, (int) $this->option('chunk'));
$skipEmpty = (bool) $this->option('skip-empty');
if ($dryRun) {
$this->warn('[DRY-RUN] No data will be written.');
}
// Verify legacy connection
try {
DB::connection('legacy')->getPdo();
} catch (\Throwable $e) {
$this->error('Cannot connect to legacy database: ' . $e->getMessage());
return self::FAILURE;
}
if (! DB::connection('legacy')->getSchemaBuilder()->hasTable('artworks_comments')) {
$this->error('Legacy table `artworks_comments` not found.');
return self::FAILURE;
}
if (! DB::getSchemaBuilder()->hasColumn('artwork_comments', 'legacy_id')) {
$this->error('Column `legacy_id` missing from `artwork_comments`. Run: php artisan migrate');
return self::FAILURE;
}
// Pre-load valid artwork IDs and user IDs from new DB for O(1) lookup
$this->info('Loading new-DB artwork and user ID sets…');
$validArtworkIds = DB::table('artworks')
->whereNull('deleted_at')
->pluck('id')
->flip()
->all();
$validUserIds = DB::table('users')
->whereNull('deleted_at')
->pluck('id')
->flip()
->all();
$this->info(sprintf(
'Found %d artworks and %d users in new DB.',
count($validArtworkIds),
count($validUserIds)
));
// Already-imported legacy IDs (to resume safely)
$this->info('Loading already-imported legacy_ids…');
$alreadyImported = DB::table('artwork_comments')
->whereNotNull('legacy_id')
->pluck('legacy_id')
->flip()
->all();
$this->info(sprintf('%d comments already imported (will be skipped).', count($alreadyImported)));
$total = DB::connection('legacy')->table('artworks_comments')->count();
$this->info("Legacy rows to process: {$total}");
if ($total === 0) {
$this->warn('No legacy rows found. Nothing to do.');
return self::SUCCESS;
}
$stats = [
'imported' => 0,
'skipped_duplicate' => 0,
'skipped_artwork' => 0,
'skipped_user' => 0,
'skipped_empty' => 0,
'errors' => 0,
];
$bar = $this->output->createProgressBar($total);
$bar->setFormat(' %current%/%max% [%bar%] %percent:3s%% | imported: %imported% | skipped: %skipped%');
$bar->setMessage('0', 'imported');
$bar->setMessage('0', 'skipped');
$bar->start();
DB::connection('legacy')
->table('artworks_comments')
->orderBy('comment_id')
->chunk($chunk, function ($rows) use (
&$stats,
&$alreadyImported,
$validArtworkIds,
$validUserIds,
$dryRun,
$skipEmpty,
$bar
) {
$inserts = [];
$now = now();
foreach ($rows as $row) {
$legacyId = (int) $row->comment_id;
$artworkId = (int) $row->artwork_id;
$userId = (int) $row->user_id;
$content = trim((string) ($row->description ?? ''));
// --- Already imported ---
if (isset($alreadyImported[$legacyId])) {
$stats['skipped_duplicate']++;
$bar->advance();
continue;
}
// --- Content ---
if ($skipEmpty && $content === '') {
$stats['skipped_empty']++;
$bar->advance();
continue;
}
// Replace empty content with a placeholder so NOT NULL is satisfied
if ($content === '') {
$content = '[no content]';
}
// --- Artwork must exist ---
if (! isset($validArtworkIds[$artworkId])) {
$stats['skipped_artwork']++;
$bar->advance();
continue;
}
// --- User must exist ---
if (! isset($validUserIds[$userId])) {
$stats['skipped_user']++;
$bar->advance();
continue;
}
// --- Build timestamp from separate date + time columns ---
$createdAt = $this->buildTimestamp($row->date, $row->time, $now);
if (! $dryRun) {
$inserts[] = [
'legacy_id' => $legacyId,
'artwork_id' => $artworkId,
'user_id' => $userId,
'content' => $content,
'is_approved' => 1,
'created_at' => $createdAt,
'updated_at' => $createdAt,
'deleted_at' => null,
];
$alreadyImported[$legacyId] = true;
}
$stats['imported']++;
$bar->advance();
}
if (! $dryRun && ! empty($inserts)) {
try {
DB::table('artwork_comments')->insert($inserts);
} catch (\Throwable $e) {
// Fallback: row-by-row with ignore on unique violations
foreach ($inserts as $row) {
try {
DB::table('artwork_comments')->insertOrIgnore([$row]);
} catch (\Throwable) {
$stats['errors']++;
}
}
}
}
$skippedTotal = $stats['skipped_duplicate']
+ $stats['skipped_artwork']
+ $stats['skipped_user']
+ $stats['skipped_empty'];
$bar->setMessage((string) $stats['imported'], 'imported');
$bar->setMessage((string) $skippedTotal, 'skipped');
});
$bar->finish();
$this->newLine(2);
// -------------------------------------------------------------------------
// Summary
// -------------------------------------------------------------------------
$this->table(
['Result', 'Count'],
[
['Imported', $stats['imported']],
['Skipped already imported', $stats['skipped_duplicate']],
['Skipped artwork gone', $stats['skipped_artwork']],
['Skipped user gone', $stats['skipped_user']],
['Skipped empty content', $stats['skipped_empty']],
['Errors', $stats['errors']],
]
);
if ($dryRun) {
$this->warn('[DRY-RUN] Nothing was written. Re-run without --dry-run to apply.');
} else {
$this->info('Migration complete.');
}
return $stats['errors'] > 0 ? self::FAILURE : self::SUCCESS;
}
/**
* Combine a legacy `date` (DATE) and `time` (TIME) column into a single datetime string.
* Falls back to $fallback when both are null.
*/
private function buildTimestamp(mixed $date, mixed $time, \Illuminate\Support\Carbon $fallback): string
{
if (! $date) {
return $fallback->toDateTimeString();
}
$datePart = substr((string) $date, 0, 10); // '2000-09-13'
$timePart = $time ? substr((string) $time, 0, 8) : '00:00:00'; // '09:34:27'
// Sanity-check: MySQL TIME can be negative or > 24h for intervals — clamp to midnight
if (! preg_match('/^\d{2}:\d{2}:\d{2}$/', $timePart) || $timePart < '00:00:00') {
$timePart = '00:00:00';
}
return $datePart . ' ' . $timePart;
}
}