chore: complete update
This commit is contained in:
133
src/Framework/Core/AppBootstrapper.php
Normal file
133
src/Framework/Core/AppBootstrapper.php
Normal file
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core;
|
||||
|
||||
use App\Framework\Cache\Cache;
|
||||
use App\Framework\Cache\CacheInitializer;
|
||||
use App\Framework\Config\Configuration;
|
||||
use App\Framework\Config\Environment;
|
||||
use App\Framework\Config\TypedConfigInitializer;
|
||||
use App\Framework\Config\TypedConfiguration;
|
||||
use App\Framework\Console\ConsoleApplication;
|
||||
use App\Framework\Console\ConsoleOutput;
|
||||
use App\Framework\Context\ExecutionContext;
|
||||
use App\Framework\DI\DefaultContainer;
|
||||
use App\Framework\DI\Container;
|
||||
use App\Framework\ErrorHandling\ErrorHandler;
|
||||
use App\Framework\Http\ResponseEmitter;
|
||||
use App\Framework\Performance\PerformanceCategory;
|
||||
use App\Framework\Performance\PerformanceMeter;
|
||||
|
||||
/**
|
||||
* Verantwortlich für die grundlegende Initialisierung der Anwendung
|
||||
*/
|
||||
final readonly class AppBootstrapper
|
||||
{
|
||||
private DefaultContainer $container;
|
||||
|
||||
private ContainerBootstrapper $bootstrapper;
|
||||
|
||||
public function __construct(
|
||||
private string $basePath,
|
||||
private PerformanceMeter $meter,
|
||||
private array $config = [],
|
||||
){
|
||||
$this->container = new DefaultContainer();
|
||||
$this->bootstrapper = new ContainerBootstrapper($this->container);
|
||||
|
||||
|
||||
$env = Environment::fromFile($this->basePath . '/.env');
|
||||
|
||||
$this->container->instance(TypedConfiguration::class, new TypedConfigInitializer($env)($this->container));
|
||||
|
||||
|
||||
// ExecutionContext detection sollte das erste sein, das nach dem Instanzieren des containers passiert. noch bevor dem bootstrap des containers.
|
||||
$executionContext = ExecutionContext::detect();
|
||||
$this->container->instance(ExecutionContext::class, $executionContext);
|
||||
|
||||
error_log("AppBootstrapper: Context detected as {$executionContext->getType()->value}");
|
||||
error_log('AppBootstrapper: Context metadata: ' . json_encode($executionContext->getMetadata()));
|
||||
}
|
||||
|
||||
public function bootstrapWeb(): Application
|
||||
{
|
||||
$this->bootstrap();
|
||||
$this->registerWebErrorHandler();
|
||||
$this->registerApplication();
|
||||
|
||||
return $this->container->get(Application::class);
|
||||
}
|
||||
|
||||
public function bootstrapConsole(): ConsoleApplication
|
||||
{
|
||||
$this->bootstrap();
|
||||
$this->registerCliErrorHandler();
|
||||
$this->registerConsoleApplication();
|
||||
|
||||
return $this->container->get(ConsoleApplication::class);
|
||||
}
|
||||
|
||||
public function bootstrapWorker(): Container
|
||||
{
|
||||
$this->bootstrap();
|
||||
$this->registerCliErrorHandler();
|
||||
|
||||
$consoleOutput = new ConsoleOutput();
|
||||
$this->container->instance(ConsoleOutput::class, $consoleOutput);
|
||||
|
||||
return $this->container;
|
||||
}
|
||||
|
||||
private function bootstrap(): void
|
||||
{
|
||||
$this->meter->startMeasure('bootstrap:start', PerformanceCategory::SYSTEM);
|
||||
|
||||
$this->bootstrapper->bootstrap($this->basePath, $this->meter, $this->config);
|
||||
|
||||
// ErrorHandler wird jetzt kontextabhängig registriert
|
||||
// $this->container->get(ErrorHandler::class)->register();
|
||||
|
||||
$this->meter->endMeasure('bootstrap:end');
|
||||
}
|
||||
|
||||
private function registerWebErrorHandler(): void
|
||||
{
|
||||
$this->container->get(ErrorHandler::class)->register();
|
||||
}
|
||||
|
||||
private function registerCliErrorHandler(): void
|
||||
{
|
||||
$output = $this->container->has(ConsoleOutput::class)
|
||||
? $this->container->get(ConsoleOutput::class)
|
||||
: new ConsoleOutput();
|
||||
|
||||
$cliErrorHandler = new \App\Framework\ErrorHandling\CliErrorHandler($output);
|
||||
$cliErrorHandler->register();
|
||||
}
|
||||
|
||||
private function registerApplication(): void
|
||||
{
|
||||
$this->container->singleton(Application::class, function (Container $c) {
|
||||
return new Application(
|
||||
$c,
|
||||
$c->get(PathProvider::class),
|
||||
$c->get(ResponseEmitter::class),
|
||||
$c->get(Configuration::class)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private function registerConsoleApplication(): void
|
||||
{
|
||||
$this->container->singleton(ConsoleApplication::class, function (Container $c) {
|
||||
return new ConsoleApplication(
|
||||
$c,
|
||||
'console',
|
||||
'My Console App',
|
||||
null,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
103
src/Framework/Core/Application.php
Normal file
103
src/Framework/Core/Application.php
Normal file
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core;
|
||||
|
||||
use App\Framework\Cache\Cache;
|
||||
use App\Framework\Config\Configuration;
|
||||
use App\Framework\Core\Events\AfterEmitResponse;
|
||||
use App\Framework\Core\Events\AfterHandleRequest;
|
||||
use App\Framework\Core\Events\ApplicationBooted;
|
||||
use App\Framework\Core\Events\BeforeEmitResponse;
|
||||
use App\Framework\Core\Events\BeforeHandleRequest;
|
||||
use App\Framework\Core\Events\EventCompilerPass;
|
||||
use App\Framework\Core\Events\EventDispatcher;
|
||||
use App\Framework\Core\Events\OnEvent;
|
||||
use App\Framework\DI\DefaultContainer;
|
||||
use App\Framework\DI\Container;
|
||||
use App\Framework\DI\Initializer;
|
||||
use App\Framework\Http\MiddlewareManager;
|
||||
use App\Framework\Http\Request;
|
||||
use App\Framework\Http\ResponseEmitter;
|
||||
use App\Framework\Router\HttpRouter;
|
||||
use DateTimeImmutable;
|
||||
|
||||
final readonly class Application
|
||||
{
|
||||
private MiddlewareManager $middlewareManager;
|
||||
private AttributeDiscoveryService $discoveryService;
|
||||
private EventDispatcher $eventDispatcher;
|
||||
|
||||
public function __construct(
|
||||
private Container $container,
|
||||
private PathProvider $pathProvider,
|
||||
private ResponseEmitter $responseEmitter,
|
||||
private Configuration $config
|
||||
) {
|
||||
// Middleware-Manager initialisieren
|
||||
$this->middlewareManager = $this->container->get(MiddlewareManager::class);
|
||||
|
||||
$this->eventDispatcher = $container->get(EventDispatcher::class);
|
||||
|
||||
// Discovery-Service initialisieren
|
||||
#$this->discoveryService = new AttributeDiscoveryService($container, $pathProvider, $config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Führt die Anwendung aus
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
// ApplicationBooted-Event dispatchen
|
||||
$environment = $this->config->get('environment', 'dev');
|
||||
$version = $this->config->get('app.version', 'dev');
|
||||
|
||||
|
||||
$bootEvent = new ApplicationBooted(
|
||||
new DateTimeImmutable(),
|
||||
$environment,
|
||||
$version
|
||||
);
|
||||
|
||||
$this->event($bootEvent);
|
||||
|
||||
// Attribute verarbeiten und Komponenten einrichten
|
||||
#$this->setupApplicationComponents();
|
||||
|
||||
// Sicherstellen, dass ein Router registriert wurde
|
||||
if (!$this->container->has(HttpRouter::class)) {
|
||||
throw new \RuntimeException('Kritischer Fehler: Router wurde nicht initialisiert');
|
||||
}
|
||||
|
||||
$this->event(new BeforeHandleRequest);
|
||||
|
||||
|
||||
$this->event(new AfterHandleRequest);
|
||||
|
||||
$request = $this->container->get(Request::class);
|
||||
|
||||
$response = $this->middlewareManager->chain->handle($request);
|
||||
|
||||
$this->event(new BeforeEmitResponse);
|
||||
|
||||
// Response ausgeben
|
||||
$this->responseEmitter->emit($response);
|
||||
|
||||
$this->event(new AfterEmitResponse);
|
||||
}
|
||||
|
||||
private function event(object $event): void
|
||||
{
|
||||
$this->eventDispatcher->dispatch($event);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gibt einen Konfigurationswert zurück
|
||||
*/
|
||||
public function config(string $key, mixed $default = null): mixed
|
||||
{
|
||||
return $this->config->get($key, $default);
|
||||
}
|
||||
}
|
||||
16
src/Framework/Core/AttributeCompiler.php
Normal file
16
src/Framework/Core/AttributeCompiler.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\Core;
|
||||
|
||||
interface AttributeCompiler
|
||||
{
|
||||
/**
|
||||
* Gibt die Attributklasse zurück, für die dieser Compiler zuständig ist
|
||||
*/
|
||||
public function getAttributeClass(): string;
|
||||
|
||||
/**
|
||||
* Kompiliert die gemappten Attributdaten in eine optimierte Form
|
||||
*/
|
||||
public function compile(array $data): mixed;
|
||||
}
|
||||
101
src/Framework/Core/AttributeDiscoveryService.php
Normal file
101
src/Framework/Core/AttributeDiscoveryService.php
Normal file
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core;
|
||||
|
||||
use App\Framework\Auth\AuthMapper;
|
||||
use App\Framework\CommandBus\CommandHandlerCompiler;
|
||||
use App\Framework\CommandBus\CommandHandlerMapper;
|
||||
use App\Framework\Config\Configuration;
|
||||
use App\Framework\Console\ConsoleCommandMapper;
|
||||
use App\Framework\DI\AttributeProcessorRegistry;
|
||||
use App\Framework\DI\DefaultContainer;
|
||||
use App\Framework\DI\InitializerMapper;
|
||||
use App\Framework\QueryBus\QueryHandlerCompiler;
|
||||
use App\Framework\QueryBus\QueryHandlerMapper;
|
||||
use Archive\Async1\DiscoveryFactory;
|
||||
|
||||
/**
|
||||
* Verwaltet die Entdeckung und Verarbeitung von Attributen
|
||||
*/
|
||||
readonly class AttributeDiscoveryService
|
||||
{
|
||||
public function __construct(
|
||||
private DefaultContainer $container,
|
||||
private PathProvider $pathProvider,
|
||||
private Configuration $config
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Führt die Attribut-Discovery durch und verarbeitet die gefundenen Attribute
|
||||
*/
|
||||
public function discoverAndProcess(): array
|
||||
{
|
||||
$useAsyncDiscovery = $this->config->get('async_discovery', true);
|
||||
$srcDir = $this->pathProvider->getSourcePath();
|
||||
|
||||
// Discovery erstellen
|
||||
$discoveryFactory = new DiscoveryFactory();
|
||||
$discovery = $discoveryFactory->create($useAsyncDiscovery, $srcDir);
|
||||
|
||||
// Attribute-Processor-Registry initialisieren
|
||||
$processorRegistry = $this->createProcessorRegistry();
|
||||
|
||||
// Attribute entdecken
|
||||
$results = $discovery->discover($srcDir);
|
||||
|
||||
// Alle Attribute verarbeiten
|
||||
return $processorRegistry->processAll($results);
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt und konfiguriert die AttributeProcessorRegistry
|
||||
*/
|
||||
private function createProcessorRegistry(): AttributeProcessorRegistry
|
||||
{
|
||||
$processorRegistry = new AttributeProcessorRegistry($this->container);
|
||||
|
||||
// Mapper registrieren
|
||||
$this->registerMappers($processorRegistry);
|
||||
|
||||
// Compiler registrieren
|
||||
$this->registerCompilers($processorRegistry);
|
||||
|
||||
return $processorRegistry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registriert alle AttributeMapper
|
||||
*/
|
||||
private function registerMappers(AttributeProcessorRegistry $registry): void
|
||||
{
|
||||
$registry
|
||||
->registerMapper(RouteMapper::class)
|
||||
->registerMapper(CommandHandlerMapper::class)
|
||||
->registerMapper(\App\Framework\Core\Events\EventHandlerMapper::class)
|
||||
->registerMapper(\App\Framework\EventBus\EventHandlerMapper::class)
|
||||
->registerMapper(QueryHandlerMapper::class)
|
||||
->registerMapper(ConsoleCommandMapper::class)
|
||||
->registerMapper(AuthMapper::class)
|
||||
|
||||
->registerMapper(InitializerMapper::class);
|
||||
|
||||
// Hier können weitere Mapper registriert werden
|
||||
}
|
||||
|
||||
/**
|
||||
* Registriert alle AttributeCompiler
|
||||
*/
|
||||
private function registerCompilers(AttributeProcessorRegistry $registry): void
|
||||
{
|
||||
$registry
|
||||
->registerCompiler(RouteCompiler::class)
|
||||
->registerCompiler(CommandHandlerCompiler::class)
|
||||
->registerCompiler(\App\Framework\Core\Events\EventHandlerCompiler::class)
|
||||
->registerCompiler(\App\Framework\EventBus\EventHandlerCompiler::class)
|
||||
->registerCompiler(QueryHandlerCompiler::class);
|
||||
|
||||
// Hier können weitere Compiler registriert werden
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,16 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core;
|
||||
|
||||
/**
|
||||
* Interface für AttributeMapper mit optimierter Performance
|
||||
*/
|
||||
interface AttributeMapper
|
||||
{
|
||||
/**
|
||||
* Gibt die Attributklasse zurück, die dieser Mapper verarbeiten kann
|
||||
*/
|
||||
public function getAttributeClass(): string;
|
||||
|
||||
/**
|
||||
@@ -14,4 +19,15 @@ interface AttributeMapper
|
||||
* @return array|null
|
||||
*/
|
||||
public function map(object $reflectionTarget, object $attributeInstance): ?array;
|
||||
|
||||
/**
|
||||
* Prüft, ob dieses Attribut auf dem Reflector verarbeitet werden kann
|
||||
* (Schnelle Prüfung ohne vollständige Verarbeitung)
|
||||
*/
|
||||
#public function canProcess(\Reflector $reflector): bool;
|
||||
|
||||
/**
|
||||
* Gibt Metadaten über das Attribut zurück (für Debugging und Caching)
|
||||
*/
|
||||
#public function getAttributeMetadata(): array;
|
||||
}
|
||||
|
||||
31
src/Framework/Core/AttributeMapperLocator.php
Normal file
31
src/Framework/Core/AttributeMapperLocator.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core;
|
||||
|
||||
use RecursiveDirectoryIterator;
|
||||
use RecursiveIteratorIterator;
|
||||
use ReflectionClass;
|
||||
|
||||
final readonly class AttributeMapperLocator
|
||||
{
|
||||
private InterfaceImplementationLocator $locator;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->locator = new InterfaceImplementationLocator();
|
||||
}
|
||||
|
||||
/**
|
||||
* Findet alle Klassen, die das AttributeMapper-Interface implementieren
|
||||
*
|
||||
* @param string $directory Das Basisverzeichnis, in dem gesucht werden soll
|
||||
* @return AttributeMapper[] Array von AttributeMapper-Instanzen
|
||||
*/
|
||||
public function locateMappers(string $directory): array
|
||||
{
|
||||
/** @var AttributeMapper[] $mappers */
|
||||
$mappers = $this->locator->locate($directory, AttributeMapper::class);
|
||||
return $mappers;
|
||||
}
|
||||
}
|
||||
270
src/Framework/Core/AttributeMappingVisitor.php
Normal file
270
src/Framework/Core/AttributeMappingVisitor.php
Normal file
@@ -0,0 +1,270 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core;
|
||||
|
||||
use App\Framework\Cache\Cache;
|
||||
use App\Framework\Discovery\FileVisitor;
|
||||
|
||||
/**
|
||||
* Visitor zum Verarbeiten von Attributen in Klassen
|
||||
*/
|
||||
final class AttributeMappingVisitor implements FileVisitor
|
||||
{
|
||||
private array $attributeMappers = [];
|
||||
private array $mappedAttributes = [];
|
||||
private array $mapperByClass = [];
|
||||
private array $processingStats = [];
|
||||
|
||||
/**
|
||||
* @param array $attributeMappers Array von AttributeMapper-Instanzen
|
||||
*/
|
||||
public function __construct(array $attributeMappers = [])
|
||||
{
|
||||
$this->attributeMappers = $attributeMappers;
|
||||
$this->buildMapperIndex();
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt einen Index der Mapper nach Attributklasse für schnelleren Zugriff
|
||||
*/
|
||||
private function buildMapperIndex(): void
|
||||
{
|
||||
$this->mapperByClass = [];
|
||||
foreach ($this->attributeMappers as $mapper) {
|
||||
$attributeClass = $mapper->getAttributeClass();
|
||||
$this->mapperByClass[$attributeClass] = $mapper;
|
||||
}
|
||||
}
|
||||
|
||||
public function onScanStart(): void
|
||||
{
|
||||
$this->mappedAttributes = [];
|
||||
$this->processingStats = [
|
||||
'total_classes' => 0,
|
||||
'classes_with_attributes' => 0,
|
||||
'total_attributes' => 0,
|
||||
'processed_attributes' => 0,
|
||||
'skipped_reflectors' => 0
|
||||
];
|
||||
}
|
||||
|
||||
public function onIncrementalScanStart(): void
|
||||
{
|
||||
// Bei inkrementellem Scan behalten wir die vorhandenen Mappings bei
|
||||
$this->processingStats = [
|
||||
'total_classes' => 0,
|
||||
'classes_with_attributes' => 0,
|
||||
'total_attributes' => 0,
|
||||
'processed_attributes' => 0,
|
||||
'skipped_reflectors' => 0
|
||||
];
|
||||
}
|
||||
|
||||
public function onIncrementalScanComplete(): void
|
||||
{
|
||||
// Logging der Verarbeitungsstatistiken für inkrementellen Scan
|
||||
error_log("Inkrementelle Attribut-Verarbeitung abgeschlossen: " . json_encode($this->processingStats));
|
||||
}
|
||||
|
||||
public function visitClass(string $className, string $filePath): void
|
||||
{
|
||||
if (!class_exists($className)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->processingStats['total_classes']++;
|
||||
$hasAttributes = false;
|
||||
|
||||
try {
|
||||
$reflection = new \ReflectionClass($className);
|
||||
|
||||
// Schnelle Vorprüfung: Hat die Klasse überhaupt relevante Attribute?
|
||||
$relevantAttributes = $this->hasRelevantAttributes($reflection);
|
||||
if (!$relevantAttributes) {
|
||||
return;
|
||||
}
|
||||
|
||||
$hasAttributes = true;
|
||||
$this->processingStats['classes_with_attributes']++;
|
||||
|
||||
// Verarbeite alle Attribute auf Klassenebene
|
||||
$this->processAttributes($reflection->getAttributes(), $reflection);
|
||||
|
||||
// Verarbeite alle Methoden
|
||||
foreach ($reflection->getMethods() as $method) {
|
||||
// Schnelle Vorprüfung für jede Methode
|
||||
if ($this->hasRelevantAttributes($method)) {
|
||||
$this->processAttributes($method->getAttributes(), $method);
|
||||
}
|
||||
}
|
||||
|
||||
// Verarbeite alle Eigenschaften
|
||||
foreach ($reflection->getProperties() as $property) {
|
||||
// Schnelle Vorprüfung für jede Eigenschaft
|
||||
if ($this->hasRelevantAttributes($property)) {
|
||||
$this->processAttributes($property->getAttributes(), $property);
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// Fehler beim Verarbeiten der Klasse protokollieren
|
||||
error_log("Fehler bei der Attributverarbeitung von {$className}: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Schnelle Vorprüfung, ob ein Reflector relevante Attribute hat
|
||||
*/
|
||||
private function hasRelevantAttributes(\Reflector $reflector): bool
|
||||
{
|
||||
if (method_exists($reflector, 'getAttributes')) {
|
||||
$attributes = $reflector->getAttributes();
|
||||
$this->processingStats['total_attributes'] += count($attributes);
|
||||
|
||||
if (empty($attributes)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($attributes as $attribute) {
|
||||
$attributeClass = $attribute->getName();
|
||||
if (isset($this->mapperByClass[$attributeClass])) {
|
||||
$mapper = $this->mapperByClass[$attributeClass];
|
||||
if ($mapper->canProcess($reflector)) {
|
||||
return true;
|
||||
} else {
|
||||
$this->processingStats['skipped_reflectors']++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function processAttributes(array $attributes, \Reflector $reflector): void
|
||||
{
|
||||
foreach ($attributes as $attribute) {
|
||||
$attributeClass = $attribute->getName();
|
||||
|
||||
// Direkter Zugriff auf den Mapper über den Index
|
||||
if (isset($this->mapperByClass[$attributeClass])) {
|
||||
$mapper = $this->mapperByClass[$attributeClass];
|
||||
|
||||
// Zusätzliche Prüfung, ob der Mapper diesen Reflector verarbeiten kann
|
||||
if ($mapper->canProcess($reflector)) {
|
||||
try {
|
||||
$attributeInstance = $attribute->newInstance();
|
||||
$mapper->map($attributeInstance, $reflector);
|
||||
$this->processingStats['processed_attributes']++;
|
||||
|
||||
// Speichere für Caching
|
||||
if (!isset($this->mappedAttributes[$attributeClass])) {
|
||||
$this->mappedAttributes[$attributeClass] = [];
|
||||
}
|
||||
|
||||
// Serialisierbare Repräsentation des Reflectors
|
||||
$reflectorKey = $this->serializeReflector($reflector);
|
||||
|
||||
$this->mappedAttributes[$attributeClass][] = [
|
||||
'attribute' => $attributeInstance,
|
||||
'reflector' => $reflectorKey,
|
||||
'metadata' => $mapper->getAttributeMetadata()
|
||||
];
|
||||
} catch (\Throwable $e) {
|
||||
error_log("Fehler beim Verarbeiten des Attributs {$attributeClass}: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt eine serialisierbare Repräsentation eines Reflectors
|
||||
*/
|
||||
private function serializeReflector(\Reflector $reflector): array
|
||||
{
|
||||
if ($reflector instanceof \ReflectionClass) {
|
||||
return [
|
||||
'type' => 'class',
|
||||
'name' => $reflector->getName()
|
||||
];
|
||||
} elseif ($reflector instanceof \ReflectionMethod) {
|
||||
return [
|
||||
'type' => 'method',
|
||||
'class' => $reflector->getDeclaringClass()->getName(),
|
||||
'name' => $reflector->getName()
|
||||
];
|
||||
} elseif ($reflector instanceof \ReflectionProperty) {
|
||||
return [
|
||||
'type' => 'property',
|
||||
'class' => $reflector->getDeclaringClass()->getName(),
|
||||
'name' => $reflector->getName()
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'type' => 'unknown',
|
||||
'name' => method_exists($reflector, 'getName') ? $reflector->getName() : 'unknown'
|
||||
];
|
||||
}
|
||||
|
||||
public function onScanComplete(): void
|
||||
{
|
||||
// Logging der Verarbeitungsstatistiken
|
||||
error_log("Attribut-Verarbeitung abgeschlossen: " . json_encode($this->processingStats));
|
||||
}
|
||||
|
||||
public function loadFromCache(Cache $cache): void
|
||||
{
|
||||
$cacheItem = $cache->get($this->getCacheKey());
|
||||
if ($cacheItem->isHit) {
|
||||
$this->mappedAttributes = $cacheItem->value;
|
||||
}
|
||||
}
|
||||
|
||||
public function getCacheKey(): string
|
||||
{
|
||||
return 'attribute_mappings';
|
||||
}
|
||||
|
||||
public function getCacheableData(): mixed
|
||||
{
|
||||
return $this->mappedAttributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fügt einen weiteren AttributeMapper hinzu
|
||||
*/
|
||||
public function addAttributeMapper(AttributeMapper $mapper): void
|
||||
{
|
||||
$this->attributeMappers[] = $mapper;
|
||||
|
||||
// Aktualisiere den Mapper-Index
|
||||
$attributeClass = $mapper->getAttributeClass();
|
||||
$this->mapperByClass[$attributeClass] = $mapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt alle Attribute eines bestimmten Typs zurück
|
||||
*/
|
||||
public function getAttributesOfType(string $attributeClass): array
|
||||
{
|
||||
return $this->mappedAttributes[$attributeClass] ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt alle gemappten Attribute zurück
|
||||
*/
|
||||
public function getAllMappedAttributes(): array
|
||||
{
|
||||
return $this->mappedAttributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt die Verarbeitungsstatistiken zurück
|
||||
*/
|
||||
public function getProcessingStats(): array
|
||||
{
|
||||
return $this->processingStats;
|
||||
}
|
||||
}
|
||||
85
src/Framework/Core/AttributeProcessor.php
Normal file
85
src/Framework/Core/AttributeProcessor.php
Normal file
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core;
|
||||
|
||||
use ReflectionClass;
|
||||
use ReflectionMethod;
|
||||
|
||||
final readonly class AttributeProcessor
|
||||
{
|
||||
/** @var array<string, AttributeMapper> */
|
||||
private array $mapperMap;
|
||||
|
||||
/**
|
||||
* @param AttributeMapper[] $mappers
|
||||
*/
|
||||
public function __construct(array $mappers)
|
||||
{
|
||||
$mapperMap = [];
|
||||
foreach ($mappers as $mapper) {
|
||||
$mapperMap[$mapper->getAttributeClass()] = $mapper;
|
||||
}
|
||||
$this->mapperMap = $mapperMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verarbeitet alle Attribute einer Klasse
|
||||
*/
|
||||
public function processClass(ReflectionClass $refClass, array &$results): void
|
||||
{
|
||||
$this->processAttributes($refClass, $results);
|
||||
|
||||
foreach ($refClass->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
|
||||
$this->processAttributes($method, $results);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verarbeitet die Attribute eines Elements (Klasse oder Methode)
|
||||
*/
|
||||
private function processAttributes(ReflectionClass|ReflectionMethod $ref, array &$results): void
|
||||
{
|
||||
foreach ($ref->getAttributes() as $attribute) {
|
||||
$attrName = $attribute->getName();
|
||||
if (!isset($this->mapperMap[$attrName])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$mapped = $this->mapperMap[$attrName]->map($ref, $attribute->newInstance());
|
||||
|
||||
if ($mapped !== null && $ref instanceof ReflectionMethod) {
|
||||
$mapped['parameters'] = $this->extractMethodParameters($ref);
|
||||
}
|
||||
|
||||
if ($mapped !== null) {
|
||||
$results[$attrName][] = $mapped;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extrahiert Parameter-Informationen aus einer Methode
|
||||
*/
|
||||
private function extractMethodParameters(ReflectionMethod $method): array
|
||||
{
|
||||
$params = [];
|
||||
foreach ($method->getParameters() as $param) {
|
||||
$paramData = [
|
||||
'name' => $param->getName(),
|
||||
'type' => $param->getType()?->getName(),
|
||||
'isBuiltin' => $param->getType()?->isBuiltin() ?? false,
|
||||
'hasDefault' => $param->isDefaultValueAvailable(),
|
||||
'default' => $param->isDefaultValueAvailable() ? $param->getDefaultValue() : null,
|
||||
'attributes' => array_map(
|
||||
fn ($a) => $a->getName(),
|
||||
$param->getAttributes()
|
||||
),
|
||||
];
|
||||
$params[] = $paramData;
|
||||
}
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
}
|
||||
218
src/Framework/Core/ClassParser.php
Normal file
218
src/Framework/Core/ClassParser.php
Normal file
@@ -0,0 +1,218 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core;
|
||||
|
||||
/**
|
||||
* Parser für PHP-Klassendateien ohne externe Abhängigkeiten
|
||||
*/
|
||||
final class ClassParser
|
||||
{
|
||||
private static array $classCache = [];
|
||||
private static array $tokenCache = [];
|
||||
|
||||
/**
|
||||
* Gibt alle Klassen, Interfaces und Traits in einer Datei zurück
|
||||
*
|
||||
* @param string $file Pfad zur PHP-Datei
|
||||
* @return array Namen der in der Datei enthaltenen Klassen
|
||||
*/
|
||||
public static function getClassesInFile(string $file): array
|
||||
{
|
||||
// Prüfe Cache
|
||||
$cacheKey = md5($file . filemtime($file));
|
||||
if (isset(self::$classCache[$cacheKey])) {
|
||||
return self::$classCache[$cacheKey];
|
||||
}
|
||||
|
||||
$classes = [];
|
||||
$namespace = '';
|
||||
$tokens = self::getTokens($file);
|
||||
|
||||
if ($tokens === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$count = count($tokens);
|
||||
$i = 0;
|
||||
|
||||
// Durchlaufe alle Tokens
|
||||
while ($i < $count) {
|
||||
if ($tokens[$i][0] === T_NAMESPACE) {
|
||||
// Namespace finden
|
||||
$namespace = self::parseNamespace($tokens, $i);
|
||||
#debug("Found namespace: '$namespace' in file: $file");
|
||||
} elseif ($tokens[$i][0] === T_CLASS || $tokens[$i][0] === T_INTERFACE || $tokens[$i][0] === T_TRAIT) {
|
||||
// Klasse/Interface/Trait finden
|
||||
$className = self::parseClassName($tokens, $i);
|
||||
if ($className !== null) {
|
||||
#debug("Found class: '$className', current namespace: '$namespace'");
|
||||
|
||||
if (empty($namespace)) {
|
||||
// Fallback: Versuche Namespace aus Dateiinhalt zu extrahieren
|
||||
$namespace = self::extractNamespaceFromContent($file);
|
||||
#debug("Fallback namespace from content: '$namespace'");
|
||||
}
|
||||
|
||||
if (!empty($namespace)) {
|
||||
$fullClassName = '\\' . trim($namespace, '\\') . '\\' . $className;
|
||||
} else {
|
||||
$fullClassName = '\\' . $className;
|
||||
}
|
||||
|
||||
#debug("Final class name: '$fullClassName'");
|
||||
$classes[] = $fullClassName;
|
||||
}
|
||||
}
|
||||
|
||||
$i++;
|
||||
}
|
||||
|
||||
// Ergebnis cachen
|
||||
self::$classCache[$cacheKey] = $classes;
|
||||
|
||||
#debug($classes);
|
||||
|
||||
return $classes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parst den Namespace aus den Tokens
|
||||
*/
|
||||
private static function parseNamespace(array $tokens, int &$i): string
|
||||
{
|
||||
$namespace = '';
|
||||
$originalI = $i;
|
||||
$i++; // T_NAMESPACE Token überspringen
|
||||
|
||||
#debug("Starting namespace parse at token $i, T_NAMESPACE was at $originalI");
|
||||
|
||||
// Debug: Zeige die nächsten paar Tokens
|
||||
for ($debugI = $i; $debugI < min($i + 10, count($tokens)); $debugI++) {
|
||||
$token = $tokens[$debugI];
|
||||
}
|
||||
|
||||
while ($i < count($tokens)) {
|
||||
$token = $tokens[$i];
|
||||
|
||||
if (is_array($token)) {
|
||||
|
||||
if ($token[0] === T_STRING || $token[0] === T_NS_SEPARATOR || $token[0] === T_NAME_QUALIFIED) {
|
||||
$namespace .= $token[1];
|
||||
} elseif ($token[0] === T_WHITESPACE) {
|
||||
// Whitespace ignorieren, aber weitermachen
|
||||
} else {
|
||||
#debug("Breaking on token: " . token_name($token[0]));
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
if ($token === ';' || $token === '{') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$i++;
|
||||
}
|
||||
|
||||
$result = trim($namespace, '\\');
|
||||
#debug("Final parsed namespace: '$result'");
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parst den Klassennamen aus den Tokens
|
||||
*/
|
||||
private static function parseClassName(array $tokens, int &$i): ?string
|
||||
{
|
||||
$i += 1; // T_CLASS/T_INTERFACE/T_TRAIT Token überspringen
|
||||
|
||||
// Suche nach dem Namen (nächstes T_STRING Token)
|
||||
while ($i < count($tokens)) {
|
||||
if ($tokens[$i][0] === T_STRING) {
|
||||
return $tokens[$i][1];
|
||||
} elseif ($tokens[$i] === '{' || $tokens[$i] === ';') {
|
||||
// Unerwartetes Ende ohne Klassennamen
|
||||
return null;
|
||||
}
|
||||
|
||||
$i++;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Liest Tokens aus einer Datei mit Caching
|
||||
*/
|
||||
private static function getTokens(string $file): ?array
|
||||
{
|
||||
if (!file_exists($file)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$cacheKey = md5($file . filemtime($file));
|
||||
if (isset(self::$tokenCache[$cacheKey])) {
|
||||
return self::$tokenCache[$cacheKey];
|
||||
}
|
||||
|
||||
$code = file_get_contents($file);
|
||||
if ($code === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$tokens = token_get_all($code);
|
||||
self::$tokenCache[$cacheKey] = $tokens;
|
||||
|
||||
// Cache-Größe begrenzen
|
||||
if (count(self::$tokenCache) > 100) {
|
||||
// Entferne ältesten Eintrag
|
||||
array_shift(self::$tokenCache);
|
||||
}
|
||||
|
||||
return $tokens;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extrahiert Namespace aus Dateiinhalt als Fallback
|
||||
*/
|
||||
private static function extractNamespaceFromContent(string $file): string
|
||||
{
|
||||
$content = file_get_contents($file);
|
||||
if ($content === false) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Regex für namespace-Deklaration
|
||||
if (preg_match('/namespace\s+([^;]+);/', $content, $matches)) {
|
||||
return trim($matches[1]);
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Leert den internen Cache
|
||||
*/
|
||||
public static function clearCache(): void
|
||||
{
|
||||
self::$classCache = [];
|
||||
self::$tokenCache = [];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Extrahiert den Klassennamen (mit Namespace) aus einer PHP-Datei
|
||||
*/
|
||||
public function getClassNameFromFile(string $file): ?string
|
||||
{
|
||||
$contents = file_get_contents($file);
|
||||
if (
|
||||
preg_match('#namespace\s+([^;]+);#', $contents, $nsMatch)
|
||||
&& preg_match('/class\s+(\w+)/', $contents, $classMatch)
|
||||
) {
|
||||
return trim($nsMatch[1]) . '\\' . trim($classMatch[1]);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
38
src/Framework/Core/Commands/ClearDiscoveryCache.php
Normal file
38
src/Framework/Core/Commands/ClearDiscoveryCache.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\Core\Commands;
|
||||
|
||||
use App\Framework\Cache\Cache;
|
||||
use App\Framework\Console\ConsoleCommand;
|
||||
use App\Framework\Console\ConsoleInput;
|
||||
use App\Framework\Console\ConsoleOutput;
|
||||
use App\Framework\Core\PathProvider;
|
||||
use App\Framework\Debug\OutputInterface;
|
||||
|
||||
final readonly class ClearDiscoveryCache
|
||||
{
|
||||
public function __construct(
|
||||
private Cache $cache,
|
||||
private PathProvider $pathProvider)
|
||||
{
|
||||
}
|
||||
|
||||
#[ConsoleCommand('discovery:clear', 'Clears the discovery cache')]
|
||||
public function __invoke(): void
|
||||
{
|
||||
$this->cache->forget('discovery_service');
|
||||
}
|
||||
|
||||
#[ConsoleCommand('clear:routes', 'Clears the routes cache')]
|
||||
public function clearRoutes(ConsoleInput $input, ConsoleOutput $output): void
|
||||
{
|
||||
if(file_exists($this->pathProvider->resolvePath('/cache/routes.cache.php')) === false) {
|
||||
$output->writeError('Routes cache not found');
|
||||
return;
|
||||
}
|
||||
|
||||
unlink($this->pathProvider->resolvePath('/cache/routes.cache.php'));
|
||||
|
||||
$output->writeSuccess('Routes cache cleared');;
|
||||
}
|
||||
}
|
||||
104
src/Framework/Core/ContainerBootstrapper.php
Normal file
104
src/Framework/Core/ContainerBootstrapper.php
Normal file
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\Core;
|
||||
|
||||
use App\Framework\Cache\Cache;
|
||||
use App\Framework\Cache\CacheInitializer;
|
||||
use App\Framework\Config\Configuration;
|
||||
use App\Framework\DI\Container;
|
||||
use App\Framework\DI\Initializer;
|
||||
use App\Framework\Discovery\DiscoveryServiceBootstrapper;
|
||||
use App\Framework\Discovery\Results\DiscoveryResults;
|
||||
use App\Framework\Http\ResponseEmitter;
|
||||
use App\Framework\Performance\PerformanceMeter;
|
||||
use App\Framework\Attributes\Route;
|
||||
|
||||
final readonly class ContainerBootstrapper
|
||||
{
|
||||
public function __construct(
|
||||
private Container $container)
|
||||
{}
|
||||
public function bootstrap(
|
||||
string $basePath,
|
||||
PerformanceMeter $meter,
|
||||
array $config = []): void
|
||||
{
|
||||
// Kern-Dienste im Container registrieren:
|
||||
$this->container->instance(PerformanceMeter::class, $meter);
|
||||
$this->container->instance(Cache::class, new CacheInitializer()());
|
||||
$this->container->instance(Configuration::class, new Configuration($config));
|
||||
$this->container->instance(PathProvider::class, new PathProvider($basePath));
|
||||
$this->container->instance(ResponseEmitter::class, new ResponseEmitter());
|
||||
|
||||
$this->autowire($this->container->get(PathProvider::class), $this->container->get(Configuration::class));
|
||||
}
|
||||
|
||||
private function autowire(PathProvider $pathProvider, Configuration $configuration):void
|
||||
{
|
||||
// Im ContainerBootstrapper
|
||||
$bootstrapper = new DiscoveryServiceBootstrapper($this->container);
|
||||
$results = $bootstrapper->bootstrap();
|
||||
|
||||
// Ergebnisse sind automatisch im Container verfügbar
|
||||
/** @var DiscoveryResults $results */
|
||||
$results = $this->container->get(DiscoveryResults::class);
|
||||
|
||||
#$routeResults = $results->get(Route::class);
|
||||
|
||||
|
||||
// Fallback: Versuche auch mit führendem Backslash
|
||||
/*if (empty($routeResults)) {
|
||||
$routeResults = $results->get('\\' . $routeClass);
|
||||
debug('Tried with leading backslash');
|
||||
}*/
|
||||
|
||||
// Fallback: Suche in allen verfügbaren Keys
|
||||
/*if (empty($routeResults)) {
|
||||
foreach (array_keys($results->getAllAttributeResults()) as $key) {
|
||||
if (str_contains($key, 'Route')) {
|
||||
debug("Found Route-related key: $key");
|
||||
$routeResults = $results->get($key);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
#$discovery = new AttributeDiscoveryService($this->container, $pathProvider, $configuration);
|
||||
|
||||
#$dresults = ProcessedResults::fromArray($discovery->discoverAndProcess())->get(Route::class);
|
||||
|
||||
#debug(count($routeResults));
|
||||
#dd(count($dresults));
|
||||
|
||||
|
||||
|
||||
|
||||
#$cache = $this->container->get(Cache::class);
|
||||
|
||||
#$cache->forget('discovery_service');
|
||||
|
||||
#$processedResults = $cache->remember('discovery_service', function () use ($discovery) {
|
||||
#
|
||||
# // Attribute entdecken und verarbeiten
|
||||
# return $discovery->discoverAndProcess();
|
||||
#
|
||||
# })->value;
|
||||
|
||||
#$results = ProcessedResults::fromArray($processedResults);
|
||||
|
||||
#$this->container->instance(ProcessedResults::class, $results);
|
||||
|
||||
#$this->executeInitializers($results);
|
||||
|
||||
}
|
||||
|
||||
private function executeInitializers(DiscoveryResults $initializerClasses): void
|
||||
{
|
||||
foreach ($initializerClasses->get(Initializer::class) as $initializerClass) {
|
||||
|
||||
$instance = $this->container->invoker->invoke($initializerClass['class'], $initializerClass['method']);
|
||||
|
||||
$this->container->instance($initializerClass['return'], $instance);
|
||||
}
|
||||
}
|
||||
}
|
||||
45
src/Framework/Core/DOKUMENTATION.md
Normal file
45
src/Framework/Core/DOKUMENTATION.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# Core-Modul Dokumentation
|
||||
|
||||
## Übersicht
|
||||
|
||||
Das Core-Modul bildet das Herzstück des Frameworks und stellt grundlegende Funktionalitäten bereit, die von anderen Modulen genutzt werden.
|
||||
|
||||
## Hauptkomponenten
|
||||
|
||||
### Events und EventDispatcher
|
||||
|
||||
Das Event-System ermöglicht die Kommunikation zwischen Komponenten über einen zentralen Event-Bus.
|
||||
|
||||
**Kernklassen:**
|
||||
- `EventDispatcher`: Zentraler Service zum Registrieren und Auslösen von Events
|
||||
- Bekannte Events:
|
||||
- `ApplicationBooted`
|
||||
- `ErrorOccurred`
|
||||
- `BeforeHandleRequest`
|
||||
- `AfterHandleRequest`
|
||||
|
||||
**Beispielverwendung:**
|
||||
```php
|
||||
// Event-Handler registrieren
|
||||
$eventDispatcher->addHandler('App\Framework\Core\Events\ApplicationBooted', function($event) {
|
||||
// Event verarbeiten
|
||||
});
|
||||
```
|
||||
|
||||
### PathProvider
|
||||
|
||||
Stellt Pfadinformationen für verschiedene Bereiche der Anwendung bereit.
|
||||
|
||||
**Hauptfunktionen:**
|
||||
- `getDataPath()`: Liefert Pfade zu Datenverzeichnissen
|
||||
|
||||
## Integration mit anderen Modulen
|
||||
|
||||
Das Core-Modul wird von vielen anderen Modulen verwendet, wie z.B.:
|
||||
|
||||
- **Analytics-Modul**: Nutzt den EventDispatcher zum Tracking von Systemereignissen
|
||||
- **DI-Container**: Nutzt Core-Komponenten für die Initialisierung von Services
|
||||
|
||||
## Architektur
|
||||
|
||||
Das Core-Modul folgt einer ereignisgesteuerten Architektur, bei der Komponenten über Events miteinander kommunizieren können, anstatt direkte Abhängigkeiten zu haben.
|
||||
@@ -4,168 +4,89 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core;
|
||||
|
||||
use App\Framework\Discovery\FileScanner;
|
||||
use Exception;
|
||||
use RecursiveDirectoryIterator;
|
||||
use RecursiveIteratorIterator;
|
||||
use ReflectionClass;
|
||||
use ReflectionMethod;
|
||||
|
||||
class Discovery
|
||||
{
|
||||
/** @var array<string, AttributeMapper> */
|
||||
private array $mapperMap;
|
||||
|
||||
private string $cacheFile;
|
||||
private AttributeProcessor $attributeProcessor;
|
||||
private FileScanner $fileScanner;
|
||||
private ClassParser $classParser;
|
||||
private DiscoveryCacheManager $cacheManager;
|
||||
|
||||
/**
|
||||
* @param AttributeMapper[] $mappers
|
||||
*/
|
||||
public function __construct(AttributeMapper ...$mappers)
|
||||
{
|
||||
$this->mapperMap = [];
|
||||
foreach ($mappers as $mapper) {
|
||||
$this->mapperMap[$mapper->getAttributeClass()] = $mapper;
|
||||
public function __construct(
|
||||
?string $srcDir = null,
|
||||
AttributeMapper ...$mappers
|
||||
) {
|
||||
// Automatisches Auffinden der Mapper, wenn ein Quellverzeichnis angegeben ist
|
||||
if ($srcDir !== null) {
|
||||
$locator = new AttributeMapperLocator();
|
||||
$autoMappers = $locator->locateMappers($srcDir);
|
||||
$mappers = array_merge($mappers, $autoMappers);
|
||||
}
|
||||
|
||||
$this->cacheFile = __DIR__ .'/../../../cache/discovery.cache.php';
|
||||
// Initialisierung der Komponenten
|
||||
$this->attributeProcessor = new AttributeProcessor($mappers);
|
||||
$this->fileScanner = new FileScanner();
|
||||
$this->classParser = new ClassParser();
|
||||
$this->cacheManager = new DiscoveryCacheManager();
|
||||
}
|
||||
|
||||
/**
|
||||
* Entdeckt und verarbeitet alle Attribute im angegebenen Verzeichnis
|
||||
*/
|
||||
public function discover(string $directory): array
|
||||
{
|
||||
$hash = md5(realpath($directory));
|
||||
$cacheFile = $this->cacheManager->getCachePathForDirectory($directory);
|
||||
$latestMTime = $this->fileScanner->getLatestMTime($directory);
|
||||
|
||||
$this->cacheFile = __DIR__ ."/../../../cache/discovery_{$hash}.cache.php";
|
||||
|
||||
$latestMTime = $this->getLatestMTime($directory);
|
||||
$data = $this->loadCache($latestMTime);
|
||||
// Versuchen, aus dem Cache zu laden
|
||||
$data = $this->cacheManager->loadCache($cacheFile, $latestMTime);
|
||||
if ($data !== null) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
$results = [];
|
||||
$rii = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($directory));
|
||||
|
||||
foreach ($rii as $file) {
|
||||
if (! $file->isFile() || $file->getExtension() !== 'php') {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$className = $this->getClassNameFromFile($file->getPathname());
|
||||
|
||||
if (! $className || ! class_exists($className)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$refClass = new ReflectionClass($className);
|
||||
|
||||
$this->discoverClass($refClass, $results);
|
||||
|
||||
} catch (Exception $e) {
|
||||
error_log("Discovery Warning: Fehler in Datei {$file->getPathname()}: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
// Keine Cache-Daten verfügbar, führe vollständige Entdeckung durch
|
||||
$results = $this->performDiscovery($directory);
|
||||
|
||||
// Speichere Ergebnisse im Cache
|
||||
$results['__discovery_mtime'] = $latestMTime;
|
||||
$this->storeCache($results);
|
||||
$this->cacheManager->storeCache($cacheFile, $results);
|
||||
|
||||
// Entferne Metadaten für die Rückgabe
|
||||
unset($results['__discovery_mtime']);
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
private function discoverClass(ReflectionClass $refClass, array &$results): void
|
||||
/**
|
||||
* Führt die eigentliche Entdeckung durch
|
||||
*/
|
||||
private function performDiscovery(string $directory): array
|
||||
{
|
||||
$this->processAttributes($refClass, $results);
|
||||
$results = [];
|
||||
|
||||
foreach ($refClass->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
|
||||
$this->processAttributes($method, $results);
|
||||
}
|
||||
}
|
||||
foreach ($this->fileScanner->scanDirectory($directory) as $file) {
|
||||
try {
|
||||
$className = $this->classParser->getClassNameFromFile($file->getPathname());
|
||||
|
||||
private function processAttributes(ReflectionClass|ReflectionMethod $ref, array &$results): void
|
||||
{
|
||||
foreach ($ref->getAttributes() as $attribute) {
|
||||
$attrName = $attribute->getName();
|
||||
if (! isset($this->mapperMap[$attrName])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$mapped = $this->mapperMap[$attrName]->map($ref, $attribute->newInstance());
|
||||
|
||||
if ($mapped !== null && $ref instanceof ReflectionMethod) {
|
||||
$params = [];
|
||||
foreach ($ref->getParameters() as $param) {
|
||||
$paramData = [
|
||||
'name' => $param->getName(),
|
||||
'type' => $param->getType()?->getName(),
|
||||
'isBuiltin' => $param->getType()?->isBuiltin() ?? false,
|
||||
'hasDefault' => $param->isDefaultValueAvailable(),
|
||||
'default' => $param->isDefaultValueAvailable() ? $param->getDefaultValue() : null,
|
||||
'attributes' => array_map(
|
||||
fn ($a) => $a->getName(),
|
||||
$param->getAttributes()
|
||||
),
|
||||
];
|
||||
$params[] = $paramData;
|
||||
if (!$className || !class_exists($className)) {
|
||||
continue;
|
||||
}
|
||||
$mapped['parameters'] = $params;
|
||||
}
|
||||
|
||||
if ($mapped !== null) {
|
||||
$results[$attrName][] = $mapped;
|
||||
}
|
||||
}
|
||||
}
|
||||
$refClass = new ReflectionClass($className);
|
||||
$this->attributeProcessor->processClass($refClass, $results);
|
||||
|
||||
public function getLatestMTime(string $directory): int
|
||||
{
|
||||
$maxMTime = 0;
|
||||
$rii = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($directory));
|
||||
foreach ($rii as $file) {
|
||||
if ($file->isFile() && $file->getExtension() === 'php') {
|
||||
$mtime = $file->getMTime();
|
||||
if ($mtime > $maxMTime) {
|
||||
$maxMTime = $mtime;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
error_log("Discovery Warnung: Fehler in Datei {$file->getPathname()}: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return $maxMTime;
|
||||
return $results;
|
||||
}
|
||||
|
||||
private function loadCache(int $latestMTime): ?array
|
||||
{
|
||||
if (! file_exists($this->cacheFile)) {
|
||||
return null;
|
||||
}
|
||||
$data = include $this->cacheFile;
|
||||
if (
|
||||
! is_array($data)
|
||||
|| ! isset($data['__discovery_mtime'])
|
||||
|| $data['__discovery_mtime'] !== $latestMTime
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
unset($data['__discovery_mtime']);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
private function storeCache(array $results): void
|
||||
{
|
||||
$export = var_export($results, true);
|
||||
file_put_contents($this->cacheFile, "<?php\nreturn {$export};\n");
|
||||
}
|
||||
|
||||
private function getClassNameFromFile(string $file): ?string
|
||||
{
|
||||
$contents = file_get_contents($file);
|
||||
if (
|
||||
preg_match('#namespace\s+([^;]+);#', $contents, $nsMatch)
|
||||
&& preg_match('/class\s+(\w+)/', $contents, $classMatch)
|
||||
) {
|
||||
return trim($nsMatch[1]) . '\\' . trim($classMatch[1]);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
55
src/Framework/Core/DiscoveryCacheManager.php
Normal file
55
src/Framework/Core/DiscoveryCacheManager.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core;
|
||||
|
||||
use RecursiveIteratorIterator;
|
||||
|
||||
class DiscoveryCacheManager
|
||||
|
||||
{
|
||||
private string $baseCachePath;
|
||||
|
||||
public function __construct(?string $baseCachePath = null)
|
||||
{
|
||||
$this->baseCachePath = $baseCachePath ?? __DIR__ . '/../../../cache';
|
||||
}
|
||||
|
||||
public function getCachePathForDirectory(string $directory): string
|
||||
{
|
||||
$hash = md5(realpath($directory));
|
||||
return $this->baseCachePath . "/discovery_{$hash}.cache.php";
|
||||
}
|
||||
|
||||
public function loadCache(string $cacheFile, int $latestMTime): ?array
|
||||
{
|
||||
if (!file_exists($cacheFile)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$data = include $cacheFile;
|
||||
if (
|
||||
!is_array($data)
|
||||
|| !isset($data['__discovery_mtime'])
|
||||
|| $data['__discovery_mtime'] !== $latestMTime
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
unset($data['__discovery_mtime']);
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function storeCache(string $cacheFile, array $results): void
|
||||
{
|
||||
// Stellen Sie sicher, dass das Verzeichnis existiert
|
||||
$cacheDir = dirname($cacheFile);
|
||||
if (!is_dir($cacheDir)) {
|
||||
mkdir($cacheDir, 0775, true);
|
||||
}
|
||||
|
||||
$export = var_export($results, true);
|
||||
file_put_contents($cacheFile, "<?php\nreturn {$export};\n");
|
||||
}
|
||||
}
|
||||
@@ -8,10 +8,13 @@ final readonly class DynamicRoute implements Route
|
||||
{
|
||||
public function __construct(
|
||||
public string $regex,
|
||||
public array $paramNames,
|
||||
public array $paramNames, // ['id', 'userId'] - URL-Parameter-Namen
|
||||
public string $controller,
|
||||
public string $method,
|
||||
public array $parameters
|
||||
) {
|
||||
}
|
||||
public string $action,
|
||||
public array $parameters,
|
||||
public array $paramValues = [], // ['id' => '123', 'userId' => '456'] - URL-Parameter-Werte
|
||||
public string $name = '',
|
||||
public string $path = '',
|
||||
public array $attributes = []
|
||||
) {}
|
||||
}
|
||||
|
||||
218
src/Framework/Core/ErrorHandler/GlobalErrorHandler.php
Normal file
218
src/Framework/Core/ErrorHandler/GlobalErrorHandler.php
Normal file
@@ -0,0 +1,218 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core\ErrorHandler;
|
||||
|
||||
use App\Application\Security\ExceptionHandlers\SecurityExceptionHandler;
|
||||
use App\Application\Security\Events\System\SystemAnomalyEvent;
|
||||
use App\Framework\Core\Events\EventDispatcher;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Throwable;
|
||||
|
||||
final class GlobalErrorHandler
|
||||
{
|
||||
public function __construct(
|
||||
private LoggerInterface $logger,
|
||||
private EventDispatcher $eventDispatcher,
|
||||
private SecurityExceptionHandler $securityHandler,
|
||||
private bool $debugMode = false
|
||||
) {}
|
||||
|
||||
public function handleException(Throwable $exception): void
|
||||
{
|
||||
// Zuerst Security Events dispatchen
|
||||
$this->handleSecurityAspects($exception);
|
||||
|
||||
// Dann Standard Error Handling
|
||||
$this->logException($exception);
|
||||
|
||||
if ($this->debugMode) {
|
||||
$this->displayDebugInfo($exception);
|
||||
} else {
|
||||
$this->displayUserFriendlyError($exception);
|
||||
}
|
||||
}
|
||||
|
||||
public function handleError(int $severity, string $message, string $file, int $line): bool
|
||||
{
|
||||
// PHP Errors in Exceptions umwandeln für einheitliche Behandlung
|
||||
$exception = new \ErrorException($message, 0, $severity, $file, $line);
|
||||
|
||||
// Security relevante Errors erkennen
|
||||
if ($this->isSecurityRelevantError($severity, $message)) {
|
||||
$this->handleSecurityAspects($exception);
|
||||
}
|
||||
|
||||
$this->logError($severity, $message, $file, $line);
|
||||
|
||||
// true = Error wurde behandelt, false = PHP default error handler
|
||||
return !$this->debugMode;
|
||||
}
|
||||
|
||||
public function handleFatalError(): void
|
||||
{
|
||||
$error = error_get_last();
|
||||
if ($error && $this->isFatal($error['type'])) {
|
||||
$exception = new \ErrorException(
|
||||
$error['message'],
|
||||
0,
|
||||
$error['type'],
|
||||
$error['file'],
|
||||
$error['line']
|
||||
);
|
||||
|
||||
$this->handleSecurityAspects($exception);
|
||||
$this->logException($exception);
|
||||
|
||||
if (!$this->debugMode) {
|
||||
$this->displayFatalErrorPage();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function handleSecurityAspects(Throwable $exception): void
|
||||
{
|
||||
try {
|
||||
// Delegiere an speziellen Security Exception Handler
|
||||
$this->securityHandler->handle($exception);
|
||||
|
||||
// Zusätzlich: System-weite Anomalien erkennen
|
||||
if ($this->isSystemAnomaly($exception)) {
|
||||
$this->dispatchSystemAnomaly($exception);
|
||||
}
|
||||
|
||||
} catch (Throwable $securityHandlingException) {
|
||||
// Fallback: Security Handler selbst darf nicht crashen
|
||||
$this->logger->critical('Security exception handler failed', [
|
||||
'original_exception' => $exception->getMessage(),
|
||||
'security_handler_exception' => $securityHandlingException->getMessage(),
|
||||
'trace' => $securityHandlingException->getTraceAsString()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private function isSecurityRelevantError(int $severity, string $message): bool
|
||||
{
|
||||
// Sicherheitskritische PHP Errors identifizieren
|
||||
$securityKeywords = [
|
||||
'permission denied', 'access denied', 'authentication',
|
||||
'authorization', 'file_get_contents', 'file_put_contents',
|
||||
'openssl_', 'hash_', 'password_', 'session_',
|
||||
'mysqli_', 'pdo_', 'curl_', 'file_upload'
|
||||
];
|
||||
|
||||
$messageLower = strtolower($message);
|
||||
foreach ($securityKeywords as $keyword) {
|
||||
if (strpos($messageLower, $keyword) !== false) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Bestimmte Error-Level sind grundsätzlich sicherheitsrelevant
|
||||
return in_array($severity, [E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR]);
|
||||
}
|
||||
|
||||
private function isSystemAnomaly(Throwable $exception): bool
|
||||
{
|
||||
// System-Anomalien erkennen
|
||||
return $exception instanceof \Error ||
|
||||
$exception instanceof \OutOfMemoryError ||
|
||||
$exception instanceof \ParseError ||
|
||||
strpos($exception->getMessage(), 'Maximum execution time') !== false ||
|
||||
strpos($exception->getMessage(), 'Allowed memory size') !== false;
|
||||
}
|
||||
|
||||
private function dispatchSystemAnomaly(Throwable $exception): void
|
||||
{
|
||||
$metrics = [
|
||||
'memory_usage' => memory_get_usage(true),
|
||||
'memory_peak' => memory_get_peak_usage(true),
|
||||
'execution_time' => microtime(true) - ($_SERVER['REQUEST_TIME_FLOAT'] ?? microtime(true))
|
||||
];
|
||||
|
||||
$severity = match (true) {
|
||||
$exception instanceof \OutOfMemoryError => 'critical',
|
||||
$exception instanceof \Error => 'high',
|
||||
default => 'medium'
|
||||
};
|
||||
|
||||
$this->eventDispatcher->dispatch(new SystemAnomalyEvent(
|
||||
anomalyType: get_class($exception),
|
||||
description: $exception->getMessage(),
|
||||
metrics: $metrics,
|
||||
severity: $severity
|
||||
));
|
||||
}
|
||||
|
||||
private function isFatal(int $type): bool
|
||||
{
|
||||
return in_array($type, [E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_PARSE]);
|
||||
}
|
||||
|
||||
private function logException(Throwable $exception): void
|
||||
{
|
||||
$context = [
|
||||
'exception_class' => get_class($exception),
|
||||
'message' => $exception->getMessage(),
|
||||
'file' => $exception->getFile(),
|
||||
'line' => $exception->getLine(),
|
||||
'trace' => $exception->getTraceAsString(),
|
||||
'user_id' => $_SESSION['user_id'] ?? null,
|
||||
'request_uri' => $_SERVER['REQUEST_URI'] ?? null,
|
||||
'request_method' => $_SERVER['REQUEST_METHOD'] ?? null,
|
||||
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? null,
|
||||
'remote_addr' => $_SERVER['REMOTE_ADDR'] ?? null
|
||||
];
|
||||
|
||||
$this->logger->error('Unhandled exception: ' . $exception->getMessage(), $context);
|
||||
}
|
||||
|
||||
private function logError(int $severity, string $message, string $file, int $line): void
|
||||
{
|
||||
$level = $this->mapErrorSeverityToLogLevel($severity);
|
||||
|
||||
$this->logger->log($level, "PHP Error: {$message}", [
|
||||
'severity' => $severity,
|
||||
'file' => $file,
|
||||
'line' => $line,
|
||||
'user_id' => $_SESSION['user_id'] ?? null
|
||||
]);
|
||||
}
|
||||
|
||||
private function mapErrorSeverityToLogLevel(int $severity): string
|
||||
{
|
||||
return match ($severity) {
|
||||
E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR => 'error',
|
||||
E_WARNING, E_CORE_WARNING, E_COMPILE_WARNING, E_USER_WARNING => 'warning',
|
||||
E_NOTICE, E_USER_NOTICE => 'notice',
|
||||
E_STRICT => 'info',
|
||||
default => 'debug'
|
||||
};
|
||||
}
|
||||
|
||||
private function displayDebugInfo(Throwable $exception): void
|
||||
{
|
||||
echo "<h1>Exception: " . get_class($exception) . "</h1>";
|
||||
echo "<p><strong>Message:</strong> " . htmlspecialchars($exception->getMessage()) . "</p>";
|
||||
echo "<p><strong>File:</strong> " . htmlspecialchars($exception->getFile()) . ":" . $exception->getLine() . "</p>";
|
||||
echo "<pre>" . htmlspecialchars($exception->getTraceAsString()) . "</pre>";
|
||||
}
|
||||
|
||||
private function displayUserFriendlyError(Throwable $exception): void
|
||||
{
|
||||
http_response_code(500);
|
||||
echo "<!DOCTYPE html><html><head><title>Fehler</title></head><body>";
|
||||
echo "<h1>Es ist ein Fehler aufgetreten</h1>";
|
||||
echo "<p>Entschuldigung, es ist ein unerwarteter Fehler aufgetreten. Bitte versuchen Sie es später erneut.</p>";
|
||||
echo "</body></html>";
|
||||
}
|
||||
|
||||
private function displayFatalErrorPage(): void
|
||||
{
|
||||
http_response_code(500);
|
||||
echo "<!DOCTYPE html><html><head><title>Schwerwiegender Fehler</title></head><body>";
|
||||
echo "<h1>Schwerwiegender Systemfehler</h1>";
|
||||
echo "<p>Die Anwendung musste aufgrund eines kritischen Fehlers beendet werden.</p>";
|
||||
echo "</body></html>";
|
||||
}
|
||||
}
|
||||
8
src/Framework/Core/Events/AfterEmitResponse.php
Normal file
8
src/Framework/Core/Events/AfterEmitResponse.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\Core\Events;
|
||||
|
||||
class AfterEmitResponse
|
||||
{
|
||||
|
||||
}
|
||||
8
src/Framework/Core/Events/AfterHandleRequest.php
Normal file
8
src/Framework/Core/Events/AfterHandleRequest.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\Core\Events;
|
||||
|
||||
class AfterHandleRequest
|
||||
{
|
||||
|
||||
}
|
||||
14
src/Framework/Core/Events/ApplicationBooted.php
Normal file
14
src/Framework/Core/Events/ApplicationBooted.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core\Events;
|
||||
|
||||
final readonly class ApplicationBooted
|
||||
{
|
||||
public function __construct(
|
||||
public \DateTimeImmutable $bootTime,
|
||||
public string $environment,
|
||||
public string $version = 'dev',
|
||||
public \DateTimeImmutable $occurredAt = new \DateTimeImmutable()
|
||||
) {}
|
||||
}
|
||||
8
src/Framework/Core/Events/BeforeEmitResponse.php
Normal file
8
src/Framework/Core/Events/BeforeEmitResponse.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\Core\Events;
|
||||
|
||||
class BeforeEmitResponse
|
||||
{
|
||||
|
||||
}
|
||||
8
src/Framework/Core/Events/BeforeHandleRequest.php
Normal file
8
src/Framework/Core/Events/BeforeHandleRequest.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\Core\Events;
|
||||
|
||||
class BeforeHandleRequest
|
||||
{
|
||||
|
||||
}
|
||||
14
src/Framework/Core/Events/ErrorOccurred.php
Normal file
14
src/Framework/Core/Events/ErrorOccurred.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core\Events;
|
||||
|
||||
final readonly class ErrorOccurred
|
||||
{
|
||||
public function __construct(
|
||||
public \Throwable $error,
|
||||
public string $context = '',
|
||||
public ?string $requestId = null,
|
||||
public \DateTimeImmutable $occurredAt = new \DateTimeImmutable()
|
||||
) {}
|
||||
}
|
||||
40
src/Framework/Core/Events/EventCompilerPass.php
Normal file
40
src/Framework/Core/Events/EventCompilerPass.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core\Events;
|
||||
|
||||
use App\Framework\DI\DefaultContainer;
|
||||
|
||||
/**
|
||||
* Führt abschließende Konfigurationen für das Event-System durch
|
||||
*/
|
||||
final class EventCompilerPass
|
||||
{
|
||||
/**
|
||||
* Richtet zusätzliche Event-Konfigurationen ein
|
||||
*
|
||||
* @param DefaultContainer $container Der DI-Container
|
||||
* @param EventDispatcher $dispatcher Der EventDispatcher
|
||||
*/
|
||||
public static function process(DefaultContainer $container, EventDispatcher $dispatcher): void
|
||||
{
|
||||
// Hier können weitere Event-Handler oder -Konfigurationen eingerichtet werden
|
||||
// Beispiel: System-Events registrieren, die nicht über Attribute verfügbar sind
|
||||
|
||||
// Beispiel für einen Shutdown-Handler
|
||||
register_shutdown_function(function () use ($dispatcher, $container) {
|
||||
$error = error_get_last();
|
||||
if ($error !== null && in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR])) {
|
||||
$exception = new \ErrorException(
|
||||
$error['message'],
|
||||
0,
|
||||
$error['type'],
|
||||
$error['file'],
|
||||
$error['line']
|
||||
);
|
||||
|
||||
$dispatcher->dispatch(new ErrorOccurred($exception, 'shutdown'));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
188
src/Framework/Core/Events/EventDispatcher.php
Normal file
188
src/Framework/Core/Events/EventDispatcher.php
Normal file
@@ -0,0 +1,188 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core\Events;
|
||||
|
||||
use App\Framework\DI\DefaultContainer;
|
||||
|
||||
/**
|
||||
* Der EventDispatcher ist verantwortlich für die Verarbeitung von Events
|
||||
* und das Aufrufen der entsprechenden Event-Handler
|
||||
*/
|
||||
final class EventDispatcher
|
||||
{
|
||||
/** @var array<string, array> Mapping von Event-Typen zu Handlern */
|
||||
private array $handlers = [];
|
||||
|
||||
/** @var bool Gibt an, ob der EventDispatcher initialisiert wurde */
|
||||
private bool $initialized = false;
|
||||
|
||||
/**
|
||||
* @param DefaultContainer $container Der DI-Container
|
||||
* @param array|null $eventHandlers Array mit Event-Handlern aus der Autodiscovery
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly DefaultContainer $container,
|
||||
private readonly ?array $eventHandlers = null
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Initialisiert den EventDispatcher mit den Event-Handlern
|
||||
*/
|
||||
private function initialize(): void
|
||||
{
|
||||
if ($this->initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->initialized = true;
|
||||
|
||||
if ($this->eventHandlers === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($this->eventHandlers as $handler) {
|
||||
$eventClass = $handler['event_class'];
|
||||
$this->handlers[$eventClass][] = $handler;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatcht ein Event an alle registrierten Handler
|
||||
*
|
||||
* @param object $event Das zu dispatchende Event-Objekt
|
||||
* @return array<mixed> Ergebnisse der Event-Handler
|
||||
*/
|
||||
public function dispatch(object $event): array
|
||||
{
|
||||
$this->initialize();
|
||||
|
||||
$eventClass = get_class($event);
|
||||
$results = [];
|
||||
|
||||
// Exakte Übereinstimmung prüfen
|
||||
if (isset($this->handlers[$eventClass])) {
|
||||
foreach ($this->handlers[$eventClass] as $handler) {
|
||||
$result = $this->invokeHandler($handler, $event);
|
||||
$results[] = $result;
|
||||
|
||||
// Wenn ein Handler die Propagation stoppt, abbrechen
|
||||
if (isset($handler['attribute_data']) && $handler['attribute_data']['stopPropagation'] ?? false) {
|
||||
return $results;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Prüfen auf Handler für Basisklassen/Interfaces
|
||||
foreach ($this->handlers as $handledEventClass => $handlersList) {
|
||||
if ($handledEventClass === $eventClass) {
|
||||
continue; // Bereits verarbeitet
|
||||
}
|
||||
|
||||
if (is_subclass_of($event, $handledEventClass) ||
|
||||
($event instanceof $handledEventClass)) {
|
||||
foreach ($handlersList as $handler) {
|
||||
$result = $this->invokeHandler($handler, $event);
|
||||
$results[] = $result;
|
||||
|
||||
// Wenn ein Handler die Propagation stoppt, abbrechen
|
||||
if (isset($handler['attribute_data']) && $handler['attribute_data']['stopPropagation'] ?? false) {
|
||||
return $results;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
public function __invoke(object $event): array
|
||||
{
|
||||
return $this->dispatch($event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ruft einen Event-Handler auf
|
||||
*
|
||||
* @param array $handler Handler-Definition
|
||||
* @param object $event Event-Objekt
|
||||
* @return mixed Ergebnis des Handler-Aufrufs
|
||||
*/
|
||||
private function invokeHandler(array $handler, object $event): mixed
|
||||
{
|
||||
if (isset($handler['callable'])) {
|
||||
// Callable direkt aufrufen
|
||||
return ($handler['callable'])($event);
|
||||
}
|
||||
|
||||
$handlerClass = $handler['class'];
|
||||
$methodName = $handler['method'];
|
||||
|
||||
// Handler-Instanz aus dem Container holen oder erstellen
|
||||
$handlerInstance = $this->container->get($handlerClass);
|
||||
|
||||
// Handler-Methode aufrufen
|
||||
return $handlerInstance->$methodName($event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registriert einen Event-Handler manuell
|
||||
*
|
||||
* @param string $eventClass Vollqualifizierter Klassenname des Events
|
||||
* @param callable $handler Der Handler, der aufgerufen werden soll
|
||||
* @param int|null $priority Priorität des Handlers (höhere Werte werden zuerst ausgeführt)
|
||||
* @param bool $stopPropagation Gibt an, ob die Event-Propagation nach diesem Handler gestoppt werden soll
|
||||
* @return self
|
||||
*/
|
||||
public function addHandler(
|
||||
string $eventClass,
|
||||
callable $handler,
|
||||
?int $priority = null,
|
||||
bool $stopPropagation = false
|
||||
): self {
|
||||
$this->initialize();
|
||||
|
||||
// OnEvent-Attribut für die Priorität und Propagation erstellen
|
||||
$attribute = new OnEvent($priority, $stopPropagation);
|
||||
|
||||
$this->handlers[$eventClass][] = [
|
||||
'callable' => $handler,
|
||||
'attribute_data' => [
|
||||
'priority' => $priority ?? 0,
|
||||
'stopPropagation' => $stopPropagation
|
||||
]
|
||||
|
||||
];
|
||||
|
||||
// Handlers nach Priorität sortieren
|
||||
if (isset($this->handlers[$eventClass]) && count($this->handlers[$eventClass]) > 1) {
|
||||
usort($this->handlers[$eventClass], function ($a, $b) {
|
||||
$priorityA = isset($a['attribute_data']) ? ($a['attribute_data']['priority'] ?? 0) : 0;
|
||||
$priorityB = isset($b['attribute_data']) ? ($b['attribute_data']['priority'] ?? 0) : 0;
|
||||
|
||||
return $priorityB <=> $priorityA;
|
||||
});
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft, ob Handler für den angegebenen Event-Typ registriert sind
|
||||
*
|
||||
* @param string $eventClass Event-Klasse
|
||||
* @return bool True, wenn Handler existieren
|
||||
*/
|
||||
public function hasHandlersFor(string $eventClass): bool
|
||||
{
|
||||
$this->initialize();
|
||||
|
||||
if (isset($this->handlers[$eventClass]) && !empty($this->handlers[$eventClass])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Prüfen auf Handler für Basisklassen/Interfaces
|
||||
return array_any(array_keys($this->handlers), fn($handledEventClass) => is_subclass_of($eventClass, $handledEventClass));
|
||||
|
||||
}
|
||||
}
|
||||
21
src/Framework/Core/Events/EventDispatcherInitializer.php
Normal file
21
src/Framework/Core/Events/EventDispatcherInitializer.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\Core\Events;
|
||||
|
||||
use App\Framework\DI\Container;
|
||||
use App\Framework\DI\Initializer;
|
||||
use App\Framework\Discovery\Results\DiscoveryResults;
|
||||
|
||||
final readonly class EventDispatcherInitializer
|
||||
{
|
||||
public function __construct(
|
||||
private Container $container,
|
||||
private DiscoveryResults $results
|
||||
){}
|
||||
|
||||
#[Initializer]
|
||||
public function __invoke(): EventDispatcher
|
||||
{
|
||||
return new EventDispatcher($this->container, $this->results->get(OnEvent::class));
|
||||
}
|
||||
}
|
||||
41
src/Framework/Core/Events/EventHandlerCompiler.php
Normal file
41
src/Framework/Core/Events/EventHandlerCompiler.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core\Events;
|
||||
|
||||
use App\Framework\Core\AttributeCompiler;
|
||||
|
||||
/**
|
||||
* Compiler für Event-Handler
|
||||
*/
|
||||
final class EventHandlerCompiler implements AttributeCompiler
|
||||
{
|
||||
/**
|
||||
* Gibt die Attributklasse zurück, die dieser Compiler verarbeitet
|
||||
*/
|
||||
public function getAttributeClass(): string
|
||||
{
|
||||
return OnEvent::class;
|
||||
}
|
||||
|
||||
/**
|
||||
* Kompiliert die Event-Handler
|
||||
*
|
||||
* @param array $attributeData Array mit Attributdaten aus dem Mapper
|
||||
* @return array Kompilierte Event-Handler
|
||||
*/
|
||||
public function compile(array $attributeData): array
|
||||
{
|
||||
// Sortieren nach Priorität (höhere Werte zuerst)
|
||||
usort($attributeData, function ($a, $b) {
|
||||
$priorityA = $a['attribute']->priority ?? 0;
|
||||
$priorityB = $b['attribute']->priority ?? 0;
|
||||
|
||||
return $priorityB <=> $priorityA;
|
||||
});
|
||||
|
||||
// Weitere Kompilierung wenn nötig (z.B. Validierung)
|
||||
|
||||
return $attributeData;
|
||||
}
|
||||
}
|
||||
80
src/Framework/Core/Events/EventHandlerMapper.php
Normal file
80
src/Framework/Core/Events/EventHandlerMapper.php
Normal file
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core\Events;
|
||||
|
||||
use App\Framework\Core\AttributeMapper;
|
||||
use ReflectionClass;
|
||||
use ReflectionMethod;
|
||||
|
||||
/**
|
||||
* Mapper für das OnEvent-Attribut
|
||||
*/
|
||||
final class EventHandlerMapper implements AttributeMapper
|
||||
{
|
||||
/**
|
||||
* Gibt die Attributklasse zurück, die dieser Mapper verarbeitet
|
||||
*/
|
||||
public function getAttributeClass(): string
|
||||
{
|
||||
return OnEvent::class;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementiert die map-Methode aus dem AttributeMapper-Interface
|
||||
*
|
||||
* @param object $reflectionTarget Das Reflektionsobjekt (ReflectionClass|ReflectionMethod)
|
||||
* @param object $attributeInstance Die Attributinstanz
|
||||
* @return array|null Die Attributdaten oder null, wenn nicht verarbeitet werden kann
|
||||
*/
|
||||
public function map(object $reflectionTarget, object $attributeInstance): ?array
|
||||
{
|
||||
if (!($reflectionTarget instanceof ReflectionMethod)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$parameters = $reflectionTarget->getParameters();
|
||||
|
||||
// Event-Handler müssen mindestens einen Parameter haben (das Event)
|
||||
if (count($parameters) < 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$eventType = $parameters[0]->getType();
|
||||
if (!$eventType || $eventType->isBuiltin()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$eventClassName = $eventType->getName();
|
||||
|
||||
return [
|
||||
'class' => $reflectionTarget->getDeclaringClass()->getName(),
|
||||
'method' => $reflectionTarget->getName(),
|
||||
'event_class' => $eventClassName,
|
||||
#'reflection' => $reflectionTarget,
|
||||
'attribute_data' => [
|
||||
'priority' => $attributeInstance->priority ?? 0,
|
||||
'stopPropagation' => $attributeInstance->stopPropagation ?? false
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Verarbeitet ein Event-Handler-Attribut
|
||||
*
|
||||
* @param array $attributeData Array mit Attributdaten aus der Discovery
|
||||
* @return array Verarbeitete Attributdaten
|
||||
*/
|
||||
public function process(array $attributeData): array
|
||||
{
|
||||
// Nach Priorität sortieren (höhere Werte zuerst)
|
||||
usort($attributeData, function ($a, $b) {
|
||||
$priorityA = $a['attribute']->priority ?? 0;
|
||||
$priorityB = $b['attribute']->priority ?? 0;
|
||||
|
||||
return $priorityB <=> $priorityA;
|
||||
});
|
||||
|
||||
return $attributeData;
|
||||
}
|
||||
}
|
||||
24
src/Framework/Core/Events/OnEvent.php
Normal file
24
src/Framework/Core/Events/OnEvent.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core\Events;
|
||||
|
||||
/**
|
||||
* Attribut, das eine Methode als Event-Handler kennzeichnet.
|
||||
*
|
||||
* Beispiel:
|
||||
* #[OnEvent]
|
||||
* public function handleUserRegistered(UserRegistered $event): void { ... }
|
||||
*/
|
||||
#[\Attribute(\Attribute::TARGET_METHOD)]
|
||||
final class OnEvent
|
||||
{
|
||||
/**
|
||||
* @param int|null $priority Priorität des Handlers (höhere Werte werden zuerst ausgeführt)
|
||||
* @param bool $stopPropagation Gibt an, ob die Event-Propagation nach diesem Handler gestoppt werden soll
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly ?int $priority = null,
|
||||
public readonly bool $stopPropagation = false
|
||||
) {}
|
||||
}
|
||||
18
src/Framework/Core/Events/UserRegistered.php
Normal file
18
src/Framework/Core/Events/UserRegistered.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core\Events;
|
||||
|
||||
/**
|
||||
* Event, das nach der Registrierung eines Benutzers ausgelöst wird
|
||||
*/
|
||||
final readonly class UserRegistered
|
||||
{
|
||||
public function __construct(
|
||||
public string $userId,
|
||||
public string $email,
|
||||
public string $username,
|
||||
public \DateTimeImmutable $registeredAt = new \DateTimeImmutable(),
|
||||
public \DateTimeImmutable $occurredAt = new \DateTimeImmutable()
|
||||
) {}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core\Exceptions;
|
||||
|
||||
use App\Framework\Exception\FrameworkException;
|
||||
|
||||
final class InvalidRouteCacheFormatException extends FrameworkException
|
||||
{
|
||||
public function __construct(
|
||||
string $cacheFile,
|
||||
int $code = 0,
|
||||
?\Throwable $previous = null
|
||||
) {
|
||||
parent::__construct(
|
||||
message: "Invalid route cache format.",
|
||||
code: $code,
|
||||
previous: $previous,
|
||||
context: ['cacheFile' => $cacheFile]
|
||||
);
|
||||
}
|
||||
}
|
||||
23
src/Framework/Core/Exceptions/RouteCacheException.php
Normal file
23
src/Framework/Core/Exceptions/RouteCacheException.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core\Exceptions;
|
||||
|
||||
use App\Framework\Exception\FrameworkException;
|
||||
|
||||
final class RouteCacheException extends FrameworkException
|
||||
{
|
||||
public function __construct(
|
||||
string $cacheFile,
|
||||
int $code = 0,
|
||||
?\Throwable $previous = null
|
||||
) {
|
||||
parent::__construct(
|
||||
message: "Route cache file not found: {$cacheFile}",
|
||||
code: $code,
|
||||
previous: $previous,
|
||||
context: ['cacheFile' => $cacheFile]
|
||||
);
|
||||
}
|
||||
}
|
||||
17
src/Framework/Core/ImplementationLocator.md
Normal file
17
src/Framework/Core/ImplementationLocator.md
Normal file
@@ -0,0 +1,17 @@
|
||||
````php
|
||||
|
||||
// Beispiel: Alle Repository-Implementierungen finden
|
||||
$locator = new InterfaceImplementationLocator();
|
||||
$repositories = $locator->locate(__DIR__ . '/../src', Repository::class);
|
||||
|
||||
// Beispiel: Alle EventListener-Implementierungen finden, ohne sie zu instanziieren
|
||||
$eventListeners = $locator->locate(__DIR__ . '/../src', EventListener::class, false);
|
||||
|
||||
// Beispiel: Mit einem Factory-Pattern verwenden
|
||||
$serviceClasses = $locator->locate(__DIR__ . '/../src', Service::class, false);
|
||||
$services = [];
|
||||
foreach ($serviceClasses as $serviceClass) {
|
||||
$services[] = $serviceFactory->create($serviceClass);
|
||||
}
|
||||
|
||||
````
|
||||
77
src/Framework/Core/InterfaceImplementationLocator.php
Normal file
77
src/Framework/Core/InterfaceImplementationLocator.php
Normal file
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\Core;
|
||||
|
||||
use RecursiveDirectoryIterator;
|
||||
use RecursiveIteratorIterator;
|
||||
use ReflectionClass;
|
||||
use Throwable;
|
||||
|
||||
class InterfaceImplementationLocator
|
||||
{
|
||||
/**
|
||||
* Findet alle Klassen, die ein bestimmtes Interface implementieren
|
||||
*
|
||||
* @param string $directory Das Basisverzeichnis, in dem gesucht werden soll
|
||||
* @param string $interfaceName Der vollständige Name des Interfaces (mit Namespace)
|
||||
* @param bool $instantiate Gibt an, ob die gefundenen Klassen instanziiert werden sollen
|
||||
* @return array<int, object|string> Array von Instanzen oder Klassennamen
|
||||
*/
|
||||
public function locate(string $directory, string $interfaceName, bool $instantiate = true): array
|
||||
{
|
||||
$implementations = [];
|
||||
$rii = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($directory));
|
||||
|
||||
foreach ($rii as $file) {
|
||||
if (!$file->isFile() || $file->getExtension() !== 'php') {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$className = $this->getClassNameFromFile($file->getPathname());
|
||||
|
||||
if (!$className || !class_exists($className)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$refClass = new ReflectionClass($className);
|
||||
|
||||
// Überprüfen, ob die Klasse das gewünschte Interface implementiert
|
||||
if ($refClass->implementsInterface($interfaceName) &&
|
||||
!$refClass->isAbstract() &&
|
||||
!$refClass->isInterface()) {
|
||||
|
||||
if ($instantiate) {
|
||||
// Prüfen, ob der Konstruktor Parameter hat
|
||||
$constructor = $refClass->getConstructor();
|
||||
if ($constructor === null || $constructor->getNumberOfRequiredParameters() === 0) {
|
||||
$implementations[] = $refClass->newInstance();
|
||||
} else {
|
||||
// Bei Konstruktoren mit Pflichtparametern nur den Klassennamen zurückgeben
|
||||
$implementations[] = $className;
|
||||
}
|
||||
} else {
|
||||
$implementations[] = $className;
|
||||
}
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
#error_log("InterfaceImplementationLocator Warnung: Fehler in Datei {$file->getPathname()}: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return $implementations;
|
||||
}
|
||||
|
||||
private function getClassNameFromFile(string $file): ?string
|
||||
{
|
||||
$contents = file_get_contents($file);
|
||||
if (
|
||||
preg_match('#namespace\s+([^;]+);#', $contents, $nsMatch)
|
||||
&& preg_match('/class\s+(\w+)/', $contents, $classMatch)
|
||||
) {
|
||||
return trim($nsMatch[1]) . '\\' . trim($classMatch[1]);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
179
src/Framework/Core/InterfaceImplementationVisitor.php
Normal file
179
src/Framework/Core/InterfaceImplementationVisitor.php
Normal file
@@ -0,0 +1,179 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core;
|
||||
|
||||
use App\Framework\Cache\Cache;
|
||||
use App\Framework\Discovery\FileVisitor;
|
||||
|
||||
/**
|
||||
* Visitor zum Auffinden von Interface-Implementierungen
|
||||
*/
|
||||
final class InterfaceImplementationVisitor implements FileVisitor
|
||||
{
|
||||
private array $interfaces = [];
|
||||
private array $implementations = [];
|
||||
private array $implementationsByClass = [];
|
||||
|
||||
/**
|
||||
* @param array $targetInterfaces Die zu suchenden Interfaces
|
||||
*/
|
||||
public function __construct(private readonly array $targetInterfaces = [])
|
||||
{
|
||||
}
|
||||
|
||||
public function onScanStart(): void
|
||||
{
|
||||
$this->implementations = [];
|
||||
$this->implementationsByClass = [];
|
||||
}
|
||||
|
||||
public function onIncrementalScanStart(): void
|
||||
{
|
||||
// Bei inkrementellem Scan behalten wir vorhandene Implementierungen
|
||||
// und aktualisieren sie mit neuen Daten
|
||||
}
|
||||
|
||||
public function onIncrementalScanComplete(): void
|
||||
{
|
||||
// Sortiere nach inkrementellem Scan
|
||||
$this->sortImplementations();
|
||||
}
|
||||
|
||||
public function visitClass(string $className, string $filePath): void
|
||||
{
|
||||
if (!class_exists($className)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$reflection = new \ReflectionClass($className);
|
||||
|
||||
// Abstrakte Klassen und Interfaces überspringen
|
||||
if ($reflection->isAbstract() || $reflection->isInterface()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Alte Einträge für diese Klasse entfernen (wichtig für inkrementelle Scans)
|
||||
$this->removeImplementationsForClass($className);
|
||||
|
||||
// Prüfe, ob die Klasse eines der Ziel-Interfaces implementiert
|
||||
foreach ($this->targetInterfaces as $interface) {
|
||||
if ($reflection->implementsInterface($interface)) {
|
||||
if (!isset($this->implementations[$interface])) {
|
||||
$this->implementations[$interface] = [];
|
||||
}
|
||||
|
||||
// Prüfe auf Duplikate
|
||||
if (!in_array($className, $this->implementations[$interface])) {
|
||||
$this->implementations[$interface][] = $className;
|
||||
|
||||
// Umgekehrten Index für schnelleren Zugriff aufbauen
|
||||
if (!isset($this->implementationsByClass[$className])) {
|
||||
$this->implementationsByClass[$className] = [];
|
||||
}
|
||||
$this->implementationsByClass[$className][] = $interface;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
error_log("Fehler beim Prüfen der Interface-Implementierung für {$className}: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Entfernt alle Interface-Implementierungen für eine bestimmte Klasse
|
||||
*/
|
||||
private function removeImplementationsForClass(string $className): void
|
||||
{
|
||||
if (!isset($this->implementationsByClass[$className])) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($this->implementationsByClass[$className] as $interface) {
|
||||
if (isset($this->implementations[$interface])) {
|
||||
$key = array_search($className, $this->implementations[$interface]);
|
||||
if ($key !== false) {
|
||||
unset($this->implementations[$interface][$key]);
|
||||
// Array-Indizes neu numerieren
|
||||
$this->implementations[$interface] = array_values($this->implementations[$interface]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unset($this->implementationsByClass[$className]);
|
||||
}
|
||||
|
||||
public function onScanComplete(): void
|
||||
{
|
||||
// Sortiere Implementierungen für konsistente Reihenfolge
|
||||
$this->sortImplementations();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sortiert alle Implementierungen für konsistente Ergebnisse
|
||||
*/
|
||||
private function sortImplementations(): void
|
||||
{
|
||||
foreach ($this->implementations as &$classes) {
|
||||
sort($classes);
|
||||
}
|
||||
}
|
||||
|
||||
public function loadFromCache(Cache $cache): void
|
||||
{
|
||||
$cacheItem = $cache->get($this->getCacheKey());
|
||||
if ($cacheItem->isHit) {
|
||||
if (is_array($cacheItem->value) && isset($cacheItem->value['implementations'], $cacheItem->value['byClass'])) {
|
||||
$this->implementations = $cacheItem->value['implementations'];
|
||||
$this->implementationsByClass = $cacheItem->value['byClass'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function getCacheKey(): string
|
||||
{
|
||||
return 'interface_implementations';
|
||||
}
|
||||
|
||||
public function getCacheableData(): mixed
|
||||
{
|
||||
return [
|
||||
'implementations' => $this->implementations,
|
||||
'byClass' => $this->implementationsByClass
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt alle Implementierungen eines Interfaces zurück
|
||||
*/
|
||||
public function getImplementations(string $interface): array
|
||||
{
|
||||
return $this->implementations[$interface] ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft, ob eine Klasse ein bestimmtes Interface implementiert
|
||||
*/
|
||||
public function doesImplement(string $className, string $interface): bool
|
||||
{
|
||||
return isset($this->implementationsByClass[$className]) &&
|
||||
in_array($interface, $this->implementationsByClass[$className]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt alle Interfaces zurück, die eine Klasse implementiert
|
||||
*/
|
||||
public function getClassInterfaces(string $className): array
|
||||
{
|
||||
return $this->implementationsByClass[$className] ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt alle gefundenen Implementierungen zurück
|
||||
*/
|
||||
public function getAllImplementations(): array
|
||||
{
|
||||
return $this->implementations;
|
||||
}
|
||||
}
|
||||
135
src/Framework/Core/PathProvider.php
Normal file
135
src/Framework/Core/PathProvider.php
Normal file
@@ -0,0 +1,135 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core;
|
||||
|
||||
/**
|
||||
* Optimierter PathProvider mit Caching
|
||||
*/
|
||||
final class PathProvider
|
||||
{
|
||||
private string $basePath;
|
||||
private array $resolvedPaths = [];
|
||||
private ?array $namespacePaths = null {
|
||||
get {
|
||||
if ($this->namespacePaths !== null) {
|
||||
return $this->namespacePaths;
|
||||
}
|
||||
|
||||
// Aus composer.json auslesen
|
||||
$composerJsonPath = $this->basePath . '/composer.json';
|
||||
if (!file_exists($composerJsonPath)) {
|
||||
return $this->namespacePaths = [];
|
||||
}
|
||||
|
||||
$composerJson = json_decode(file_get_contents($composerJsonPath), true);
|
||||
$this->namespacePaths = [];
|
||||
|
||||
if (isset($composerJson['autoload']['psr-4'])) {
|
||||
foreach ($composerJson['autoload']['psr-4'] as $namespace => $path) {
|
||||
$this->namespacePaths[$namespace] = $this->resolvePath($path);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->namespacePaths;
|
||||
}
|
||||
}
|
||||
|
||||
public function __construct(string $basePath)
|
||||
{
|
||||
$this->basePath = rtrim($basePath, '/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt den Basispfad des Projekts zurück
|
||||
*/
|
||||
public function getBasePath(): string
|
||||
{
|
||||
return $this->basePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Löst einen relativen Pfad zum Basispfad auf
|
||||
*/
|
||||
public function resolvePath(string $relativePath): string
|
||||
{
|
||||
if (isset($this->resolvedPaths[$relativePath])) {
|
||||
return $this->resolvedPaths[$relativePath];
|
||||
}
|
||||
|
||||
$path = $this->basePath . '/' . ltrim($relativePath, '/');
|
||||
$this->resolvedPaths[$relativePath] = $path;
|
||||
|
||||
return $path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft, ob ein Pfad im Projektverzeichnis existiert
|
||||
*/
|
||||
public function pathExists(string $relativePath): bool
|
||||
{
|
||||
return file_exists($this->resolvePath($relativePath));
|
||||
}
|
||||
|
||||
/**
|
||||
* Konvertiert einen Namespace in einen relativen Pfad
|
||||
*/
|
||||
public function namespaceToPath(string $namespace): ?string
|
||||
{
|
||||
$namespacePaths = $this->namespacePaths;
|
||||
|
||||
// Finde den passenden Namespace-Präfix
|
||||
foreach ($namespacePaths as $prefix => $path) {
|
||||
if (str_starts_with($namespace, $prefix)) {
|
||||
$relativeNamespace = substr($namespace, strlen($prefix));
|
||||
$relativePath = str_replace('\\', '/', $relativeNamespace);
|
||||
return rtrim($path, '/') . '/' . $relativePath . '.php';
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Konvertiert einen Pfad in einen Namespace
|
||||
*/
|
||||
public function pathToNamespace(string $path): ?string
|
||||
{
|
||||
$namespacePaths = $this->namespacePaths;
|
||||
$absolutePath = realpath($path);
|
||||
|
||||
if (!$absolutePath) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Finde den passenden Pfad-Präfix
|
||||
foreach ($namespacePaths as $namespace => $nsPath) {
|
||||
$realNsPath = realpath($nsPath);
|
||||
|
||||
if ($realNsPath && str_starts_with($absolutePath, $realNsPath)) {
|
||||
$relativePath = substr($absolutePath, strlen($realNsPath));
|
||||
$relativePath = rtrim(str_replace('.php', '', $relativePath), '/');
|
||||
$relativeNamespace = str_replace('/', '\\', $relativePath);
|
||||
return $namespace . ltrim($relativeNamespace, '\\');
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt den Pfad zum Cache-Verzeichnis zurück
|
||||
*/
|
||||
public function getCachePath(string $path = ''): string
|
||||
{
|
||||
return $this->basePath . '/cache/' . ltrim($path, '/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt den Pfad zum Quellverzeichnis zurück
|
||||
*/
|
||||
public function getSourcePath(string $path = ''): string
|
||||
{
|
||||
return $this->basePath . '/src/' . ltrim($path, '/');
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,7 @@ class PhpObjectExporter
|
||||
return "new StaticRoute("
|
||||
. var_export($value->controller, true) . ', '
|
||||
. var_export($value->action, true) . ', '
|
||||
. self::export($value->params, true)
|
||||
. self::export($value->parameters, true)
|
||||
. ")";
|
||||
}
|
||||
if ($value instanceof DynamicRoute) {
|
||||
@@ -32,7 +32,7 @@ class PhpObjectExporter
|
||||
. var_export($value->regex, true) . ', '
|
||||
. self::export($value->parameters, true) . ', '
|
||||
. var_export($value->controller, true) . ', '
|
||||
. var_export($value->method, true) . ', '
|
||||
. var_export($value->action, true) . ', '
|
||||
. var_export($value->parameters, true)
|
||||
. ")";
|
||||
}
|
||||
|
||||
135
src/Framework/Core/ProgressMeter.php
Normal file
135
src/Framework/Core/ProgressMeter.php
Normal file
@@ -0,0 +1,135 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core;
|
||||
|
||||
/**
|
||||
* Einfacher Progressmeter für Konsolenanwendungen
|
||||
*/
|
||||
final class ProgressMeter
|
||||
{
|
||||
private int $total;
|
||||
private int $current = 0;
|
||||
private int $lastPercent = 0;
|
||||
private bool $isConsole;
|
||||
private int $width;
|
||||
private string $format;
|
||||
|
||||
/**
|
||||
* @param int $total Gesamtanzahl der zu verarbeitenden Elemente
|
||||
* @param int $width Breite der Fortschrittsanzeige
|
||||
* @param string $format Format der Anzeige: 'bar' für Balken, 'percent' für Prozent, 'both' für beides
|
||||
*/
|
||||
public function __construct(
|
||||
int $total,
|
||||
int $width = 50,
|
||||
string $format = 'both'
|
||||
) {
|
||||
$this->total = max(1, $total); // Verhindere Division durch Null
|
||||
$this->width = $width;
|
||||
$this->format = $format;
|
||||
$this->isConsole = php_sapi_name() === 'cli';
|
||||
}
|
||||
|
||||
/**
|
||||
* Erhöht den Fortschritt um einen oder mehrere Schritte
|
||||
*/
|
||||
public function advance(int $step = 1): void
|
||||
{
|
||||
$this->current += $step;
|
||||
|
||||
if ($this->isConsole) {
|
||||
$percent = (int)(($this->current / $this->total) * 100);
|
||||
|
||||
if ($percent > $this->lastPercent || $this->current >= $this->total) {
|
||||
$this->display();
|
||||
$this->lastPercent = $percent;
|
||||
}
|
||||
|
||||
if ($this->current >= $this->total) {
|
||||
echo PHP_EOL;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Zeigt den aktuellen Fortschritt an
|
||||
*/
|
||||
private function display(): void
|
||||
{
|
||||
$percent = (int)(($this->current / $this->total) * 100);
|
||||
|
||||
switch ($this->format) {
|
||||
case 'bar':
|
||||
$this->displayBar($percent);
|
||||
break;
|
||||
case 'percent':
|
||||
$this->displayPercent($percent);
|
||||
break;
|
||||
case 'both':
|
||||
default:
|
||||
$this->displayBoth($percent);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Zeigt einen Fortschrittsbalken an
|
||||
*/
|
||||
private function displayBar(int $percent): void
|
||||
{
|
||||
$completed = (int)(($this->width * $percent) / 100);
|
||||
$remaining = $this->width - $completed;
|
||||
|
||||
echo "\r[";
|
||||
echo str_repeat("=", $completed);
|
||||
echo str_repeat(" ", $remaining);
|
||||
echo "] {$this->current}/{$this->total}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Zeigt den Fortschritt als Prozentwert an
|
||||
*/
|
||||
private function displayPercent(int $percent): void
|
||||
{
|
||||
echo "\rFortschritt: {$percent}% [{$this->current}/{$this->total}]";
|
||||
}
|
||||
|
||||
/**
|
||||
* Zeigt sowohl Balken als auch Prozentwert an
|
||||
*/
|
||||
private function displayBoth(int $percent): void
|
||||
{
|
||||
$completed = (int)(($this->width * $percent) / 100);
|
||||
$remaining = $this->width - $completed;
|
||||
|
||||
echo "\r[";
|
||||
echo str_repeat("=", $completed);
|
||||
echo str_repeat(" ", $remaining);
|
||||
echo "] {$percent}% [{$this->current}/{$this->total}]";
|
||||
}
|
||||
|
||||
/**
|
||||
* Markiert den Fortschritt als abgeschlossen
|
||||
*/
|
||||
public function finish(): void
|
||||
{
|
||||
if ($this->isConsole) {
|
||||
$this->current = $this->total;
|
||||
$this->display();
|
||||
echo PHP_EOL;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt den Fortschritt auf einen bestimmten Wert
|
||||
*/
|
||||
public function setProgress(int $current): void
|
||||
{
|
||||
$this->current = max(0, min($current, $this->total));
|
||||
|
||||
if ($this->isConsole) {
|
||||
$this->display();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,4 +6,13 @@ namespace App\Framework\Core;
|
||||
|
||||
interface Route
|
||||
{
|
||||
public string $controller {get;}
|
||||
|
||||
public string $action {get;}
|
||||
|
||||
public array $parameters {get;}
|
||||
|
||||
public string $name {get;}
|
||||
|
||||
public string $path {get;}
|
||||
}
|
||||
|
||||
@@ -4,32 +4,40 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core;
|
||||
|
||||
class RouteCache
|
||||
use App\Framework\Core\Exceptions\InvalidRouteCacheFormatException;
|
||||
use App\Framework\Core\Exceptions\RouteCacheException;
|
||||
|
||||
final readonly class RouteCache
|
||||
{
|
||||
public function __construct(
|
||||
private string $cacheFile
|
||||
) {
|
||||
}
|
||||
|
||||
public function save(array $routes)
|
||||
public function save(array $routes): bool
|
||||
{
|
||||
$phpExport = '<?php' . PHP_EOL;
|
||||
$phpExport .= 'use App\Framework\Core\StaticRoute;' . PHP_EOL;
|
||||
$phpExport .= 'use App\Framework\Core\DynamicRoute;' . PHP_EOL;
|
||||
$phpExport .= '// Automatisch generiert am ' . date('Y-m-d H:i:s') . PHP_EOL;
|
||||
$phpExport .= 'use App\\Framework\\Core\\StaticRoute;' . PHP_EOL;
|
||||
$phpExport .= 'use App\\Framework\\Core\\DynamicRoute;' . PHP_EOL;
|
||||
$phpExport .= 'return ' . PhpObjectExporter::export($routes) . ';' . PHP_EOL;
|
||||
|
||||
file_put_contents($this->cacheFile, $phpExport);
|
||||
if (file_put_contents($this->cacheFile, $phpExport) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function load(): array
|
||||
{
|
||||
if (! file_exists($this->cacheFile)) {
|
||||
throw new \RuntimeException("Route cache file not found: {$this->cacheFile}");
|
||||
throw new RouteCacheException($this->cacheFile);
|
||||
}
|
||||
$data = include $this->cacheFile;
|
||||
|
||||
if (! is_array($data)) {
|
||||
throw new \RuntimeException("Invalid route cache format.");
|
||||
throw new InvalidRouteCacheFormatException($this->cacheFile);
|
||||
}
|
||||
|
||||
return $data;
|
||||
@@ -42,7 +50,8 @@ class RouteCache
|
||||
return false;
|
||||
}
|
||||
|
||||
// Eigenes Format prüfen? Datei-mtime prüfen? Optional!
|
||||
return true;
|
||||
$mtime = filemtime($this->cacheFile);
|
||||
|
||||
return $mtime !== false && $mtime <= $expectedMTime;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,12 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core;
|
||||
|
||||
final class RouteCompiler
|
||||
use App\Framework\Router\CompiledPattern;
|
||||
use App\Framework\Router\CompiledRoutes;
|
||||
|
||||
final readonly class RouteCompiler implements AttributeCompiler
|
||||
{
|
||||
private array $named;
|
||||
/**
|
||||
* @param array<int, array{method: string, path: string, controller: class-string, handler: string}> $routes
|
||||
* @return array<string, array{static: array<string, array>, dynamic: array<int, array{regex: string, params: array, handler: array}>}>
|
||||
@@ -13,36 +17,70 @@ final class RouteCompiler
|
||||
public function compile(array $routes): array
|
||||
{
|
||||
$compiled = [];
|
||||
$named = [];
|
||||
|
||||
foreach ($routes as $route) {
|
||||
$method = strtoupper($route['http_method']);
|
||||
|
||||
$method = is_string($route['http_method']) ? strtoupper($route['http_method']) : $route['http_method']->value;
|
||||
$path = $route['path'];
|
||||
$routeName = $route['name'] ?? '';
|
||||
|
||||
$compiled[$method] ??= ['static' => [], 'dynamic' => []];
|
||||
|
||||
if (! str_contains($path, '{')) {
|
||||
// Statische Route
|
||||
$compiled[$method]['static'][$path] = new StaticRoute(
|
||||
$staticRoute = new StaticRoute(
|
||||
$route['class'],
|
||||
$route['method'],
|
||||
$route['parameters']
|
||||
$route['parameters'] ?? [],
|
||||
$routeName,
|
||||
$path,
|
||||
$route['attributes']
|
||||
);
|
||||
|
||||
$compiled[$method]['static'][$path] = $staticRoute;
|
||||
|
||||
if($routeName) {
|
||||
$named[$routeName] = $staticRoute;
|
||||
}
|
||||
|
||||
} else {
|
||||
// Dynamische Route
|
||||
$paramNames = [];
|
||||
$regex = $this->convertPathToRegex($path, $paramNames);
|
||||
$compiled[$method]['dynamic'][] = new DynamicRoute(
|
||||
|
||||
$dynamicRoute = new DynamicRoute(
|
||||
$regex,
|
||||
$paramNames,
|
||||
$route['class'],
|
||||
$route['method'],
|
||||
$route['parameters']
|
||||
$route['parameters'],
|
||||
[],
|
||||
$routeName,
|
||||
$path,
|
||||
$route['attributes']
|
||||
);
|
||||
|
||||
$compiled[$method]['dynamic'][] = $dynamicRoute;
|
||||
|
||||
if($routeName) {
|
||||
$named[$routeName] = $dynamicRoute;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(!isset($this->named)) {
|
||||
$this->named = $named;
|
||||
}
|
||||
|
||||
return $compiled;
|
||||
}
|
||||
|
||||
public function compileNamedRoutes(array $routes): array
|
||||
{
|
||||
return $this->named;
|
||||
}
|
||||
|
||||
/**
|
||||
* Konvertiert zB. /user/{id}/edit → ~^/user/([^/]+)/edit$~ und gibt ['id'] als Parameternamen zurück.
|
||||
*
|
||||
@@ -53,12 +91,77 @@ final class RouteCompiler
|
||||
private function convertPathToRegex(string $path, array &$paramNames): string
|
||||
{
|
||||
$paramNames = [];
|
||||
$regex = preg_replace_callback('#\{(\w+)\}#', function ($matches) use (&$paramNames) {
|
||||
$regex = preg_replace_callback('#\{(\w+)(\*)?}#', function ($matches) use (&$paramNames) {
|
||||
$paramNames[] = $matches[1];
|
||||
|
||||
return '([^/]+)';
|
||||
// Wenn {id*} dann erlaube Slashes, aber mache es non-greedy
|
||||
if (isset($matches[2]) && $matches[2] === '*') {
|
||||
return '(.+?)'; // Non-greedy: matcht so wenig wie möglich
|
||||
}
|
||||
|
||||
return '([^/]+)'; // Keine Slashes
|
||||
}, $path);
|
||||
|
||||
return '~^' . $regex . '$~';
|
||||
}
|
||||
|
||||
public function getAttributeClass(): string
|
||||
{
|
||||
return Route::class;
|
||||
}
|
||||
|
||||
public function compileOptimized(array $routes): CompiledRoutes
|
||||
{
|
||||
$compiled = $this->compile($routes);
|
||||
$optimizedStatic = [];
|
||||
$optimizedDynamic = [];
|
||||
|
||||
foreach($compiled as $method => $routes) {
|
||||
$optimizedStatic[$method] = $routes['static'];
|
||||
|
||||
if(!empty($routes['dynamic'])) {
|
||||
$optimizedDynamic[$method] = $this->createCompiledPattern($routes['dynamic']);
|
||||
}
|
||||
}
|
||||
|
||||
return new CompiledRoutes($optimizedStatic, $optimizedDynamic, $this->named);
|
||||
}
|
||||
|
||||
private function createCompiledPattern(mixed $dynamicRoutes): CompiledPattern
|
||||
{
|
||||
$patterns = [];
|
||||
$routeData = [];
|
||||
$currentIndex = 1; // Nach Full Match (Index 0)
|
||||
|
||||
foreach($dynamicRoutes as $index => $route) {
|
||||
$pattern = $this->stripAnchors($route->regex);
|
||||
$patterns[] = "({$pattern})";
|
||||
|
||||
// Route-Gruppe Index merken
|
||||
$routeGroupIndex = $currentIndex++;
|
||||
|
||||
// Parameter-Mapping KORREKT berechnen
|
||||
$paramMap = [];
|
||||
foreach($route->paramNames as $paramName) {
|
||||
$paramMap[$paramName] = $currentIndex++;
|
||||
}
|
||||
|
||||
$routeData[$index] = [
|
||||
'route' => $route,
|
||||
'paramMap' => $paramMap,
|
||||
'routeGroupIndex' => $routeGroupIndex
|
||||
];
|
||||
}
|
||||
|
||||
$combinedRegex = '~^(?:' . implode('|', $patterns) . ')$~';
|
||||
return new CompiledPattern($combinedRegex, $routeData);
|
||||
}
|
||||
|
||||
private function stripAnchors($regex): string
|
||||
{
|
||||
if (preg_match('/^~\^(.+)\$~$/', $regex, $matches)) {
|
||||
return $matches[1];
|
||||
}
|
||||
return $regex;
|
||||
}
|
||||
}
|
||||
|
||||
231
src/Framework/Core/RouteDiscoveryVisitor.php
Normal file
231
src/Framework/Core/RouteDiscoveryVisitor.php
Normal file
@@ -0,0 +1,231 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core;
|
||||
|
||||
use App\Framework\Attributes\Route as RouteAttribute;
|
||||
use App\Framework\Cache\Cache;
|
||||
use App\Framework\Discovery\FileVisitor;
|
||||
|
||||
/**
|
||||
* Visitor zum Auffinden von Routen
|
||||
*/
|
||||
final class RouteDiscoveryVisitor implements FileVisitor
|
||||
{
|
||||
private array $routes = [];
|
||||
private array $routesByPath = [];
|
||||
private array $routesByController = [];
|
||||
private bool $routesSorted = false;
|
||||
|
||||
public function onScanStart(): void
|
||||
{
|
||||
$this->routes = [];
|
||||
$this->routesByPath = [];
|
||||
$this->routesByController = [];
|
||||
$this->routesSorted = false;
|
||||
}
|
||||
|
||||
public function onIncrementalScanStart(): void
|
||||
{
|
||||
// Bei inkrementellem Scan behalten wir bestehende Routen
|
||||
// markieren sie aber als unsortiert
|
||||
$this->routesSorted = false;
|
||||
}
|
||||
|
||||
public function onIncrementalScanComplete(): void
|
||||
{
|
||||
// Nach inkrementellem Scan sortieren
|
||||
$this->sortRoutes();
|
||||
}
|
||||
|
||||
public function visitClass(string $className, string $filePath): void
|
||||
{
|
||||
if (!class_exists($className)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$reflection = new \ReflectionClass($className);
|
||||
|
||||
// Prüfe, ob die Klasse überhaupt Methoden mit Route-Attributen hat
|
||||
$hasRouteMethods = false;
|
||||
foreach ($reflection->getMethods() as $method) {
|
||||
if (!empty($method->getAttributes(RouteAttribute::class))) {
|
||||
$hasRouteMethods = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$hasRouteMethods) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Nur Methoden mit Route-Attribut verarbeiten
|
||||
foreach ($reflection->getMethods() as $method) {
|
||||
$routeAttributes = $method->getAttributes(RouteAttribute::class);
|
||||
|
||||
foreach ($routeAttributes as $routeAttr) {
|
||||
/** @var RouteAttribute $route */
|
||||
$route = $routeAttr->newInstance();
|
||||
|
||||
$routeData = [
|
||||
'path' => $route->path,
|
||||
'method' => $route->method,
|
||||
'controller' => $className,
|
||||
'action' => $method->getName(),
|
||||
'middleware' => $route->middleware ?? [],
|
||||
'priority' => $route->priority ?? 0
|
||||
];
|
||||
|
||||
$this->routes[] = $routeData;
|
||||
|
||||
// Erstelle Indizes für schnelleren Zugriff
|
||||
$path = strtolower($route->path);
|
||||
$httpMethod = strtoupper($route->method->value);
|
||||
$key = "{$httpMethod}:{$path}";
|
||||
|
||||
if (!isset($this->routesByPath[$key])) {
|
||||
$this->routesByPath[$key] = [];
|
||||
}
|
||||
$this->routesByPath[$key][] = $routeData;
|
||||
|
||||
if (!isset($this->routesByController[$className])) {
|
||||
$this->routesByController[$className] = [];
|
||||
}
|
||||
$this->routesByController[$className][] = $routeData;
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
error_log("Fehler beim Verarbeiten der Routen für {$className}: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function onScanComplete(): void
|
||||
{
|
||||
// Sortiere Routen nach Priorität
|
||||
$this->sortRoutes();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sortiert die Routen nach Priorität (absteigend)
|
||||
*/
|
||||
private function sortRoutes(): void
|
||||
{
|
||||
if ($this->routesSorted) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Sortiere Hauptrouten-Array
|
||||
usort($this->routes, function($a, $b) {
|
||||
return $b['priority'] <=> $a['priority'];
|
||||
});
|
||||
|
||||
// Sortiere Indizes
|
||||
foreach ($this->routesByPath as $key => $routes) {
|
||||
usort($this->routesByPath[$key], function($a, $b) {
|
||||
return $b['priority'] <=> $a['priority'];
|
||||
});
|
||||
}
|
||||
|
||||
foreach ($this->routesByController as $className => $routes) {
|
||||
usort($this->routesByController[$className], function($a, $b) {
|
||||
return $b['priority'] <=> $a['priority'];
|
||||
});
|
||||
}
|
||||
|
||||
$this->routesSorted = true;
|
||||
}
|
||||
|
||||
public function loadFromCache(Cache $cache): void
|
||||
{
|
||||
$cacheItem = $cache->get($this->getCacheKey());
|
||||
if ($cacheItem->isHit) {
|
||||
if (is_array($cacheItem->value) && isset($cacheItem->value['routes'], $cacheItem->value['byPath'], $cacheItem->value['byController'])) {
|
||||
$this->routes = $cacheItem->value['routes'];
|
||||
$this->routesByPath = $cacheItem->value['byPath'];
|
||||
$this->routesByController = $cacheItem->value['byController'];
|
||||
$this->routesSorted = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function getCacheKey(): string
|
||||
{
|
||||
return 'routes';
|
||||
}
|
||||
|
||||
public function getCacheableData(): mixed
|
||||
{
|
||||
// Stelle sicher, dass die Routen sortiert sind, bevor wir sie cachen
|
||||
if (!$this->routesSorted) {
|
||||
$this->sortRoutes();
|
||||
}
|
||||
|
||||
return [
|
||||
'routes' => $this->routes,
|
||||
'byPath' => $this->routesByPath,
|
||||
'byController' => $this->routesByController
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt alle gefundenen Routen zurück
|
||||
*/
|
||||
public function getRoutes(): array
|
||||
{
|
||||
if (!$this->routesSorted) {
|
||||
$this->sortRoutes();
|
||||
}
|
||||
return $this->routes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Findet eine Route für einen bestimmten Pfad und HTTP-Methode
|
||||
*/
|
||||
public function findRoute(string $path, string $httpMethod): ?array
|
||||
{
|
||||
$path = strtolower(rtrim($path, '/'));
|
||||
$httpMethod = strtoupper($httpMethod);
|
||||
$key = "{$httpMethod}:{$path}";
|
||||
|
||||
// Direkte Übereinstimmung
|
||||
if (isset($this->routesByPath[$key]) && !empty($this->routesByPath[$key])) {
|
||||
return $this->routesByPath[$key][0]; // Höchste Priorität zuerst
|
||||
}
|
||||
|
||||
// Prüfe auf parametrisierte Routen, wenn keine direkte Übereinstimmung
|
||||
foreach ($this->routesByPath as $routeKey => $routes) {
|
||||
list($routeMethod, $routePath) = explode(':', $routeKey, 2);
|
||||
|
||||
if ($routeMethod !== $httpMethod) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Konvertiere Route-Pfad in ein reguläres Ausdrucksmuster
|
||||
$pattern = $this->routePathToRegex($routePath);
|
||||
if (preg_match($pattern, $path)) {
|
||||
return $routes[0]; // Höchste Priorität zuerst
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Konvertiert einen Route-Pfad in ein reguläres Ausdrucksmuster
|
||||
*/
|
||||
private function routePathToRegex(string $routePath): string
|
||||
{
|
||||
// Ersetze Platzhalter wie {id} durch Regex-Gruppen
|
||||
$pattern = preg_replace('/\{([^}]+)\}/', '([^/]+)', $routePath);
|
||||
return '/^' . str_replace('/', '\/', $pattern) . '$/';
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt alle Routen für einen bestimmten Controller zurück
|
||||
*/
|
||||
public function getRoutesForController(string $controllerClass): array
|
||||
{
|
||||
return $this->routesByController[$controllerClass] ?? [];
|
||||
}
|
||||
}
|
||||
@@ -20,11 +20,20 @@ final readonly class RouteMapper implements AttributeMapper
|
||||
return null;
|
||||
}
|
||||
|
||||
$attributes = [];
|
||||
foreach ($reflectionTarget->getAttributes() as $attribute) {
|
||||
if ($attribute->getName() !== Route::class) {
|
||||
$attributes[] = $attribute->getName();
|
||||
}
|
||||
};
|
||||
|
||||
return [
|
||||
'class' => $reflectionTarget->getDeclaringClass()->getName(),
|
||||
'method' => $reflectionTarget->getName(),
|
||||
'http_method' => $attributeInstance->method,
|
||||
'path' => $attributeInstance->path,
|
||||
'name' => $attributeInstance->name,
|
||||
'attributes' => $attributes
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,12 +4,14 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core;
|
||||
|
||||
class StaticRoute implements Route
|
||||
final readonly class StaticRoute implements Route
|
||||
{
|
||||
public function __construct(
|
||||
public string $controller,
|
||||
public string $action,
|
||||
public array $params
|
||||
) {
|
||||
}
|
||||
public array $parameters,
|
||||
public string $name = '',
|
||||
public string $path = '',
|
||||
public array $attributes = []
|
||||
) {}
|
||||
}
|
||||
|
||||
54
src/Framework/Core/VersionInfo.php
Normal file
54
src/Framework/Core/VersionInfo.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core;
|
||||
|
||||
/**
|
||||
* Stellt Versionsinformationen für das Framework bereit.
|
||||
*/
|
||||
final readonly class VersionInfo
|
||||
{
|
||||
/**
|
||||
* Die aktuelle Version des Frameworks.
|
||||
*/
|
||||
private const string VERSION = '1.0.0';
|
||||
|
||||
/**
|
||||
* Gibt die aktuelle Version des Frameworks zurück.
|
||||
*/
|
||||
public function getVersion(): string
|
||||
{
|
||||
return self::VERSION;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft, ob die aktuelle Version mindestens der angegebenen Version entspricht.
|
||||
*/
|
||||
public function isAtLeast(string $version): bool
|
||||
{
|
||||
return version_compare(self::VERSION, $version, '>=');
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft, ob die aktuelle Version höher als die angegebene Version ist.
|
||||
*/
|
||||
public function isHigherThan(string $version): bool
|
||||
{
|
||||
return version_compare(self::VERSION, $version, '>');
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt Informationen über die Umgebung zurück.
|
||||
*
|
||||
* @return array{framework: string, php: string, os: string}
|
||||
*/
|
||||
public function getEnvironmentInfo(): array
|
||||
{
|
||||
return [
|
||||
'framework' => self::VERSION,
|
||||
'php' => PHP_VERSION,
|
||||
'os' => PHP_OS,
|
||||
];
|
||||
}
|
||||
}
|
||||
8
src/Framework/Core/ViewModel.php
Normal file
8
src/Framework/Core/ViewModel.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\Core;
|
||||
|
||||
interface ViewModel
|
||||
{
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user