Add news article comments and reactions
This commit is contained in:
229
app/Console/Commands/ImportLegacyNewsCommentsCommand.php
Normal file
229
app/Console/Commands/ImportLegacyNewsCommentsCommand.php
Normal file
@@ -0,0 +1,229 @@
|
||||
<?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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user