refactor(di): enhance InitializerDependencyAnalyzer with fallback and initializer-based return type analysis

- Add fallback mechanism to resolve return types for closures without explicit return types.
- Introduce methods for discovering initializer classes based on naming conventions and interface analysis.
- Implement functionality to analyze the `__invoke()` method's return types, including actual return class extraction.
- Improve dependency resolution with comprehensive initializer discovery strategies.
This commit is contained in:
2025-11-03 21:56:27 +01:00
parent ca30385f97
commit d0c36b9245

View File

@@ -554,6 +554,20 @@ final readonly class InitializerDependencyAnalyzer
if ($returnType !== null && class_exists($returnType)) {
return $returnType;
}
// Fallback: Wenn Closure keinen Return-Type hat, suche Initializer-Klasse
// und analysiere deren __invoke() Return-Type
$initializerClass = $this->findInitializerForInterface($interface);
if ($initializerClass !== null && class_exists($initializerClass)) {
$invokeReturnType = $this->getInitializerInvokeReturnType($initializerClass);
if ($invokeReturnType !== null) {
// getInitializerInvokeReturnType() extrahiert bereits die tatsächliche Klasse
// aus dem Code (z.B. "return new Engine(...)"), also können wir sie direkt verwenden
if (class_exists($invokeReturnType)) {
return $invokeReturnType;
}
}
}
}
}
}
@@ -625,5 +639,172 @@ final readonly class InitializerDependencyAnalyzer
return null;
}
}
/**
* Finde Initializer-Klasse für ein Interface (basierend auf Namenskonvention)
*/
private function findInitializerForInterface(string $interface): ?string
{
// Namenskonvention:
// - ComponentRegistryInterface -> ComponentRegistryInitializer
// - TemplateRenderer -> TemplateRendererInitializer (Interface ohne "Interface" Suffix)
$interfaceName = basename(str_replace('\\', '/', $interface));
// Wenn Interface kein "Interface" Suffix hat, füge einfach "Initializer" hinzu
if (str_ends_with($interfaceName, 'Interface')) {
$suggestedName = str_replace('Interface', '', $interfaceName) . 'Initializer';
} else {
// TemplateRenderer -> TemplateRendererInitializer
$suggestedName = $interfaceName . 'Initializer';
}
$namespace = substr($interface, 0, strrpos($interface, '\\'));
// Strategie 1: Suche im gleichen Namespace (falls Interface nicht in Contracts ist)
$suggestedClass = $namespace . '\\' . $suggestedName;
if (class_exists($suggestedClass)) {
return $suggestedClass;
}
// Strategie 2: Entferne "Contracts" aus dem Namespace
if (str_ends_with($namespace, '\\Contracts')) {
$parentNamespace = substr($namespace, 0, -10); // Entferne '\Contracts'
$suggestedClass = $parentNamespace . '\\' . $suggestedName;
if (class_exists($suggestedClass)) {
return $suggestedClass;
}
}
// Strategie 3: Suche im übergeordneten Namespace (einen Level höher)
if (strrpos($namespace, '\\') !== false) {
$parentNamespace = substr($namespace, 0, strrpos($namespace, '\\'));
$suggestedClass = $parentNamespace . '\\' . $suggestedName;
if (class_exists($suggestedClass)) {
return $suggestedClass;
}
}
// Strategie 4: Suche mit vollständigem App\Framework\ Präfix
$interfaceParts = explode('\\', $interface);
if (count($interfaceParts) >= 3 && $interfaceParts[0] === 'App' && $interfaceParts[1] === 'Framework') {
// Entferne 'Contracts' falls vorhanden
$filteredParts = array_filter($interfaceParts, fn($part) => $part !== 'Contracts');
$filteredParts = array_values($filteredParts);
// Baue Namespace ohne Interface-Name, aber mit Initializer-Name
$baseNamespace = implode('\\', array_slice($filteredParts, 0, -1));
$suggestedClass = $baseNamespace . '\\' . $suggestedName;
if (class_exists($suggestedClass)) {
return $suggestedClass;
}
}
// Strategie 5: Fallback - direkter App\Framework\ Präfix
$suggestedClassShort = 'App\\Framework\\' . $suggestedName;
if (class_exists($suggestedClassShort)) {
return $suggestedClassShort;
}
return null;
}
/**
* Analysiere Return-Type der __invoke() Methode eines Initializers
*
* Versucht sowohl den deklarierten Return-Type als auch die tatsächlich zurückgegebene Klasse
* aus dem Code zu extrahieren (z.B. "return new Engine(...)")
*/
private function getInitializerInvokeReturnType(string $initializerClass): ?string
{
try {
if (!class_exists($initializerClass)) {
return null;
}
$reflection = new \ReflectionClass($initializerClass);
// Versuche __invoke() Methode zu finden
if (!$reflection->hasMethod('__invoke')) {
return null;
}
$invokeMethod = $reflection->getMethod('__invoke');
// 1. Versuche deklarierten Return-Type
$returnType = $invokeMethod->getReturnType();
if ($returnType instanceof \ReflectionNamedType && !$returnType->isBuiltin()) {
$declaredReturnType = $returnType->getName();
// Wenn Return-Type ein Interface ist, versuche die tatsächliche Klasse aus dem Code zu finden
if (interface_exists($declaredReturnType) || (class_exists($declaredReturnType) && (new \ReflectionClass($declaredReturnType))->isAbstract())) {
$actualClass = $this->extractActualReturnClassFromInvoke($invokeMethod);
if ($actualClass !== null) {
return $actualClass;
}
}
return $declaredReturnType;
}
// 2. Versuche tatsächliche Klasse aus dem Code zu extrahieren
$actualClass = $this->extractActualReturnClassFromInvoke($invokeMethod);
if ($actualClass !== null) {
return $actualClass;
}
return null;
} catch (\Throwable) {
return null;
}
}
/**
* Extrahiere die tatsächlich zurückgegebene Klasse aus dem __invoke() Code
* (z.B. "return new Engine(...)" → "Engine")
*/
private function extractActualReturnClassFromInvoke(\ReflectionMethod $invokeMethod): ?string
{
try {
$fileName = $invokeMethod->getFileName();
if ($fileName === false || !file_exists($fileName)) {
return null;
}
$fileContent = file_get_contents($fileName);
if ($fileContent === false) {
return null;
}
$startLine = $invokeMethod->getStartLine();
$endLine = $invokeMethod->getEndLine();
if ($startLine === false || $endLine === false) {
return null;
}
// Extrahiere nur den __invoke() Method-Code
$lines = explode("\n", $fileContent);
$methodLines = array_slice($lines, $startLine - 1, $endLine - $startLine + 1);
$methodCode = implode("\n", $methodLines);
// Suche nach "return new ClassName(" oder "return new ClassName;"
$pattern = '/return\s+new\s+([A-Z][A-Za-z0-9\\\\]+)\s*[\(;]/';
if (preg_match($pattern, $methodCode, $matches)) {
$className = $matches[1];
// Resolve vollständigen Namespace
$fullClassName = $this->resolveClassNameFromMethod($className, $fileContent, $invokeMethod);
if ($fullClassName !== null && class_exists($fullClassName)) {
return $fullClassName;
} elseif (class_exists($className)) {
return $className;
}
}
return null;
} catch (\Throwable) {
return null;
}
}
}