chore: lots of changes

This commit is contained in:
2025-05-24 07:09:22 +02:00
parent 77ee769d5e
commit 899227b0a4
178 changed files with 5145 additions and 53 deletions

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace App\Framework\Core;
interface AttributeMapper
{
public function getAttributeClass(): string;
/**
* @param object $reflectionTarget ReflectionClass|ReflectionMethod
* @param object $attributeInstance
* @return array|null
*/
public function map(object $reflectionTarget, object $attributeInstance): ?array;
}

View File

@@ -0,0 +1,171 @@
<?php
declare(strict_types=1);
namespace App\Framework\Core;
use Exception;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use ReflectionClass;
use ReflectionMethod;
class Discovery
{
/** @var array<string, AttributeMapper> */
private array $mapperMap;
private string $cacheFile;
/**
* @param AttributeMapper[] $mappers
*/
public function __construct(AttributeMapper ...$mappers)
{
$this->mapperMap = [];
foreach ($mappers as $mapper) {
$this->mapperMap[$mapper->getAttributeClass()] = $mapper;
}
$this->cacheFile = __DIR__ .'/../../../cache/discovery.cache.php';
}
public function discover(string $directory): array
{
$hash = md5(realpath($directory));
$this->cacheFile = __DIR__ ."/../../../cache/discovery_{$hash}.cache.php";
$latestMTime = $this->getLatestMTime($directory);
$data = $this->loadCache($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());
}
}
$results['__discovery_mtime'] = $latestMTime;
$this->storeCache($results);
return $results;
}
private function discoverClass(ReflectionClass $refClass, array &$results): void
{
$this->processAttributes($refClass, $results);
foreach ($refClass->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
$this->processAttributes($method, $results);
}
}
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;
}
$mapped['parameters'] = $params;
}
if ($mapped !== null) {
$results[$attrName][] = $mapped;
}
}
}
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;
}
}
}
return $maxMTime;
}
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,17 @@
<?php
declare(strict_types=1);
namespace App\Framework\Core;
final readonly class DynamicRoute implements Route
{
public function __construct(
public string $regex,
public array $paramNames,
public string $controller,
public string $method,
public array $parameters
) {
}
}

View File

@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace App\Framework\Core;
class PhpObjectExporter
{
/**
* Eigene Export-Funktion, die rekursiv ein PHP-Array mit echten Konstruktoraufrufen exportiert.
* (Variante unten für Standardfälle ausreichend! Für verschachtelte/nicht-indizierte Arrays ggf. anpassen.)
*/
public static function export($value)
{
if (is_array($value)) {
$exported = [];
foreach ($value as $key => $v) {
$exported[] = var_export($key, true) . ' => ' . self::export($v);
}
return '[' . implode(",\n", $exported) . ']';
}
if ($value instanceof StaticRoute) {
return "new StaticRoute("
. var_export($value->controller, true) . ', '
. var_export($value->action, true) . ', '
. self::export($value->params, true)
. ")";
}
if ($value instanceof DynamicRoute) {
return "new DynamicRoute("
. var_export($value->regex, true) . ', '
. self::export($value->parameters, true) . ', '
. var_export($value->controller, true) . ', '
. var_export($value->method, true) . ', '
. var_export($value->parameters, true)
. ")";
}
// Fallback für skalare Werte
return var_export($value, true);
}
}

View File

@@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace App\Framework\Core;
interface Route
{
}

View File

@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace App\Framework\Core;
class RouteCache
{
public function __construct(
private string $cacheFile
) {
}
public function save(array $routes)
{
$phpExport = '<?php' . 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);
}
public function load(): array
{
if (! file_exists($this->cacheFile)) {
throw new \RuntimeException("Route cache file not found: {$this->cacheFile}");
}
$data = include $this->cacheFile;
if (! is_array($data)) {
throw new \RuntimeException("Invalid route cache format.");
}
return $data;
}
public function isFresh(int $expectedMTime): bool
{
// Optional: prüfe eine Timestamp-Struktur etc.
if (! file_exists($this->cacheFile)) {
return false;
}
// Eigenes Format prüfen? Datei-mtime prüfen? Optional!
return true;
}
}

View File

@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
namespace App\Framework\Core;
final class RouteCompiler
{
/**
* @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}>}>
*/
public function compile(array $routes): array
{
$compiled = [];
foreach ($routes as $route) {
$method = strtoupper($route['http_method']);
$path = $route['path'];
$compiled[$method] ??= ['static' => [], 'dynamic' => []];
if (! str_contains($path, '{')) {
// Statische Route
$compiled[$method]['static'][$path] = new StaticRoute(
$route['class'],
$route['method'],
$route['parameters']
);
} else {
// Dynamische Route
$regex = $this->convertPathToRegex($path, $paramNames);
$compiled[$method]['dynamic'][] = new DynamicRoute(
$regex,
$paramNames,
$route['class'],
$route['method'],
$route['parameters']
);
}
}
return $compiled;
}
/**
* Konvertiert zB. /user/{id}/edit → ~^/user/([^/]+)/edit$~ und gibt ['id'] als Parameternamen zurück.
*
* @param string $path
* @param array<string> &$paramNames
* @return string
*/
private function convertPathToRegex(string $path, array &$paramNames): string
{
$paramNames = [];
$regex = preg_replace_callback('#\{(\w+)\}#', function ($matches) use (&$paramNames) {
$paramNames[] = $matches[1];
return '([^/]+)';
}, $path);
return '~^' . $regex . '$~';
}
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace App\Framework\Core;
use App\Framework\Attributes\Route;
use ReflectionMethod;
final readonly class RouteMapper implements AttributeMapper
{
public function getAttributeClass(): string
{
return Route::class;
}
public function map(object $reflectionTarget, object $attributeInstance): ?array
{
if (! $reflectionTarget instanceof ReflectionMethod || ! $attributeInstance instanceof Route) {
return null;
}
return [
'class' => $reflectionTarget->getDeclaringClass()->getName(),
'method' => $reflectionTarget->getName(),
'http_method' => $attributeInstance->method,
'path' => $attributeInstance->path,
];
}
}

View File

@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace App\Framework\Core;
class StaticRoute implements Route
{
public function __construct(
public string $controller,
public string $action,
public array $params
) {
}
}