- Add DISCOVERY_LOG_LEVEL=debug - Add DISCOVERY_SHOW_PROGRESS=true - Temporary changes for debugging InitializerProcessor fixes on production
282 lines
9.1 KiB
PHP
282 lines
9.1 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Framework\Discovery\Processing;
|
|
|
|
use App\Framework\Core\AttributeMapper;
|
|
use App\Framework\Core\ValueObjects\ClassName;
|
|
use App\Framework\Core\ValueObjects\MethodName;
|
|
use App\Framework\Discovery\DiscoveryDataCollector;
|
|
use App\Framework\Discovery\ValueObjects\AttributeTarget;
|
|
use App\Framework\Discovery\ValueObjects\DiscoveredAttribute;
|
|
use App\Framework\Discovery\ValueObjects\FileContext;
|
|
use App\Framework\Discovery\ValueObjects\TemplateMapping;
|
|
use App\Framework\Filesystem\File;
|
|
use App\Framework\Http\Method;
|
|
use App\Framework\Logging\Logger;
|
|
|
|
/**
|
|
* Coordinates visitor execution with shared reflection context
|
|
*
|
|
* This replaces the duplicate visitor logic in UnifiedDiscoveryService
|
|
*/
|
|
final class VisitorCoordinator
|
|
{
|
|
/** @var array<string, AttributeMapper> */
|
|
private array $mapperMap;
|
|
|
|
/** @var array<string> */
|
|
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);
|
|
}
|
|
}
|