Enable Discovery debug logging for production troubleshooting

- Add DISCOVERY_LOG_LEVEL=debug
- Add DISCOVERY_SHOW_PROGRESS=true
- Temporary changes for debugging InitializerProcessor fixes on production
This commit is contained in:
2025-08-11 20:13:26 +02:00
parent 59fd3dd3b1
commit 55a330b223
3683 changed files with 2956207 additions and 16948 deletions

View File

@@ -0,0 +1,668 @@
<?php
declare(strict_types=1);
namespace App\Framework\Discovery\Storage;
use App\Framework\Cache\Cache;
use App\Framework\Cache\CacheItem;
use App\Framework\Cache\CacheKey;
use App\Framework\Core\Events\EventDispatcher;
use App\Framework\Core\ValueObjects\Byte;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Core\ValueObjects\Score;
use App\Framework\DateTime\Clock;
use App\Framework\Discovery\Events\CacheCompressionEvent;
use App\Framework\Discovery\Events\CacheHitEvent;
use App\Framework\Discovery\Events\CacheMissEvent;
use App\Framework\Discovery\Memory\DiscoveryMemoryManager;
use App\Framework\Discovery\Memory\MemoryStatus;
use App\Framework\Discovery\Results\DiscoveryRegistry;
use App\Framework\Discovery\ValueObjects\CacheLevel;
use App\Framework\Discovery\ValueObjects\CacheMetrics;
use App\Framework\Discovery\ValueObjects\CacheTier;
use App\Framework\Discovery\ValueObjects\CompressionLevel;
use App\Framework\Discovery\ValueObjects\DiscoveryContext;
use App\Framework\Filesystem\FilePath;
use App\Framework\Filesystem\FileSystemService;
use App\Framework\Logging\Logger;
/**
* Enhanced Discovery cache manager with memory-aware and tiered caching strategies
*
* Combines traditional discovery caching with:
* - Memory-aware compression and eviction
* - Tiered caching for different access patterns
* - Automatic cache level adjustment based on memory pressure
* - Comprehensive cache metrics and monitoring
*/
final class DiscoveryCacheManager
{
private const string CACHE_PREFIX = 'discovery:';
private const int DEFAULT_TTL_HOURS = 24;
private array $cacheMetrics = [];
private array $accessPatterns = [];
private CacheLevel $currentLevel = CacheLevel::NORMAL;
public function __construct(
private readonly Cache $cache,
private readonly Clock $clock,
private readonly FileSystemService $fileSystemService,
private readonly ?Logger $logger = null,
private readonly int $ttlHours = self::DEFAULT_TTL_HOURS,
private readonly ?DiscoveryMemoryManager $memoryManager = null,
private readonly ?EventDispatcher $eventDispatcher = null,
private float $compressionThreshold = 0.75,
private readonly float $evictionThreshold = 0.85
) {
}
/**
* Get cached discovery results with memory-aware retrieval
*/
public function get(DiscoveryContext $context): ?DiscoveryRegistry
{
// Try tiered cache first
$result = $this->getTieredCache($context);
if ($result !== null) {
return $result;
}
// Fallback to standard cache
$key = $this->buildCacheKey($context);
$item = $this->cache->get($key);
if (! $item->isHit) {
$this->emitCacheMissEvent($key->toString(), 'not_found');
return null;
}
$cached = $this->processRetrievedData($item->value);
if (! $cached instanceof DiscoveryRegistry) {
$this->logger?->warning('Invalid cache data type', [
'key' => $key->toString(),
'type' => gettype($cached),
]);
$this->emitCacheMissEvent($key->toString(), 'corrupted');
return null;
}
// Validate cache freshness
if ($this->isStale($context, $cached)) {
$this->logger?->info('Discovery cache is stale', [
'key' => $key->toString(),
]);
$this->invalidate($context);
$this->emitCacheMissEvent($key->toString(), 'expired');
return null;
}
// Track access and emit hit event
$this->trackAccess($key->toString());
$dataSize = $this->estimateRegistrySize($cached);
$this->emitCacheHitEvent($key->toString(), $dataSize);
return $cached;
}
/**
* Store discovery results with memory-aware and tiered caching
*/
public function store(DiscoveryContext $context, DiscoveryRegistry $registry): bool
{
// Optimize registry before caching
$registry->optimize();
// Determine cache strategy based on memory status
if ($this->memoryManager !== null) {
return $this->storeWithMemoryAwareness($context, $registry);
}
// Fallback to standard caching
return $this->storeStandard($context, $registry);
}
/**
* Store with memory-aware strategy
*/
private function storeWithMemoryAwareness(DiscoveryContext $context, DiscoveryRegistry $registry): bool
{
$memoryStatus = $this->memoryManager->getMemoryStatus('cache_store');
$dataSize = $this->estimateRegistrySize($registry);
// Determine appropriate cache level and tier
$cacheLevel = $this->determineCacheLevel($memoryStatus);
$cacheTier = $this->determineCacheTier($context, $registry, $memoryStatus);
// Apply memory-aware processing
$processedData = $this->applyMemoryProcessing($registry, $cacheLevel, $cacheTier);
$adjustedTtl = $this->calculateAdjustedTtl($cacheTier, $memoryStatus);
// Store in appropriate tier
$key = $this->buildTieredCacheKey($context, $cacheTier);
$cacheItem = CacheItem::forSet(
key: $key,
value: $processedData,
ttl: $adjustedTtl
);
$success = $this->cache->set($cacheItem);
if ($success) {
$this->updateCacheMetrics($key->toString(), $dataSize, $cacheLevel, 'store');
$this->trackAccess($key->toString());
$this->logger?->info('Discovery results cached with memory awareness', [
'key' => $key->toString(),
'items' => count($registry),
'cache_level' => $cacheLevel->value,
'cache_tier' => $cacheTier->value,
'memory_pressure' => $memoryStatus->memoryPressure->toDecimal(),
'data_size' => $dataSize->toHumanReadable(),
'ttl' => $adjustedTtl->toHours(),
]);
}
return $success;
}
/**
* Standard storage method (fallback)
*/
private function storeStandard(DiscoveryContext $context, DiscoveryRegistry $registry): bool
{
$key = $this->buildCacheKey($context);
$ttl = Duration::fromHours($this->ttlHours);
$cacheItem = CacheItem::forSet(
key: $key,
value: $registry,
ttl: $ttl
);
$success = $this->cache->set($cacheItem);
if ($success) {
$this->logger?->info('Discovery results cached (standard)', [
'key' => $key->toString(),
'items' => count($registry),
'ttl' => $ttl->toHours(),
]);
}
return $success;
}
/**
* Invalidate cache for a context
*/
public function invalidate(DiscoveryContext $context): bool
{
$key = $this->buildCacheKey($context);
return $this->cache->forget($key);
}
/**
* Clear all discovery caches
*/
public function clearAll(): bool
{
// This would ideally use cache tags, but for now we'll use pattern matching
$this->logger?->info('Clearing all discovery caches');
// Since we can't iterate cache keys, we'll just return true
// In a real implementation, you'd want cache tags support
return true;
}
/**
* Get enhanced cache health status with memory awareness
*/
public function getHealthStatus(): array
{
$baseStatus = [
'cache_driver' => get_class($this->cache),
'ttl_hours' => $this->ttlHours,
'prefix' => self::CACHE_PREFIX,
'memory_aware' => $this->memoryManager !== null,
];
if ($this->memoryManager !== null) {
$memoryStatus = $this->memoryManager->getMemoryStatus('health_check');
$baseStatus['memory_management'] = [
'status' => $memoryStatus->status->value,
'current_usage' => $memoryStatus->currentUsage->toHumanReadable(),
'memory_pressure' => $memoryStatus->memoryPressure->toDecimal(),
'cache_level' => $this->currentLevel->value,
];
}
return $baseStatus;
}
/**
* Get comprehensive cache metrics
*/
public function getCacheMetrics(): ?CacheMetrics
{
if ($this->memoryManager === null) {
return null;
}
$memoryStatus = $this->memoryManager->getMemoryStatus('metrics');
$hitRate = $this->calculateHitRate();
$totalSize = $this->calculateTotalCacheSize();
$compressionRatio = $this->calculateCompressionRatio();
return new CacheMetrics(
memoryStatus: $memoryStatus,
cacheLevel: $this->currentLevel,
totalItems: count($this->cacheMetrics),
hitRate: Score::fromRatio((int) ($hitRate * 100), 100),
totalSize: $totalSize,
compressionRatio: Score::fromRatio((int) ($compressionRatio * 100), 100),
evictionCount: 0, // Would need to track this
timestamp: $this->clock->time()
);
}
/**
* Perform memory pressure management
*/
public function performMemoryPressureManagement(): array
{
if ($this->memoryManager === null) {
return ['message' => 'Memory management not available'];
}
$memoryStatus = $this->memoryManager->getMemoryStatus('pressure_management');
$actions = [];
if ($memoryStatus->status === MemoryStatus::CRITICAL) {
// Clear all caches in critical situations
$this->clearAll();
$actions[] = 'emergency_cache_clear';
} elseif ($memoryStatus->memoryPressure->toDecimal() > $this->evictionThreshold) {
// Selective eviction based on access patterns
$evicted = $this->performSelectiveEviction();
$actions[] = "evicted_{$evicted}_items";
}
// Adjust cache level based on memory pressure
$newLevel = CacheLevel::fromMemoryPressure($memoryStatus->memoryPressure->toDecimal());
if ($newLevel !== $this->currentLevel) {
$this->currentLevel = $newLevel;
$actions[] = "level_changed_to_{$newLevel->value}";
}
return [
'actions' => $actions,
'memory_status' => $memoryStatus->toArray(),
'cache_level' => $this->currentLevel->value,
];
}
/**
* Build cache key for context
*/
private function buildCacheKey(DiscoveryContext $context): CacheKey
{
// Context returns already a CacheKey, so we use it directly
return $context->getCacheKey();
}
/**
* Check if cached data is stale
*/
private function isStale(DiscoveryContext $context, DiscoveryRegistry $registry): bool
{
// For incremental scans, always consider cache stale
if ($context->isIncremental()) {
return true;
}
// Check if any source directories have been modified
foreach ($context->paths as $path) {
if ($this->directoryModifiedSince($path, $context->startTime)) {
return true;
}
}
return false;
}
/**
* Check if directory has been modified since given time
*/
private function directoryModifiedSince(string $path, \DateTimeInterface $since): bool
{
try {
$filePath = FilePath::create($path);
$metadata = $this->fileSystemService->getMetadata($filePath);
// Check if modification time is after the given time
return $metadata->modifiedAt->getTimestamp() > $since->getTimestamp();
} catch (\Throwable) {
// If we can't check, assume it's been modified
return true;
}
}
// === Memory-Aware Helper Methods ===
/**
* Get cache from tiered storage
*/
private function getTieredCache(DiscoveryContext $context): ?DiscoveryRegistry
{
if ($this->memoryManager === null) {
return null;
}
// Try each tier in order of preference
foreach (CacheTier::orderedByPriority() as $tier) {
$key = $this->buildTieredCacheKey($context, $tier);
$item = $this->cache->get($key);
if ($item->isHit) {
$data = $this->processRetrievedData($item->value);
if ($data instanceof DiscoveryRegistry) {
$this->trackAccess($key->toString());
return $data;
}
}
}
return null;
}
/**
* Determine cache level based on memory status
*/
private function determineCacheLevel(object $memoryStatus): CacheLevel
{
return CacheLevel::fromMemoryPressure($memoryStatus->memoryPressure->toDecimal());
}
/**
* Determine cache tier based on context and memory status
*/
private function determineCacheTier(DiscoveryContext $context, DiscoveryRegistry $registry, object $memoryStatus): CacheTier
{
$dataSize = $this->estimateRegistrySize($registry)->toBytes();
$accessFrequency = $this->getAccessFrequency($context->getCacheKey());
$memoryPressure = $memoryStatus->memoryPressure->toDecimal();
return CacheTier::suggest($dataSize, $accessFrequency, $memoryPressure);
}
/**
* Apply memory-aware processing (compression, etc.)
*/
private function applyMemoryProcessing(DiscoveryRegistry $registry, CacheLevel $level, CacheTier $tier): mixed
{
$compressionLevel = $tier->getCompressionLevel();
if ($compressionLevel === CompressionLevel::NONE) {
return $registry;
}
// Serialize registry for compression
$serialized = serialize($registry);
$originalSize = Byte::fromBytes(strlen($serialized));
if ($originalSize->lessThan(Byte::fromBytes($compressionLevel->getMinimumDataSize()))) {
return $registry;
}
$compressed = $this->compressData($serialized, $compressionLevel);
if ($compressed !== null) {
$compressedSize = Byte::fromBytes(strlen($compressed));
// Emit compression event
$this->emitCompressionEvent($originalSize, $compressedSize, $compressionLevel);
return [
'__discovery_compressed__' => true,
'data' => $compressed,
'level' => $compressionLevel->value,
'tier' => $tier->value,
];
}
return $registry;
}
/**
* Process retrieved data (decompress if needed)
*/
private function processRetrievedData(mixed $data): mixed
{
if (! is_array($data) || ! ($data['__discovery_compressed__'] ?? false)) {
return $data;
}
$compressionLevel = CompressionLevel::from($data['level']);
$decompressed = $this->decompressData($data['data'], $compressionLevel);
if ($decompressed !== null) {
return unserialize($decompressed);
}
return $data;
}
/**
* Calculate adjusted TTL based on tier and memory status
*/
private function calculateAdjustedTtl(CacheTier $tier, object $memoryStatus): Duration
{
$baseTtl = Duration::fromHours($this->ttlHours);
$tierMultiplier = $tier->getTtlMultiplier();
$pressureAdjustment = 1.0 - ($memoryStatus->memoryPressure->toDecimal() * 0.3);
$finalMultiplier = $tierMultiplier * max(0.1, $pressureAdjustment);
return Duration::fromSeconds((int) ($baseTtl->toSeconds() * $finalMultiplier));
}
/**
* Build tiered cache key
*/
private function buildTieredCacheKey(DiscoveryContext $context, CacheTier $tier): CacheKey
{
return CacheKey::fromString(self::CACHE_PREFIX . "tier_{$tier->value}:" . $context->getCacheKey()->toString());
}
/**
* Estimate registry size
*/
private function estimateRegistrySize(DiscoveryRegistry $registry): Byte
{
return Byte::fromBytes(strlen(serialize($registry)));
}
/**
* Track access patterns
*/
private function trackAccess(string $key): void
{
$timestamp = $this->clock->time();
if (! isset($this->accessPatterns[$key])) {
$this->accessPatterns[$key] = [
'access_count' => 0,
'last_access' => $timestamp,
'access_history' => [],
];
}
$this->accessPatterns[$key]['access_count']++;
$this->accessPatterns[$key]['last_access'] = $timestamp;
$this->accessPatterns[$key]['access_history'][] = $timestamp;
// Trim history to prevent memory growth
if (count($this->accessPatterns[$key]['access_history']) > 100) {
$this->accessPatterns[$key]['access_history'] = array_slice(
$this->accessPatterns[$key]['access_history'],
-50
);
}
}
/**
* Get access frequency for a key
*/
private function getAccessFrequency(string $key): float
{
if (! isset($this->accessPatterns[$key])) {
return 0.0;
}
$pattern = $this->accessPatterns[$key];
$timeWindow = 3600; // 1 hour
$currentTime = $this->clock->time();
$recentAccesses = array_filter(
$pattern['access_history'],
fn ($timestamp) => ($currentTime - $timestamp) <= $timeWindow
);
return count($recentAccesses) / ($timeWindow / 3600);
}
/**
* Update cache metrics
*/
private function updateCacheMetrics(string $key, Byte $size, CacheLevel $level, string $operation): void
{
if (! isset($this->cacheMetrics[$key])) {
$this->cacheMetrics[$key] = [
'size' => $size,
'level' => $level,
'hits' => 0,
'misses' => 0,
'created' => $this->clock->time(),
];
}
$this->cacheMetrics[$key]['last_access'] = $this->clock->time();
match ($operation) {
'hit' => $this->cacheMetrics[$key]['hits']++,
'miss' => $this->cacheMetrics[$key]['misses']++,
default => null
};
}
/**
* Calculate hit rate
*/
private function calculateHitRate(): float
{
$totalHits = array_sum(array_column($this->cacheMetrics, 'hits'));
$totalMisses = array_sum(array_column($this->cacheMetrics, 'misses'));
$total = $totalHits + $totalMisses;
return $total > 0 ? $totalHits / $total : 0.0;
}
/**
* Calculate total cache size
*/
private function calculateTotalCacheSize(): Byte
{
$totalSize = Byte::zero();
foreach ($this->cacheMetrics as $metrics) {
$totalSize = $totalSize->add($metrics['size']);
}
return $totalSize;
}
/**
* Calculate compression ratio
*/
private function calculateCompressionRatio(): float
{
// Placeholder - would need to track actual compression ratios
return 0.6; // Assume 40% compression on average
}
/**
* Perform selective eviction
*/
private function performSelectiveEviction(): int
{
// Placeholder - would implement LRU-based eviction
return 0;
}
/**
* Compress data
*/
private function compressData(string $data, CompressionLevel $level): ?string
{
$compressed = gzcompress($data, $level->getGzipLevel());
return $compressed !== false ? $compressed : null;
}
/**
* Decompress data
*/
private function decompressData(string $data, CompressionLevel $level): ?string
{
$decompressed = gzuncompress($data);
return $decompressed !== false ? $decompressed : null;
}
/**
* Emit cache hit event
*/
private function emitCacheHitEvent(string $key, Byte $dataSize): void
{
$this->eventDispatcher?->dispatch(new CacheHitEvent(
cacheKey: CacheKey::fromString($key),
itemCount: 1,
cacheAge: Duration::fromSeconds(0), // Would need actual age
timestamp: $this->clock->time(),
));
}
/**
* Emit cache miss event
*/
private function emitCacheMissEvent(string $key, string $reason): void
{
$this->eventDispatcher?->dispatch(new CacheMissEvent(
cacheKey: CacheKey::fromString($key),
reason: $reason,
cacheLevel: $this->currentLevel,
timestamp: $this->clock->time()
));
}
/**
* Emit compression event
*/
private function emitCompressionEvent(Byte $originalSize, Byte $compressedSize, CompressionLevel $level): void
{
$this->eventDispatcher?->dispatch(new CacheCompressionEvent(
originalSize: $originalSize,
compressedSize: $compressedSize,
compressionLevel: $level,
compressionRatio: $compressedSize->percentOf($originalSize)->toDecimal(),
timestamp: $this->clock->time()
));
}
}

