refactor(di): add analysis components for dependency parsing and resolution

- Introduce `CodeParser` to extract dependencies from `container->get()` calls and `return new` statements.
- Add `DependencyPathAnalyzer` for recursive analysis of dependency paths with cycle detection.
- Implement `InitializerFinder` to locate initializers based on naming conventions.
- Include `InterfaceResolver` to determine interface implementations using introspection and initializers.
- Add `NamespaceResolver` for resolving class names from use statements and namespaces.
- Introduce `ReturnTypeAnalyzer` for method and closure return type analysis.
This commit is contained in:
2025-11-03 22:38:06 +01:00
parent 703d9b04fe
commit a93a086ee4
12 changed files with 1120 additions and 838 deletions

View File

@@ -0,0 +1,254 @@
<?php
declare(strict_types=1);
namespace App\Framework\DI\Analysis;
use App\Framework\Core\ValueObjects\ClassName;
/**
* Analysiert PHP-Code um Dependencies zu extrahieren
*
* Findet container->get() Aufrufe und return new Statements
*/
final readonly class CodeParser
{
/**
* Parse container->get() Aufrufe aus einer Method
*
* @return array{dependencies: array<ClassName>, hasCalls: bool}
*/
public function parseContainerGetCalls(\ReflectionMethod $method): array
{
try {
$fileName = $method->getFileName();
if ($fileName === false || !file_exists($fileName)) {
return ['dependencies' => [], 'hasCalls' => false];
}
$fileContent = file_get_contents($fileName);
if ($fileContent === false) {
return ['dependencies' => [], 'hasCalls' => false];
}
$startLine = $method->getStartLine();
$endLine = $method->getEndLine();
if ($startLine === false || $endLine === false) {
return ['dependencies' => [], 'hasCalls' => false];
}
// Extrahiere nur die Method
$lines = explode("\n", $fileContent);
$methodLines = array_slice($lines, $startLine - 1, $endLine - $startLine + 1);
$methodCode = implode("\n", $methodLines);
// Finde container->get(...) Aufrufe
// Pattern: $container->get(ClassName::class) oder $this->container->get(ClassName::class)
// Unterstützt auch ::class Notation
$pattern = '/(?:\$container|\$this->container)->get\(([^,\)]+::class|[^,\)]+)\)/';
preg_match_all($pattern, $methodCode, $matches);
$dependencies = [];
if (!empty($matches[1])) {
foreach ($matches[1] as $match) {
$match = trim($match);
// Entferne ::class falls vorhanden
$className = str_replace('::class', '', $match);
$className = trim($className, '\'"');
// Prüfe ob es ein gültiger Klassenname ist (beginnt mit \ oder Namespace)
if (preg_match('/^\\\\?[A-Z][A-Za-z0-9\\\\]*$/', $className)) {
try {
// Normalisiere (füge \ am Anfang hinzu wenn nicht vorhanden)
if (!str_starts_with($className, '\\')) {
// Versuche vollständigen Namespace zu finden (z.B. über use statements)
// Note: NamespaceResolver wird später injiziert
$fullClassName = $this->resolveClassNameFromMethod($className, $fileContent, $method);
if ($fullClassName !== null) {
$dependencies[] = ClassName::create($fullClassName);
} else {
// Fallback: Verwende wie angegeben
$dependencies[] = ClassName::create($className);
}
} else {
$dependencies[] = ClassName::create($className);
}
} catch (\InvalidArgumentException) {
// Ungültiger Klassenname - überspringe
continue;
}
}
}
// Entferne Duplikate
$dependencies = $this->uniqueClassNames($dependencies);
}
return [
'dependencies' => $dependencies,
'hasCalls' => !empty($matches[0]),
];
} catch (\Throwable) {
return ['dependencies' => [], 'hasCalls' => false];
}
}
/**
* Extrahiere die tatsächlich zurückgegebene Klasse aus dem Code
* (z.B. "return new Engine(...)" → ClassName)
*/
public function extractReturnClass(\ReflectionMethod $method): ?ClassName
{
try {
$fileName = $method->getFileName();
if ($fileName === false || !file_exists($fileName)) {
return null;
}
$fileContent = file_get_contents($fileName);
if ($fileContent === false) {
return null;
}
$startLine = $method->getStartLine();
$endLine = $method->getEndLine();
if ($startLine === false || $endLine === false) {
return null;
}
// Extrahiere nur den 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 muss auch Named Parameters unterstützen: "return new Engine(...)"
$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, $method);
if ($fullClassName !== null) {
try {
return ClassName::create($fullClassName);
} catch (\InvalidArgumentException) {
return null;
}
}
// Fallback: Versuche direkt
try {
return ClassName::create($className);
} catch (\InvalidArgumentException) {
return null;
}
}
// Fallback: Suche auch nach Named Parameters Syntax: "return new Engine(...)"
// mit Named Parameters: loader: ..., processor: ...
$pattern2 = '/return\s+new\s+([A-Z][A-Za-z0-9\\\\]+)\s*\(/';
if (preg_match($pattern2, $methodCode, $matches)) {
$className = $matches[1];
// Resolve vollständigen Namespace
$fullClassName = $this->resolveClassNameFromMethod($className, $fileContent, $method);
if ($fullClassName !== null) {
try {
return ClassName::create($fullClassName);
} catch (\InvalidArgumentException) {
return null;
}
}
// Fallback: Versuche direkt
try {
return ClassName::create($className);
} catch (\InvalidArgumentException) {
return null;
}
}
return null;
} catch (\Throwable) {
return null;
}
}
/**
* Versuche vollständigen Klassenname aus use statements zu resolven
*
* @param string $shortName Kurzer Klassenname (z.B. "Engine")
* @param string $fileContent Vollständiger Dateiinhalt
* @param \ReflectionMethod $method Method-Kontext
* @return string|null Vollständiger Klassenname oder null
*/
private function resolveClassNameFromMethod(string $shortName, string $fileContent, \ReflectionMethod $method): ?string
{
try {
// Finde use statements im File
preg_match_all('/^use\s+([^;]+);/m', $fileContent, $useMatches);
// Suche nach exakter Übereinstimmung oder Alias
foreach ($useMatches[1] as $useStatement) {
$useStatement = trim($useStatement);
// Prüfe auf Alias (use Full\Class\Name as Alias)
if (preg_match('/^(.+?)\s+as\s+(\w+)$/', $useStatement, $aliasMatch)) {
$fullClassName = $aliasMatch[1];
$alias = $aliasMatch[2];
if ($alias === $shortName) {
return $fullClassName;
}
} else {
// Prüfe ob letzter Teil übereinstimmt
$parts = explode('\\', $useStatement);
$lastPart = end($parts);
if ($lastPart === $shortName) {
return $useStatement;
}
}
}
// Fallback: Prüfe ob Klasse im gleichen Namespace ist
// Extrahiere Namespace aus der Datei
if (preg_match('/^namespace\s+([^;]+);/m', $fileContent, $namespaceMatch)) {
$namespace = trim($namespaceMatch[1]);
$fullClassName = $namespace . '\\' . $shortName;
if (class_exists($fullClassName)) {
return $fullClassName;
}
}
return null;
} catch (\Throwable) {
return null;
}
}
/**
* Entferne Duplikate aus ClassName Array
*
* @param array<ClassName> $classNames
* @return array<ClassName>
*/
private function uniqueClassNames(array $classNames): array
{
$seen = [];
$unique = [];
foreach ($classNames as $className) {
$key = $className->toString();
if (!isset($seen[$key])) {
$seen[$key] = true;
$unique[] = $className;
}
}
return array_values($unique);
}
}

