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:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user