229 lines
8.0 KiB
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();
|
|
}
|
|
}
|
|
} |