View File

@@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
namespace App\Framework\Discovery\Storage;
use App\Framework\Filesystem\FileMetadata;
use App\Framework\Filesystem\Storage;
use SplFileInfo;
/**
* Spezialisiertes Storage-Interface für Discovery-Operationen
* Erweitert das Standard-Storage-Interface mit Discovery-spezifischen Methoden
*/
interface DiscoveryStorage extends Storage
{
/**
* Findet geänderte Dateien seit dem letzten Scan
*
* @param string $directory Zu durchsuchendes Verzeichnis
* @param array<string, array> $fileMetadata Vorhandene Metadaten [path => metadata]
* @return array<SplFileInfo> Liste geänderter Dateien
*/
public function findChangedFiles(string $directory, array $fileMetadata): array;
/**
* Führt einen inkrementellen Scan durch
*
* @param string $directory Zu durchsuchendes Verzeichnis
* @param array<string, array> $fileMetadata Vorhandene Metadaten [path => metadata]
* @return array<string, array> Aktualisierte Metadaten [path => metadata]
*/
public function incrementalScan(string $directory, array $fileMetadata): array;
/**
* Findet alle Dateien mit einem bestimmten Muster
*
* @param string $directory Zu durchsuchendes Verzeichnis
* @param string $pattern Suchmuster (z.B. '*.php')
* @return array<SplFileInfo> Liste gefundener Dateien
*/
public function findFiles(string $directory, string $pattern): array;
/**
* Erstellt Metadaten für eine Datei
*
* @param string $filePath Pfad zur Datei
* @param bool $calculateChecksum Ob eine Prüfsumme berechnet werden soll
* @return FileMetadata Metadaten der Datei
*/
public function getDiscoveryMetadata(string $filePath, bool $calculateChecksum = true): FileMetadata;
/**
* Erstellt Metadaten für mehrere Dateien parallel
*
* @param array<string> $filePaths Liste von Dateipfaden
* @param bool $calculateChecksum Ob Prüfsummen berechnet werden sollen
* @return array<string, FileMetadata> Metadaten [path => metadata]
*/
public function getDiscoveryMetadataMultiple(array $filePaths, bool $calculateChecksum = true): array;
/**
* Registriert Dateisystem-Events für Änderungserkennung
*
* @param string $directory Zu überwachendes Verzeichnis
* @param callable $callback Callback-Funktion für Events (string $path, string $event)
* @return bool Erfolg der Registrierung
*/
public function registerFileSystemEvents(string $directory, callable $callback): bool;
}

