chore: lots of changes
This commit is contained in:
17
src/Framework/Core/AttributeMapper.php
Normal file
17
src/Framework/Core/AttributeMapper.php
Normal 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;
|
||||
}
|
||||
171
src/Framework/Core/Discovery.php
Normal file
171
src/Framework/Core/Discovery.php
Normal 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;
|
||||
}
|
||||
}
|
||||
17
src/Framework/Core/DynamicRoute.php
Normal file
17
src/Framework/Core/DynamicRoute.php
Normal 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
|
||||
) {
|
||||
}
|
||||
}
|
||||
43
src/Framework/Core/PhpObjectExporter.php
Normal file
43
src/Framework/Core/PhpObjectExporter.php
Normal 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);
|
||||
}
|
||||
}
|
||||
9
src/Framework/Core/Route.php
Normal file
9
src/Framework/Core/Route.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core;
|
||||
|
||||
interface Route
|
||||
{
|
||||
}
|
||||
48
src/Framework/Core/RouteCache.php
Normal file
48
src/Framework/Core/RouteCache.php
Normal 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;
|
||||
}
|
||||
}
|
||||
64
src/Framework/Core/RouteCompiler.php
Normal file
64
src/Framework/Core/RouteCompiler.php
Normal 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 . '$~';
|
||||
}
|
||||
}
|
||||
30
src/Framework/Core/RouteMapper.php
Normal file
30
src/Framework/Core/RouteMapper.php
Normal 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,
|
||||
];
|
||||
}
|
||||
}
|
||||
15
src/Framework/Core/StaticRoute.php
Normal file
15
src/Framework/Core/StaticRoute.php
Normal 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
|
||||
) {
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user