chore: complete update

This commit is contained in:
2025-07-17 16:24:20 +02:00
parent 899227b0a4
commit 64a7051137
1300 changed files with 85570 additions and 2756 deletions

View 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,
);
});
}
}

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

View 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;
}

View 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
}
}

View File

@@ -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;
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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');;
}
}

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

View 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.

View File

@@ -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;
}
}

View 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");
}
}

View File

@@ -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 = []
) {}
}

View 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>";
}
}

View File

@@ -0,0 +1,8 @@
<?php
namespace App\Framework\Core\Events;
class AfterEmitResponse
{
}

View File

@@ -0,0 +1,8 @@
<?php
namespace App\Framework\Core\Events;
class AfterHandleRequest
{
}

View 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()
) {}
}

View File

@@ -0,0 +1,8 @@
<?php
namespace App\Framework\Core\Events;
class BeforeEmitResponse
{
}

View File

@@ -0,0 +1,8 @@
<?php
namespace App\Framework\Core\Events;
class BeforeHandleRequest
{
}

View 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()
) {}
}

View 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'));
}
});
}
}

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

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

View 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;
}
}

View 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;
}
}

View 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
) {}
}

View 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()
) {}
}

View File

@@ -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]
);
}
}

View 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]
);
}
}

View 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);
}
````

View 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;
}
}

View 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;
}
}

View 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, '/');
}
}

View File

@@ -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)
. ")";
}

View 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();
}
}
}

View File

@@ -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;}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View 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] ?? [];
}
}

View File

@@ -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
];
}
}

View File

@@ -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 = []
) {}
}

View 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,
];
}
}

View File

@@ -0,0 +1,8 @@
<?php
namespace App\Framework\Core;
interface ViewModel
{
}