pdo = $pdo; $this->source = $source; if (!isset($source['id']) || empty($source['id'])) { throw new \Exception('Source ID is required for sync service'); } $this->sourceId = (int) $source['id']; $this->currentSyncLogId = $existingSyncLogId; // Create log file for this sync operation $this->initializeLogFile(); } private function initializeLogFile(): void { $timestamp = date('Y-m-d_H-i-s'); $sourceName = strtolower($this->source['name'] ?? 'unknown'); $this->logFilePath = "logs/{$sourceName}_sync_{$timestamp}.log"; // Create logs directory if it doesn't exist $logDir = dirname($this->logFilePath); if (!is_dir($logDir)) { mkdir($logDir, 0755, true); } $this->logFileHandle = fopen($this->logFilePath, 'w'); if ($this->logFileHandle) { $this->logProgress("=== Starting {$sourceName} sync at " . date('Y-m-d H:i:s') . " ==="); } } public function __destruct() { if ($this->logFileHandle) { $this->logProgress("=== Sync completed at " . date('Y-m-d H:i:s') . " ==="); if ($this->currentSyncLogId !== null) { $this->updateSyncLog($this->currentSyncLogId, 'completed', [ 'processed_items' => $this->getProcessedCount(), 'new_items' => $this->getNewCount(), 'updated_items' => $this->getUpdatedCount(), 'deleted_items' => $this->getDeletedCount(), 'message' => $this->getCompletionMessage() ]); } fclose($this->logFileHandle); } } protected $deletedCount = 0; protected $totalItems = 0; public function startSync(string $syncType = 'full'): int { // Set higher limits for long-running syncs ini_set('max_execution_time', 3600); // 1 hour ini_set('memory_limit', '512M'); // Use existing sync log ID if provided, otherwise create a new one if ($this->currentSyncLogId) { $syncLogId = $this->currentSyncLogId; // Update the existing log to 'started' status $this->updateSyncLog($syncLogId, 'started', [ 'sync_type' => $syncType, 'started_at' => date('Y-m-d H:i:s') ]); } else { // Create sync log entry $syncLogId = $this->createSyncLog($syncType, 'started'); } $this->currentSyncLogId = $syncLogId; try { $this->logProgress("Starting {$syncType} sync for source: " . ($this->source['display_name'] ?? $this->source['name'])); // Execute cleanup if requested if ($syncType === 'cleanup') { $this->executeCleanup(); } else { $this->executeSync($syncType); // Optionally run cleanup after regular sync if (in_array($syncType, ['full', 'incremental'])) { $this->logProgress("Running cleanup after sync..."); $this->executeCleanup(); } } // Update sync log as completed $this->updateSyncLog($syncLogId, 'completed', [ 'processed_items' => $this->getProcessedCount(), 'new_items' => $this->getNewCount(), 'updated_items' => $this->getUpdatedCount(), 'deleted_items' => $this->getDeletedCount(), 'total_items' => $this->getTotalItems(), 'message' => $this->getCompletionMessage() ]); // Log completion to file but don't update database (already completed above) $this->logProgressToFile("Sync completed successfully"); } catch (Exception $e) { // Log the full error details $errorMessage = $e->getMessage(); $errorFile = $e->getFile(); $errorLine = $e->getLine(); $errorTrace = $e->getTraceAsString(); $this->logProgress("CRITICAL ERROR - Sync failed: {$errorMessage}"); $this->logProgress("Error location: {$errorFile}:{$errorLine}"); $this->logProgress("Stack trace: {$errorTrace}"); // Update sync log as failed with full error details $this->updateSyncLog($syncLogId, 'failed', [ 'message' => $errorMessage, 'errors' => [ $errorMessage, "File: {$errorFile}:{$errorLine}", "Stack: " . substr($errorTrace, 0, 1000) // Limit trace size ] ]); throw $e; } return $syncLogId; } protected function executeCleanup(): void { // Override in subclasses to implement cleanup logic $this->logProgress("Cleanup not implemented for this sync service"); } private function createSyncLog(string $syncType, string $status): int { $data = [ 'source_id' => $this->sourceId, 'sync_type' => $syncType, 'status' => $status, 'total_items' => 0, 'processed_items' => 0, 'new_items' => 0, 'updated_items' => 0, 'deleted_items' => 0, 'started_at' => date('Y-m-d H:i:s') ]; $columns = array_keys($data); $placeholders = array_map(fn($col) => ":$col", $columns); $sql = "INSERT INTO sync_logs (" . implode(', ', $columns) . ") VALUES (" . implode(', ', $placeholders) . ")"; $stmt = $this->pdo->prepare($sql); $stmt->execute($data); return (int) $this->pdo->lastInsertId(); } protected function updateSyncLog(int $syncLogId, string $status, array $stats = []): bool { $data = [ 'status' => $status, 'processed_items' => $stats['processed_items'] ?? 0, 'new_items' => $stats['new_items'] ?? 0, 'updated_items' => $stats['updated_items'] ?? 0, 'deleted_items' => $stats['deleted_items'] ?? 0, 'completed_at' => date('Y-m-d H:i:s') ]; if (!empty($stats['errors'])) { $data['errors'] = json_encode($stats['errors']); } if (!empty($stats['message'])) { $data['message'] = $stats['message']; } if (isset($stats['total_items'])) { $data['total_items'] = $stats['total_items']; } $setClause = array_map(fn($col) => "$col = :$col", array_keys($data)); $sql = "UPDATE sync_logs SET " . implode(', ', $setClause) . " WHERE id = :id"; $data['id'] = $syncLogId; $stmt = $this->pdo->prepare($sql); return $stmt->execute($data); } abstract protected function executeSync(string $syncType): void; protected function getProcessedCount(): int { return 0; // Override in subclasses } protected function getNewCount(): int { return 0; // Override in subclasses } protected function getUpdatedCount(): int { return 0; // Override in subclasses } protected function getDeletedCount(): int { return $this->deletedCount; // Return the deleted count } protected function getTotalItems(): int { return $this->totalItems; } protected function setTotalItems(int $total): void { $this->totalItems = $total; } protected function getCompletionMessage(): string { $new = $this->getNewCount(); $updated = $this->getUpdatedCount(); $deleted = $this->getDeletedCount(); $message = "Sync completed"; if ($new > 0 || $updated > 0 || $deleted > 0) { $parts = []; if ($new > 0) $parts[] = "{$new} new"; if ($updated > 0) $parts[] = "{$updated} updated"; if ($deleted > 0) $parts[] = "{$deleted} deleted"; $message .= ": " . implode(", ", $parts); } return $message; } protected $currentSyncLogId = null; protected function logProgressToFile(string $message): void { $timestamp = date('H:i:s'); $logMessage = "[{$timestamp}] {$message}\n"; // Write to log file if available if ($this->logFileHandle) { fwrite($this->logFileHandle, $logMessage); } // Also write to error log for immediate visibility error_log($message); } protected function logProgress(string $message): void { $timestamp = date('H:i:s'); $logMessage = "[{$timestamp}] {$message}\n"; // Write to log file if available if ($this->logFileHandle) { fwrite($this->logFileHandle, $logMessage); } // Also write to error log for immediate visibility error_log($message); // Update sync log with progress message if we have a current sync log if ($this->currentSyncLogId) { $updateData = [ 'message' => $message, 'processed_items' => $this->getProcessedCount(), 'new_items' => $this->getNewCount(), 'updated_items' => $this->getUpdatedCount(), 'deleted_items' => $this->getDeletedCount() ]; // Only update total_items if it's greater than 0 (to avoid overwriting with 0) if ($this->getTotalItems() > 0) { $updateData['total_items'] = $this->getTotalItems(); } // Don't update status for completion messages - status should remain as set by completion logic $newStatus = 'running'; $this->updateSyncLog($this->currentSyncLogId, $newStatus, $updateData); } } public function getLogFilePath(): string { return $this->logFilePath ?? ''; } }