Files
michaelschiemer/src/Framework/Discovery/Factory/DiscoveryServiceFactory.php
Michael Schiemer 9b74ade5b0 feat: Fix discovery system critical issues
Resolved multiple critical discovery system issues:

## Discovery System Fixes
- Fixed console commands not being discovered on first run
- Implemented fallback discovery for empty caches
- Added context-aware caching with separate cache keys
- Fixed object serialization preventing __PHP_Incomplete_Class

## Cache System Improvements
- Smart caching that only caches meaningful results
- Separate caches for different execution contexts (console, web, test)
- Proper array serialization/deserialization for cache compatibility
- Cache hit logging for debugging and monitoring

## Object Serialization Fixes
- Fixed DiscoveredAttribute serialization with proper string conversion
- Sanitized additional data to prevent object reference issues
- Added fallback for corrupted cache entries

## Performance & Reliability
- All 69 console commands properly discovered and cached
- 534 total discovery items successfully cached and restored
- No more __PHP_Incomplete_Class cache corruption
- Improved error handling and graceful fallbacks

## Testing & Quality
- Fixed code style issues across discovery components
- Enhanced logging for better debugging capabilities
- Improved cache validation and error recovery

Ready for production deployment with stable discovery system.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-13 12:04:17 +02:00

343 lines
11 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Framework\Discovery\Factory;
use App\Framework\Cache\Cache;
use App\Framework\CommandBus\CommandHandlerMapper;
use App\Framework\Console\ConsoleCommandMapper;
use App\Framework\Context\ExecutionContext;
use App\Framework\Core\AttributeMapper;
use App\Framework\Core\Events\EventDispatcher;
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\DateTime\Clock;
use App\Framework\DI\Container;
use App\Framework\DI\DefaultContainer;
use App\Framework\DI\InitializerMapper;
use App\Framework\Discovery\UnifiedDiscoveryService;
use App\Framework\Discovery\ValueObjects\DiscoveryConfiguration;
use App\Framework\Filesystem\FileSystemService;
use App\Framework\Http\HttpMiddleware;
use App\Framework\Logging\Logger;
use App\Framework\Mcp\McpResourceMapper;
use App\Framework\Mcp\McpToolMapper;
use App\Framework\Performance\MemoryMonitor;
use App\Framework\QueryBus\QueryHandlerMapper;
use App\Framework\Reflection\CachedReflectionProvider;
use App\Framework\Reflection\ReflectionProvider;
use App\Framework\Template\Processing\DomProcessor;
use App\Framework\Template\Processing\StringProcessor;
/**
* Factory for creating properly configured DiscoveryService instances
*
* Centralizes the complex dependency creation and configuration logic
* that was previously scattered in constructors and bootstrappers.
*/
final readonly class DiscoveryServiceFactory
{
public function __construct(
private Container $container,
private PathProvider $pathProvider,
private Cache $cache,
private Clock $clock
) {
}
/**
* Create a fully configured DiscoveryService
*/
public function create(DiscoveryConfiguration $config): UnifiedDiscoveryService
{
// Validate configuration
$config->validate();
// Resolve or create required dependencies
$reflectionProvider = $this->resolveReflectionProvider();
$fileSystemService = $this->resolveFileSystemService();
// Resolve optional dependencies based on configuration
$logger = $config->enablePerformanceTracking
? $this->resolveLogger()
: null;
$eventDispatcher = $config->enableEventDispatcher
? $this->resolveEventDispatcher()
: null;
$memoryMonitor = $config->enableMemoryMonitoring
? $this->resolveMemoryMonitor()
: null;
// Merge configured mappers with defaults
$attributeMappers = $this->buildAttributeMappers($config->attributeMappers);
$targetInterfaces = $this->buildTargetInterfaces($config->targetInterfaces);
// Try to get ExecutionContext from container, or detect it
$executionContext = null;
if ($this->container->has(ExecutionContext::class)) {
$executionContext = $this->container->get(ExecutionContext::class);
} else {
$executionContext = ExecutionContext::detect();
}
return new UnifiedDiscoveryService(
pathProvider: $this->pathProvider,
cache: $this->cache,
clock: $this->clock,
reflectionProvider: $reflectionProvider,
configuration: $config,
attributeMappers: $attributeMappers,
targetInterfaces: $targetInterfaces,
logger: $logger,
eventDispatcher: $eventDispatcher,
memoryMonitor: $memoryMonitor,
fileSystemService: $fileSystemService,
executionContext: $executionContext
);
}
/**
* Create DiscoveryService for development environment
*/
public function createForDevelopment(array $paths = []): UnifiedDiscoveryService
{
$config = DiscoveryConfiguration::development();
$scanPaths = ! empty($paths) ? $paths : $this->getDefaultScanPaths();
$config = $config->withPaths($scanPaths);
return $this->create($config);
}
/**
* Create DiscoveryService for production environment
*/
public function createForProduction(array $paths = []): UnifiedDiscoveryService
{
$config = DiscoveryConfiguration::production();
$scanPaths = ! empty($paths) ? $paths : $this->getDefaultScanPaths();
$config = $config->withPaths($scanPaths);
return $this->create($config);
}
/**
* Create DiscoveryService for testing environment
*/
public function createForTesting(array $paths = []): UnifiedDiscoveryService
{
$config = DiscoveryConfiguration::testing();
$scanPaths = ! empty($paths) ? $paths : $this->getDefaultScanPaths();
$config = $config->withPaths($scanPaths);
return $this->create($config);
}
/**
* Create DiscoveryService with custom configuration
*/
public function createWithCustomConfig(
array $paths = [],
bool $useCache = true,
array $attributeMappers = [],
array $targetInterfaces = []
): UnifiedDiscoveryService {
$config = new DiscoveryConfiguration(
paths: $paths,
attributeMappers: $attributeMappers,
targetInterfaces: $targetInterfaces,
useCache: $useCache
);
return $this->create($config);
}
/**
* Resolve ReflectionProvider from container or create default
*/
private function resolveReflectionProvider(): ReflectionProvider
{
if ($this->container->has(ReflectionProvider::class)) {
return $this->container->get(ReflectionProvider::class);
}
return new CachedReflectionProvider();
}
/**
* Resolve FileSystemService from container or create default
*/
private function resolveFileSystemService(): FileSystemService
{
if ($this->container->has(FileSystemService::class)) {
return $this->container->get(FileSystemService::class);
}
return new FileSystemService();
}
/**
* Resolve Logger from container if available
*/
private function resolveLogger(): ?Logger
{
return $this->container->has(Logger::class)
? $this->container->get(Logger::class)
: null;
}
/**
* Resolve EventDispatcher from container if available
*/
private function resolveEventDispatcher(): ?EventDispatcher
{
return $this->container->has(EventDispatcher::class)
? $this->container->get(EventDispatcher::class)
: null;
}
/**
* Resolve MemoryMonitor from container if available
*/
private function resolveMemoryMonitor(): ?MemoryMonitor
{
return $this->container->has(MemoryMonitor::class)
? $this->container->get(MemoryMonitor::class)
: null;
}
/**
* Build attribute mappers array with defaults and user-provided mappers
*/
private function buildAttributeMappers(array $customMappers = []): array
{
$defaultMappers = [
new RouteMapper(),
new EventHandlerMapper(),
new \App\Framework\EventBus\EventHandlerMapper(),
new QueryHandlerMapper(),
new CommandHandlerMapper(),
new InitializerMapper(),
new McpToolMapper(),
new McpResourceMapper(),
new ConsoleCommandMapper(),
];
// Merge custom mappers with defaults, allowing override by class name
$mappers = [];
$mapperClasses = [];
// Add default mappers first
foreach ($defaultMappers as $mapper) {
$className = get_class($mapper);
$mappers[] = $mapper;
$mapperClasses[] = $className;
}
// Add custom mappers, replacing defaults if same class
foreach ($customMappers as $mapper) {
$className = get_class($mapper);
$existingIndex = array_search($className, $mapperClasses, true);
if ($existingIndex !== false) {
// Replace existing mapper
$mappers[$existingIndex] = $mapper;
} else {
// Add new mapper
$mappers[] = $mapper;
$mapperClasses[] = $className;
}
}
return $mappers;
}
/**
* Build target interfaces array with defaults and user-provided interfaces
*/
private function buildTargetInterfaces(array $customInterfaces = []): array
{
$defaultInterfaces = [
AttributeMapper::class,
HttpMiddleware::class,
DomProcessor::class,
StringProcessor::class,
Migration::class,
];
// Merge and deduplicate
return array_values(array_unique(array_merge($defaultInterfaces, $customInterfaces)));
}
/**
* Create a lightweight factory for specific use cases
*/
public static function lightweight(
PathProvider $pathProvider,
Cache $cache,
Clock $clock
): self {
// Create a minimal container for basic dependencies
$container = new DefaultContainer();
return new self($container, $pathProvider, $cache, $clock);
}
/**
* Validate that all required services are available
* @return string[]
*/
public function validateDependencies(DiscoveryConfiguration $config): array
{
$issues = [];
// Check required dependencies
if (! $this->container->has(PathProvider::class) && ! isset($this->pathProvider)) {
$issues[] = 'PathProvider is required but not available';
}
if (! $this->container->has(Cache::class) && ! isset($this->cache)) {
$issues[] = 'Cache is required but not available';
}
if (! $this->container->has(Clock::class) && ! isset($this->clock)) {
$issues[] = 'Clock is required but not available';
}
// Check optional dependencies if enabled
if ($config->enableEventDispatcher && ! $this->container->has(EventDispatcher::class)) {
$issues[] = 'EventDispatcher is enabled but not available in container';
}
if ($config->enableMemoryMonitoring && ! $this->container->has(MemoryMonitor::class)) {
$issues[] = 'MemoryMonitor is enabled but not available in container';
}
if ($config->enablePerformanceTracking && ! $this->container->has(Logger::class)) {
$issues[] = 'Logger is needed for performance tracking but not available in container';
}
return $issues;
}
/**
* Get default scan paths for discovery
* @return string[]
*/
private function getDefaultScanPaths(): array
{
$basePath = $this->pathProvider->getBasePath();
return [
$this->pathProvider->getSourcePath(),
#$basePath . '/src',
#$basePath . '/app', // Support for app/ directory structure
];
}
}