View File

@@ -0,0 +1,202 @@
<?php
declare(strict_types=1);
namespace App\Framework\DI\Analysis;
use App\Framework\Core\ValueObjects\ClassName;
use App\Framework\DI\Container;
/**
* Rekursive Analyse von Dependency-Pfaden
*/
final readonly class DependencyPathAnalyzer
{
private const MAX_RECURSION_DEPTH = 4;
public function __construct(
private InterfaceResolver $interfaceResolver,
private CodeParser $codeParser
) {
}
/**
* Finde rekursiv den Pfad von einer Dependency bis zum Interface
*
* @param ClassName $dependencyClass Die Dependency-Klasse
* @param ClassName $targetInterface Das gesuchte Interface
* @param array<ClassName> $visited Bereits besuchte Klassen (für Cycle-Detection)
* @param array<ClassName> $currentPath Aktueller Pfad
* @param int $depth Aktuelle Rekursionstiefe
* @return array<ClassName>|null Vollständiger Pfad [Dep1, Dep2, ..., Interface] oder null
*/
public function findPathToInterface(
ClassName $dependencyClass,
ClassName $targetInterface,
array $visited = [],
array $currentPath = [],
int $depth = 0
): ?array {
// Max. Rekursionstiefe erreicht
if ($depth >= self::MAX_RECURSION_DEPTH) {
return null;
}
// Cycle-Detection: Vermeide Endlosschleifen
foreach ($visited as $visitedClass) {
if ($visitedClass->equals($dependencyClass)) {
return null;
}
}
// Prüfe ob diese Klasse direkt das Interface benötigt
if ($this->dependencyNeedsInterface($dependencyClass, $targetInterface)) {
return array_merge($currentPath, [$dependencyClass, $targetInterface]);
}
// Rekursiv: Analysiere Dependencies dieser Klasse
$dependencies = $this->getClassDependencies($dependencyClass);
if (empty($dependencies)) {
return null;
}
$newVisited = array_merge($visited, [$dependencyClass]);
$newPath = array_merge($currentPath, [$dependencyClass]);
foreach ($dependencies as $subDependency) {
// Überspringe Container selbst (würde alle Dependencies auflisten)
if ($subDependency->toString() === Container::class || $subDependency->toString() === 'App\Framework\DI\Container') {
continue;
}
$path = $this->findPathToInterface(
$subDependency,
$targetInterface,
$newVisited,
$newPath,
$depth + 1
);
if ($path !== null) {
return $path;
}
}
return null;
}
/**
* Hole Dependencies einer Klasse (Constructor-Parameter)
*
* @return array<ClassName>
*/
public function getClassDependencies(ClassName $className): array
{
try {
if (!$className->exists()) {
return [];
}
$reflection = new \ReflectionClass($className->toString());
// Wenn es ein Interface ist, versuche die Implementierung zu finden
if ($reflection->isInterface()) {
$implClass = $this->interfaceResolver->findImplementation($className);
if ($implClass !== null && $implClass->exists()) {
$reflection = new \ReflectionClass($implClass->toString());
} else {
// Kann keine Dependencies für Interfaces ohne Implementierung finden
return [];
}
}
$constructor = $reflection->getConstructor();
if ($constructor === null) {
return [];
}
$dependencies = [];
foreach ($constructor->getParameters() as $parameter) {
$type = $parameter->getType();
if ($type instanceof \ReflectionNamedType && !$type->isBuiltin()) {
try {
$depClassName = ClassName::create($type->getName());
$dependencies[] = $depClassName;
} catch (\InvalidArgumentException) {
// Ungültiger Klassenname - überspringe
continue;
}
}
}
return $dependencies;
} catch (\Throwable) {
return [];
}
}
/**
* Prüfe ob eine Klasse ein Interface benötigt (über Reflection)
*/
private function dependencyNeedsInterface(ClassName $dependencyClass, ClassName $interface): bool
{
try {
if (!$dependencyClass->exists()) {
return false;
}
$reflection = new \ReflectionClass($dependencyClass->toString());
$constructor = $reflection->getConstructor();
if ($constructor === null) {
return false;
}
// Prüfe alle Constructor-Parameter
foreach ($constructor->getParameters() as $parameter) {
$type = $parameter->getType();
if ($type === null) {
continue;
}
// Direkter NamedType
if ($type instanceof \ReflectionNamedType && !$type->isBuiltin()) {
if ($type->getName() === $interface->toString()) {
return true;
}
}
// Union Types (PHP 8.0+)
if ($type instanceof \ReflectionUnionType) {
foreach ($type->getTypes() as $subType) {
if ($subType instanceof \ReflectionNamedType && !$subType->isBuiltin()) {
if ($subType->getName() === $interface->toString()) {
return true;
}
}
}
}
// Intersection Types (PHP 8.1+)
if ($type instanceof \ReflectionIntersectionType) {
foreach ($type->getTypes() as $subType) {
if ($subType instanceof \ReflectionNamedType && !$subType->isBuiltin()) {
if ($subType->getName() === $interface->toString()) {
return true;
}
}
}
}
}
return false;
} catch (\Throwable) {
return false;
}
}
}

View File

@@ -0,0 +1,125 @@
<?php
declare(strict_types=1);
namespace App\Framework\DI\Analysis;
use App\Framework\Core\ValueObjects\ClassName;
use App\Framework\Core\ValueObjects\PhpNamespace;
/**
* Findet Initializer-Klassen basierend auf Namenskonventionen
*/
final readonly class InitializerFinder
{
/**
* Findet Initializer für ein Interface
*
* Namenskonvention: ComponentRegistryInterface → ComponentRegistryInitializer
*
* @param ClassName $interface Interface-Name
* @return ClassName|null Initializer-Klasse oder null
*/
public function findForInterface(ClassName $interface): ?ClassName
{
$interfaceName = $interface->getShortName();
// 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 = $interface->getNamespaceObject();
// Strategie 1: Suche im gleichen Namespace (falls Interface nicht in Contracts ist)
$suggestedClass = ClassName::fromNamespace($namespace, $suggestedName);
if ($suggestedClass->exists()) {
return $suggestedClass;
}
// Strategie 2: Entferne "Contracts" aus dem Namespace
if (str_ends_with($namespace->toString(), '\\Contracts')) {
$parentNamespaceStr = substr($namespace->toString(), 0, -10); // Entferne '\Contracts'
$parentNamespace = PhpNamespace::fromString($parentNamespaceStr);
$suggestedClass = ClassName::fromNamespace($parentNamespace, $suggestedName);
if ($suggestedClass->exists()) {
return $suggestedClass;
}
}
// Strategie 3: Suche im übergeordneten Namespace (einen Level höher)
$parentNamespace = $namespace->parent();
if ($parentNamespace !== null) {
$suggestedClass = ClassName::fromNamespace($parentNamespace, $suggestedName);
if ($suggestedClass->exists()) {
return $suggestedClass;
}
}
// Strategie 4: Suche mit vollständigem App\Framework\ Präfix
$interfaceParts = explode('\\', $interface->toString());
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));
$baseNamespaceObj = PhpNamespace::fromString($baseNamespace);
$suggestedClass = ClassName::fromNamespace($baseNamespaceObj, $suggestedName);
if ($suggestedClass->exists()) {
return $suggestedClass;
}
}
// Strategie 5: Fallback - direkter App\Framework\ Präfix
$suggestedClassShort = ClassName::create('App\\Framework\\' . $suggestedName);
if ($suggestedClassShort->exists()) {
return $suggestedClassShort;
}
return null;
}
/**
* Findet Initializer für eine Klasse
*
* Namenskonvention: TemplateRenderer → TemplateRendererInitializer
*
* @param ClassName $class Klassen-Name
* @return ClassName|null Initializer-Klasse oder null
*/
public function findForClass(ClassName $class): ?ClassName
{
$className = $class->getShortName();
$suggestedName = $className . 'Initializer';
$namespace = $class->getNamespaceObject();
// Strategie 1: Suche im gleichen Namespace
$suggestedClass = ClassName::fromNamespace($namespace, $suggestedName);
if ($suggestedClass->exists()) {
return $suggestedClass;
}
// Strategie 2: Suche im übergeordneten Namespace
$parentNamespace = $namespace->parent();
if ($parentNamespace !== null) {
$suggestedClass = ClassName::fromNamespace($parentNamespace, $suggestedName);
if ($suggestedClass->exists()) {
return $suggestedClass;
}
}
// Strategie 3: Fallback - direkter App\Framework\ Präfix
$suggestedClassShort = ClassName::create('App\\Framework\\' . $suggestedName);
if ($suggestedClassShort->exists()) {
return $suggestedClassShort;
}
return null;
}
}

