Files
MediaCollectorLibary/app/Services/BaseSyncService.php
Lars Behrends 552bb72370 snyc...
2025-10-19 22:05:21 +02:00

312 lines
10 KiB
PHP

<?php
namespace App\Services;
use Exception;
abstract class BaseSyncService
{
protected \PDO $pdo;
protected array $source;
protected int $sourceId;
protected $logFileHandle;
protected $logFilePath;
public function __construct(\PDO $pdo, array $source, ?int $existingSyncLogId = null)
{
$this->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') . " ===");
$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 ?? '';
}
}