option('dry-run'); $chunk = max(1, (int) $this->option('chunk')); if ($dryRun) { $this->warn('[DRY-RUN] No data will be written.'); } // ── Check legacy connection ─────────────────────────────────────────── try { DB::connection('legacy')->getPdo(); } catch (Throwable $e) { $this->error('Cannot connect to legacy database: ' . $e->getMessage()); return self::FAILURE; } $legacySchema = DB::connection('legacy')->getSchemaBuilder(); if (! $legacySchema->hasTable('chat')) { $this->error('Legacy table `chat` not found on the legacy connection.'); return self::FAILURE; } $columns = $legacySchema->getColumnListing('chat'); $this->info('Legacy chat columns: ' . implode(', ', $columns)); // Map expected legacy columns (adapt if your legacy schema differs) $hasReadDate = in_array('read_date', $columns, true); $hasSoftDelete = in_array('deleted', $columns, true); // ── Count total rows ────────────────────────────────────────────────── $query = DB::connection('legacy')->table('chat'); if ($hasSoftDelete) { $query->where('deleted', 0); } $total = $query->count(); $this->info("Total legacy rows to process: {$total}"); if ($total === 0) { $this->info('Nothing to migrate.'); return self::SUCCESS; } $bar = $this->output->createProgressBar($total); $inserted = 0; $skipped = 0; $offset = 0; // ── Chunk processing ────────────────────────────────────────────────── while (true) { $rows = DB::connection('legacy') ->table('chat') ->when($hasSoftDelete, fn ($q) => $q->where('deleted', 0)) ->orderBy('id') ->offset($offset) ->limit($chunk) ->get(); if ($rows->isEmpty()) { break; } foreach ($rows as $row) { $senderId = (int) ($row->sender_user_id ?? $row->from_user_id ?? $row->user_id ?? 0); $receiverId = (int) ($row->receiver_user_id ?? $row->to_user_id ?? $row->recipient_id ?? 0); $body = trim((string) ($row->message ?? $row->body ?? $row->content ?? '')); $createdAt = $row->created_at ?? $row->date ?? $row->timestamp ?? now(); $readDate = $hasReadDate ? $row->read_date : null; if ($senderId === 0 || $receiverId === 0 || $body === '') { $skipped++; $this->skipped[] = ['id' => $row->id ?? '?', 'reason' => 'missing sender/receiver/body']; $bar->advance(); continue; } // Skip self-messages if ($senderId === $receiverId) { $skipped++; $this->skipped[] = ['id' => $row->id ?? '?', 'reason' => 'self-message']; $bar->advance(); continue; } // Sanitize: strip HTML, convert smileys to emoji $body = $this->sanitize($body); if ($dryRun) { $inserted++; $bar->advance(); continue; } try { DB::transaction(function () use ($senderId, $receiverId, $body, $createdAt, $readDate, &$inserted) { // Find or create direct conversation $conv = Conversation::findDirect($senderId, $receiverId); if (! $conv) { $conv = Conversation::create([ 'type' => 'direct', 'created_by' => $senderId, 'last_message_at' => $createdAt, ]); ConversationParticipant::insert([ [ 'conversation_id' => $conv->id, 'user_id' => $senderId, 'role' => 'admin', 'joined_at' => $createdAt, 'last_read_at' => $readDate, ], [ 'conversation_id' => $conv->id, 'user_id' => $receiverId, 'role' => 'member', 'joined_at' => $createdAt, 'last_read_at' => $readDate, ], ]); } else { // Update last_read_at on existing participants when available if ($readDate) { ConversationParticipant::where('conversation_id', $conv->id) ->where('user_id', $receiverId) ->whereNull('last_read_at') ->update(['last_read_at' => $readDate]); } } Message::create([ 'conversation_id' => $conv->id, 'sender_id' => $senderId, 'body' => $body, 'created_at' => $createdAt, 'updated_at' => $createdAt, ]); // Keep last_message_at up to date if ($conv->last_message_at < $createdAt) { $conv->update(['last_message_at' => $createdAt]); } $inserted++; }); } catch (Throwable $e) { $skipped++; $this->skipped[] = ['id' => $row->id ?? '?', 'reason' => $e->getMessage()]; Log::warning('MigrateMessages: skipped row', [ 'id' => $row->id ?? '?', 'reason' => $e->getMessage(), ]); } $bar->advance(); } $offset += $chunk; } $bar->finish(); $this->newLine(); $this->info("Done. Inserted: {$inserted} | Skipped: {$skipped}"); if ($skipped > 0 && $this->option('verbose')) { $this->table(['ID', 'Reason'], $this->skipped); } return self::SUCCESS; } /** * Strip HTML tags and convert common legacy smileys to emoji. */ private function sanitize(string $body): string { // Strip raw HTML $body = strip_tags($body); // Decode HTML entities $body = html_entity_decode($body, ENT_QUOTES | ENT_HTML5, 'UTF-8'); // Common smiley → emoji mapping $smileys = [ ':)' => '🙂', ':-)' => '🙂', ':(' => '🙁', ':-(' => '🙁', ':D' => '😀', ':-D' => '😀', ':P' => '😛', ':-P' => '😛', ';)' => '😉', ';-)' => '😉', ':o' => '😮', ':O' => '😮', ':|' => '😐', ':-|' => '😐', ':/' => '😕', ':-/' => '😕', '<3' => '❤️', 'xD' => '😂', 'XD' => '😂', ]; return str_replace(array_keys($smileys), array_values($smileys), $body); } }