Files
michaelschiemer/src/Framework/DI/ProactiveInitializerFinder.php
Michael Schiemer 9cad445aaf 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
2025-11-03 17:45:47 +01:00

317 lines
10 KiB
PHP

<?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);
}
}