- 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
317 lines
10 KiB
PHP
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);
|
|
}
|
|
}
|
|
|