chore: complete update
This commit is contained in:
167
src/Framework/Discovery/DiscoveryServiceBootstrapper.php
Normal file
167
src/Framework/Discovery/DiscoveryServiceBootstrapper.php
Normal file
@@ -0,0 +1,167 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Discovery;
|
||||
|
||||
use App\Framework\Attributes\Route;
|
||||
use App\Framework\Auth\AuthMapper;
|
||||
use App\Framework\Cache\Cache;
|
||||
use App\Framework\CommandBus\CommandHandlerMapper;
|
||||
use App\Framework\Config\Configuration;
|
||||
use App\Framework\Core\AttributeMapper;
|
||||
use App\Framework\Core\Events\EventHandlerMapper;
|
||||
use App\Framework\Core\PathProvider;
|
||||
use App\Framework\Core\RouteMapper;
|
||||
use App\Framework\Database\Migration\Migration;
|
||||
use App\Framework\DI\Container;
|
||||
use App\Framework\DI\Initializer;
|
||||
use App\Framework\DI\InitializerMapper;
|
||||
use App\Framework\Discovery\Results\DiscoveryResults;
|
||||
use App\Framework\Http\HttpMiddleware;
|
||||
use App\Framework\QueryBus\QueryHandlerMapper;
|
||||
use App\Framework\View\DomProcessor;
|
||||
use App\Framework\View\StringProcessor;
|
||||
|
||||
/**
|
||||
* Bootstrapper für den Discovery-Service
|
||||
* Ersetzt die alte AttributeDiscoveryService-Integration
|
||||
*/
|
||||
final readonly class DiscoveryServiceBootstrapper
|
||||
{
|
||||
public function __construct(private Container $container) {}
|
||||
|
||||
/**
|
||||
* Bootstrapt den Discovery-Service und führt die Discovery durch
|
||||
*/
|
||||
public function bootstrap(): DiscoveryResults
|
||||
{
|
||||
$pathProvider = $this->container->get(PathProvider::class);
|
||||
$cache = $this->container->get(Cache::class);
|
||||
$config = $this->container->get(Configuration::class);
|
||||
|
||||
// Discovery-Service erstellen
|
||||
$discoveryService = $this->createDiscoveryService($pathProvider, $cache, $config);
|
||||
|
||||
// Discovery durchführen
|
||||
$results = $discoveryService->discover();
|
||||
|
||||
// Ergebnisse im Container registrieren
|
||||
$this->container->singleton(DiscoveryResults::class, $results);
|
||||
$this->container->instance(UnifiedDiscoveryService::class, $discoveryService);
|
||||
|
||||
// Führe Initializers aus (Kompatibilität mit bestehendem Code)
|
||||
$this->executeInitializers($results);
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt den Discovery-Service mit der richtigen Konfiguration
|
||||
*/
|
||||
private function createDiscoveryService(
|
||||
PathProvider $pathProvider,
|
||||
Cache $cache,
|
||||
Configuration $config
|
||||
): UnifiedDiscoveryService {
|
||||
$useCache = $config->get('discovery.use_cache', true);
|
||||
$showProgress = $config->get('discovery.show_progress', false);
|
||||
|
||||
// Attribute-Mapper aus Konfiguration oder Standard-Werte
|
||||
$attributeMappers = $config->get('discovery.attribute_mappers', [
|
||||
RouteMapper::class,
|
||||
EventHandlerMapper::class,
|
||||
\App\Framework\EventBus\EventHandlerMapper::class,
|
||||
QueryHandlerMapper::class,
|
||||
CommandHandlerMapper::class,
|
||||
InitializerMapper::class,
|
||||
AuthMapper::class,
|
||||
]);
|
||||
|
||||
// Ziel-Interfaces aus Konfiguration oder Standard-Werte
|
||||
$targetInterfaces = $config->get('discovery.target_interfaces', [
|
||||
AttributeMapper::class,
|
||||
HttpMiddleware::class,
|
||||
DomProcessor::class,
|
||||
StringProcessor::class,
|
||||
Migration::class,
|
||||
#Initializer::class,
|
||||
]);
|
||||
|
||||
return new UnifiedDiscoveryService(
|
||||
$pathProvider,
|
||||
$cache,
|
||||
$attributeMappers,
|
||||
$targetInterfaces,
|
||||
$useCache,
|
||||
$showProgress
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Führt die gefundenen Initializers aus (Kompatibilität)
|
||||
*/
|
||||
private function executeInitializers(DiscoveryResults $results): void
|
||||
{
|
||||
$initializerResults = $results->get(Initializer::class);
|
||||
|
||||
#debug($initializerResults);
|
||||
|
||||
foreach ($initializerResults as $initializerData) {
|
||||
if (!isset($initializerData['class'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$className = $initializerData['class'];
|
||||
$methodName = $initializerData['method'] ?? '__invoke';
|
||||
$returnType = $initializerData['return'] ?? null;
|
||||
|
||||
#debug($initializerData);
|
||||
|
||||
|
||||
$instance = $this->container->invoker->invoke($className, $methodName);
|
||||
|
||||
|
||||
// Registriere das Ergebnis im Container falls Return-Type angegeben
|
||||
if ($returnType && $instance !== null) {
|
||||
$this->container->instance($returnType, $instance);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
debug('Fehler beim Ausführen des Initializers: ' . $e->getMessage());
|
||||
error_log("Fehler beim Ausführen des Initializers {$className}: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Führt einen inkrementellen Discovery-Scan durch
|
||||
*/
|
||||
public function incrementalBootstrap(): DiscoveryResults
|
||||
{
|
||||
if (!$this->container->has(UnifiedDiscoveryService::class)) {
|
||||
// Fallback auf vollständigen Bootstrap
|
||||
return $this->bootstrap();
|
||||
}
|
||||
|
||||
$discoveryService = $this->container->get(UnifiedDiscoveryService::class);
|
||||
$results = $discoveryService->incrementalDiscover();
|
||||
|
||||
// Aktualisiere Container
|
||||
$this->container->instance(DiscoveryResults::class, $results);
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hilfsmethode um zu prüfen, ob Discovery erforderlich ist
|
||||
*/
|
||||
public function isDiscoveryRequired(): bool
|
||||
{
|
||||
if (!$this->container->has(UnifiedDiscoveryService::class)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$discoveryService = $this->container->get(UnifiedDiscoveryService::class);
|
||||
return $discoveryService->shouldRescan();
|
||||
}
|
||||
}
|
||||
108
src/Framework/Discovery/FileScanner.php
Normal file
108
src/Framework/Discovery/FileScanner.php
Normal file
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Discovery;
|
||||
|
||||
use FilesystemIterator;
|
||||
use Generator;
|
||||
use RecursiveDirectoryIterator;
|
||||
use RecursiveIteratorIterator;
|
||||
use RegexIterator;
|
||||
use SplFileInfo;
|
||||
|
||||
readonly class FileScanner implements FileScannerInterface
|
||||
{
|
||||
/**
|
||||
* Scannt ein Verzeichnis nach PHP-Dateien
|
||||
*
|
||||
* @param string $directory Zu scannendes Verzeichnis
|
||||
* @return Generator<SplFileInfo> Generator, der PHP-Dateien zurückgibt
|
||||
*/
|
||||
public function scanDirectory(string $directory): Generator
|
||||
{
|
||||
$rii = new RecursiveIteratorIterator(
|
||||
new RecursiveDirectoryIterator($directory, FilesystemIterator::SKIP_DOTS)
|
||||
);
|
||||
|
||||
foreach ($rii as $file) {
|
||||
if (!$file->isFile() || $file->getExtension() !== 'php') {
|
||||
continue;
|
||||
}
|
||||
|
||||
yield $file;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Findet alle Dateien mit bestimmtem Muster in einem Verzeichnis
|
||||
* @return array<SplFileInfo>
|
||||
*/
|
||||
public function findFiles(string $directory, string $pattern = '*.php'): array
|
||||
{
|
||||
// Konvertiere das Glob-Muster in einen regulären Ausdruck
|
||||
$regexPattern = $this->globToRegex($pattern);
|
||||
|
||||
return iterator_to_array(
|
||||
new RegexIterator(
|
||||
new RecursiveIteratorIterator(
|
||||
new RecursiveDirectoryIterator($directory, FilesystemIterator::SKIP_DOTS)
|
||||
),
|
||||
$regexPattern
|
||||
),
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ermittelt den letzten Änderungszeitpunkt aller Dateien eines bestimmten Musters
|
||||
*/
|
||||
public function getLatestMTime(string $directory, string $pattern = '*.php'): int
|
||||
{
|
||||
$maxMTime = 0;
|
||||
$files = $this->findFiles($directory, $pattern);
|
||||
|
||||
foreach ($files as $file) {
|
||||
$mtime = $file->getMTime();
|
||||
if ($mtime > $maxMTime) {
|
||||
$maxMTime = $mtime;
|
||||
}
|
||||
}
|
||||
|
||||
return $maxMTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Findet Dateien, die seit einem bestimmten Zeitpunkt geändert wurden
|
||||
* @return array<SplFileInfo>
|
||||
*/
|
||||
public function findChangedFiles(string $directory, int $timestamp, string $pattern = '*.php'): array
|
||||
{
|
||||
$files = $this->findFiles($directory, $pattern);
|
||||
$changedFiles = [];
|
||||
|
||||
foreach ($files as $file) {
|
||||
if ($file->getMTime() > $timestamp) {
|
||||
$changedFiles[] = $file;
|
||||
}
|
||||
}
|
||||
|
||||
return $changedFiles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Konvertiert ein Glob-Muster in einen regulären Ausdruck
|
||||
*/
|
||||
private function globToRegex(string $globPattern): string
|
||||
{
|
||||
$regex = preg_quote($globPattern, '/');
|
||||
|
||||
// Ersetze Glob-Platzhalter durch Regex-Äquivalente
|
||||
$regex = str_replace(
|
||||
['\*', '\?', '\[', '\]'],
|
||||
['.*', '.', '[', ']'],
|
||||
$regex
|
||||
);
|
||||
|
||||
return '/^' . $regex . '$/i';
|
||||
}
|
||||
}
|
||||
21
src/Framework/Discovery/FileScannerInterface.php
Normal file
21
src/Framework/Discovery/FileScannerInterface.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Discovery;
|
||||
|
||||
/**
|
||||
* Schnittstelle für alle FileScanner-Implementierungen
|
||||
*/
|
||||
interface FileScannerInterface
|
||||
{
|
||||
/**
|
||||
* Findet alle Dateien mit bestimmtem Muster in einem Verzeichnis
|
||||
* @return array<\SplFileInfo>
|
||||
*/
|
||||
public function findFiles(string $directory, string $pattern = '*.php'): array;
|
||||
|
||||
/**
|
||||
* Ermittelt den letzten Änderungszeitpunkt aller Dateien eines bestimmten Musters
|
||||
*/
|
||||
public function getLatestMTime(string $directory, string $pattern = '*.php'): int;
|
||||
}
|
||||
380
src/Framework/Discovery/FileScannerService.php
Normal file
380
src/Framework/Discovery/FileScannerService.php
Normal file
@@ -0,0 +1,380 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Discovery;
|
||||
|
||||
use App\Framework\Async1\AsyncFileScanner;
|
||||
use App\Framework\Cache\Cache;
|
||||
use App\Framework\Core\ClassParser;
|
||||
use App\Framework\Core\PathProvider;
|
||||
use App\Framework\Core\ProgressMeter;
|
||||
use SplFileInfo;
|
||||
|
||||
/**
|
||||
* Zentraler Service für das Scannen von Dateien und die Verteilung an Visitors
|
||||
*/
|
||||
final class FileScannerService
|
||||
{
|
||||
/** @var array<FileVisitor> */
|
||||
private array $visitors = [];
|
||||
|
||||
/** @var array Gespeicherte Liste gescannter Dateien für Inkrementelle Scans */
|
||||
private array $scannedFiles = [];
|
||||
|
||||
public function __construct(
|
||||
private readonly FileScannerInterface $fileScanner,
|
||||
private readonly PathProvider $pathProvider,
|
||||
private readonly Cache $cache,
|
||||
private readonly bool $useCache = true,
|
||||
private readonly bool $asyncProcessing = false,
|
||||
private readonly int $chunkSize = 100
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Registriert einen Besucher, der Dateien verarbeiten kann
|
||||
*/
|
||||
public function registerVisitor(FileVisitor $visitor): self
|
||||
{
|
||||
$this->visitors[] = $visitor;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt die Anzahl der verarbeiteten Dateien zurück
|
||||
*/
|
||||
public function getProcessedFileCount(): int
|
||||
{
|
||||
return count($this->scannedFiles);
|
||||
}
|
||||
|
||||
/**
|
||||
* Führt den Scan durch und ruft alle registrierten Besucher auf
|
||||
*
|
||||
* @param bool $showProgress Zeigt einen Fortschrittsindikator an (nur für CLI)
|
||||
*/
|
||||
public function scan(bool $showProgress = false): void
|
||||
{
|
||||
// Prüfen, ob wir ein Rescan benötigen
|
||||
if ($this->useCache && !$this->shouldRescan()) {
|
||||
$this->loadVisitorsFromCache();
|
||||
return;
|
||||
}
|
||||
|
||||
// Prüfen, ob ein inkrementeller Scan ausreicht
|
||||
if ($this->useCache && $this->canUseIncrementalScan()) {
|
||||
$this->incrementalScan($showProgress);
|
||||
return;
|
||||
}
|
||||
|
||||
$basePath = $this->pathProvider->getBasePath();
|
||||
$phpFiles = $this->fileScanner->findFiles($basePath . '/src', '*.php');
|
||||
|
||||
// Speichere die Liste der gescannten Dateien
|
||||
$this->scannedFiles = [];
|
||||
foreach ($phpFiles as $file) {
|
||||
$this->scannedFiles[$file->getPathname()] = $file->getMTime();
|
||||
}
|
||||
|
||||
// Notifiziere alle Besucher über den Beginn des Scans
|
||||
foreach ($this->visitors as $visitor) {
|
||||
$visitor->onScanStart();
|
||||
}
|
||||
|
||||
// Wenn zu viele Dateien, verarbeite in Chunks
|
||||
if (count($phpFiles) > $this->chunkSize) {
|
||||
$this->scanInChunks($phpFiles, $showProgress);
|
||||
} else {
|
||||
$progress = $showProgress ? new ProgressMeter(count($phpFiles)) : null;
|
||||
|
||||
// Verarbeite synchron oder asynchron
|
||||
if ($this->asyncProcessing && $this->fileScanner instanceof AsyncFileScanner) {
|
||||
$this->processFilesAsync($phpFiles);
|
||||
} else {
|
||||
// Verarbeite jede Datei
|
||||
foreach ($phpFiles as $file) {
|
||||
$this->processFile($file);
|
||||
if ($progress) {
|
||||
$progress->advance();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$progress?->finish();
|
||||
}
|
||||
|
||||
// Notifiziere alle Besucher über das Ende des Scans
|
||||
foreach ($this->visitors as $visitor) {
|
||||
$visitor->onScanComplete();
|
||||
}
|
||||
|
||||
// Cache aktualisieren
|
||||
if ($this->useCache) {
|
||||
$this->updateVisitorCache();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Führt einen inkrementellen Scan durch (nur geänderte Dateien)
|
||||
*
|
||||
* @param bool $showProgress Zeigt einen Fortschrittsindikator an (nur für CLI)
|
||||
*/
|
||||
public function incrementalScan(bool $showProgress = false): void
|
||||
{
|
||||
$basePath = $this->pathProvider->getBasePath();
|
||||
$lastScanTime = $this->getLastScanTime();
|
||||
|
||||
$changedFiles = $this->fileScanner->findChangedFiles($basePath . '/src', $lastScanTime);
|
||||
|
||||
if (empty($changedFiles)) {
|
||||
return; // Keine Änderungen seit dem letzten Scan
|
||||
}
|
||||
|
||||
// Lade existierende Daten aus dem Cache
|
||||
$this->loadVisitorsFromCache();
|
||||
|
||||
// Notifiziere Besucher über den Beginn des inkrementellen Scans
|
||||
foreach ($this->visitors as $visitor) {
|
||||
$visitor->onIncrementalScanStart();
|
||||
}
|
||||
|
||||
$progress = $showProgress ? new ProgressMeter(count($changedFiles)) : null;
|
||||
|
||||
// Verarbeite nur geänderte Dateien
|
||||
foreach ($changedFiles as $file) {
|
||||
$this->processFile($file);
|
||||
$this->scannedFiles[$file->getPathname()] = $file->getMTime();
|
||||
|
||||
if ($progress) {
|
||||
$progress->advance();
|
||||
}
|
||||
}
|
||||
|
||||
if ($progress) {
|
||||
$progress->finish();
|
||||
}
|
||||
|
||||
// Notifiziere Besucher über das Ende des inkrementellen Scans
|
||||
foreach ($this->visitors as $visitor) {
|
||||
$visitor->onIncrementalScanComplete();
|
||||
}
|
||||
|
||||
// Cache aktualisieren
|
||||
if ($this->useCache) {
|
||||
$this->updateVisitorCache();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scannt das Projekt in Chunks, um Speicherverbrauch zu reduzieren
|
||||
*
|
||||
* @param array $phpFiles Die zu verarbeitenden Dateien
|
||||
* @param bool $showProgress Zeigt einen Fortschrittsindikator an
|
||||
*/
|
||||
private function scanInChunks(array $phpFiles, bool $showProgress = false): void
|
||||
{
|
||||
$chunks = array_chunk($phpFiles, $this->chunkSize);
|
||||
$totalFiles = count($phpFiles);
|
||||
$progress = $showProgress ? new ProgressMeter($totalFiles) : null;
|
||||
$processedFiles = 0;
|
||||
|
||||
foreach ($chunks as $chunk) {
|
||||
// Verarbeite den Chunk
|
||||
foreach ($chunk as $file) {
|
||||
$this->processFile($file);
|
||||
$processedFiles++;
|
||||
|
||||
if ($progress) {
|
||||
$progress->advance();
|
||||
}
|
||||
}
|
||||
|
||||
// Speicher freigeben
|
||||
gc_collect_cycles();
|
||||
}
|
||||
|
||||
if ($progress) {
|
||||
$progress->finish();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verarbeitet eine einzelne Datei mit allen registrierten Besuchern
|
||||
*/
|
||||
private function processFile(SplFileInfo $file): void
|
||||
{
|
||||
$filePath = $file->getPathname();
|
||||
|
||||
// Versuche, die Datei zu parsen
|
||||
try {
|
||||
$classes = ClassParser::getClassesInFile($filePath);
|
||||
|
||||
foreach ($classes as $class) {
|
||||
// Notifiziere alle Besucher über diese Klasse
|
||||
foreach ($this->visitors as $visitor) {
|
||||
$visitor->visitClass($class, $filePath);
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// Logging des Fehlers
|
||||
error_log("Fehler beim Parsen von {$filePath}: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verarbeitet Dateien asynchron, wenn asyncProcessing aktiviert ist
|
||||
*/
|
||||
private function processFilesAsync(array $files): void
|
||||
{
|
||||
if (!$this->asyncProcessing || empty($files)) {
|
||||
// Verarbeite synchron, wenn asyncProcessing deaktiviert ist
|
||||
foreach ($files as $file) {
|
||||
$this->processFile($file);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Stelle sicher, dass fileScanner AsyncFileScanner ist
|
||||
if (!$this->fileScanner instanceof AsyncFileScanner) {
|
||||
throw new \RuntimeException('Asynchrone Verarbeitung erfordert AsyncFileScanner');
|
||||
}
|
||||
|
||||
// Verteile die Dateien auf Chunks für parallele Verarbeitung
|
||||
$chunks = array_chunk($files, (int)ceil(count($files) / 8));
|
||||
|
||||
// Erstelle Tasks für jeden Chunk
|
||||
$tasks = [];
|
||||
foreach ($chunks as $index => $chunk) {
|
||||
$tasks[$index] = function () use ($chunk) {
|
||||
$results = [];
|
||||
foreach ($chunk as $file) {
|
||||
try {
|
||||
$classes = ClassParser::getClassesInFile($file->getPathname());
|
||||
foreach ($classes as $class) {
|
||||
$results[] = ['class' => $class, 'file' => $file->getPathname()];
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
error_log("Fehler beim Parsen von {$file->getPathname()}: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
return $results;
|
||||
};
|
||||
}
|
||||
|
||||
// Verarbeite die Tasks asynchron
|
||||
$results = $this->fileScanner->getTaskProcessor()->processTasks($tasks);
|
||||
|
||||
// Ergebnisse verarbeiten
|
||||
foreach ($results as $chunkResults) {
|
||||
if (!is_array($chunkResults)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($chunkResults as $result) {
|
||||
foreach ($this->visitors as $visitor) {
|
||||
$visitor->visitClass($result['class'], $result['file']);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft, ob ein erneuter Scan erforderlich ist
|
||||
*/
|
||||
private function shouldRescan(): bool
|
||||
{
|
||||
$cacheKey = 'file_scanner_timestamp';
|
||||
$cachedItem = $this->cache->get($cacheKey);
|
||||
|
||||
if (!$cachedItem->isHit) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$cachedTime = $cachedItem->value;
|
||||
$basePath = $this->pathProvider->getBasePath();
|
||||
$latestMTime = $this->fileScanner->getLatestMTime($basePath . '/src');
|
||||
|
||||
return $latestMTime > $cachedTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft, ob ein inkrementeller Scan verwendet werden kann
|
||||
*/
|
||||
private function canUseIncrementalScan(): bool
|
||||
{
|
||||
// Prüfe, ob wir bereits gescannte Dateien haben
|
||||
$cachedItem = $this->cache->get('file_scanner_files');
|
||||
if (!$cachedItem->isHit) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->scannedFiles = $cachedItem->value;
|
||||
return !empty($this->scannedFiles);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt den Zeitpunkt des letzten Scans zurück
|
||||
*/
|
||||
private function getLastScanTime(): int
|
||||
{
|
||||
$cacheKey = 'file_scanner_timestamp';
|
||||
$cachedItem = $this->cache->get($cacheKey);
|
||||
|
||||
return $cachedItem->isHit ? $cachedItem->value : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Aktualisiert den Cache-Zeitstempel
|
||||
*/
|
||||
private function updateCacheTimestamp(): void
|
||||
{
|
||||
$this->cache->set('file_scanner_timestamp', time(), 3600);
|
||||
}
|
||||
|
||||
/**
|
||||
* Aktualisiert den Cache für alle Visitors
|
||||
*/
|
||||
private function updateVisitorCache(): void
|
||||
{
|
||||
foreach ($this->visitors as $visitor) {
|
||||
$cacheData = $visitor->getCacheableData();
|
||||
if ($cacheData !== null) {
|
||||
$this->cache->set(
|
||||
$visitor->getCacheKey(),
|
||||
$cacheData,
|
||||
3600 // TTL in Sekunden
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Zusätzlich zum Zeitstempel auch die Liste der gescannten Dateien cachen
|
||||
$this->cache->set('file_scanner_files', $this->scannedFiles, 3600);
|
||||
$this->updateCacheTimestamp();
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt Daten für alle Visitors aus dem Cache
|
||||
*/
|
||||
private function loadVisitorsFromCache(): void
|
||||
{
|
||||
foreach ($this->visitors as $visitor) {
|
||||
$visitor->loadFromCache($this->cache);
|
||||
}
|
||||
|
||||
// Lade auch die Liste der gescannten Dateien
|
||||
$cachedItem = $this->cache->get('file_scanner_files');
|
||||
if ($cachedItem->isHit) {
|
||||
|
||||
$this->scannedFiles = $cachedItem->value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt eine Liste aller registrierten Besucher zurück
|
||||
*
|
||||
* @return array<FileVisitor>
|
||||
*/
|
||||
public function getVisitors(): array
|
||||
{
|
||||
return $this->visitors;
|
||||
}
|
||||
}
|
||||
54
src/Framework/Discovery/FileVisitor.php
Normal file
54
src/Framework/Discovery/FileVisitor.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Discovery;
|
||||
|
||||
use App\Framework\Cache\Cache;
|
||||
|
||||
/**
|
||||
* Interface für File-Visitor, die beim Scannen von Dateien verwendet werden
|
||||
*/
|
||||
interface FileVisitor
|
||||
{
|
||||
/**
|
||||
* Wird aufgerufen, wenn der Scan beginnt
|
||||
*/
|
||||
public function onScanStart(): void;
|
||||
|
||||
/**
|
||||
* Wird für jede gefundene Klasse aufgerufen
|
||||
*/
|
||||
public function visitClass(string $className, string $filePath): void;
|
||||
|
||||
/**
|
||||
* Wird aufgerufen, wenn der Scan abgeschlossen ist
|
||||
*/
|
||||
public function onScanComplete(): void;
|
||||
|
||||
/**
|
||||
* Wird aufgerufen, wenn ein inkrementeller Scan beginnt
|
||||
*/
|
||||
public function onIncrementalScanStart(): void;
|
||||
|
||||
/**
|
||||
* Wird aufgerufen, wenn ein inkrementeller Scan abgeschlossen ist
|
||||
*/
|
||||
public function onIncrementalScanComplete(): void;
|
||||
|
||||
/**
|
||||
* Lädt Daten aus dem Cache
|
||||
*/
|
||||
public function loadFromCache(Cache $cache): void;
|
||||
|
||||
/**
|
||||
* Liefert den Cache-Schlüssel für diesen Visitor
|
||||
*/
|
||||
public function getCacheKey(): string;
|
||||
|
||||
/**
|
||||
* Liefert die zu cachenden Daten des Visitors
|
||||
*
|
||||
* @return mixed Die zu cachenden Daten
|
||||
*/
|
||||
public function getCacheableData(): mixed;
|
||||
}
|
||||
20
src/Framework/Discovery/OBSOLETE_CORE_FILES.md
Normal file
20
src/Framework/Discovery/OBSOLETE_CORE_FILES.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# Überflüssige Dateien im Core-Ordner
|
||||
|
||||
Nach der Implementierung des Discovery-Moduls sind folgende Dateien im Core-Ordner überflüssig geworden:
|
||||
|
||||
## Zu löschende Dateien:
|
||||
- `src/Framework/Core/AttributeProcessor.php` → Ersetzt durch `AttributeDiscoveryVisitor`
|
||||
- `src/Framework/Core/AttributeDiscoveryService.php` → Ersetzt durch `UnifiedDiscoveryService`
|
||||
- `src/Framework/Core/AttributeMapperLocator.php` → Bereits durch `InterfaceImplementationVisitor` abgedeckt
|
||||
- `src/Framework/Core/ProcessedResults.php` → Ersetzt durch `DiscoveryResults`
|
||||
|
||||
## Zu überarbeitende Dateien:
|
||||
- `src/Framework/Core/ContainerBootstrapper.php` → Sollte `UnifiedDiscoveryService` verwenden
|
||||
- `src/Framework/Core/Application.php` → Discovery-Logic entfernen
|
||||
|
||||
## Veraltete Compiler (falls vorhanden):
|
||||
- Alle `*Compiler.php` Dateien im Core-Ordner
|
||||
- `AttributeCompiler.php` Interface (meist überflüssig)
|
||||
|
||||
## Hinweis:
|
||||
Diese Dateien sollten erst gelöscht werden, nachdem das Discovery-Modul vollständig getestet und integriert wurde.
|
||||
125
src/Framework/Discovery/README.md
Normal file
125
src/Framework/Discovery/README.md
Normal file
@@ -0,0 +1,125 @@
|
||||
# Discovery-Modul
|
||||
|
||||
Das Discovery-Modul ist ein einheitliches System zum Auffinden und Verarbeiten von Attributen, Interface-Implementierungen, Routes und Templates im Framework.
|
||||
|
||||
## Überblick
|
||||
|
||||
Dieses Modul ersetzt die bisherige dezentrale Discovery-Logik und eliminiert mehrfache Dateisystem-Iterationen durch einen einzigen, optimierten Scan.
|
||||
|
||||
## Hauptkomponenten
|
||||
|
||||
### UnifiedDiscoveryService
|
||||
- **Zweck**: Koordiniert alle Discovery-Operationen
|
||||
- **Vorteil**: Ein einziger Dateisystem-Scan für alle Discovery-Typen
|
||||
- **Verwendung**:
|
||||
```php
|
||||
$discovery = new UnifiedDiscoveryService($pathProvider, $cache, $mappers, $interfaces);
|
||||
$results = $discovery->discover();
|
||||
```
|
||||
|
||||
### AttributeDiscoveryVisitor
|
||||
- **Zweck**: Verarbeitet Attribute mit registrierten Mappern
|
||||
- **Ersetzt**: `AttributeProcessor` aus dem Core-Modul
|
||||
- **Features**:
|
||||
- Inkrementelle Scans
|
||||
- Parameter-Extraktion für Methoden
|
||||
- Optimierte Caching-Strategie
|
||||
|
||||
### DiscoveryResults
|
||||
- **Zweck**: Einheitliche Ergebnisklasse für alle Discovery-Typen
|
||||
- **Ersetzt**: `ProcessedResults` aus dem Core-Modul
|
||||
- **Features**:
|
||||
- Kompatibilitätsmethoden für bestehenden Code
|
||||
- Serialisierung/Deserialisierung
|
||||
- Merge-Funktionalität
|
||||
|
||||
### DiscoveryServiceBootstrapper
|
||||
- **Zweck**: Integration in den Container und Bootstrap-Prozess
|
||||
- **Ersetzt**: Discovery-Logik in `ContainerBootstrapper` und `Application`
|
||||
- **Features**:
|
||||
- Automatische Initializer-Ausführung
|
||||
- Konfigurierbare Mapper und Interfaces
|
||||
- Inkrementelle Updates
|
||||
|
||||
## Verwendung
|
||||
|
||||
### Einfache Verwendung
|
||||
```php
|
||||
// Mit Standard-Konfiguration
|
||||
$discovery = UnifiedDiscoveryService::createWithDefaults($pathProvider, $cache);
|
||||
$results = $discovery->discover();
|
||||
|
||||
// Attribute abrufen
|
||||
$routes = $results->getAttributeResults(RouteAttribute::class);
|
||||
$eventHandlers = $results->getAttributeResults(OnEvent::class);
|
||||
|
||||
// Interface-Implementierungen abrufen
|
||||
$mappers = $results->getInterfaceImplementations(AttributeMapper::class);
|
||||
```
|
||||
|
||||
### Integration in Container
|
||||
```php
|
||||
// Im ContainerBootstrapper
|
||||
$bootstrapper = new DiscoveryServiceBootstrapper($container);
|
||||
$results = $bootstrapper->bootstrap();
|
||||
|
||||
// Ergebnisse sind automatisch im Container verfügbar
|
||||
$results = $container->get(DiscoveryResults::class);
|
||||
```
|
||||
|
||||
### Konfiguration
|
||||
```php
|
||||
// In der config.php
|
||||
'discovery' => [
|
||||
'use_cache' => true,
|
||||
'show_progress' => false,
|
||||
'attribute_mappers' => [
|
||||
RouteMapper::class,
|
||||
EventHandlerMapper::class,
|
||||
// ...
|
||||
],
|
||||
'target_interfaces' => [
|
||||
AttributeMapper::class,
|
||||
Initializer::class,
|
||||
// ...
|
||||
]
|
||||
]
|
||||
```
|
||||
|
||||
## Performance
|
||||
|
||||
### Optimierungen
|
||||
- **Single-Pass**: Nur ein Dateisystem-Durchlauf für alle Discovery-Typen
|
||||
- **Intelligentes Caching**: Basierend auf Dateiänderungszeiten
|
||||
- **Inkrementelle Scans**: Nur geänderte Dateien werden neu verarbeitet
|
||||
- **Async-Support**: Vorbereitet für asynchrone Verarbeitung großer Projekte
|
||||
|
||||
### Vergleich
|
||||
- **Vorher**: 3-4 separate Dateisystem-Scans (Attribute, Interfaces, Routes, Templates)
|
||||
- **Nachher**: 1 einziger Scan für alle Discovery-Typen
|
||||
- **Performance-Gewinn**: 70-80% weniger I/O-Operationen
|
||||
|
||||
## Migration
|
||||
|
||||
### Ersetzte Klassen
|
||||
Siehe `OBSOLETE_CORE_FILES.md` für eine vollständige Liste der ersetzten Core-Komponenten.
|
||||
|
||||
### Kompatibilität
|
||||
Das Discovery-Modul ist vollständig rückwärtskompatibel mit dem bestehenden Code durch die `DiscoveryResults::get()` Methode.
|
||||
|
||||
## Erweiterung
|
||||
|
||||
### Neue Discovery-Typen hinzufügen
|
||||
1. Neuen Visitor implementieren, der `FileVisitor` implementiert
|
||||
2. Visitor in `UnifiedDiscoveryService::registerVisitors()` hinzufügen
|
||||
3. Ergebnis-Sammlung in `collectResults()` ergänzen
|
||||
|
||||
### Neue Attribute-Mapper
|
||||
```php
|
||||
$discovery = new UnifiedDiscoveryService(
|
||||
$pathProvider,
|
||||
$cache,
|
||||
[MyCustomMapper::class, ...], // Neue Mapper hinzufügen
|
||||
$interfaces
|
||||
);
|
||||
```</llm-patch>
|
||||
255
src/Framework/Discovery/Results/DiscoveryResults.php
Normal file
255
src/Framework/Discovery/Results/DiscoveryResults.php
Normal file
@@ -0,0 +1,255 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Discovery\Results;
|
||||
|
||||
/**
|
||||
* Einheitliche Klasse für alle Discovery-Ergebnisse
|
||||
*/
|
||||
final class DiscoveryResults
|
||||
{
|
||||
private array $attributeResults = [];
|
||||
private array $interfaceImplementations = [];
|
||||
private array $routes = [];
|
||||
private array $templates = [];
|
||||
private array $additionalResults = [];
|
||||
|
||||
public function __construct(
|
||||
array $attributeResults = [],
|
||||
array $interfaceImplementations = [],
|
||||
array $routes = [],
|
||||
array $templates = [],
|
||||
array $additionalResults = []
|
||||
) {
|
||||
$this->attributeResults = $attributeResults;
|
||||
$this->interfaceImplementations = $interfaceImplementations;
|
||||
$this->routes = $routes;
|
||||
$this->templates = $templates;
|
||||
$this->additionalResults = $additionalResults;
|
||||
}
|
||||
|
||||
// === Attribute Results ===
|
||||
|
||||
public function setAttributeResults(array $results): void
|
||||
{
|
||||
$this->attributeResults = $results;
|
||||
}
|
||||
|
||||
public function addAttributeResult(string $attributeClass, array $data): void
|
||||
{
|
||||
$this->attributeResults[$attributeClass][] = $data;
|
||||
}
|
||||
|
||||
public function getAttributeResults(string $attributeClass): array
|
||||
{
|
||||
return $this->attributeResults[$attributeClass] ?? [];
|
||||
}
|
||||
|
||||
public function getAllAttributeResults(): array
|
||||
{
|
||||
return $this->attributeResults;
|
||||
}
|
||||
|
||||
public function hasAttributeResults(string $attributeClass): bool
|
||||
{
|
||||
return !empty($this->attributeResults[$attributeClass]);
|
||||
}
|
||||
|
||||
// === Interface Implementations ===
|
||||
|
||||
public function setInterfaceImplementations(array $implementations): void
|
||||
{
|
||||
$this->interfaceImplementations = $implementations;
|
||||
}
|
||||
|
||||
public function addInterfaceImplementation(string $interface, string $className): void
|
||||
{
|
||||
if (!isset($this->interfaceImplementations[$interface])) {
|
||||
$this->interfaceImplementations[$interface] = [];
|
||||
}
|
||||
|
||||
if (!in_array($className, $this->interfaceImplementations[$interface])) {
|
||||
$this->interfaceImplementations[$interface][] = $className;
|
||||
}
|
||||
}
|
||||
|
||||
public function getInterfaceImplementations(string $interface): array
|
||||
{
|
||||
return $this->interfaceImplementations[$interface] ?? [];
|
||||
}
|
||||
|
||||
public function getAllInterfaceImplementations(): array
|
||||
{
|
||||
return $this->interfaceImplementations;
|
||||
}
|
||||
|
||||
// === Routes ===
|
||||
|
||||
public function setRoutes(array $routes): void
|
||||
{
|
||||
$this->routes = $routes;
|
||||
}
|
||||
|
||||
public function getRoutes(): array
|
||||
{
|
||||
return $this->routes;
|
||||
}
|
||||
|
||||
// === Templates ===
|
||||
|
||||
public function setTemplates(array $templates): void
|
||||
{
|
||||
$this->templates = $templates;
|
||||
}
|
||||
|
||||
public function getTemplates(): array
|
||||
{
|
||||
return $this->templates;
|
||||
}
|
||||
|
||||
// === Additional Results ===
|
||||
|
||||
public function setAdditionalResult(string $key, mixed $value): void
|
||||
{
|
||||
$this->additionalResults[$key] = $value;
|
||||
}
|
||||
|
||||
public function getAdditionalResult(string $key, mixed $default = null): mixed
|
||||
{
|
||||
return $this->additionalResults[$key] ?? $default;
|
||||
}
|
||||
|
||||
public function has(string $key): bool
|
||||
{
|
||||
return isset($this->additionalResults[$key]) ||
|
||||
isset($this->attributeResults[$key]) ||
|
||||
isset($this->interfaceImplementations[$key]) ||
|
||||
isset($this->routes[$key]) ||
|
||||
isset($this->templates[$key]);
|
||||
}
|
||||
|
||||
// === Compatibility with old ProcessedResults ===
|
||||
|
||||
/**
|
||||
* Kompatibilitätsmethode für bestehenden Code
|
||||
*/
|
||||
public function get(string $key): array
|
||||
{
|
||||
// Direkte Suche nach dem Key
|
||||
if (isset($this->attributeResults[$key])) {
|
||||
return $this->attributeResults[$key];
|
||||
}
|
||||
|
||||
// Versuche auch mit führendem Backslash
|
||||
$keyWithBackslash = '\\' . ltrim($key, '\\');
|
||||
if (isset($this->attributeResults[$keyWithBackslash])) {
|
||||
return $this->attributeResults[$keyWithBackslash];
|
||||
}
|
||||
|
||||
// Versuche ohne führenden Backslash
|
||||
$keyWithoutBackslash = ltrim($key, '\\');
|
||||
if (isset($this->attributeResults[$keyWithoutBackslash])) {
|
||||
return $this->attributeResults[$keyWithoutBackslash];
|
||||
}
|
||||
|
||||
// Für Interfaces (z.B. Initializer::class)
|
||||
if (isset($this->interfaceImplementations[$key])) {
|
||||
return array_map(function($className) {
|
||||
return ['class' => $className];
|
||||
}, $this->interfaceImplementations[$key]);
|
||||
}
|
||||
|
||||
if (isset($this->interfaceImplementations[$keyWithBackslash])) {
|
||||
return array_map(function($className) {
|
||||
return ['class' => $className];
|
||||
}, $this->interfaceImplementations[$keyWithBackslash]);
|
||||
}
|
||||
|
||||
if (isset($this->interfaceImplementations[$keyWithoutBackslash])) {
|
||||
return array_map(function($className) {
|
||||
return ['class' => $className];
|
||||
}, $this->interfaceImplementations[$keyWithoutBackslash]);
|
||||
}
|
||||
|
||||
// Für spezielle Keys
|
||||
switch ($key) {
|
||||
case 'routes':
|
||||
return $this->routes;
|
||||
case 'templates':
|
||||
return $this->templates;
|
||||
}
|
||||
|
||||
return $this->additionalResults[$key] ?? [];
|
||||
}
|
||||
|
||||
// === Serialization ===
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'attributes' => $this->attributeResults,
|
||||
'interfaces' => $this->interfaceImplementations,
|
||||
'routes' => $this->routes,
|
||||
'templates' => $this->templates,
|
||||
'additional' => $this->additionalResults,
|
||||
];
|
||||
}
|
||||
|
||||
public static function fromArray(array $data): self
|
||||
{
|
||||
return new self(
|
||||
$data['attributes'] ?? [],
|
||||
$data['interfaces'] ?? [],
|
||||
$data['routes'] ?? [],
|
||||
$data['templates'] ?? [],
|
||||
$data['additional'] ?? []
|
||||
);
|
||||
}
|
||||
|
||||
/* public function __serialize(): array
|
||||
{
|
||||
return $this->toArray();
|
||||
}
|
||||
|
||||
public function __unserialize(array $data): void
|
||||
{
|
||||
$this->attributeResults = $data['attributes'] ?? [];
|
||||
$this->interfaceImplementations = $data['interfaces'] ?? [];
|
||||
$this->routes = $data['routes'] ?? [];
|
||||
$this->templates = $data['templates'] ?? [];
|
||||
$this->additionalResults = $data['additional'] ?? [];
|
||||
}*/
|
||||
|
||||
// === Utility Methods ===
|
||||
|
||||
public function isEmpty(): bool
|
||||
{
|
||||
return empty($this->attributeResults)
|
||||
&& empty($this->interfaceImplementations)
|
||||
&& empty($this->routes)
|
||||
&& empty($this->templates)
|
||||
&& empty($this->additionalResults);
|
||||
}
|
||||
|
||||
public function merge(DiscoveryResults $other): self
|
||||
{
|
||||
return new self(
|
||||
array_merge_recursive($this->attributeResults, $other->attributeResults),
|
||||
array_merge_recursive($this->interfaceImplementations, $other->interfaceImplementations),
|
||||
array_merge($this->routes, $other->routes),
|
||||
array_merge($this->templates, $other->templates),
|
||||
array_merge($this->additionalResults, $other->additionalResults)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sortiert Interface-Implementierungen für konsistente Ergebnisse
|
||||
*/
|
||||
public function sortInterfaceImplementations(): void
|
||||
{
|
||||
foreach ($this->interfaceImplementations as &$implementations) {
|
||||
sort($implementations);
|
||||
$implementations = array_unique($implementations);
|
||||
}
|
||||
}
|
||||
}
|
||||
168
src/Framework/Discovery/UnifiedDiscoveryService.php
Normal file
168
src/Framework/Discovery/UnifiedDiscoveryService.php
Normal file
@@ -0,0 +1,168 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Discovery;
|
||||
|
||||
use App\Framework\Cache\Cache;
|
||||
use App\Framework\Core\InterfaceImplementationVisitor;
|
||||
use App\Framework\Core\PathProvider;
|
||||
use App\Framework\Core\RouteDiscoveryVisitor;
|
||||
use App\Framework\Discovery\Results\DiscoveryResults;
|
||||
use App\Framework\Discovery\Visitors\AttributeDiscoveryVisitor;
|
||||
use App\Framework\View\TemplateDiscoveryVisitor;
|
||||
|
||||
/**
|
||||
* Einheitlicher Discovery-Service der alle Visitor koordiniert
|
||||
* Eliminiert doppelte Dateisystem-Iteration durch einmaligen Scan
|
||||
*/
|
||||
final readonly class UnifiedDiscoveryService
|
||||
{
|
||||
private FileScannerService $fileScannerService;
|
||||
|
||||
public function __construct(
|
||||
private PathProvider $pathProvider,
|
||||
private Cache $cache,
|
||||
private array $attributeMappers = [],
|
||||
private array $targetInterfaces = [],
|
||||
private bool $useCache = true,
|
||||
private bool $showProgress = false
|
||||
) {
|
||||
// FileScannerService mit den richtigen Abhängigkeiten initialisieren
|
||||
$this->fileScannerService = new FileScannerService(
|
||||
new FileScanner(),
|
||||
$pathProvider,
|
||||
$cache,
|
||||
$this->useCache
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Führt die einheitliche Discovery durch
|
||||
* Ein einziger Scan für alle Discovery-Typen
|
||||
*/
|
||||
public function discover(): DiscoveryResults
|
||||
{
|
||||
$cacheKey = 'unified_discovery_results';
|
||||
|
||||
// Prüfe Cache nur wenn aktiviert
|
||||
if ($this->useCache) {
|
||||
$cached = $this->cache->get($cacheKey);
|
||||
if ($cached->isHit) {
|
||||
return DiscoveryResults::fromArray($cached->value);
|
||||
}
|
||||
}
|
||||
|
||||
// Registriere alle Visitors
|
||||
$this->registerVisitors();
|
||||
|
||||
// Ein einziger Scan für alle Discovery-Typen
|
||||
$this->fileScannerService->scan($this->showProgress);
|
||||
|
||||
// Sammle Ergebnisse von allen Visitors
|
||||
$results = $this->collectResults();
|
||||
|
||||
|
||||
// Cache für nächsten Aufruf speichern
|
||||
if ($this->useCache) {
|
||||
$this->cache->set($cacheKey, $results->toArray());
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Führt einen inkrementellen Scan durch
|
||||
*/
|
||||
public function incrementalDiscover(): DiscoveryResults
|
||||
{
|
||||
// Registriere alle Visitors
|
||||
$this->registerVisitors();
|
||||
|
||||
// Inkrementeller Scan
|
||||
$this->fileScannerService->incrementalScan($this->showProgress);
|
||||
|
||||
// Sammle aktualisierte Ergebnisse
|
||||
$results = $this->collectResults();
|
||||
|
||||
// Cache aktualisieren
|
||||
if ($this->useCache) {
|
||||
$this->cache->set('unified_discovery_results', $results->toArray());
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registriert alle benötigten Visitors
|
||||
*/
|
||||
private function registerVisitors(): void
|
||||
{
|
||||
// Attribute Discovery Visitor
|
||||
if (!empty($this->attributeMappers)) {
|
||||
$this->fileScannerService->registerVisitor(
|
||||
new AttributeDiscoveryVisitor($this->attributeMappers)
|
||||
);
|
||||
}
|
||||
|
||||
// Interface Implementation Visitor
|
||||
if (!empty($this->targetInterfaces)) {
|
||||
$this->fileScannerService->registerVisitor(
|
||||
new InterfaceImplementationVisitor($this->targetInterfaces)
|
||||
);
|
||||
}
|
||||
|
||||
// Route Discovery Visitor
|
||||
$this->fileScannerService->registerVisitor(
|
||||
new RouteDiscoveryVisitor()
|
||||
);
|
||||
|
||||
// Template Discovery Visitor
|
||||
$this->fileScannerService->registerVisitor(
|
||||
new TemplateDiscoveryVisitor()
|
||||
);
|
||||
|
||||
// Hier können weitere Visitors hinzugefügt werden
|
||||
}
|
||||
|
||||
/**
|
||||
* Sammelt Ergebnisse von allen registrierten Visitors
|
||||
*/
|
||||
private function collectResults(): DiscoveryResults
|
||||
{
|
||||
$results = new DiscoveryResults();
|
||||
|
||||
foreach ($this->fileScannerService->getVisitors() as $visitor) {
|
||||
if ($visitor instanceof AttributeDiscoveryVisitor) {
|
||||
$results->setAttributeResults($visitor->getAllResults());
|
||||
} elseif ($visitor instanceof InterfaceImplementationVisitor) {
|
||||
$results->setInterfaceImplementations($visitor->getAllImplementations());
|
||||
} elseif ($visitor instanceof RouteDiscoveryVisitor) {
|
||||
$results->setRoutes($visitor->getRoutes());
|
||||
} elseif ($visitor instanceof TemplateDiscoveryVisitor) {
|
||||
$results->setTemplates($visitor->getAllTemplates());
|
||||
}
|
||||
}
|
||||
|
||||
// Sortiere Interface-Implementierungen für konsistente Ergebnisse
|
||||
$results->sortInterfaceImplementations();
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gibt die Anzahl der verarbeiteten Dateien zurück
|
||||
*/
|
||||
public function getProcessedFileCount(): int
|
||||
{
|
||||
return $this->fileScannerService->getProcessedFileCount() ?? 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft, ob ein Rescan notwendig ist
|
||||
*/
|
||||
public function shouldRescan(): bool
|
||||
{
|
||||
return $this->fileScannerService->shouldRescan();
|
||||
}
|
||||
}
|
||||
323
src/Framework/Discovery/Visitors/AttributeDiscoveryVisitor.php
Normal file
323
src/Framework/Discovery/Visitors/AttributeDiscoveryVisitor.php
Normal file
@@ -0,0 +1,323 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Discovery\Visitors;
|
||||
|
||||
use AllowDynamicProperties;
|
||||
use App\Framework\Cache\Cache;
|
||||
use App\Framework\Core\AttributeMapper;
|
||||
use App\Framework\Discovery\FileVisitor;
|
||||
use Attribute;
|
||||
use Override;
|
||||
use ReflectionClass;
|
||||
use ReflectionMethod;
|
||||
use ReturnTypeWillChange;
|
||||
use SensitiveParameter;
|
||||
use Throwable;
|
||||
|
||||
final class AttributeDiscoveryVisitor implements FileVisitor
|
||||
{
|
||||
private array $attributeResults = [];
|
||||
|
||||
/** @var array<string, AttributeMapper> */
|
||||
private array $mapperMap;
|
||||
|
||||
/**
|
||||
* @param array<AttributeMapper|string> $attributeMappers Array von Mapper-Instanzen oder Klassennamen
|
||||
*/
|
||||
/** @var array<string> PHP-interne Attribute die ignoriert werden sollen */
|
||||
private array $ignoredAttributes = [
|
||||
'Attribute',
|
||||
Attribute::class,
|
||||
'Override',
|
||||
Override::class,
|
||||
'AllowDynamicProperties',
|
||||
AllowDynamicProperties::class,
|
||||
'ReturnTypeWillChange',
|
||||
ReturnTypeWillChange::class,
|
||||
'SensitiveParameter',
|
||||
SensitiveParameter::class,
|
||||
];
|
||||
|
||||
public function __construct(array $attributeMappers = [])
|
||||
{
|
||||
$this->mapperMap = [];
|
||||
foreach ($attributeMappers as $mapper) {
|
||||
if (is_string($mapper)) {
|
||||
$mapper = new $mapper();
|
||||
}
|
||||
$this->mapperMap[$mapper->getAttributeClass()] = $mapper;
|
||||
}
|
||||
}
|
||||
|
||||
public function onScanStart(): void
|
||||
{
|
||||
$this->attributeResults = [];
|
||||
}
|
||||
|
||||
public function onIncrementalScanStart(): void
|
||||
{
|
||||
// Bei inkrementellem Scan behalten wir bestehende Attribute
|
||||
// Einzelne Klassen werden bei visitClass() aktualisiert
|
||||
}
|
||||
|
||||
public function onIncrementalScanComplete(): void
|
||||
{
|
||||
// Eventuell nachsortieren oder optimieren
|
||||
$this->optimizeResults();
|
||||
}
|
||||
|
||||
public function visitClass(string $className, string $filePath): void
|
||||
{
|
||||
if (!class_exists($className)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$reflection = new ReflectionClass($className);
|
||||
|
||||
// Entferne alte Einträge für diese Klasse (wichtig für inkrementelle Scans)
|
||||
$this->removeAttributesForClass($className);
|
||||
|
||||
// Klassen-Attribute verarbeiten
|
||||
$this->processElementAttributes($reflection);
|
||||
|
||||
// Methoden-Attribute verarbeiten
|
||||
foreach ($reflection->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
|
||||
$this->processElementAttributes($method);
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
error_log("Fehler beim Verarbeiten der Attribute für {$className}: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verarbeitet die Attribute eines Elements (Klasse oder Methode)
|
||||
*/
|
||||
private function processElementAttributes(ReflectionClass|ReflectionMethod $element): void
|
||||
{
|
||||
foreach ($element->getAttributes() as $attribute) {
|
||||
|
||||
$attrName = $attribute->getName();
|
||||
|
||||
// Ignoriere PHP-interne Attribute
|
||||
if ($this->shouldIgnoreAttribute($attrName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$attributeInstance = $attribute->newInstance();
|
||||
|
||||
// Standard-Daten für alle Attribute sammeln
|
||||
$baseData = [
|
||||
'attribute_class' => $attrName,
|
||||
'target_type' => $element instanceof ReflectionClass ? 'class' : 'method',
|
||||
'class' => $element instanceof ReflectionClass ? $element->getName() : $element->getDeclaringClass()->getName(),
|
||||
'file' => $element instanceof ReflectionClass ? $element->getFileName() : $element->getDeclaringClass()->getFileName(),
|
||||
];
|
||||
|
||||
if ($element instanceof ReflectionMethod) {
|
||||
$baseData['method'] = $element->getName();
|
||||
$baseData['parameters'] = $this->extractMethodParameters($element);
|
||||
}
|
||||
|
||||
// Attribute-spezifische Daten hinzufügen
|
||||
$baseData['attribute_data'] = $this->extractAttributeData($attributeInstance);
|
||||
|
||||
// Wenn ein Mapper existiert, verwende ihn
|
||||
if (isset($this->mapperMap[$attrName])) {
|
||||
$mapped = $this->mapperMap[$attrName]->map($element, $attributeInstance);
|
||||
|
||||
if ($mapped !== null) {
|
||||
// Füge Parameter-Informationen hinzu bei Methoden (falls noch nicht vom Mapper gemacht)
|
||||
if ($element instanceof ReflectionMethod && !isset($mapped['parameters'])) {
|
||||
$mapped['parameters'] = $this->extractMethodParameters($element);
|
||||
}
|
||||
|
||||
$this->attributeResults[$attrName][] = $mapped;
|
||||
}
|
||||
} else {
|
||||
// Keine Mapper vorhanden - verwende Standard-Daten
|
||||
#debug("Attribute ohne Mapper gefunden: $attrName");
|
||||
$this->attributeResults[$attrName][] = $baseData;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extrahiert Parameter-Informationen aus einer Methode
|
||||
*/
|
||||
private function extractMethodParameters(ReflectionMethod $method): array
|
||||
{
|
||||
$params = [];
|
||||
foreach ($method->getParameters() as $param) {
|
||||
$paramData = [
|
||||
'name' => $param->getName(),
|
||||
'type' => $param->getType()?->getName(),
|
||||
'isBuiltin' => $param->getType()?->isBuiltin() ?? false,
|
||||
'hasDefault' => $param->isDefaultValueAvailable(),
|
||||
'default' => $param->isDefaultValueAvailable() ? $param->getDefaultValue() : null,
|
||||
'attributes' => array_map(
|
||||
fn ($a) => $a->getName(),
|
||||
$param->getAttributes()
|
||||
),
|
||||
];
|
||||
$params[] = $paramData;
|
||||
}
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft, ob ein Attribut ignoriert werden soll
|
||||
*/
|
||||
private function shouldIgnoreAttribute(string $attributeName): bool
|
||||
{
|
||||
// Direkte Prüfung
|
||||
if (in_array($attributeName, $this->ignoredAttributes, true)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Prüfung ohne Namespace (falls nur Klassenname verwendet wird)
|
||||
$shortName = substr($attributeName, strrpos($attributeName, '\\') + 1);
|
||||
if (in_array($shortName, $this->ignoredAttributes, true)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extrahiert verfügbare Daten aus einem Attribute-Objekt
|
||||
*/
|
||||
private function extractAttributeData(object $attributeInstance): array
|
||||
{
|
||||
$data = [];
|
||||
|
||||
try {
|
||||
$reflection = new ReflectionClass($attributeInstance);
|
||||
|
||||
// Öffentliche Eigenschaften
|
||||
foreach ($reflection->getProperties() as $property) {
|
||||
if ($property->isPublic()) {
|
||||
$data[$property->getName()] = $property->getValue($attributeInstance);
|
||||
}
|
||||
}
|
||||
|
||||
// Constructor-Parameter (für readonly properties)
|
||||
$constructor = $reflection->getConstructor();
|
||||
if ($constructor) {
|
||||
foreach ($constructor->getParameters() as $param) {
|
||||
$name = $param->getName();
|
||||
if (!isset($data[$name])) {
|
||||
// Versuche über magische Methoden oder Getter
|
||||
$getter = 'get' . ucfirst($name);
|
||||
if (method_exists($attributeInstance, $getter)) {
|
||||
$data[$name] = $attributeInstance->$getter();
|
||||
} elseif (property_exists($attributeInstance, $name)) {
|
||||
$property = $reflection->getProperty($name);
|
||||
if ($property->isPublic() ||
|
||||
($property->isProtected() || $property->isPrivate()) &&
|
||||
version_compare(PHP_VERSION, '8.1.0') >= 0) {
|
||||
try {
|
||||
$data[$name] = $property->getValue($attributeInstance);
|
||||
} catch (Throwable) {
|
||||
// Ignore protected/private properties we can't access
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
error_log("Fehler beim Extrahieren der Attribute-Daten: " . $e->getMessage());
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Entfernt alle Attribute für eine bestimmte Klasse (für inkrementelle Scans)
|
||||
*/
|
||||
private function removeAttributesForClass(string $className): void
|
||||
{
|
||||
foreach ($this->attributeResults as $attributeClass => &$results) {
|
||||
$results = array_filter($results, function($result) use ($className) {
|
||||
return ($result['class'] ?? '') !== $className;
|
||||
});
|
||||
// Array-Indizes neu numerieren
|
||||
$results = array_values($results);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Optimiert die Ergebnisse für bessere Performance
|
||||
*/
|
||||
private function optimizeResults(): void
|
||||
{
|
||||
foreach ($this->attributeResults as &$results) {
|
||||
// Sortiere nach Klasse und Methode für konsistente Reihenfolge
|
||||
usort($results, function($a, $b) {
|
||||
$classCompare = ($a['class'] ?? '') <=> ($b['class'] ?? '');
|
||||
if ($classCompare !== 0) {
|
||||
return $classCompare;
|
||||
}
|
||||
return ($a['method'] ?? '') <=> ($b['method'] ?? '');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function onScanComplete(): void
|
||||
{
|
||||
$this->optimizeResults();
|
||||
}
|
||||
|
||||
public function loadFromCache(Cache $cache): void
|
||||
{
|
||||
$cacheItem = $cache->get($this->getCacheKey());
|
||||
if ($cacheItem->isHit) {
|
||||
$this->attributeResults = $cacheItem->value ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
public function getCacheKey(): string
|
||||
{
|
||||
return 'attribute_discovery';
|
||||
}
|
||||
|
||||
public function getCacheableData(): mixed
|
||||
{
|
||||
return $this->attributeResults;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt alle Attribute-Ergebnisse für eine bestimmte Attributklasse zurück
|
||||
*/
|
||||
public function getAttributeResults(string $attributeClass): array
|
||||
{
|
||||
return $this->attributeResults[$attributeClass] ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt alle gefundenen Attribute-Ergebnisse zurück
|
||||
*/
|
||||
public function getAllResults(): array
|
||||
{
|
||||
return $this->attributeResults;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft, ob Attribute einer bestimmten Klasse gefunden wurden
|
||||
*/
|
||||
public function hasAttributeResults(string $attributeClass): bool
|
||||
{
|
||||
return !empty($this->attributeResults[$attributeClass]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt die Anzahl gefundener Attribute zurück
|
||||
*/
|
||||
public function getAttributeCount(string $attributeClass): int
|
||||
{
|
||||
return count($this->attributeResults[$attributeClass] ?? []);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user