Files
michaelschiemer/src/Framework/Discovery/DiscoveryServiceBootstrapper.php

264 lines
11 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Framework\Discovery;
use App\Framework\Cache\Cache;
use App\Framework\Cache\CacheItem;
use App\Framework\Config\AppConfig;
use App\Framework\Config\DiscoveryConfig;
use App\Framework\Config\TypedConfiguration;
use App\Framework\Context\ExecutionContext;
use App\Framework\Core\PathProvider;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\DateTime\Clock;
use App\Framework\DI\Container;
use App\Framework\Discovery\Cache\DiscoveryCacheIdentifiers;
use App\Framework\Discovery\Factory\DiscoveryServiceFactory;
use App\Framework\Discovery\Results\DiscoveryRegistry;
use App\Framework\Logging\Logger;
use App\Framework\Logging\ValueObjects\LogContext;
/**
* Bootstrapper für den Discovery-Service
* Ersetzt die alte AttributeDiscoveryService-Integration
*/
final readonly class DiscoveryServiceBootstrapper
{
public function __construct(
private Container $container,
private Clock $clock,
private Logger $logger
) {
}
/**
* Bootstrapt den Discovery-Service und führt die Discovery durch
*/
public function bootstrap(): DiscoveryRegistry
{
$pathProvider = $this->container->get(PathProvider::class);
$cache = $this->container->get(Cache::class);
// Get DiscoveryConfig if available, otherwise use defaults
$discoveryConfig = null;
if ($this->container->has(DiscoveryConfig::class)) {
$discoveryConfig = $this->container->get(DiscoveryConfig::class);
}
// Context-spezifische Discovery mit separaten Caches
$currentContext = ExecutionContext::detect();
$contextString = $currentContext->getType()->value;
// Debug logging via framework Logger
$this->logger->debug("Context detected = {$contextString}", LogContext::withData([
'source_path' => $pathProvider->getSourcePath()
]));
// Direkter Cache-Check mit expliziter toArray/fromArray Serialisierung
$defaultPaths = [$pathProvider->getSourcePath()];
$cacheKey = DiscoveryCacheIdentifiers::fullDiscoveryKey($defaultPaths, $contextString);
$this->logger->debug("Cache key = {$cacheKey->toString()}");
$cachedItem = $cache->get($cacheKey);
$this->logger->debug("Cache hit = " . ($cachedItem->isHit ? 'YES' : 'NO'));
if ($cachedItem->isHit) {
$this->logger->debug("Loading from cache...");
// Ensure DiscoveryRegistry class is loaded before attempting deserialization
if (! class_exists(DiscoveryRegistry::class, true)) {
$cachedRegistry = null;
} else {
// Versuche die gecachten Daten zu laden
$cachedRegistry = null;
try {
// Skip incomplete classes - they indicate autoloader issues
if (is_object($cachedItem->value) && get_class($cachedItem->value) === '__PHP_Incomplete_Class') {
$cachedRegistry = null;
} elseif ($cachedItem->value instanceof DiscoveryRegistry) {
$cachedRegistry = $cachedItem->value;
} elseif (is_array($cachedItem->value)) {
$cachedRegistry = DiscoveryRegistry::fromArray($cachedItem->value);
} elseif (is_string($cachedItem->value)) {
$cachedRegistry = DiscoveryRegistry::fromArray(json_decode($cachedItem->value, true, 512, JSON_THROW_ON_ERROR));
} else {
$cachedRegistry = null;
}
} catch (\Throwable $e) {
$cachedRegistry = null;
}
}
if ($cachedRegistry !== null && ! $cachedRegistry->isEmpty()) {
$routeCount = count($cachedRegistry->attributes->get(\App\Framework\Attributes\Route::class));
$this->logger->debug("Cached registry loaded - Route count: {$routeCount}");
$this->container->singleton(DiscoveryRegistry::class, $cachedRegistry);
// Process DefaultImplementation attributes first (before Initializers)
$defaultImplProcessor = $this->container->get(\App\Framework\DI\DefaultImplementationProcessor::class);
$defaultImplProcessor->process($cachedRegistry);
// Initializer-Verarbeitung für gecachte Registry
$initializerProcessor = $this->container->get(InitializerProcessor::class);
$initializerProcessor->processInitializers($cachedRegistry);
return $cachedRegistry;
}
}
// Fallback: Vollständige Discovery durchführen
$this->logger->debug("Performing fresh discovery...");
$results = $this->performBootstrap($pathProvider, $cache, $discoveryConfig);
$this->logger->debug("Discovery completed", LogContext::withData([
'is_empty' => $results->isEmpty()
]));
// Nach der Discovery explizit in unserem eigenen Cache-Format speichern
$consoleCommandCount = count($results->attributes->get(\App\Framework\Console\ConsoleCommand::class));
$routeCount = count($results->attributes->get(\App\Framework\Attributes\Route::class));
$initializerCount = count($results->attributes->get(\App\Framework\DI\Initializer::class));
$this->logger->debug("Discovery results", LogContext::withData([
'routes' => $routeCount,
'console_commands' => $consoleCommandCount,
'initializers' => $initializerCount
]));
// Only cache if we found meaningful results
// An empty discovery likely indicates initialization timing issues
if (! $results->isEmpty() && $consoleCommandCount > 0) {
$arrayData = $results->toArray();
$cacheItem = CacheItem::forSet(
key: $cacheKey,
value: $arrayData,
ttl: Duration::fromHours(1)
);
$cache->set($cacheItem);
}
return $results;
}
/**
* Führt den Discovery-Prozess durch und verarbeitet die Ergebnisse
*/
public function performBootstrap(PathProvider $pathProvider, Cache $cache, ?DiscoveryConfig $config): DiscoveryRegistry
{
// Context-spezifische Discovery
$currentContext = ExecutionContext::detect();
$discoveryService = $this->createDiscoveryService($pathProvider, $cache, $config, $currentContext);
// Discovery durchführen
$results = $discoveryService->discover();
// Ergebnisse im Container registrieren
$this->container->singleton(DiscoveryRegistry::class, $results);
$this->container->instance(UnifiedDiscoveryService::class, $discoveryService);
// Process DefaultImplementation attributes first (before Initializers, as they may depend on them)
$defaultImplProcessor = $this->container->get(\App\Framework\DI\DefaultImplementationProcessor::class);
$defaultImplProcessor->process($results);
// Initializer-Verarbeitung mit dedizierter Klasse
$initializerProcessor = $this->container->get(InitializerProcessor::class);
$initializerProcessor->processInitializers($results);
return $results;
}
/**
* Creates the modern discovery service with enhanced features using the new factory
*/
private function createDiscoveryService(
PathProvider $pathProvider,
Cache $cache,
?DiscoveryConfig $config,
?ExecutionContext $context = null
): UnifiedDiscoveryService {
// Create factory
$factory = new DiscoveryServiceFactory(
$this->container,
$pathProvider,
$cache,
$this->clock
);
// Determine environment-based factory method
$envType = 'production'; // Default fallback
if ($this->container->has(AppConfig::class)) {
$appConfig = $this->container->get(AppConfig::class);
$envType = $appConfig->environment;
} elseif ($this->container->has(TypedConfiguration::class)) {
$typedConfig = $this->container->get(TypedConfiguration::class);
$appConfigFromTyped = $typedConfig->appConfig();
$envType = is_string($appConfigFromTyped->environment)
? $appConfigFromTyped->environment
: $appConfigFromTyped->environment->value;
}
// Use factory methods which include default paths
// For console/CLI contexts, always use development configuration
// to ensure consistent discovery behavior across CLI environments
$currentContext = $context?->getType();
return match (true) {
// CLI contexts (console, cli-script, test) should use development config
// to ensure consistent command discovery
$currentContext?->value === 'console' => $factory->createForDevelopment(),
$currentContext?->value === 'cli-script' => $factory->createForDevelopment(),
$currentContext?->value === 'test' => $factory->createForDevelopment(),
// Environment-based fallback for non-CLI contexts
$envType === 'development' => $factory->createForDevelopment(),
$envType === 'testing' => $factory->createForDevelopment(), // Use development config for testing too
default => $factory->createForProduction()
};
}
/**
* Führt einen inkrementellen Discovery-Scan durch und verarbeitet die Ergebnisse
* mit context-aware Initializer-Verarbeitung
*/
public function incrementalBootstrap(): DiscoveryRegistry
{
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(DiscoveryRegistry::class, $results);
// Re-process initializers for current context
$initializerProcessor = $this->container->get(InitializerProcessor::class);
$initializerProcessor->processInitializers($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();
}
}