*/ private array $mapperMap; /** @var array */ private array $ignoredAttributes = [ 'Attribute', 'Override', 'AllowDynamicProperties', 'ReturnTypeWillChange', 'SensitiveParameter', ]; public function __construct( private readonly ProcessingContext $processingContext, private readonly array $attributeMappers = [], private readonly array $targetInterfaces = [], private ?Logger $logger = null ) { // Build mapper map for quick lookup $mapperMap = []; foreach ($this->attributeMappers as $mapper) { $mapperMap[$mapper->getAttributeClass()] = $mapper; } $this->mapperMap = $mapperMap; } /** * Process a file with all discovery logic */ public function processFile( File $file, FileContext $fileContext, DiscoveryDataCollector $collector ): void { // Process each class in the file foreach ($fileContext->getClassNames() as $className) { $this->processClass($className, $fileContext, $collector); } // Process templates (file-based) $this->processTemplates($file, $collector); } /** * Process a single class */ private function processClass( ClassName $className, FileContext $fileContext, DiscoveryDataCollector $collector ): void { // Get shared reflection instance $reflection = $this->processingContext->getReflection($className); if ($reflection === null) { return; } // Process attributes $this->processClassAttributes($className, $fileContext, $reflection, $collector); $this->processMethodAttributes($className, $fileContext, $reflection, $collector); // Process interface implementations if (! empty($this->targetInterfaces)) { $this->processInterfaces($className, $reflection, $collector); } } /** * Process class-level attributes */ private function processClassAttributes( ClassName $className, FileContext $fileContext, $reflection, DiscoveryDataCollector $collector ): void { foreach ($reflection->getAttributes() as $attribute) { $attributeClass = $attribute->getName(); if ($this->shouldIgnoreAttribute($attributeClass)) { continue; } $mappedData = $this->applyMapper($attributeClass, $reflection, $attribute); $discovered = new DiscoveredAttribute( className: $className, attributeClass: $attributeClass, target: AttributeTarget::TARGET_CLASS, methodName: null, propertyName: null, arguments: $this->extractAttributeArguments($attribute), filePath: $fileContext->path, additionalData: $mappedData ?? [] ); $collector->getAttributeRegistry()->add($attributeClass, $discovered); } } /** * Process method-level attributes */ private function processMethodAttributes( ClassName $className, FileContext $fileContext, $reflection, DiscoveryDataCollector $collector ): void { foreach ($reflection->getMethods() as $method) { foreach ($method->getAttributes() as $attribute) { $attributeClass = $attribute->getName(); if ($this->shouldIgnoreAttribute($attributeClass)) { continue; } $mappedData = $this->applyMapper($attributeClass, $method, $attribute); $discovered = new DiscoveredAttribute( className: $className, attributeClass: $attributeClass, target: AttributeTarget::METHOD, methodName: MethodName::create($method->getName()), propertyName: null, arguments: $this->extractAttributeArguments($attribute), filePath: $fileContext->path, additionalData: $mappedData ?? [] ); $collector->getAttributeRegistry()->add($attributeClass, $discovered); } } } /** * Process interface implementations */ private function processInterfaces( ClassName $className, $reflection, DiscoveryDataCollector $collector ): void { foreach ($this->targetInterfaces as $targetInterface) { if ($reflection->implementsInterface($targetInterface)) { $collector->getInterfaceRegistry()->add( $targetInterface, $className->getFullyQualified() ); } } } /** * Process templates */ private function processTemplates(File $file, DiscoveryDataCollector $collector): void { $filePath = $file->getPath()->toString(); if (str_ends_with($filePath, '.view.php') || str_contains($filePath, '/views/')) { $templateName = basename($filePath, '.php'); $mapping = TemplateMapping::create($templateName, $filePath); $collector->getTemplateRegistry()->add($mapping); } } /** * Apply attribute mapper if available */ private function applyMapper(string $attributeClass, $reflectionElement, $attribute): ?array { if (! isset($this->mapperMap[$attributeClass])) { return $this->tryDefaultMapper($attributeClass, $reflectionElement, $attribute); } try { $mapper = $this->mapperMap[$attributeClass]; $attributeInstance = $attribute->newInstance(); return $mapper->map($reflectionElement, $attributeInstance); } catch (\Throwable) { return null; } } /** * Try to use a default mapper for common attributes */ private function tryDefaultMapper(string $attributeClass, $reflectionElement, $attribute): ?array { $defaultMapper = match ($attributeClass) { 'App\Framework\Attributes\Route' => new \App\Framework\Core\RouteMapper(), 'App\Framework\Core\Events\OnEvent' => new \App\Framework\Core\Events\EventHandlerMapper(), 'App\Framework\CommandBus\CommandHandler' => new \App\Framework\CommandBus\CommandHandlerMapper(), 'App\Framework\QueryBus\QueryHandler' => new \App\Framework\QueryBus\QueryHandlerMapper(), 'App\Framework\DI\Initializer' => new \App\Framework\DI\InitializerMapper(), default => null, }; if ($defaultMapper === null) { return null; } try { $attributeInstance = $attribute->newInstance(); return $defaultMapper->map($reflectionElement, $attributeInstance); } catch (\Throwable) { return null; } } /** * Extract and normalize attribute arguments */ private function extractAttributeArguments($attribute): array { try { $arguments = $attribute->getArguments(); $normalizedArgs = []; $reflection = new \ReflectionClass($attribute->getName()); $constructor = $reflection->getConstructor(); if ($constructor) { $parameters = $constructor->getParameters(); foreach ($arguments as $key => $value) { if (is_int($key) && isset($parameters[$key])) { $normalizedArgs[$parameters[$key]->getName()] = $value; } else { $normalizedArgs[$key] = $value; } } } else { $normalizedArgs = $arguments; } return $normalizedArgs; } catch (\Throwable) { return []; } } /** * Check if an attribute should be ignored */ private function shouldIgnoreAttribute(string $attributeName): bool { if (in_array($attributeName, $this->ignoredAttributes, true)) { return true; } $shortName = substr($attributeName, strrpos($attributeName, '\\') + 1); return in_array($shortName, $this->ignoredAttributes, true); } }