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:
254
src/Framework/DI/Analysis/CodeParser.php
Normal file
254
src/Framework/DI/Analysis/CodeParser.php
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
202
src/Framework/DI/Analysis/DependencyPathAnalyzer.php
Normal file
202
src/Framework/DI/Analysis/DependencyPathAnalyzer.php
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
125
src/Framework/DI/Analysis/InitializerFinder.php
Normal file
125
src/Framework/DI/Analysis/InitializerFinder.php
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
122
src/Framework/DI/Analysis/InterfaceResolver.php
Normal file
122
src/Framework/DI/Analysis/InterfaceResolver.php
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
96
src/Framework/DI/Analysis/NamespaceResolver.php
Normal file
96
src/Framework/DI/Analysis/NamespaceResolver.php
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
110
src/Framework/DI/Analysis/ReturnTypeAnalyzer.php
Normal file
110
src/Framework/DI/Analysis/ReturnTypeAnalyzer.php
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,13 @@ declare(strict_types=1);
|
||||
|
||||
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
|
||||
*
|
||||
@@ -13,12 +20,37 @@ namespace App\Framework\DI;
|
||||
*/
|
||||
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(
|
||||
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
|
||||
*
|
||||
@@ -47,14 +79,14 @@ final readonly class InitializerDependencyAnalyzer
|
||||
// 1. Analysiere Constructor-Dependencies
|
||||
$constructor = $reflection->getConstructor();
|
||||
$hasContainerInConstructor = false;
|
||||
|
||||
|
||||
if ($constructor !== null) {
|
||||
foreach ($constructor->getParameters() as $parameter) {
|
||||
$type = $parameter->getType();
|
||||
if ($type instanceof \ReflectionNamedType && !$type->isBuiltin()) {
|
||||
$typeName = $type->getName();
|
||||
$constructorDeps[] = $typeName;
|
||||
|
||||
|
||||
// Prüfe ob Container verfügbar ist
|
||||
if ($typeName === Container::class || $typeName === 'App\Framework\DI\Container') {
|
||||
$hasContainerInConstructor = true;
|
||||
@@ -67,10 +99,10 @@ final readonly class InitializerDependencyAnalyzer
|
||||
$containerGetDeps = [];
|
||||
$hasContainerGetCalls = false;
|
||||
$hasContainerInInvoke = false;
|
||||
|
||||
|
||||
try {
|
||||
$invokeMethod = $reflection->getMethod('__invoke');
|
||||
|
||||
|
||||
// Prüfe ob Container als Parameter in __invoke() verfügbar ist
|
||||
foreach ($invokeMethod->getParameters() as $parameter) {
|
||||
$type = $parameter->getType();
|
||||
@@ -79,21 +111,25 @@ final readonly class InitializerDependencyAnalyzer
|
||||
if ($typeName === Container::class || $typeName === 'App\Framework\DI\Container') {
|
||||
$hasContainerInInvoke = true;
|
||||
}
|
||||
|
||||
|
||||
// Füge auch zu constructorDeps hinzu wenn nicht schon vorhanden
|
||||
if (!in_array($typeName, $constructorDeps, true)) {
|
||||
$constructorDeps[] = $typeName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Nur nach container->get() suchen wenn Container verfügbar ist
|
||||
$hasContainerAvailable = $hasContainerInConstructor || $hasContainerInInvoke;
|
||||
|
||||
|
||||
if ($hasContainerAvailable) {
|
||||
// Parse Code der __invoke() Methode um container->get() Aufrufe zu finden
|
||||
$parsedDeps = $this->parseContainerGetCalls($invokeMethod);
|
||||
$containerGetDeps = $parsedDeps['dependencies'];
|
||||
$parsedDeps = $this->codeParser->parseContainerGetCalls($invokeMethod);
|
||||
// Konvertiere ClassName[] zurück zu string[]
|
||||
$containerGetDeps = array_map(
|
||||
fn(ClassName $className) => $className->toString(),
|
||||
$parsedDeps['dependencies']
|
||||
);
|
||||
$hasContainerGetCalls = $parsedDeps['hasCalls'];
|
||||
}
|
||||
} 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
|
||||
*
|
||||
@@ -257,576 +170,37 @@ final readonly class InitializerDependencyAnalyzer
|
||||
array $currentPath = [],
|
||||
int $depth = 0
|
||||
): ?array {
|
||||
// Max. Rekursionstiefe erreicht
|
||||
if ($depth >= self::MAX_RECURSION_DEPTH) {
|
||||
if ($this->dependencyPathAnalyzer === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Cycle-Detection: Vermeide Endlosschleifen
|
||||
if (in_array($dependencyClass, $visited, true)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
$dependencyClassName = ClassName::create($dependencyClass);
|
||||
$targetInterfaceName = ClassName::create($targetInterface);
|
||||
|
||||
// Prüfe ob diese Klasse direkt das Interface benötigt
|
||||
if ($this->dependencyNeedsInterface($dependencyClass, $targetInterface)) {
|
||||
return array_merge($currentPath, [$dependencyClass, $targetInterface]);
|
||||
}
|
||||
// Konvertiere visited und currentPath zu ClassName[]
|
||||
$visitedClassNames = array_map(fn(string $class) => ClassName::create($class), $visited);
|
||||
$currentPathClassNames = array_map(fn(string $class) => ClassName::create($class), $currentPath);
|
||||
|
||||
// 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 === Container::class || $subDependency === 'App\Framework\DI\Container') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$path = $this->findDependencyPathToInterface(
|
||||
$subDependency,
|
||||
$targetInterface,
|
||||
$newVisited,
|
||||
$newPath,
|
||||
$depth + 1
|
||||
$path = $this->dependencyPathAnalyzer->findPathToInterface(
|
||||
$dependencyClassName,
|
||||
$targetInterfaceName,
|
||||
$visitedClassNames,
|
||||
$currentPathClassNames,
|
||||
$depth
|
||||
);
|
||||
|
||||
if ($path !== null) {
|
||||
return $path;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüfe ob eine Klasse ein Interface benötigt (über Reflection)
|
||||
*/
|
||||
private function dependencyNeedsInterface(string $dependencyClass, string $interface): bool
|
||||
{
|
||||
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)) {
|
||||
if ($path === null) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// Konvertiere ClassName[] zurück zu string[]
|
||||
return array_map(fn(ClassName $className) => $className->toString(), $path);
|
||||
} catch (\InvalidArgumentException) {
|
||||
// Ungültiger Klassenname
|
||||
return null;
|
||||
} catch (\Throwable) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ namespace App\Framework\View;
|
||||
|
||||
use App\Framework\Http\Session\SessionInterface;
|
||||
use App\Framework\Meta\MetaData;
|
||||
use App\Framework\View\Loading\TemplateLoader;
|
||||
use App\Framework\View\TemplateProcessor;
|
||||
|
||||
/**
|
||||
* Renders LiveComponent templates
|
||||
@@ -16,7 +18,8 @@ use App\Framework\Meta\MetaData;
|
||||
final readonly class LiveComponentRenderer
|
||||
{
|
||||
public function __construct(
|
||||
private TemplateRenderer $templateRenderer,
|
||||
private TemplateLoader $templateLoader,
|
||||
private TemplateProcessor $templateProcessor,
|
||||
private SessionInterface $session
|
||||
) {
|
||||
}
|
||||
@@ -46,7 +49,13 @@ final readonly class LiveComponentRenderer
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
39
src/Framework/View/TemplateLoaderInitializer.php
Normal file
39
src/Framework/View/TemplateLoaderInitializer.php
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
72
src/Framework/View/TemplateProcessorInitializer.php
Normal file
72
src/Framework/View/TemplateProcessorInitializer.php
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,102 +5,30 @@ declare(strict_types=1);
|
||||
namespace App\Framework\View;
|
||||
|
||||
use App\Framework\Cache\Cache;
|
||||
use App\Framework\Core\PathProvider;
|
||||
use App\Framework\DI\DefaultContainer;
|
||||
use App\Framework\DI\Initializer;
|
||||
use App\Framework\Discovery\Results\DiscoveryRegistry;
|
||||
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\Processors\PlaceholderReplacer;
|
||||
use App\Framework\View\Processors\VoidElementsSelfClosingProcessor;
|
||||
|
||||
final readonly class TemplateRendererInitializer
|
||||
{
|
||||
public function __construct(
|
||||
private DefaultContainer $container,
|
||||
private DiscoveryRegistry $results,
|
||||
) {}
|
||||
|
||||
#[Initializer]
|
||||
public function __invoke(): TemplateRenderer
|
||||
{
|
||||
// AST Transformers (new approach) - Modern template processing
|
||||
$astTransformers = [
|
||||
// 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);
|
||||
$templateProcessor = $this->container->get(TemplateProcessor::class);
|
||||
$loader = $this->container->get(TemplateLoader::class);
|
||||
|
||||
/** @var PerformanceService $performanceService */
|
||||
$performanceService = $this->container->get(PerformanceService::class);
|
||||
|
||||
// Define caching state centrally
|
||||
$cacheEnabled = false; // Keep caching disabled while debugging template processing
|
||||
/** @var Cache $cache */
|
||||
$cache = $this->container->get(Cache::class);
|
||||
|
||||
$loader = new TemplateLoader(
|
||||
pathProvider: $pathProvider,
|
||||
cache: $cache,
|
||||
discoveryRegistry: $this->results,
|
||||
cacheEnabled: $cacheEnabled // Pass cache state to loader
|
||||
);
|
||||
|
||||
$this->container->singleton(TemplateLoader::class, $loader);
|
||||
$cacheEnabled = false;
|
||||
|
||||
return new Engine(
|
||||
loader: $loader,
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
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\Contracts\LiveComponentContract;
|
||||
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\ComponentData;
|
||||
use App\Framework\LiveComponents\ValueObjects\ComponentId;
|
||||
use App\Framework\Random\SecureRandomGenerator;
|
||||
use App\Framework\Security\CsrfToken;
|
||||
use App\Framework\Security\CsrfTokenGenerator;
|
||||
use App\Framework\View\LiveComponentRenderer;
|
||||
use App\Framework\View\TemplateRenderer;
|
||||
use App\Framework\View\Loading\TemplateLoader;
|
||||
use App\Framework\View\TemplateProcessor;
|
||||
|
||||
beforeEach(function () {
|
||||
// Create mock SessionInterface for CSRF testing
|
||||
$this->session = new class () implements SessionInterface {
|
||||
private array $tokens = [];
|
||||
|
||||
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->session = Session::fromArray(
|
||||
SessionId::fromString(str_repeat('a', 32)),
|
||||
new SystemClock(),
|
||||
new CsrfTokenGenerator(new SecureRandomGenerator()),
|
||||
[]
|
||||
);
|
||||
|
||||
$this->eventDispatcher = new ComponentEventDispatcher();
|
||||
$this->handler = new LiveComponentHandler($this->eventDispatcher, $this->session);
|
||||
@@ -86,7 +34,8 @@ beforeEach(function () {
|
||||
describe('CSRF Token Generation', function () {
|
||||
it('generates unique CSRF token for each component instance', function () {
|
||||
$renderer = new LiveComponentRenderer(
|
||||
$this->createMock(TemplateRenderer::class),
|
||||
$this->createMock(TemplateLoader::class),
|
||||
$this->createMock(TemplateProcessor::class),
|
||||
$this->session
|
||||
);
|
||||
|
||||
@@ -126,7 +75,8 @@ describe('CSRF Token Generation', function () {
|
||||
|
||||
it('includes CSRF token in rendered wrapper HTML', function () {
|
||||
$renderer = new LiveComponentRenderer(
|
||||
$this->createMock(TemplateRenderer::class),
|
||||
$this->createMock(TemplateLoader::class),
|
||||
$this->createMock(TemplateProcessor::class),
|
||||
$this->session
|
||||
);
|
||||
|
||||
@@ -465,7 +415,8 @@ describe('End-to-End CSRF Flow', function () {
|
||||
it('completes full CSRF flow from render to action execution', function () {
|
||||
// Step 1: Render component with CSRF token
|
||||
$renderer = new LiveComponentRenderer(
|
||||
$this->createMock(TemplateRenderer::class),
|
||||
$this->createMock(TemplateLoader::class),
|
||||
$this->createMock(TemplateProcessor::class),
|
||||
$this->session
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user