addOption('force', 'f', InputOption::VALUE_NONE, 'Force sync even if local data is up to date') ->addOption('dry-run', null, InputOption::VALUE_NONE, 'Check for updates without downloading'); } protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); if (false === $this->lock()) { $io->warning('Sync is already running in another process'); $this->logger->info('XML sync skipped: another instance is running'); return Command::SUCCESS; } $force = $input->getOption('force'); $dryRun = $input->getOption('dry-run'); try { $remoteInfo = $this->readRemoteInfo(); } catch (FilesystemException|\InvalidArgumentException $e) { $io->error(sprintf('Failed to read remote info: %s', $e->getMessage())); $this->logger->error('XML sync failed: could not read remote info', ['exception' => $e]); return Command::FAILURE; } $io->text(sprintf( 'Remote: %s (%d files)', $remoteInfo->lastTransfer->format('d.m.Y H:i:s'), $remoteInfo->fileCount )); $localInfo = $this->readLocalInfo(); if (null !== $localInfo) { $io->text(sprintf( 'Local: %s (%d files)', $localInfo->lastTransfer->format('d.m.Y H:i:s'), $localInfo->fileCount )); } else { $io->text('Local: no data'); } $needsSync = $force || null === $localInfo || $remoteInfo->isNewerThan($localInfo); if (false === $needsSync) { $io->success('Local data is up to date, no sync required'); $this->logger->info('XML sync skipped: local data is up to date'); return Command::SUCCESS; } if ($dryRun) { $io->note('Sync required (dry-run mode, no files downloaded)'); return Command::SUCCESS; } $io->section('Syncing files'); try { $syncedCount = $this->syncFiles($io); } catch (FilesystemException $e) { $io->error(sprintf('Sync failed: %s', $e->getMessage())); $this->logger->error('XML sync failed during file transfer', ['exception' => $e]); return Command::FAILURE; } $this->invalidateCaches(); $io->text(sprintf('Invalidated %d cache keys', count(self::CACHE_KEYS_TO_INVALIDATE))); $io->success(sprintf('Synced %d files', $syncedCount)); $this->logger->info('XML sync completed', [ 'files_synced' => $syncedCount, 'remote_timestamp' => $remoteInfo->lastTransfer->format('c'), ]); return Command::SUCCESS; } private function readRemoteInfo(): XmlExportInfo { $content = $this->xmlSource->read(XmlExportInfo::getFilename()); return XmlExportInfo::fromString($content); } private function readLocalInfo(): ?XmlExportInfo { try { if (false === $this->xmlExport->fileExists(XmlExportInfo::getFilename())) { return null; } $content = $this->xmlExport->read(XmlExportInfo::getFilename()); return XmlExportInfo::fromString($content); } catch (FilesystemException|\InvalidArgumentException) { return null; } } /** * @throws FilesystemException */ private function syncFiles(SymfonyStyle $io): int { $files = $this->xmlSource ->listContents('.') ->filter(fn (StorageAttributes $attributes) => $attributes->isFile()) ->map(fn (StorageAttributes $attributes) => $attributes->path()) ->toArray(); $io->progressStart(count($files)); foreach ($files as $path) { $content = $this->xmlSource->read($path); $this->xmlExport->write($path, $content); $io->progressAdvance(); } $io->progressFinish(); return count($files); } private function invalidateCaches(): void { foreach (self::CACHE_KEYS_TO_INVALIDATE as $key) { $this->cache->delete($key); } } }