View File

@@ -0,0 +1,122 @@
<?php
declare(strict_types=1);
namespace App\Framework\DI\Analysis;
use App\Framework\Core\ValueObjects\ClassName;
use App\Framework\DI\ContainerIntrospector;
/**
* Findet Implementierungen für Interfaces
*/
final readonly class InterfaceResolver
{
public function __construct(
private ?ContainerIntrospector $containerIntrospector,
private InitializerFinder $initializerFinder,
private ReturnTypeAnalyzer $returnTypeAnalyzer
) {
}
/**
* Findet Implementierung für ein Interface
*
* Prüft zuerst Container-Bindings, dann Initializer, dann Namenskonventionen
*/
public function findImplementation(ClassName $interface): ?ClassName
{
// 1. Versuche über Container-Bindings (falls ContainerIntrospector verfügbar)
if ($this->containerIntrospector !== null) {
try {
$binding = $this->containerIntrospector->getBinding($interface->toString());
if ($binding !== null) {
$impl = $this->resolveFromBinding($binding, $interface);
if ($impl !== null) {
return $impl;
}
}
} catch (\Throwable) {
// Container-Zugriff fehlgeschlagen - ignoriere und versuche Fallback
}
}
// 2. Versuche Namenskonvention: Interface -> DefaultInterfaceName
$interfaceName = $interface->getShortName();
$namespace = $interface->getNamespaceObject();
// Versuche "Default" + InterfaceName ohne "Interface"
$implName = str_replace('Interface', '', $interfaceName);
$defaultImplName = 'Default' . $implName;
// Suche im gleichen Namespace
$defaultImpl = ClassName::fromNamespace($namespace, $defaultImplName);
if ($defaultImpl->exists()) {
return $defaultImpl;
}
// Suche im übergeordneten Namespace
$parentNamespace = $namespace->parent();
if ($parentNamespace !== null) {
$defaultImpl = ClassName::fromNamespace($parentNamespace, $defaultImplName);
if ($defaultImpl->exists()) {
return $defaultImpl;
}
}
return null;
}
/**
* Resolved Implementierung aus einem Container-Binding
*
* @param callable|string|object $binding Binding vom Container
* @param ClassName $interface Interface das aufgelöst werden soll
* @return ClassName|null
*/
private function resolveFromBinding(callable|string|object $binding, ClassName $interface): ?ClassName
{
// Wenn Binding ein String ist (Klassenname), verwende diesen
if (is_string($binding)) {
try {
$className = ClassName::create($binding);
if ($className->exists()) {
return $className;
}
} catch (\InvalidArgumentException) {
return null;
}
}
// Wenn Binding ein Objekt ist, verwende dessen Klasse
// ABER: Überspringe Closure, da wir den Return-Type analysieren müssen
if (is_object($binding) && !($binding instanceof \Closure)) {
try {
return ClassName::fromObject($binding);
} catch (\InvalidArgumentException) {
return null;
}
}
// Wenn Binding ein Closure ist, versuche Return-Type zu analysieren
if ($binding instanceof \Closure) {
$returnType = $this->returnTypeAnalyzer->getClosureReturnType($binding);
if ($returnType !== null && $returnType->exists()) {
return $returnType;
}
// Fallback: Wenn Closure keinen Return-Type hat, suche Initializer-Klasse
// und analysiere deren __invoke() Return-Type
$initializerClass = $this->initializerFinder->findForInterface($interface);
if ($initializerClass !== null && $initializerClass->exists()) {
$invokeReturnType = $this->returnTypeAnalyzer->getInitializerReturnType($initializerClass);
if ($invokeReturnType !== null && $invokeReturnType->exists()) {
return $invokeReturnType;
}
}
}
return null;
}
}

View File

@@ -0,0 +1,96 @@
<?php
declare(strict_types=1);
namespace App\Framework\DI\Analysis;
use App\Framework\Core\ValueObjects\ClassName;
use App\Framework\Core\ValueObjects\PhpNamespace;
/**
* Resolved Klassen-Namen aus use Statements und Namespace
*/
final readonly class NamespaceResolver
{
/**
* Resolved einen Klassenname über use statements und Namespace
*
* @param string $shortName Kurzer Klassenname (z.B. "Engine")
* @param \ReflectionClass $context Klasse die als Kontext dient
* @return ClassName|null Vollständiger Klassenname oder null
*/
public function resolve(string $shortName, \ReflectionClass $context): ?ClassName
{
try {
$fileName = $context->getFileName();
if ($fileName === false || !file_exists($fileName)) {
return null;
}
$fileContent = file_get_contents($fileName);
if ($fileContent === false) {
return null;
}
// 1. Versuche über use statements zu finden
$resolved = $this->resolveFromUseStatements($shortName, $fileContent);
if ($resolved !== null) {
return $resolved;
}
// 2. Fallback: Prüfe ob Klasse im gleichen Namespace ist
$namespace = PhpNamespace::fromClass($context->getName());
if (!$namespace->isGlobal()) {
$fullClassName = $namespace->toString() . '\\' . $shortName;
if (class_exists($fullClassName)) {
return ClassName::create($fullClassName);
}
}
return null;
} catch (\Throwable) {
return null;
}
}
/**
* Resolved Klassenname aus use statements
*
* @param string $shortName Kurzer Klassenname
* @param string $fileContent Vollständiger Dateiinhalt
* @return ClassName|null
*/
private function resolveFromUseStatements(string $shortName, string $fileContent): ?ClassName
{
try {
// Finde use statements im File
preg_match_all('/^use\s+([^;]+);/m', $fileContent, $useMatches);
// Suche nach exakter Übereinstimmung oder Alias
foreach ($useMatches[1] as $useStatement) {
$useStatement = trim($useStatement);
// Prüfe auf Alias (use Full\Class\Name as Alias)
if (preg_match('/^(.+?)\s+as\s+(\w+)$/', $useStatement, $aliasMatch)) {
$fullClassName = $aliasMatch[1];
$alias = $aliasMatch[2];
if ($alias === $shortName) {
return ClassName::create($fullClassName);
}
} else {
// Prüfe ob letzter Teil übereinstimmt
$parts = explode('\\', $useStatement);
$lastPart = end($parts);
if ($lastPart === $shortName) {
return ClassName::create($useStatement);
}
}
}
return null;
} catch (\Throwable) {
return null;
}
}
}

