feat(di): add proactive initializer finder for interface resolution
- Add ProactiveInitializerFinder to search for initializers when not found in registry - Add InitializerInfo value object to store initializer metadata - Implement multi-step search strategy: DefaultImplementation, naming convention, directory, subdirectories, module - Integrate proactive finder into DefaultContainer for better interface resolution - Simplify AppBootstrapper by moving initialization logic to DefaultContainer - Improve error messages in ClassNotInstantiable with proactive finder context
This commit is contained in:
316
src/Framework/DI/ProactiveInitializerFinder.php
Normal file
316
src/Framework/DI/ProactiveInitializerFinder.php
Normal file
@@ -0,0 +1,316 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\DI;
|
||||
|
||||
use App\Framework\Core\PathProvider;
|
||||
use App\Framework\Core\ValueObjects\ClassName;
|
||||
use App\Framework\Core\ValueObjects\MethodName;
|
||||
use App\Framework\Core\ValueObjects\PhpNamespace;
|
||||
use App\Framework\DI\Attributes\DefaultImplementation;
|
||||
use App\Framework\DI\ValueObjects\InitializerInfo;
|
||||
use App\Framework\Discovery\Processing\ClassExtractor;
|
||||
use App\Framework\Discovery\Results\DiscoveryRegistry;
|
||||
use App\Framework\Filesystem\File;
|
||||
use App\Framework\Filesystem\FileScanner;
|
||||
use App\Framework\Filesystem\ValueObjects\FilePath;
|
||||
use App\Framework\Filesystem\ValueObjects\FilePattern;
|
||||
use App\Framework\Reflection\ReflectionProvider;
|
||||
|
||||
/**
|
||||
* Proaktive Suche nach Initializern für Interfaces
|
||||
*
|
||||
* Wenn kein Initializer in der Discovery Registry gefunden wird, sucht diese Klasse
|
||||
* proaktiv nach Initializern basierend auf Namenskonventionen, Verzeichnisstruktur,
|
||||
* Modulen und DefaultImplementation Attributen.
|
||||
*/
|
||||
final readonly class ProactiveInitializerFinder
|
||||
{
|
||||
public function __construct(
|
||||
private ReflectionProvider $reflectionProvider,
|
||||
private DiscoveryRegistry $discoveryRegistry,
|
||||
private FileScanner $fileScanner,
|
||||
private ClassExtractor $classExtractor,
|
||||
private PathProvider $pathProvider
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Findet einen Initializer für ein Interface
|
||||
*
|
||||
* Suchreihenfolge:
|
||||
* 1. DefaultImplementation Attribut
|
||||
* 2. Namenskonvention
|
||||
* 3. Selber Ordner
|
||||
* 4. Unterordner
|
||||
* 5. Ganzes Modul
|
||||
*/
|
||||
public function findInitializerForInterface(string $interface): ?InitializerInfo
|
||||
{
|
||||
// 1. DefaultImplementation Attribut (ERSTE Suche)
|
||||
$result = $this->findByDefaultImplementation($interface);
|
||||
if ($result !== null) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
// 2. Namenskonvention
|
||||
$result = $this->findByName($interface);
|
||||
if ($result !== null) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
// 3. Selber Ordner
|
||||
$result = $this->findInDirectory($interface);
|
||||
if ($result !== null) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
// 4. Unterordner
|
||||
$result = $this->findInSubdirectories($interface);
|
||||
if ($result !== null) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
// 5. Ganzes Modul
|
||||
return $this->findInModule($interface);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sucht nach Initializer basierend auf DefaultImplementation Attribut
|
||||
*/
|
||||
private function findByDefaultImplementation(string $interface): ?InitializerInfo
|
||||
{
|
||||
try {
|
||||
// Suche in DiscoveryRegistry nach DefaultImplementation Attributen
|
||||
$defaultImplResults = $this->discoveryRegistry->attributes->get(DefaultImplementation::class);
|
||||
|
||||
if (empty($defaultImplResults)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach ($defaultImplResults as $defaultImpl) {
|
||||
$implClassName = $defaultImpl->className;
|
||||
|
||||
// Prüfe ob das Attribut das richtige Interface spezifiziert
|
||||
$defaultImplAttr = $defaultImpl->createAttributeInstance();
|
||||
if ($defaultImplAttr instanceof DefaultImplementation) {
|
||||
// Wenn Interface explizit angegeben, muss es übereinstimmen
|
||||
if ($defaultImplAttr->interface !== null && $defaultImplAttr->interface !== $interface) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Prüfe ob die Klasse das Interface implementiert
|
||||
$reflection = $this->reflectionProvider->getClass($implClassName);
|
||||
|
||||
if (!$reflection->implementsInterface($interface)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Prüfe ob die Klasse auch Initializer Attribut hat
|
||||
$initializerInfo = $this->checkInitializerAttribute($implClassName, $interface);
|
||||
if ($initializerInfo !== null) {
|
||||
return $initializerInfo;
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// Silently ignore errors
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sucht nach Initializer basierend auf Namenskonvention
|
||||
* SomeClassInterface -> SomeClassInitializer
|
||||
*/
|
||||
private function findByName(string $interface): ?InitializerInfo
|
||||
{
|
||||
try {
|
||||
$interfaceName = ClassName::create($interface);
|
||||
$shortName = $interfaceName->getShortName();
|
||||
|
||||
// Entferne "Interface" Suffix
|
||||
if (!str_ends_with($shortName, 'Interface')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$baseName = substr($shortName, 0, -9); // "Interface" = 9 Zeichen
|
||||
$initializerName = $baseName . 'Initializer';
|
||||
|
||||
// Konstruiere vollständigen Klassennamen
|
||||
$namespace = $interfaceName->getNamespaceObject();
|
||||
$initializerClassName = ClassName::fromNamespace($namespace, $initializerName);
|
||||
|
||||
// Prüfe ob Klasse existiert
|
||||
if (!$initializerClassName->exists()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Prüfe Initializer Attribut
|
||||
return $this->checkInitializerAttribute($initializerClassName, $interface);
|
||||
} catch (\Throwable $e) {
|
||||
// Silently ignore errors
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sucht nach Initializer im selben Ordner wie das Interface
|
||||
*/
|
||||
private function findInDirectory(string $interface): ?InitializerInfo
|
||||
{
|
||||
try {
|
||||
$interfaceName = ClassName::create($interface);
|
||||
$namespace = $interfaceName->getNamespaceObject();
|
||||
|
||||
$directoryPath = $this->namespaceToDirectoryPath($namespace);
|
||||
if ($directoryPath === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->searchInPath(FilePath::create($directoryPath), $interface, false);
|
||||
} catch (\Throwable $e) {
|
||||
// Silently ignore errors
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sucht nach Initializer in Unterordnern des Interface-Ordners
|
||||
*/
|
||||
private function findInSubdirectories(string $interface): ?InitializerInfo
|
||||
{
|
||||
try {
|
||||
$interfaceName = ClassName::create($interface);
|
||||
$namespace = $interfaceName->getNamespaceObject();
|
||||
|
||||
$directoryPath = $this->namespaceToDirectoryPath($namespace);
|
||||
if ($directoryPath === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->searchInPath(FilePath::create($directoryPath), $interface, true);
|
||||
} catch (\Throwable $e) {
|
||||
// Silently ignore errors
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sucht nach Initializer im ganzen Modul
|
||||
*/
|
||||
private function findInModule(string $interface): ?InitializerInfo
|
||||
{
|
||||
try {
|
||||
$interfaceName = ClassName::create($interface);
|
||||
$namespace = $interfaceName->getNamespaceObject();
|
||||
|
||||
// Bestimme Modul-Namespace (Parent-Namespace)
|
||||
$moduleNamespace = $namespace->parent();
|
||||
if ($moduleNamespace === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$directoryPath = $this->namespaceToDirectoryPath($moduleNamespace);
|
||||
if ($directoryPath === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->searchInPath(FilePath::create($directoryPath), $interface, true);
|
||||
} catch (\Throwable $e) {
|
||||
// Silently ignore errors
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Durchsucht einen Pfad nach Initializern
|
||||
*/
|
||||
private function searchInPath(FilePath $path, string $interface, bool $recursive): ?InitializerInfo
|
||||
{
|
||||
try {
|
||||
foreach ($this->fileScanner->streamFiles($path, FilePattern::php()) as $file) {
|
||||
$classes = $this->classExtractor->extractFromFile($file);
|
||||
|
||||
foreach ($classes as $className) {
|
||||
if (!$className->exists()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$initializerInfo = $this->checkInitializerAttribute($className, $interface);
|
||||
if ($initializerInfo !== null) {
|
||||
return $initializerInfo;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// Silently ignore errors
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft ob eine Klasse ein Initializer Attribut hat und das Interface zurückgibt
|
||||
*/
|
||||
private function checkInitializerAttribute(ClassName $className, string $interface): ?InitializerInfo
|
||||
{
|
||||
try {
|
||||
$reflection = $this->reflectionProvider->getClass($className);
|
||||
$methods = $reflection->getMethods();
|
||||
|
||||
foreach ($methods as $method) {
|
||||
// Prüfe ob Methode Initializer Attribut hat
|
||||
if (!$method->hasAttribute(Initializer::class)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Prüfe Return-Type
|
||||
$returnType = $method->getReturnType();
|
||||
if ($returnType === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Normalisiere Return-Type (entferne leading backslash, handle nullable)
|
||||
$returnType = ltrim($returnType, '\\?');
|
||||
|
||||
// Prüfe ob Return-Type das Interface ist
|
||||
if ($returnType !== $interface && $returnType !== '\\' . $interface) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Gefunden!
|
||||
return new InitializerInfo(
|
||||
initializerClass: $className,
|
||||
methodName: MethodName::create($method->getName()),
|
||||
returnType: ClassName::create($interface)
|
||||
);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// Silently ignore errors
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Konvertiert einen PhpNamespace in ein Verzeichnis
|
||||
*/
|
||||
private function namespaceToDirectoryPath(PhpNamespace $namespace): ?string
|
||||
{
|
||||
// Nutze PathProvider um Namespace zu Pfad zu konvertieren
|
||||
$filePath = $this->pathProvider->namespaceToPath($namespace);
|
||||
if ($filePath === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Entferne .php Extension und Dateiname - wir wollen das Verzeichnis
|
||||
return dirname($filePath);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user