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

229 lines
8.0 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
class ImportLegacyNewsCommentsCommand extends Command
{
protected $signature = 'news:comments-import-legacy
{--dry-run : Preview only — no writes to DB}
{--chunk=500 : Rows to process per batch}
{--skip-empty : Skip comments with empty or whitespace-only content}
{--table= : Override legacy source table name (defaults to auto-detect news_comment/news_comments)}';
protected $description = 'Import legacy news comments into news_article_comments';
public function handle(): int
{
$dryRun = (bool) $this->option('dry-run');
$chunk = max(1, (int) $this->option('chunk'));
$skipEmpty = (bool) $this->option('skip-empty');
try {
DB::connection('legacy')->getPdo();
} catch (\Throwable $exception) {
$this->error('Cannot connect to legacy database: ' . $exception->getMessage());
return self::FAILURE;
}
$legacyTable = $this->resolveLegacyTable();
if ($legacyTable === null) {
$this->error('Legacy table `news_comment` or `news_comments` was not found.');
return self::FAILURE;
}
if (! DB::getSchemaBuilder()->hasTable('news_article_comments')) {
$this->error('Target table `news_article_comments` is missing. Run migrations first.');
return self::FAILURE;
}
if (! DB::getSchemaBuilder()->hasColumn('news_articles', 'legacy_news_id')) {
$this->error('Column `news_articles.legacy_news_id` is missing. Run migrations first.');
return self::FAILURE;
}
if ($dryRun) {
$this->warn('[DRY-RUN] No data will be written.');
}
$articleMap = DB::table('news_articles')
->whereNotNull('legacy_news_id')
->pluck('id', 'legacy_news_id')
->mapWithKeys(fn ($articleId, $legacyId): array => [(int) $legacyId => (int) $articleId])
->all();
$validUserIds = DB::table('users')
->whereNull('deleted_at')
->pluck('id')
->flip()
->all();
$alreadyImported = DB::table('news_article_comments')
->whereNotNull('legacy_id')
->pluck('legacy_id')
->flip()
->all();
$total = DB::connection('legacy')->table($legacyTable)->count();
if ($total === 0) {
$this->warn('No legacy news comments found.');
return self::SUCCESS;
}
$stats = [
'imported' => 0,
'skipped_duplicate' => 0,
'skipped_article' => 0,
'skipped_empty' => 0,
'users_unmapped' => 0,
'errors' => 0,
];
$touchedArticleIds = [];
DB::connection('legacy')
->table($legacyTable)
->orderBy('comment_id')
->chunk($chunk, function ($rows) use (&$alreadyImported, $articleMap, $validUserIds, $dryRun, $skipEmpty, &$stats, &$touchedArticleIds): void {
$inserts = [];
foreach ($rows as $row) {
$legacyId = (int) ($row->comment_id ?? 0);
$legacyNewsId = (int) ($row->news_id ?? 0);
$legacyUserId = (int) ($row->user_id ?? 0);
$body = trim((string) ($row->message ?? ''));
if ($legacyId < 1) {
$stats['errors']++;
continue;
}
if (isset($alreadyImported[$legacyId])) {
$stats['skipped_duplicate']++;
continue;
}
if ($body === '') {
if ($skipEmpty) {
$stats['skipped_empty']++;
continue;
}
$body = '[no content]';
}
$articleId = $articleMap[$legacyNewsId] ?? null;
if (! $articleId) {
$stats['skipped_article']++;
continue;
}
$resolvedUserId = isset($validUserIds[$legacyUserId]) ? $legacyUserId : null;
if ($resolvedUserId === null && $legacyUserId > 0) {
$stats['users_unmapped']++;
}
$timestamp = $this->normalizeTimestamp($row->posted ?? null);
$authorName = trim((string) ($row->author ?? ''));
$record = [
'legacy_id' => $legacyId,
'legacy_user_id' => $legacyUserId > 0 ? $legacyUserId : null,
'article_id' => $articleId,
'user_id' => $resolvedUserId,
'parent_id' => null,
'author_name' => $authorName !== '' ? $authorName : null,
'body' => $body,
'rendered_body' => nl2br(e($body)),
'status' => 'visible',
'legacy_posted_at' => $timestamp,
'created_at' => $timestamp,
'updated_at' => $timestamp,
'deleted_at' => null,
];
if (! $dryRun) {
$inserts[] = $record;
$alreadyImported[$legacyId] = true;
$touchedArticleIds[$articleId] = $articleId;
}
$stats['imported']++;
}
if (! $dryRun && $inserts !== []) {
try {
DB::table('news_article_comments')->insert($inserts);
} catch (\Throwable) {
foreach ($inserts as $insert) {
try {
DB::table('news_article_comments')->insertOrIgnore([$insert]);
} catch (\Throwable) {
$stats['errors']++;
}
}
}
}
});
if (! $dryRun && $touchedArticleIds !== []) {
DB::table('news_articles')
->whereIn('id', array_values($touchedArticleIds))
->update(['comments_enabled' => true]);
}
$this->table(
['Result', 'Count'],
[
['Imported', $stats['imported']],
['Skipped - already imported', $stats['skipped_duplicate']],
['Skipped - article missing', $stats['skipped_article']],
['Skipped - empty body', $stats['skipped_empty']],
['Imported with unmapped user', $stats['users_unmapped']],
['Errors', $stats['errors']],
]
);
return $stats['errors'] > 0 ? self::FAILURE : self::SUCCESS;
}
private function resolveLegacyTable(): ?string
{
$configured = trim((string) $this->option('table'));
if ($configured !== '') {
return DB::connection('legacy')->getSchemaBuilder()->hasTable($configured) ? $configured : null;
}
foreach (['news_comment', 'news_comments'] as $candidate) {
if (DB::connection('legacy')->getSchemaBuilder()->hasTable($candidate)) {
return $candidate;
}
}
return null;
}
private function normalizeTimestamp(mixed $value): string
{
$raw = trim((string) ($value ?? ''));
if ($raw === '' || str_starts_with($raw, '0000-00-00')) {
return now()->toDateTimeString();
}
try {
return Carbon::parse($raw)->toDateTimeString();
} catch (\Throwable) {
return now()->toDateTimeString();
}
}
}