186 lines
6.6 KiB
PHP
186 lines
6.6 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 ExportLegacyNewsCommentsSqlCommand extends Command
|
|
{
|
|
protected $signature = 'news:comments-export-legacy-sql
|
|
{--path=database/sql/news_article_comments_legacy_import.sql : Output SQL file path}
|
|
{--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 = 'Generate a production-safe SQL file for legacy news comments import';
|
|
|
|
public function handle(): int
|
|
{
|
|
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;
|
|
}
|
|
|
|
$outputPath = $this->resolveOutputPath((string) $this->option('path'));
|
|
$skipEmpty = (bool) $this->option('skip-empty');
|
|
|
|
$directory = dirname($outputPath);
|
|
if (! is_dir($directory)) {
|
|
mkdir($directory, 0777, true);
|
|
}
|
|
|
|
$handle = fopen($outputPath, 'wb');
|
|
if ($handle === false) {
|
|
$this->error('Unable to write SQL file: ' . $outputPath);
|
|
|
|
return self::FAILURE;
|
|
}
|
|
|
|
$written = 0;
|
|
$skippedEmpty = 0;
|
|
$legacyNewsIds = [];
|
|
|
|
fwrite($handle, "-- Legacy news comments import generated at " . now()->toDateTimeString() . PHP_EOL);
|
|
fwrite($handle, "START TRANSACTION;" . PHP_EOL . PHP_EOL);
|
|
|
|
DB::connection('legacy')
|
|
->table($legacyTable)
|
|
->orderBy('comment_id')
|
|
->chunk(500, function ($rows) use ($handle, $skipEmpty, &$written, &$skippedEmpty, &$legacyNewsIds): void {
|
|
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 || $legacyNewsId < 1) {
|
|
continue;
|
|
}
|
|
|
|
if ($body === '') {
|
|
if ($skipEmpty) {
|
|
$skippedEmpty++;
|
|
continue;
|
|
}
|
|
|
|
$body = '[no content]';
|
|
}
|
|
|
|
$legacyNewsIds[$legacyNewsId] = $legacyNewsId;
|
|
|
|
$authorName = trim((string) ($row->author ?? ''));
|
|
$timestamp = $this->normalizeTimestamp($row->posted ?? null);
|
|
$renderedBody = nl2br(e($body));
|
|
$userExpression = $legacyUserId > 0
|
|
? "CASE WHEN EXISTS (SELECT 1 FROM users WHERE users.id = {$legacyUserId} AND users.deleted_at IS NULL) THEN {$legacyUserId} ELSE NULL END"
|
|
: 'NULL';
|
|
|
|
$statement = "INSERT IGNORE INTO news_article_comments (legacy_id, legacy_user_id, article_id, user_id, parent_id, author_name, body, rendered_body, status, legacy_posted_at, created_at, updated_at, deleted_at)\n"
|
|
. "SELECT {$legacyId}, " . ($legacyUserId > 0 ? (string) $legacyUserId : 'NULL') . ", news_articles.id, {$userExpression}, NULL, " . $this->quote($authorName !== '' ? $authorName : null) . ", " . $this->quote($body) . ", " . $this->quote($renderedBody) . ", 'visible', " . $this->quote($timestamp) . ", " . $this->quote($timestamp) . ", " . $this->quote($timestamp) . ", NULL\n"
|
|
. "FROM news_articles\n"
|
|
. "WHERE news_articles.legacy_news_id = {$legacyNewsId}\n"
|
|
. "LIMIT 1;\n\n";
|
|
|
|
fwrite($handle, $statement);
|
|
$written++;
|
|
}
|
|
});
|
|
|
|
if ($legacyNewsIds !== []) {
|
|
foreach (array_chunk(array_values($legacyNewsIds), 250) as $chunk) {
|
|
fwrite($handle, 'UPDATE news_articles SET comments_enabled = 1 WHERE legacy_news_id IN (' . implode(', ', array_map('intval', $chunk)) . ');' . PHP_EOL);
|
|
}
|
|
|
|
fwrite($handle, PHP_EOL);
|
|
}
|
|
|
|
fwrite($handle, 'COMMIT;' . PHP_EOL);
|
|
fclose($handle);
|
|
|
|
$this->info('SQL export written to ' . $outputPath);
|
|
$this->table(
|
|
['Result', 'Count'],
|
|
[
|
|
['Statements written', $written],
|
|
['Skipped - empty body', $skippedEmpty],
|
|
['Articles enabled for comments', count($legacyNewsIds)],
|
|
]
|
|
);
|
|
|
|
return 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 resolveOutputPath(string $path): string
|
|
{
|
|
$trimmed = trim($path);
|
|
|
|
if ($trimmed === '') {
|
|
return base_path('database/sql/news_article_comments_legacy_import.sql');
|
|
}
|
|
|
|
if (preg_match('/^[A-Za-z]:\\\\|^\\\\\\\\|^\//', $trimmed) === 1) {
|
|
return $trimmed;
|
|
}
|
|
|
|
return base_path(str_replace(['/', '\\\\'], DIRECTORY_SEPARATOR, $trimmed));
|
|
}
|
|
|
|
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();
|
|
}
|
|
}
|
|
|
|
private function quote(?string $value): string
|
|
{
|
|
if ($value === null) {
|
|
return 'NULL';
|
|
}
|
|
|
|
$escaped = str_replace(
|
|
["\\", "\0", "\n", "\r", "\x1a", "'"],
|
|
["\\\\", "\\0", "\\n", "\\r", "\\Z", "\\'"],
|
|
$value,
|
|
);
|
|
|
|
return "'{$escaped}'";
|
|
}
|
|
} |