logPath = storage_path('logs/forum_migration.log'); } public function handle(): int { $this->info('Starting forum migration'); $this->log('Starting forum migration'); $dry = $this->option('dry-run'); $only = $this->option('only'); $chunk = (int)$this->option('chunk'); try { if (!$only || $only === 'categories') { $this->migrateCategories($dry); } if (!$only || $only === 'threads') { $this->migrateThreads($dry, $chunk); } if (!$only || $only === 'posts') { $this->migratePosts($dry, $chunk); } if (!$only || $only === 'gallery') { $this->migrateGallery($dry, $chunk); } if ($this->option('report')) { $this->generateReport(); } $this->info('Forum migration finished'); $this->log('Forum migration finished'); return 0; } catch (Exception $e) { $this->error('Migration failed: ' . $e->getMessage()); $this->log('Migration failed: ' . $e->getMessage()); return 1; } } protected function migrateCategories(bool $dry) { $this->info('Migrating categories'); $legacy = DB::connection('legacy'); $roots = $legacy->table('forum_topics') ->select('root_id') ->distinct() ->where('root_id', '>', 0) ->pluck('root_id'); $this->info('Found ' . $roots->count() . ' legacy root ids'); foreach ($roots as $rootId) { $row = $legacy->table('forum_topics')->where('topic_id', $rootId)->first(); $name = $row->topic ?? 'Category ' . $rootId; $slug = Str::slug(substr($name, 0, 150)); $this->line("-> root {$rootId}: {$name}"); if ($dry) { $this->log("[dry] create category {$name} ({$slug})"); continue; } ForumCategory::updateOrCreate( ['id' => $rootId], ['name' => $name, 'slug' => $slug] ); } $this->info('Categories migrated'); } protected function migrateThreads(bool $dry, int $chunk) { $this->info('Migrating threads'); $legacy = DB::connection('legacy'); $query = $legacy->table('forum_topics')->orderBy('topic_id'); $total = $query->count(); $this->info("Total threads to process: {$total}"); $bar = $this->output->createProgressBar($total); $bar->start(); // chunk by legacy primary key `topic_id` $query->chunkById($chunk, function ($rows) use ($dry, $bar) { foreach ($rows as $r) { $bar->advance(); $data = [ 'id' => $r->topic_id, 'category_id' => $this->resolveCategoryId($r->root_id ?? null, $r->topic_id), // resolve user id or assign to system user (1) when missing or not found 'user_id' => $this->resolveUserId($r->user_id ?? null), 'title' => $r->topic, 'slug' => $this->uniqueSlug(Str::slug(substr($r->topic,0,200)) ?: 'thread-'.$r->topic_id, $r->topic_id), 'content' => $r->preview ?? '', 'views' => $r->views ?? 0, 'is_locked' => isset($r->open) ? !((bool)$r->open) : false, 'is_pinned' => false, 'visibility' => $this->mapPrivilegeToVisibility($r->privilege ?? 0), 'last_post_at' => $this->normalizeDate($r->last_update ?? null), ]; if ($dry) { $this->log('[dry] thread: ' . $r->topic_id . ' - ' . $r->topic); continue; } ForumThread::updateOrCreate(['id' => $data['id']], $data); } }, 'topic_id'); $bar->finish(); $this->line(''); $this->info('Threads migrated'); } protected function migratePosts(bool $dry, int $chunk) { $this->info('Migrating posts'); $legacy = DB::connection('legacy'); $query = $legacy->table('forum_posts')->orderBy('post_id'); $total = $query->count(); $this->info("Total posts to process: {$total}"); $bar = $this->output->createProgressBar($total); $bar->start(); // legacy forum_posts uses `post_id` as primary key $query->chunkById($chunk, function ($rows) use ($dry, $bar) { foreach ($rows as $r) { $bar->advance(); $data = [ 'id' => $r->post_id, 'thread_id' => $r->topic_id, 'user_id' => $r->user_id ?? null, 'content' => $this->convertLegacyMessage($r->message ?? ''), 'is_edited' => isset($r->isupdated) ? (bool)$r->isupdated : false, 'edited_at' => $r->updated ?? null, ]; if ($dry) { $this->log('[dry] post: ' . $r->post_id); continue; } ForumPost::updateOrCreate(['id' => $data['id']], $data); } }, 'post_id'); $bar->finish(); $this->line(''); $this->info('Posts migrated'); } protected function mapPrivilegeToVisibility($priv) { // legacy privilege: 0 public, 1 members, 4 staff? adjust mapping conservatively if ($priv >= 4) return 'staff'; if ($priv >= 1) return 'members'; return 'public'; } protected function normalizeDate($val) { if (empty($val)) return null; $s = trim((string)$val); // legacy sometimes contains sentinel invalid dates like -0001-11-30 or zero dates if (strpos($s, '-0001') !== false) return null; if (strpos($s, '0000-00-00') !== false) return null; if (strtotime($s) === false) return null; return date('Y-m-d H:i:s', strtotime($s)); } protected function uniqueSlug(string $base, int $id) { $slug = $base; $i = 0; while (ForumThread::where('slug', $slug)->where('id', '<>', $id)->exists()) { $i++; $slug = $base . '-' . $id; // if somehow still exists, append counter if (ForumThread::where('slug', $slug)->where('id', '<>', $id)->exists()) { $slug = $base . '-' . $id . '-' . $i; } } return $slug; } protected function resolveCategoryId($rootId, $topicId) { // prefer explicit rootId if (!empty($rootId)) { // ensure category exists if (ForumCategory::where('id', $rootId)->exists()) return $rootId; } // if this topic itself is a category if (ForumCategory::where('id', $topicId)->exists()) return $topicId; // fallback: use first available category $first = ForumCategory::first(); if ($first) return $first->id; // as last resort, create Uncategorized $cat = ForumCategory::create(['name' => 'Uncategorized', 'slug' => 'uncategorized']); return $cat->id; } protected function resolveUserId($userId) { if (empty($userId)) { return 1; } // check users table in default connection if (\DB::table('users')->where('id', $userId)->exists()) { return $userId; } return 1; // fallback system user } protected function convertLegacyMessage($msg) { $converter = new BbcodeConverter(); return $converter->convert($msg); } protected function generateReport() { $this->info('Generating migration report'); $legacy = DB::connection('legacy'); $legacyCounts = [ 'categories' => $legacy->table('forum_topics')->where('root_id','>',0)->distinct('root_id')->count('root_id'), 'threads' => $legacy->table('forum_topics')->count(), 'posts' => $legacy->table('forum_posts')->count(), ]; $newCounts = [ 'categories' => ForumCategory::count(), 'threads' => ForumThread::count(), 'posts' => ForumPost::count(), 'attachments' => \DB::table('forum_attachments')->count(), ]; $this->info('Legacy counts: ' . json_encode($legacyCounts)); $this->info('New counts: ' . json_encode($newCounts)); $this->log('Report: legacy=' . json_encode($legacyCounts) . ' new=' . json_encode($newCounts)); } protected function log(string $msg) { $line = '[' . date('c') . '] ' . $msg . "\n"; file_put_contents($this->logPath, $line, FILE_APPEND | LOCK_EX); } protected function migrateGallery(bool $dry, int $chunk) { $this->info('Migrating gallery (forum_topics_gallery → forum_attachments)'); $legacy = DB::connection('legacy'); if (!$legacy->getSchemaBuilder()->hasTable('forum_topics_gallery')) { $this->info('No legacy forum_topics_gallery table found, skipping'); return; } $query = $legacy->table('forum_topics_gallery')->orderBy('id'); $total = $query->count(); $this->info("Total gallery items to process: {$total}"); $bar = $this->output->createProgressBar($total); $bar->start(); $query->chunkById($chunk, function ($rows) use ($dry, $bar) { foreach ($rows as $r) { $bar->advance(); // expected legacy fields: id, name, category (topic id), folder, datum, description $topicId = $r->category ?? ($r->topic_id ?? null); $fileName = $r->name ?? null; if (empty($topicId) || empty($fileName)) { $this->log('Skipping gallery row with missing topic or name: ' . json_encode($r)); continue; } $nid = floor($topicId / 100); $relativePath = "files/news/{$nid}/{$topicId}/{$fileName}"; $publicPath = public_path($relativePath); $fileSize = null; $mimeType = null; $width = null; $height = null; if (file_exists($publicPath)) { $fileSize = filesize($publicPath); $img = @getimagesize($publicPath); if ($img !== false) { $width = $img[0]; $height = $img[1]; $mimeType = $img['mime'] ?? null; } else { $finfo = finfo_open(FILEINFO_MIME_TYPE); $mimeType = finfo_file($finfo, $publicPath); finfo_close($finfo); } } // find legacy first post id for this topic $legacy = DB::connection('legacy'); $firstPostId = $legacy->table('forum_posts') ->where('topic_id', $topicId) ->orderBy('post_date') ->value('post_id'); // map to new forum_posts id (we preserved ids when migrating) $postId = null; if ($firstPostId && \App\Models\ForumPost::where('id', $firstPostId)->exists()) { $postId = $firstPostId; } else { // fallback: find any post in new DB for thread $post = \App\Models\ForumPost::where('thread_id', $topicId)->orderBy('created_at')->first(); if ($post) $postId = $post->id; } if (empty($postId)) { $this->log('No target post found for gallery item: topic ' . $topicId . ' file ' . $fileName); continue; } if ($dry) { $this->log("[dry] attach {$relativePath} -> post {$postId}"); continue; } \App\Models\ForumAttachment::create([ 'post_id' => $postId, 'file_path' => $relativePath, 'file_size' => $fileSize ?? 0, 'mime_type' => $mimeType, 'width' => $width, 'height' => $height, 'created_at' => now(), 'updated_at' => now(), ]); } }, 'id'); $bar->finish(); $this->line(''); $this->info('Gallery migrated'); } }