diff --git a/app/Console/Commands/MigrateNewsCommand.php b/app/Console/Commands/MigrateNewsCommand.php index 60d55a3..8fe8088 100644 --- a/app/Console/Commands/MigrateNewsCommand.php +++ b/app/Console/Commands/MigrateNewsCommand.php @@ -5,7 +5,9 @@ use App\Enums\NewsStatus; use App\Models\News; use Illuminate\Console\Command; +use Illuminate\Support\Carbon; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Storage; use Illuminate\Support\Str; class MigrateNewsCommand extends Command @@ -22,63 +24,154 @@ class MigrateNewsCommand extends Command * * @var string */ - protected $description = 'Migrate news data from legacy table where category = 1'; + protected $description = 'Migrate news data from legacy table with optimized processing and detailed output'; + + /** + * Store migrated news IDs to optimize processing. + */ + private array $migratedIds = []; /** * Execute the console command. */ public function handle() { - $this->info('Starting news migration...'); - $legacyConn = DB::connection('mysql_second'); + $query = $legacyConn->table('news')->where('category', '0'); - // Get legacy news with category 1 - $legacyNews = $legacyConn->table('news') - ->where('category', '0') - ->get(); - - if ($legacyNews->isEmpty()) { - $this->warn('No news found with category 1.'); + $totalCount = $query->count(); + if ($totalCount === 0) { + $this->warn('No news found with category 0.'); return; } - // Get a default author ID (prefer Developer or Admin) - $defaultAuthorId = DB::table('users')->first()?->id ?? 1; - $this->withProgressBar($legacyNews, function ($legacy) use ($defaultAuthorId) { - // Use updateOrInsert for the basic record - DB::table('news')->updateOrInsert( - ['id' => $legacy->id], - [ - 'author_id' => $defaultAuthorId, - 'title' => $legacy->title, - 'slug' => Str::slug($legacy->title).'-'.$legacy->id, - 'content' => $legacy->content, - 'excerpt' => Str::limit(strip_tags($legacy->content), 160), - 'link' => $legacy->link ? Str::limit($legacy->link, 50, '') : null, - 'views' => 0, - 'status' => NewsStatus::PUBLISHED->value, - 'published_at' => $legacy->created_at, - 'created_at' => $legacy->created_at ?? now(), - 'updated_at' => $legacy->updated_at ?? now(), - 'deleted_at' => $legacy->deleted_at, - ] - ); + $this->info("Found {$totalCount} legacy news records."); - // Handle Tags using the Model (if tag exists in legacy) - if (! empty($legacy->tag)) { - $news = News::find($legacy->id); - if ($news) { - // split by comma or other delimiter if multiple tags, - // legacy 'tag' column is varchar(20), likely a single tag or comma separated - $tags = array_filter(array_map('trim', explode(',', $legacy->tag))); - $news->syncTags($tags); - } + // Cache existing news IDs with media to avoid N+1 media queries + $this->migratedIds = DB::table('media') + ->where('model_type', News::class) + ->where('collection_name', 'news') + ->pluck('model_id') + ->flip() + ->all(); + + // Process in chunks for memory efficiency + $query->orderBy('id')->chunk(50, function ($chunk) { + foreach ($chunk as $legacy) { + $this->processNews($legacy); } }); - $this->newLine(); + $this->newLine(); $this->info('News migration completed successfully!'); } + + /** + * Process a single legacy news record. + */ + private function processNews($legacy): void + { + $idText = "[#{$legacy->id}]"; + $idOutput = $legacy->deleted_at ? "{$idText}" : "{$idText}"; + + $this->output->write("{$idOutput} Processing: ".Str::limit($legacy->title, 40).'... '); + + // Step 1: Basic data update (Always sync basic info) + DB::table('news')->updateOrInsert( + ['id' => $legacy->id], + [ + 'author_id' => $legacy->enhancer, + 'title' => $legacy->title, + 'slug' => Str::slug($legacy->title).'-'.$legacy->id, + 'content' => $legacy->content, + 'excerpt' => Str::limit(strip_tags($legacy->content), 160), + 'link' => $legacy->link ? Str::limit($legacy->link, 255, '') : null, + 'views' => 0, + 'status' => NewsStatus::PUBLISHED->value, + 'published_at' => $legacy->created_at, + 'created_at' => $legacy->created_at ?? now(), + 'updated_at' => $legacy->updated_at ?? now(), + 'deleted_at' => $legacy->deleted_at, + ] + ); + + // Step 2: Skip heavy operations (tags & media) if record already has media + if (isset($this->migratedIds[$legacy->id])) { + $this->output->writeln('SKIP'); + + return; + } + + $news = News::withTrashed()->find($legacy->id); + if (! $news) { + $this->output->writeln('NOT FOUND'); + + return; + } + + // Step 3: Handle Tags + if (! empty($legacy->tag)) { + $tags = array_filter(array_map('trim', explode(',', $legacy->tag))); + $news->syncTags($tags); + } + + // Step 4: Handle Media + if (! empty($legacy->image)) { + $this->migrateMedia($news, $legacy); + } else { + $this->output->writeln('DONE'); + } + } + + /** + * Migrate media for a news record. + */ + private function migrateMedia(News $news, $legacy): void + { + $filename = trim(basename($legacy->image)); + $sourcePath = 'old data/news/'.$filename; + + $exists = false; + $attempts = 0; + $maxAttempts = 3; + + while ($attempts < $maxAttempts) { + try { + $exists = Storage::disk('s3')->exists($sourcePath); + break; + } catch (\Exception $e) { + $attempts++; + if ($attempts >= $maxAttempts) { + $actualError = $e->getPrevious() ? $e->getPrevious()->getMessage() : $e->getMessage(); + $this->output->writeln("S3 ERROR ({$attempts}x): ".$actualError.''); + + return; + } + usleep(200000); + } + } + + if (! $exists) { + $this->output->writeln('IMAGE MISSING'); + + return; + } + + try { + $date = Carbon::parse($legacy->created_at ?? now())->toDateString(); + + $news->addMediaFromDisk($sourcePath, 's3') + ->withCustomProperties([ + 'feature' => 'news', + 'date' => $date, + ]) + ->preservingOriginal() + ->toMediaCollection('news', 's3'); + + $this->output->writeln('DONE + IMG'); + } catch (\Exception $e) { + $this->output->writeln("FAILED: {$e->getMessage()}"); + } + } }