Enable Discovery debug logging for production troubleshooting
- Add DISCOVERY_LOG_LEVEL=debug - Add DISCOVERY_SHOW_PROGRESS=true - Temporary changes for debugging InitializerProcessor fixes on production
This commit is contained in:
281
src/Framework/Discovery/Processing/VisitorCoordinator.php
Normal file
281
src/Framework/Discovery/Processing/VisitorCoordinator.php
Normal file
@@ -0,0 +1,281 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user