View File

@@ -0,0 +1,343 @@
<?php
declare(strict_types=1);
namespace App\Framework\Discovery\Storage;
use App\Framework\Async\FiberManager;
use App\Framework\Filesystem\Directory;
use App\Framework\Filesystem\File;
use App\Framework\Filesystem\FileMetadata;
use App\Framework\Filesystem\PermissionChecker;
use App\Framework\Filesystem\Storage;
use SplFileInfo;
/**
* Implementierung des DiscoveryStorage-Interfaces, die das Filesystem-Modul nutzt
* und die Discovery-spezifischen Methoden implementiert.
*
* Diese Klasse verwendet Composition, um die Funktionalität des Storage-Interfaces
* zu delegieren und erweitert sie um Discovery-spezifische Funktionen.
*/
final class FileSystemDiscoveryStorage implements DiscoveryStorage
{
public readonly PermissionChecker $permissions;
public readonly FiberManager $fiberManager;
/**
* @param Storage $storage Basis-Storage-Implementierung für Delegation
* @param PermissionChecker $permissions Permissions-Checker
* @param FiberManager $fiberManager Fiber-Manager für asynchrone Operationen
*/
public function __construct(
private readonly Storage $storage,
?PermissionChecker $permissions = null,
?FiberManager $fiberManager = null
) {
$this->permissions = $permissions ?? new PermissionChecker($storage);
$this->fiberManager = $fiberManager ?? new FiberManager(
new \App\Framework\DateTime\SystemClock(),
new \App\Framework\DateTime\SystemTimer()
);
}
/**
* {@inheritdoc}
*/
public function get(string $path): string
{
return $this->storage->get($path);
}
/**
* {@inheritdoc}
*/
public function put(string $path, string $content): void
{
$this->storage->put($path, $content);
}
/**
* {@inheritdoc}
*/
public function exists(string $path): bool
{
return $this->storage->exists($path);
}
/**
* {@inheritdoc}
*/
public function delete(string $path): void
{
$this->storage->delete($path);
}
/**
* {@inheritdoc}
*/
public function copy(string $source, string $destination): void
{
$this->storage->copy($source, $destination);
}
/**
* {@inheritdoc}
*/
public function size(string $path): int
{
return $this->storage->size($path);
}
/**
* {@inheritdoc}
*/
public function lastModified(string $path): int
{
return $this->storage->lastModified($path);
}
/**
* {@inheritdoc}
*/
public function getMimeType(string $path): string
{
return $this->storage->getMimeType($path);
}
/**
* {@inheritdoc}
*/
public function isReadable(string $path): bool
{
return $this->storage->isReadable($path);
}
/**
* {@inheritdoc}
*/
public function isWritable(string $path): bool
{
return $this->storage->isWritable($path);
}
/**
* {@inheritdoc}
*/
public function listDirectory(string $directory): array
{
return $this->storage->listDirectory($directory);
}
/**
* {@inheritdoc}
*/
public function createDirectory(string $path, int $permissions = 0755, bool $recursive = true): void
{
$this->storage->createDirectory($path, $permissions, $recursive);
}
/**
* {@inheritdoc}
*/
public function file(string $path): File
{
return $this->storage->file($path);
}
/**
* {@inheritdoc}
*/
public function directory(string $path): Directory
{
return $this->storage->directory($path);
}
/**
* {@inheritdoc}
*/
public function batch(array $operations): array
{
return $this->storage->batch($operations);
}
/**
* {@inheritdoc}
*/
public function getMultiple(array $paths): array
{
return $this->storage->getMultiple($paths);
}
/**
* {@inheritdoc}
*/
public function putMultiple(array $files): void
{
$this->storage->putMultiple($files);
}
/**
* {@inheritdoc}
*/
public function getMetadataMultiple(array $paths): array
{
return $this->storage->getMetadataMultiple($paths);
}
/**
* {@inheritdoc}
*/
public function findChangedFiles(string $directory, array $fileMetadata): array
{
// Finde alle Dateien mit dem Muster
$allFiles = $this->findFiles($directory, '*.php');
$changedFiles = [];
$newFiles = [];
$modifiedFiles = [];
// Prüfe jede Datei auf Änderungen
foreach ($allFiles as $file) {
$filePath = $file->getPathname();
// Neue Datei (nicht in Metadaten)
if (! isset($fileMetadata[$filePath])) {
$newFiles[] = $file;
$changedFiles[] = $file;
continue;
}
// Prüfe auf Änderungen mit detaillierten Metadaten
try {
$storedMetadata = FileMetadata::fromArray($fileMetadata[$filePath]);
if ($storedMetadata->hasChanged($filePath, $this)) {
$modifiedFiles[] = $file;
$changedFiles[] = $file;
}
} catch (\Throwable) {
// Bei Problemen mit Metadaten, behandle als geändert
$changedFiles[] = $file;
}
}
return $changedFiles;
}
/**
* {@inheritdoc}
*/
public function incrementalScan(string $directory, array $fileMetadata): array
{
$changedFiles = $this->findChangedFiles($directory, $fileMetadata);
$updatedMetadata = $fileMetadata;
// Aktualisiere Metadaten für geänderte Dateien
foreach ($changedFiles as $file) {
$filePath = $file->getPathname();
try {
$metadata = $this->getDiscoveryMetadata($filePath, true);
$updatedMetadata[$filePath] = $metadata->toArray();
} catch (\Throwable) {
// Bei Fehlern, entferne die Metadaten
unset($updatedMetadata[$filePath]);
}
}
// Entferne gelöschte Dateien aus Metadaten
$allFiles = $this->findFiles($directory, '*.php');
$existingPaths = array_map(fn (SplFileInfo $file) => $file->getPathname(), $allFiles);
foreach (array_keys($updatedMetadata) as $path) {
if (! in_array($path, $existingPaths)) {
unset($updatedMetadata[$path]);
}
}
return $updatedMetadata;
}
/**
* {@inheritdoc}
*/
public function findFiles(string $directory, string $pattern): array
{
$result = [];
$this->findFilesRecursive($directory, $pattern, $result);
return $result;
}
/**
* Rekursive Hilfsmethode für findFiles
*/
private function findFilesRecursive(string $directory, string $pattern, array &$result): void
{
if (! is_dir($directory)) {
return;
}
$files = scandir($directory);
if ($files === false) {
return;
}
foreach ($files as $file) {
if ($file === '.' || $file === '..') {
continue;
}
$path = $directory . DIRECTORY_SEPARATOR . $file;
if (is_dir($path)) {
$this->findFilesRecursive($path, $pattern, $result);
} elseif ($this->matchesPattern($file, $pattern)) {
$result[] = new SplFileInfo($path);
}
}
}
/**
* Prüft, ob ein Dateiname einem Muster entspricht
*/
private function matchesPattern(string $filename, string $pattern): bool
{
return fnmatch($pattern, $filename);
}
/**
* {@inheritdoc}
*/
public function getDiscoveryMetadata(string $filePath, bool $calculateChecksum = true): FileMetadata
{
return FileMetadata::fromFile($filePath, $this, $calculateChecksum);
}
/**
* {@inheritdoc}
*/
public function getDiscoveryMetadataMultiple(array $filePaths, bool $calculateChecksum = true): array
{
$operations = [];
foreach ($filePaths as $path) {
$operations[$path] = fn () => $this->getDiscoveryMetadata($path, $calculateChecksum);
}
return $this->batch($operations);
}
/**
* {@inheritdoc}
*/
public function registerFileSystemEvents(string $directory, callable $callback): bool
{
// Implementierung hängt von der konkreten Event-System-Unterstützung ab
// Hier eine einfache Implementierung, die immer false zurückgibt
// In einer realen Implementierung würde hier ein Filesystem-Watcher registriert
return false;
}
}