View File

@@ -0,0 +1,110 @@
<?php
declare(strict_types=1);
namespace App\Framework\DI\Analysis;
use App\Framework\Core\ValueObjects\ClassName;
/**
* Analysiert Return-Types von Closures und Methoden
*/
final readonly class ReturnTypeAnalyzer
{
public function __construct(
private CodeParser $codeParser,
private InitializerFinder $initializerFinder
) {
}
/**
* Analysiert Return-Type eines Closures
*/
public function getClosureReturnType(\Closure $closure): ?ClassName
{
try {
$reflection = new \ReflectionFunction($closure);
$returnType = $reflection->getReturnType();
if ($returnType instanceof \ReflectionNamedType && !$returnType->isBuiltin()) {
return ClassName::create($returnType->getName());
}
return null;
} catch (\Throwable) {
return null;
}
}
/**
* Analysiert Return-Type einer Methode
*/
public function getMethodReturnType(\ReflectionMethod $method): ?ClassName
{
try {
$returnType = $method->getReturnType();
if ($returnType instanceof \ReflectionNamedType && !$returnType->isBuiltin()) {
return ClassName::create($returnType->getName());
}
return null;
} catch (\Throwable) {
return null;
}
}
/**
* Analysiert Initializer __invoke() Return-Type
*
* Versucht sowohl den deklarierten Return-Type als auch die tatsächlich zurückgegebene Klasse
* aus dem Code zu extrahieren (z.B. "return new Engine(...)")
*/
public function getInitializerReturnType(ClassName $initializerClass): ?ClassName
{
try {
if (!$initializerClass->exists()) {
return null;
}
$reflection = new \ReflectionClass($initializerClass->toString());
// 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 = ClassName::create($returnType->getName());
// Wenn Return-Type ein Interface ist, versuche die tatsächliche Klasse aus dem Code zu finden
if ($declaredReturnType->exists()) {
$reflectionClass = new \ReflectionClass($declaredReturnType->toString());
if ($reflectionClass->isInterface() || $reflectionClass->isAbstract()) {
$actualClass = $this->codeParser->extractReturnClass($invokeMethod);
if ($actualClass !== null) {
return $actualClass;
}
}
}
return $declaredReturnType;
}
// 2. Versuche tatsächliche Klasse aus dem Code zu extrahieren
$actualClass = $this->codeParser->extractReturnClass($invokeMethod);
if ($actualClass !== null) {
return $actualClass;
}
return null;
} catch (\Throwable) {
return null;
}
}
}

View File

