264 lines
11 KiB
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();
|
|
}
|
|
}
|