@@ -4,6 +4,13 @@ declare(strict_types=1);
namespace App\Framework\DI; namespace App\Framework\DI;
use App\Framework\Core\ValueObjects\ClassName;
use App\Framework\DI\Analysis\CodeParser;
use App\Framework\DI\Analysis\DependencyPathAnalyzer;
use App\Framework\DI\Analysis\InitializerFinder;
use App\Framework\DI\Analysis\InterfaceResolver;
use App\Framework\DI\Analysis\ReturnTypeAnalyzer;
/** /**
* Analysiert Dependencies von Initializern * Analysiert Dependencies von Initializern
* *
@@ -13,12 +20,37 @@ namespace App\Framework\DI;
*/ */
final readonly class InitializerDependencyAnalyzer final readonly class InitializerDependencyAnalyzer
{ {
private const MAX_RECURSION_DEPTH = 4; private CodeParser $codeParser;
private InitializerFinder $initializerFinder;
private ReturnTypeAnalyzer $returnTypeAnalyzer;
private ?InterfaceResolver $interfaceResolver;
private ?DependencyPathAnalyzer $dependencyPathAnalyzer;
public function __construct( public function __construct(
private ?Container $container = null private ?Container $container = null
) { ) {
$this->codeParser = new CodeParser();
$this->initializerFinder = new InitializerFinder();
$this->returnTypeAnalyzer = new ReturnTypeAnalyzer($this->codeParser, $this->initializerFinder);
// Initialisiere ContainerIntrospector wenn Container verfügbar
$containerIntrospector = null;
if ($this->container !== null && property_exists($this->container, 'introspector')) {
$containerIntrospector = $this->container->introspector;
} }
$this->interfaceResolver = new InterfaceResolver(
$containerIntrospector,
$this->initializerFinder,
$this->returnTypeAnalyzer
);
$this->dependencyPathAnalyzer = new DependencyPathAnalyzer(
$this->interfaceResolver,
$this->codeParser
);
}
/** /**
* Analysiere Dependencies eines Initializers * Analysiere Dependencies eines Initializers
* *
@@ -92,8 +124,12 @@ final readonly class InitializerDependencyAnalyzer
if ($hasContainerAvailable) { if ($hasContainerAvailable) {
// Parse Code der __invoke() Methode um container->get() Aufrufe zu finden // Parse Code der __invoke() Methode um container->get() Aufrufe zu finden
$parsedDeps = $this->parseContainerGetCalls($invokeMethod); $parsedDeps = $this->codeParser->parseContainerGetCalls($invokeMethod);
$containerGetDeps = $parsedDeps['dependencies']; // Konvertiere ClassName[] zurück zu string[]
$containerGetDeps = array_map(
fn(ClassName $className) => $className->toString(),
$parsedDeps['dependencies']
);
$hasContainerGetCalls = $parsedDeps['hasCalls']; $hasContainerGetCalls = $parsedDeps['hasCalls'];
} }
} catch (\ReflectionException) { } catch (\ReflectionException) {
@@ -117,129 +153,6 @@ final readonly class InitializerDependencyAnalyzer
} }
} }
/**
* Parse container->get() Aufrufe aus einer Method
*
* @return array{dependencies: array<string>, hasCalls: bool}
*/
private function parseContainerGetCalls(\ReflectionMethod $method): array
{
try {
$fileName = $method->getFileName();
if ($fileName === false || !file_exists($fileName)) {
return ['dependencies' => [], 'hasCalls' => false];
}
$fileContent = file_get_contents($fileName);
if ($fileContent === false) {
return ['dependencies' => [], 'hasCalls' => false];
}
$startLine = $method->getStartLine();
$endLine = $method->getEndLine();
if ($startLine === false || $endLine === false) {
return ['dependencies' => [], 'hasCalls' => false];
}
// Extrahiere nur die Method
$lines = explode("\n", $fileContent);
$methodLines = array_slice($lines, $startLine - 1, $endLine - $startLine + 1);
$methodCode = implode("\n", $methodLines);
// Finde container->get(...) Aufrufe
// Pattern: $container->get(ClassName::class) oder $this->container->get(ClassName::class)
// Unterstützt auch ::class Notation
$pattern = '/(?:\$container|\$this->container)->get\(([^,\)]+::class|[^,\)]+)\)/';
preg_match_all($pattern, $methodCode, $matches);
$dependencies = [];
if (!empty($matches[1])) {
foreach ($matches[1] as $match) {
$match = trim($match);
// Entferne ::class falls vorhanden
$className = str_replace('::class', '', $match);
$className = trim($className, '\'"');
// Prüfe ob es ein gültiger Klassenname ist (beginnt mit \ oder Namespace)
if (preg_match('/^\\\\?[A-Z][A-Za-z0-9\\\\]*$/', $className)) {
// Normalisiere (füge \ am Anfang hinzu wenn nicht vorhanden)
if (!str_starts_with($className, '\\')) {
// Versuche vollständigen Namespace zu finden (z.B. über use statements)
$fullClassName = $this->resolveClassNameFromMethod($className, $fileContent, $method);
if ($fullClassName !== null) {
$dependencies[] = $fullClassName;
} else {
// Fallback: Verwende wie angegeben
$dependencies[] = $className;
}
} else {
$dependencies[] = $className;
}
}
}
// Entferne Duplikate
$dependencies = array_unique($dependencies);
$dependencies = array_values($dependencies);
}
return [
'dependencies' => $dependencies,
'hasCalls' => !empty($matches[0]),
];
} catch (\Throwable) {
return ['dependencies' => [], 'hasCalls' => false];
}
}
/**
* Versuche vollständigen Klassenname aus use statements zu resolven
*/
private function resolveClassNameFromMethod(string $shortName, string $fileContent, \ReflectionMethod $method): ?string
{
try {
// Finde use statements im File
preg_match_all('/^use\s+([^;]+);/m', $fileContent, $useMatches);
// Suche nach exakter Übereinstimmung oder Alias
foreach ($useMatches[1] as $useStatement) {
$useStatement = trim($useStatement);
// Prüfe auf Alias (use Full\Class\Name as Alias)
if (preg_match('/^(.+?)\s+as\s+(\w+)$/', $useStatement, $aliasMatch)) {
$fullClassName = $aliasMatch[1];
$alias = $aliasMatch[2];
if ($alias === $shortName) {
return $fullClassName;
}
} else {
// Prüfe ob letzter Teil übereinstimmt
$parts = explode('\\', $useStatement);
$lastPart = end($parts);
if ($lastPart === $shortName) {
return $useStatement;
}
}
}
// Fallback: Prüfe ob Klasse im gleichen Namespace ist
// Extrahiere Namespace aus der Datei
if (preg_match('/^namespace\s+([^;]+);/m', $fileContent, $namespaceMatch)) {
$namespace = trim($namespaceMatch[1]);
$fullClassName = $namespace . '\\' . $shortName;
if (class_exists($fullClassName)) {
return $fullClassName;
}
}
return null;
} catch (\Throwable) {
return null;
}
}
/** /**
* Finde rekursiv den Pfad von einer Dependency bis zum Interface * Finde rekursiv den Pfad von einer Dependency bis zum Interface
* *
@@ -257,576 +170,37 @@ final readonly class InitializerDependencyAnalyzer
array $currentPath = [], array $currentPath = [],
int $depth = 0 int $depth = 0
): ?array { ): ?array {
// Max. Rekursionstiefe erreicht if ($this->dependencyPathAnalyzer === null) {
if ($depth >= self::MAX_RECURSION_DEPTH) {
return null; return null;
} }
// Cycle-Detection: Vermeide Endlosschleifen try {
if (in_array($dependencyClass, $visited, true)) { $dependencyClassName = ClassName::create($dependencyClass);
return null; $targetInterfaceName = ClassName::create($targetInterface);
}
// Prüfe ob diese Klasse direkt das Interface benötigt // Konvertiere visited und currentPath zu ClassName[]
if ($this->dependencyNeedsInterface($dependencyClass, $targetInterface)) { $visitedClassNames = array_map(fn(string $class) => ClassName::create($class), $visited);
return array_merge($currentPath, [$dependencyClass, $targetInterface]); $currentPathClassNames = array_map(fn(string $class) => ClassName::create($class), $currentPath);
}
// Rekursiv: Analysiere Dependencies dieser Klasse $path = $this->dependencyPathAnalyzer->findPathToInterface(
$dependencies = $this->getClassDependencies($dependencyClass); $dependencyClassName,
$targetInterfaceName,
if (empty($dependencies)) { $visitedClassNames,
return null; $currentPathClassNames,
} $depth
$newVisited = array_merge($visited, [$dependencyClass]);
$newPath = array_merge($currentPath, [$dependencyClass]);
foreach ($dependencies as $subDependency) {
// Überspringe Container selbst (würde alle Dependencies auflisten)
if ($subDependency === Container::class || $subDependency === 'App\Framework\DI\Container') {
continue;
}
$path = $this->findDependencyPathToInterface(
$subDependency,
$targetInterface,
$newVisited,
$newPath,
$depth + 1
); );
if ($path !== null) { if ($path === null) {
return $path;
}
}
return null; return null;
} }
/** // Konvertiere ClassName[] zurück zu string[]
* Prüfe ob eine Klasse ein Interface benötigt (über Reflection) return array_map(fn(ClassName $className) => $className->toString(), $path);
*/ } catch (\InvalidArgumentException) {
private function dependencyNeedsInterface(string $dependencyClass, string $interface): bool // Ungültiger Klassenname
{
try {
if (!class_exists($dependencyClass)) {
return false;
}
$reflection = new \ReflectionClass($dependencyClass);
$constructor = $reflection->getConstructor();
if ($constructor === null) {
return false;
}
// Prüfe alle Constructor-Parameter
foreach ($constructor->getParameters() as $parameter) {
$type = $parameter->getType();
if ($type === null) {
continue;
}
// Direkter NamedType
if ($type instanceof \ReflectionNamedType && !$type->isBuiltin()) {
if ($type->getName() === $interface) {
return true;
}
}
// Union Types (PHP 8.0+)
if ($type instanceof \ReflectionUnionType) {
foreach ($type->getTypes() as $subType) {
if ($subType instanceof \ReflectionNamedType && !$subType->isBuiltin()) {
if ($subType->getName() === $interface) {
return true;
}
}
}
}
// Intersection Types (PHP 8.1+)
if ($type instanceof \ReflectionIntersectionType) {
foreach ($type->getTypes() as $subType) {
if ($subType instanceof \ReflectionNamedType && !$subType->isBuiltin()) {
if ($subType->getName() === $interface) {
return true;
}
}
}
}
}
return false;
} catch (\Throwable) {
return false;
}
}
/**
* Hole Dependencies einer Klasse (Constructor-Parameter + Array-Elemente)
*
* @return array<string>
*/
private function getClassDependencies(string $className): array
{
try {
if (!class_exists($className) && !interface_exists($className)) {
return [];
}
$reflection = new \ReflectionClass($className);
// Wenn es ein Interface ist, versuche die Implementierung zu finden
if ($reflection->isInterface()) {
// Suche nach bekannten Implementierungen (basierend auf Namenskonvention)
$implClass = $this->findInterfaceImplementation($className);
if ($implClass !== null && class_exists($implClass)) {
$reflection = new \ReflectionClass($implClass);
} else {
// Kann keine Dependencies für Interfaces ohne Implementierung finden
return [];
}
}
$constructor = $reflection->getConstructor();
if ($constructor === null) {
return [];
}
$dependencies = [];
foreach ($constructor->getParameters() as $parameter) {
$type = $parameter->getType();
if ($type instanceof \ReflectionNamedType && !$type->isBuiltin()) {
$dependencies[] = $type->getName();
}
}
// Zusätzlich: Prüfe ob Parameter Arrays sind, die Klassen-Namen enthalten könnten
// (z.B. TemplateProcessor hat $astTransformers als Array von Klassen-Namen)
foreach ($constructor->getParameters() as $parameter) {
$type = $parameter->getType();
if ($type instanceof \ReflectionNamedType && $type->getName() === 'array') {
// Versuche Klassen-Namen aus dem Array zu extrahieren (via Code-Parsing)
$arrayDeps = $this->extractClassNamesFromArrayParameter($reflection, $parameter);
$dependencies = array_merge($dependencies, $arrayDeps);
}
}
return array_unique($dependencies);
} catch (\Throwable) {
return [];
}
}
/**
* Extrahiere Klassen-Namen aus Array-Parametern (z.B. $astTransformers = [XComponentTransformer::class])
*
* Sucht sowohl im Constructor als auch in Initializern (__invoke)
*/
private function extractClassNamesFromArrayParameter(\ReflectionClass $class, \ReflectionParameter $parameter): array
{
try {
$fileName = $class->getFileName();
if ($fileName === false || !file_exists($fileName)) {
return [];
}
$fileContent = file_get_contents($fileName);
if ($fileContent === false) {
return [];
}
$paramName = $parameter->getName();
$classes = [];
// 1. Suche im Constructor
$constructor = $class->getConstructor();
if ($constructor !== null) {
$startLine = $constructor->getStartLine();
$endLine = $constructor->getEndLine();
if ($startLine !== false && $endLine !== false) {
$lines = explode("\n", $fileContent);
$constructorLines = array_slice($lines, $startLine - 1, $endLine - $startLine + 1);
$constructorCode = implode("\n", $constructorLines);
// Finde Array-Initialisierungen für diesen Parameter
// Pattern: [ClassName::class, ...] oder ['ClassName', ...]
$pattern = '/\$' . preg_quote($paramName, '/') . '\s*=\s*\[(.*?)\]/s';
if (preg_match($pattern, $constructorCode, $matches)) {
$arrayClasses = $this->extractClassesFromArrayContent($matches[1], $fileContent, $constructor);
$classes = array_merge($classes, $arrayClasses);
}
}
}
// 2. Suche auch in __invoke() (für Initializer)
try {
$invokeMethod = $class->getMethod('__invoke');
$startLine = $invokeMethod->getStartLine();
$endLine = $invokeMethod->getEndLine();
if ($startLine !== false && $endLine !== false) {
$lines = explode("\n", $fileContent);
$invokeLines = array_slice($lines, $startLine - 1, $endLine - $startLine + 1);
$invokeCode = implode("\n", $invokeLines);
// Finde Array-Initialisierungen die an diesen Parameter übergeben werden
// Pattern: [$paramName] = [...] oder new Class([...])
// Oder: $paramName = [...]
$pattern = '/\$' . preg_quote($paramName, '/') . '\s*=\s*\[(.*?)\]/s';
if (preg_match($pattern, $invokeCode, $matches)) {
$arrayClasses = $this->extractClassesFromArrayContent($matches[1], $fileContent, $invokeMethod);
$classes = array_merge($classes, $arrayClasses);
}
// Suche auch nach direkten Array-Definitionen die an den Parameter übergeben werden
// z.B. new TemplateProcessor(astTransformers: [XComponentTransformer::class, ...])
$pattern = '/\b' . preg_quote($paramName, '/') . '\s*:\s*\[(.*?)\]/s';
if (preg_match($pattern, $invokeCode, $matches)) {
$arrayClasses = $this->extractClassesFromArrayContent($matches[1], $fileContent, $invokeMethod);
$classes = array_merge($classes, $arrayClasses);
}
}
} catch (\ReflectionException) {
// __invoke() existiert nicht - das ist okay
}
return array_unique($classes);
} catch (\Throwable) {
return [];
}
}
/**
* Extrahiere Klassen-Namen aus Array-Content-String
*/
private function extractClassesFromArrayContent(string $arrayContent, string $fileContent, \ReflectionMethod $method): array
{
// Finde alle Klassen-Namen (::class oder als String)
$classPattern = '/([A-Z][A-Za-z0-9\\\\]+)(::class|\')/';
preg_match_all($classPattern, $arrayContent, $classMatches);
$classes = [];
if (!empty($classMatches[1])) {
foreach ($classMatches[1] as $classMatch) {
// Normalisiere (füge \ am Anfang hinzu wenn nicht vorhanden)
if (!str_starts_with($classMatch, '\\') && !str_starts_with($classMatch, 'App\\')) {
// Versuche vollständigen Namespace zu finden
$fullClassName = $this->resolveClassNameFromMethod($classMatch, $fileContent, $method);
if ($fullClassName !== null) {
$classes[] = $fullClassName;
} elseif (class_exists($classMatch)) {
$classes[] = $classMatch;
}
} elseif (class_exists($classMatch)) {
$classes[] = $classMatch;
}
}
}
return $classes;
}
/**
* Versuche Implementierung eines Interfaces zu finden
*
* Prüft zuerst Container-Bindings, dann Namenskonventionen
*/
private function findInterfaceImplementation(string $interface): ?string
{
// 1. Versuche über Container-Bindings (falls Container verfügbar)
if ($this->container !== null) {
try {
// Prüfe ob Interface gebunden ist
if ($this->container->has($interface)) {
$binding = $this->getBindingForInterface($interface);
if ($binding !== null) {
// Wenn Binding ein String ist (Klassenname), verwende diesen
if (is_string($binding) && class_exists($binding)) {
return $binding;
}
// Wenn Binding ein Objekt ist, verwende dessen Klasse
// ABER: Überspringe Closure, da wir den Return-Type analysieren müssen
if (is_object($binding) && !($binding instanceof \Closure)) {
return $binding::class;
}
// Wenn Binding ein Closure ist, versuche Return-Type zu analysieren
if ($binding instanceof \Closure) {
$returnType = $this->getClosureReturnType($binding);
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 && class_exists($invokeReturnType)) {
return $invokeReturnType;
}
}
}
}
}
} catch (\Throwable) {
// Container-Zugriff fehlgeschlagen - ignoriere und versuche Fallback
}
}
// 2. Versuche Namenskonvention: Interface -> DefaultInterfaceName
$interfaceName = basename(str_replace('\\', '/', $interface));
$namespace = substr($interface, 0, strrpos($interface, '\\'));
// Versuche "Default" + InterfaceName ohne "Interface"
$implName = str_replace('Interface', '', $interfaceName);
$defaultImpl = $namespace . '\\Default' . $implName;
if (class_exists($defaultImpl)) {
return $defaultImpl;
}
// Versuche einfach InterfaceName ohne "Interface"
$simpleImpl = $namespace . '\\' . $implName;
if (class_exists($simpleImpl)) {
return $simpleImpl;
}
return null;
}
/**
* Hole Binding für ein Interface aus dem Container
*/
private function getBindingForInterface(string $interface): callable|string|object|null
{
if ($this->container === null) {
return null;
}
try {
// Versuche über Introspector (sicherer, keine Instanziierung)
// Introspector ist eine readonly Property im Container
if (property_exists($this->container, 'introspector')) {
$introspector = $this->container->introspector;
return $introspector->getBinding($interface);
}
// Fallback: Versuche direkt über BindingRegistry (wenn verfügbar)
// Das ist nicht ideal, aber besser als nichts
return null;
} catch (\Throwable) {
return null;
}
}
/**
* Versuche Return-Type eines Closures zu extrahieren
*/
private function getClosureReturnType(\Closure $closure): ?string
{
try {
$reflection = new \ReflectionFunction($closure);
$returnType = $reflection->getReturnType();
if ($returnType instanceof \ReflectionNamedType && !$returnType->isBuiltin()) {
return $returnType->getName();
}
return null;
} catch (\Throwable) {
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 muss auch Named Parameters unterstützen: "return new Engine(...)"
$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;
}
}
// Fallback: Suche auch nach Named Parameters Syntax: "return new Engine(...)"
// mit Named Parameters: loader: ..., processor: ...
$pattern2 = '/return\s+new\s+([A-Z][A-Za-z0-9\\\\]+)\s*\(/';
if (preg_match($pattern2, $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; return null;
} catch (\Throwable) { } catch (\Throwable) {
return null; return null;
} }
} }
} }

View File

@@ -6,6 +6,8 @@ namespace App\Framework\View;
use App\Framework\Http\Session\SessionInterface; use App\Framework\Http\Session\SessionInterface;
use App\Framework\Meta\MetaData; use App\Framework\Meta\MetaData;
use App\Framework\View\Loading\TemplateLoader;
use App\Framework\View\TemplateProcessor;
/** /**
* Renders LiveComponent templates * Renders LiveComponent templates
@@ -16,7 +18,8 @@ use App\Framework\Meta\MetaData;
final readonly class LiveComponentRenderer final readonly class LiveComponentRenderer
{ {
public function __construct( public function __construct(
private TemplateRenderer $templateRenderer, private TemplateLoader $templateLoader,
private TemplateProcessor $templateProcessor,
private SessionInterface $session private SessionInterface $session
) { ) {
} }
@@ -46,7 +49,13 @@ final readonly class LiveComponentRenderer
processingMode: ProcessingMode::COMPONENT processingMode: ProcessingMode::COMPONENT
); );
return $this->templateRenderer->renderPartial($context); $templateHtml = $this->templateLoader->load(
template: $templatePath,
controllerClass: null,
context: $context
);
return $this->templateProcessor->render($context, $templateHtml, component: true);
} }
/** /**

View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace App\Framework\View;
use App\Framework\Cache\Cache;
use App\Framework\DI\DefaultContainer;
use App\Framework\DI\Initializer;
use App\Framework\Discovery\Results\DiscoveryRegistry;
use App\Framework\View\Loading\TemplateLoader;
use App\Framework\Core\PathProvider;
final readonly class TemplateLoaderInitializer
{
private const CACHE_ENABLED = false;
public function __construct(
private DefaultContainer $container,
private DiscoveryRegistry $discoveryRegistry,
) {
}
#[Initializer]
public function __invoke(PathProvider $pathProvider, Cache $cache): TemplateLoader
{
$loader = new TemplateLoader(
pathProvider: $pathProvider,
cache: $cache,
discoveryRegistry: $this->discoveryRegistry,
cacheEnabled: self::CACHE_ENABLED
);
$this->container->singleton(TemplateLoader::class, $loader);
return $loader;
}
}

View File

@@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
namespace App\Framework\View;
use App\Framework\Cache\Cache;
use App\Framework\DI\DefaultContainer;
use App\Framework\DI\Initializer;
use App\Framework\View\Dom\Transformer\AssetInjectorTransformer;
use App\Framework\View\Dom\Transformer\CommentStripTransformer;
use App\Framework\View\Dom\Transformer\ForTransformer;
use App\Framework\View\Dom\Transformer\HoneypotTransformer;
use App\Framework\View\Dom\Transformer\IfTransformer;
use App\Framework\View\Dom\Transformer\LayoutTagTransformer;
use App\Framework\View\Dom\Transformer\MetaManipulatorTransformer;
use App\Framework\View\Dom\Transformer\WhitespaceCleanupTransformer;
use App\Framework\View\Dom\Transformer\XComponentTransformer;
use App\Framework\View\Processors\PlaceholderReplacer;
use App\Framework\View\Processors\VoidElementsSelfClosingProcessor;
final readonly class TemplateProcessorInitializer
{
public function __construct(
private DefaultContainer $container,
) {
}
#[Initializer]
public function __invoke(Cache $cache): TemplateProcessor
{
$astTransformers = [
LayoutTagTransformer::class,
XComponentTransformer::class,
ForTransformer::class,
IfTransformer::class,
MetaManipulatorTransformer::class,
AssetInjectorTransformer::class,
HoneypotTransformer::class,
CommentStripTransformer::class,
WhitespaceCleanupTransformer::class,
];
$stringProcessors = [
PlaceholderReplacer::class,
VoidElementsSelfClosingProcessor::class,
];
$chainOptimizer = new ProcessorChainOptimizer($cache);
$compiledTemplateCache = new CompiledTemplateCache($cache);
$performanceTracker = null;
if (getenv('ENABLE_TEMPLATE_PROFILING') === 'true') {
$performanceTracker = new ProcessorPerformanceTracker();
$performanceTracker->enable();
}
$processor = new TemplateProcessor(
astTransformers: $astTransformers,
stringProcessors: $stringProcessors,
container: $this->container,
chainOptimizer: $chainOptimizer,
compiledTemplateCache: $compiledTemplateCache,
performanceTracker: $performanceTracker
);
$this->container->singleton(TemplateProcessor::class, $processor);
return $processor;
}
}

View File

@@ -5,102 +5,30 @@ declare(strict_types=1);
namespace App\Framework\View; namespace App\Framework\View;
use App\Framework\Cache\Cache; use App\Framework\Cache\Cache;
use App\Framework\Core\PathProvider;
use App\Framework\DI\DefaultContainer; use App\Framework\DI\DefaultContainer;
use App\Framework\DI\Initializer; use App\Framework\DI\Initializer;
use App\Framework\Discovery\Results\DiscoveryRegistry;
use App\Framework\Performance\PerformanceService; use App\Framework\Performance\PerformanceService;
use App\Framework\View\Dom\Transformer\AssetInjectorTransformer;
use App\Framework\View\Dom\Transformer\CommentStripTransformer;
use App\Framework\View\Dom\Transformer\ForTransformer;
use App\Framework\View\Dom\Transformer\HoneypotTransformer;
use App\Framework\View\Dom\Transformer\IfTransformer;
use App\Framework\View\Dom\Transformer\LayoutTagTransformer;
use App\Framework\View\Dom\Transformer\MetaManipulatorTransformer;
use App\Framework\View\Dom\Transformer\WhitespaceCleanupTransformer;
use App\Framework\View\Dom\Transformer\XComponentTransformer;
use App\Framework\View\Loading\TemplateLoader; use App\Framework\View\Loading\TemplateLoader;
use App\Framework\View\Processors\PlaceholderReplacer;
use App\Framework\View\Processors\VoidElementsSelfClosingProcessor;
final readonly class TemplateRendererInitializer final readonly class TemplateRendererInitializer
{ {
public function __construct( public function __construct(
private DefaultContainer $container, private DefaultContainer $container,
private DiscoveryRegistry $results,
) {} ) {}
#[Initializer] #[Initializer]
public function __invoke(): TemplateRenderer public function __invoke(): TemplateRenderer
{ {
// AST Transformers (new approach) - Modern template processing $templateProcessor = $this->container->get(TemplateProcessor::class);
$astTransformers = [ $loader = $this->container->get(TemplateLoader::class);
// Core transformers (order matters!)
LayoutTagTransformer::class, // Process <layout> tags FIRST (before other processing)
XComponentTransformer::class, // Process <x-*> components (LiveComponents + HtmlComponents)
ForTransformer::class, // Process foreach loops and <for> elements (BEFORE if/placeholders)
IfTransformer::class, // Conditional rendering (if/condition attributes)
MetaManipulatorTransformer::class, // Set meta tags from context
AssetInjectorTransformer::class, // Inject Vite assets (CSS/JS)
HoneypotTransformer::class, // Add honeypot spam protection to forms
CommentStripTransformer::class, // Remove HTML comments
WhitespaceCleanupTransformer::class, // Remove empty text nodes
];
// TODO: Migrate remaining DOM processors to AST transformers:
// - ComponentProcessor (for <component> tags) - COMPLEX, keep in DOM for now
// - TableProcessor (for table rendering) - OPTIONAL
// - FormProcessor (for form handling) - OPTIONAL
$strings = [
PlaceholderReplacer::class, // PlaceholderReplacer handles simple {{ }} replacements
VoidElementsSelfClosingProcessor::class,
];
/** @var Cache $cache */
$cache = $this->container->get(Cache::class);
// Performance-Optimierungen (optional)
$chainOptimizer = new ProcessorChainOptimizer($cache);
$compiledTemplateCache = new CompiledTemplateCache($cache);
// Performance Tracker nur in Development/Profiling
$performanceTracker = null;
if (getenv('ENABLE_TEMPLATE_PROFILING') === 'true') {
$performanceTracker = new ProcessorPerformanceTracker();
$performanceTracker->enable();
}
$templateProcessor = new TemplateProcessor(
astTransformers: $astTransformers,
stringProcessors: $strings,
container: $this->container,
chainOptimizer: $chainOptimizer,
compiledTemplateCache: $compiledTemplateCache,
performanceTracker: $performanceTracker
);
$this->container->singleton(TemplateProcessor::class, $templateProcessor);
/** @var PathProvider $pathProvider */
$pathProvider = $this->container->get(PathProvider::class);
/** @var Cache $cache */
$cache = $this->container->get(Cache::class);
/** @var PerformanceService $performanceService */ /** @var PerformanceService $performanceService */
$performanceService = $this->container->get(PerformanceService::class); $performanceService = $this->container->get(PerformanceService::class);
// Define caching state centrally /** @var Cache $cache */
$cacheEnabled = false; // Keep caching disabled while debugging template processing $cache = $this->container->get(Cache::class);
$loader = new TemplateLoader( $cacheEnabled = false;
pathProvider: $pathProvider,
cache: $cache,
discoveryRegistry: $this->results,
cacheEnabled: $cacheEnabled // Pass cache state to loader
);
$this->container->singleton(TemplateLoader::class, $loader);
return new Engine( return new Engine(
loader: $loader, loader: $loader,

View File

@@ -2,7 +2,9 @@
declare(strict_types=1); declare(strict_types=1);
use App\Framework\Http\Session\SessionInterface; use App\Framework\DateTime\SystemClock;
use App\Framework\Http\Session\Session;
use App\Framework\Http\Session\SessionId;
use App\Framework\LiveComponents\ComponentEventDispatcher; use App\Framework\LiveComponents\ComponentEventDispatcher;
use App\Framework\LiveComponents\Contracts\LiveComponentContract; use App\Framework\LiveComponents\Contracts\LiveComponentContract;
use App\Framework\LiveComponents\LiveComponentHandler; use App\Framework\LiveComponents\LiveComponentHandler;
@@ -10,74 +12,20 @@ use App\Framework\LiveComponents\ValueObjects\ActionParameters;
use App\Framework\LiveComponents\ValueObjects\ComponentAction; use App\Framework\LiveComponents\ValueObjects\ComponentAction;
use App\Framework\LiveComponents\ValueObjects\ComponentData; use App\Framework\LiveComponents\ValueObjects\ComponentData;
use App\Framework\LiveComponents\ValueObjects\ComponentId; use App\Framework\LiveComponents\ValueObjects\ComponentId;
use App\Framework\Random\SecureRandomGenerator;
use App\Framework\Security\CsrfToken; use App\Framework\Security\CsrfToken;
use App\Framework\Security\CsrfTokenGenerator;
use App\Framework\View\LiveComponentRenderer; use App\Framework\View\LiveComponentRenderer;
use App\Framework\View\TemplateRenderer; use App\Framework\View\Loading\TemplateLoader;
use App\Framework\View\TemplateProcessor;
beforeEach(function () { beforeEach(function () {
// Create mock SessionInterface for CSRF testing $this->session = Session::fromArray(
$this->session = new class () implements SessionInterface { SessionId::fromString(str_repeat('a', 32)),
private array $tokens = []; new SystemClock(),
new CsrfTokenGenerator(new SecureRandomGenerator()),
public function __get(string $name): mixed []
{ );
if ($name === 'csrf') {
return new class ($this) {
public function __construct(private $session)
{
}
public function generateToken(string $formId): CsrfToken
{
$token = CsrfToken::generate();
$this->session->tokens[$formId] = $token->toString();
return $token;
}
public function validateToken(string $formId, CsrfToken $token): bool
{
return isset($this->session->tokens[$formId])
&& hash_equals($this->session->tokens[$formId], $token->toString());
}
};
}
return null;
}
public function get(string $key, mixed $default = null): mixed
{
return $default;
}
public function set(string $key, mixed $value): void
{
}
public function has(string $key): bool
{
return false;
}
public function remove(string $key): void
{
}
public function regenerate(): bool
{
return true;
}
public function destroy(): void
{
}
public function getId(): string
{
return 'test-session-id';
}
};
$this->eventDispatcher = new ComponentEventDispatcher(); $this->eventDispatcher = new ComponentEventDispatcher();
$this->handler = new LiveComponentHandler($this->eventDispatcher, $this->session); $this->handler = new LiveComponentHandler($this->eventDispatcher, $this->session);
@@ -86,7 +34,8 @@ beforeEach(function () {
describe('CSRF Token Generation', function () { describe('CSRF Token Generation', function () {
it('generates unique CSRF token for each component instance', function () { it('generates unique CSRF token for each component instance', function () {
$renderer = new LiveComponentRenderer( $renderer = new LiveComponentRenderer(
$this->createMock(TemplateRenderer::class), $this->createMock(TemplateLoader::class),
$this->createMock(TemplateProcessor::class),
$this->session $this->session
); );
@@ -126,7 +75,8 @@ describe('CSRF Token Generation', function () {
it('includes CSRF token in rendered wrapper HTML', function () { it('includes CSRF token in rendered wrapper HTML', function () {
$renderer = new LiveComponentRenderer( $renderer = new LiveComponentRenderer(
$this->createMock(TemplateRenderer::class), $this->createMock(TemplateLoader::class),
$this->createMock(TemplateProcessor::class),
$this->session $this->session
); );
@@ -465,7 +415,8 @@ describe('End-to-End CSRF Flow', function () {
it('completes full CSRF flow from render to action execution', function () { it('completes full CSRF flow from render to action execution', function () {
// Step 1: Render component with CSRF token // Step 1: Render component with CSRF token
$renderer = new LiveComponentRenderer( $renderer = new LiveComponentRenderer(
$this->createMock(TemplateRenderer::class), $this->createMock(TemplateLoader::class),
$this->createMock(TemplateProcessor::class),
$this->session $this->session
); );