chore: complete update
This commit is contained in:
@@ -1,20 +1,8 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Router;
|
||||
|
||||
use App\Framework\Http\Status;
|
||||
|
||||
final readonly class ActionResult
|
||||
interface ActionResult
|
||||
{
|
||||
public function __construct(
|
||||
public ResultType $resultType,
|
||||
public string $template,
|
||||
public array $data = [],
|
||||
public Status $status = Status::OK,
|
||||
public string $layout = '',
|
||||
public array $slots = [],
|
||||
public ?string $controllerClass = null
|
||||
) {}
|
||||
|
||||
}
|
||||
|
||||
15
src/Framework/Router/CompiledPattern.php
Normal file
15
src/Framework/Router/CompiledPattern.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\Router;
|
||||
|
||||
final readonly class CompiledPattern
|
||||
{
|
||||
/**
|
||||
* @param string $regex Kombinierter Regex
|
||||
* @param array<int, array{route: object, paramMap: array<string, int>}> $routes
|
||||
*/
|
||||
public function __construct(
|
||||
public string $regex,
|
||||
public array $routes
|
||||
) {}
|
||||
}
|
||||
77
src/Framework/Router/CompiledRoutes.php
Normal file
77
src/Framework/Router/CompiledRoutes.php
Normal file
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\Router;
|
||||
|
||||
use App\Framework\Core\Route;
|
||||
|
||||
final readonly class CompiledRoutes
|
||||
{
|
||||
/**
|
||||
* @param array<string, array<string, Route>> $staticRoutes
|
||||
* @param array<string, CompiledPattern|null> $dynamicPatterns
|
||||
*/
|
||||
public function __construct(
|
||||
private array $staticRoutes,
|
||||
private array $dynamicPatterns,
|
||||
private array $namedRoutes,
|
||||
) {}
|
||||
|
||||
public function getStaticRoute(string $method, string $path): ?Route
|
||||
{
|
||||
return $this->staticRoutes[$method][$path] ?? null;
|
||||
}
|
||||
|
||||
public function getCompiledPattern(string $method): ?CompiledPattern
|
||||
{
|
||||
return $this->dynamicPatterns[$method] ?? null;
|
||||
}
|
||||
|
||||
public function getNamedRoute(string $name): ?Route
|
||||
{
|
||||
return $this->namedRoutes[$name] ?? null;
|
||||
}
|
||||
|
||||
public function hasNamedRoute(string $name): bool
|
||||
{
|
||||
return isset($this->namedRoutes[$name]);
|
||||
}
|
||||
|
||||
public function getAllNamedRoutes(): array
|
||||
{
|
||||
return $this->namedRoutes;
|
||||
}
|
||||
|
||||
public function generateUrl(string $name, array $params = []): ?string
|
||||
{
|
||||
$route = $this->getNamedRoute($name);
|
||||
if(!$route) {
|
||||
return null;
|
||||
}
|
||||
return $this->buildUrlFromRoute($route, $params);
|
||||
}
|
||||
|
||||
|
||||
public function getStats(): array
|
||||
{
|
||||
$staticCount = array_sum(array_map('count', $this->staticRoutes));
|
||||
$dynamicCount = count(array_filter($this->dynamicPatterns));
|
||||
$namedCount = count($this->namedRoutes);
|
||||
|
||||
return [
|
||||
'static_routes' => $staticCount,
|
||||
'dynamic_patterns' => $dynamicCount,
|
||||
'named_routes' => $namedCount,
|
||||
'total_methods' => count($this->staticRoutes)
|
||||
];
|
||||
}
|
||||
|
||||
private function buildUrlFromRoute(Route $route, array $params): string
|
||||
{
|
||||
$path = $route->path;
|
||||
|
||||
foreach ($params as $key => $value) {
|
||||
$path = str_replace("{{$key}}", (string)$value, $path);
|
||||
}
|
||||
return $path;
|
||||
}
|
||||
}
|
||||
61
src/Framework/Router/ControllerRequestFactory.php
Normal file
61
src/Framework/Router/ControllerRequestFactory.php
Normal file
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Router;
|
||||
|
||||
use App\Framework\DI\DefaultContainer;
|
||||
use App\Framework\Http\Request;
|
||||
use App\Framework\Http\RequestBody;
|
||||
use App\Framework\Http\Session\Session;
|
||||
use App\Framework\Validation\Exceptions\ValidationException;
|
||||
use App\Framework\Validation\Validator;
|
||||
use Exception;
|
||||
use ReflectionClass;
|
||||
use ReflectionNamedType;
|
||||
use ReflectionProperty;
|
||||
|
||||
final readonly class ControllerRequestFactory
|
||||
{
|
||||
public function __construct(
|
||||
private DefaultContainer $container,
|
||||
private PropertyValueConverter $propertyValueConverter
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Erstellt und validiert eine ControllerRequest-Instanz
|
||||
*/
|
||||
public function createAndValidate(ReflectionClass $requestClass, RequestBody $data): object
|
||||
{
|
||||
// Instanz erstellen
|
||||
$instance = $requestClass->newInstance();
|
||||
|
||||
// Eigenschaften befüllen und validieren
|
||||
foreach ($requestClass->getProperties() as $property) {
|
||||
$propertyName = $property->getName();
|
||||
|
||||
// Wert aus den Daten abrufen
|
||||
$value = $data->get($propertyName, null);
|
||||
|
||||
try {
|
||||
$this->propertyValueConverter->setPropertyValue($property, $instance, $value);
|
||||
} catch (\Throwable $e) {
|
||||
#throw new ValidationException(
|
||||
# ['Ungültiger Wert für den Typ: ' . $e->getMessage()],
|
||||
# $propertyName
|
||||
#);
|
||||
}
|
||||
}
|
||||
|
||||
$validator = $this->container->get(Validator::class);
|
||||
|
||||
$validationResult = $validator->validate($instance);
|
||||
|
||||
if ($validationResult->hasErrors()) {
|
||||
|
||||
throw new ValidationException($validationResult, 'test');
|
||||
}
|
||||
|
||||
return $instance;
|
||||
}
|
||||
}
|
||||
27
src/Framework/Router/Exception/RouteNotFound.php
Normal file
27
src/Framework/Router/Exception/RouteNotFound.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\Router\Exception;
|
||||
|
||||
use App\Framework\Exception\FrameworkException;
|
||||
use App\Framework\Http\Exception\HttpException;
|
||||
use App\Framework\Http\Status;
|
||||
|
||||
class RouteNotFound extends FrameworkException implements HttpException
|
||||
{
|
||||
public Status $status {
|
||||
get {
|
||||
return Status::NOT_FOUND;
|
||||
}
|
||||
}
|
||||
|
||||
protected string $route;
|
||||
|
||||
public function __construct(string $route, ?\Throwable $previous = null, int $code = 0, array $context = [])
|
||||
{
|
||||
$this->route = $route;
|
||||
$message = "Route not found: {$route}";
|
||||
|
||||
#$context
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Router\Exceptions;
|
||||
|
||||
use App\Framework\Exception\FrameworkException;
|
||||
|
||||
final class ParameterResolutionException extends FrameworkException
|
||||
{
|
||||
|
||||
/**
|
||||
* @param string $string
|
||||
* @param int $int
|
||||
* @param \Exception|ReflectionException $e
|
||||
*/
|
||||
public function __construct(string $string, int $int, \Throwable $e) {
|
||||
parent::__construct($string, $int, $e);
|
||||
}
|
||||
}
|
||||
23
src/Framework/Router/Exceptions/UnknownResultException.php
Normal file
23
src/Framework/Router/Exceptions/UnknownResultException.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Router\Exceptions;
|
||||
|
||||
use App\Framework\Exception\FrameworkException;
|
||||
|
||||
final class UnknownResultException extends FrameworkException
|
||||
{
|
||||
public function __construct(
|
||||
string $resultClass,
|
||||
int $code = 0,
|
||||
?\Throwable $previous = null
|
||||
) {
|
||||
parent::__construct(
|
||||
message: 'Unbekanntes Ergebnisobjekt: ' . $resultClass,
|
||||
code: $code,
|
||||
previous: $previous,
|
||||
context: ['resultClass' => $resultClass]
|
||||
);
|
||||
}
|
||||
}
|
||||
20
src/Framework/Router/GenericActionResult.php
Normal file
20
src/Framework/Router/GenericActionResult.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Router;
|
||||
|
||||
use App\Framework\Http\Status;
|
||||
|
||||
final readonly class GenericActionResult implements ActionResult
|
||||
{
|
||||
private function __construct(
|
||||
public ResultType $resultType,
|
||||
public string $template,
|
||||
public array $data = [],
|
||||
public Status $status = Status::OK,
|
||||
#public string $layout = '',
|
||||
public array $slots = [],
|
||||
public ?string $controllerClass = null
|
||||
) {}
|
||||
}
|
||||
@@ -4,13 +4,17 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Router;
|
||||
|
||||
use App\Framework\Attributes\Singleton;
|
||||
use App\Framework\Core\DynamicRoute;
|
||||
use App\Framework\Http\HttpMethod;
|
||||
use App\Framework\Http\Method;
|
||||
use App\Framework\Http\Request;
|
||||
|
||||
#[Singleton]
|
||||
final readonly class HttpRouter
|
||||
{
|
||||
public function __construct(
|
||||
private RouteCollection $routes
|
||||
#public RouteCollection $routes,
|
||||
public CompiledRoutes $optimizedRoutes,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -19,30 +23,58 @@ final readonly class HttpRouter
|
||||
* @param string $method HTTP-Methode, zB. GET, POST
|
||||
* @param string $path URI-Pfad, zB. /user/123
|
||||
*/
|
||||
public function match(string $method, string $path): RouteContext
|
||||
public function match(Request $request): RouteContext
|
||||
{
|
||||
$method = HttpMethod::tryFrom(strtoupper($method));
|
||||
$method = $request->method;
|
||||
$path = $request->path;
|
||||
|
||||
if ($method === null || !$this->routes->has($method->value)) {
|
||||
if($staticRoute = $this->optimizedRoutes->getStaticRoute($method->value, $path)) {
|
||||
return new RouteContext(
|
||||
match: new NoRouteMatch(),
|
||||
method: $method->value,
|
||||
match: new RouteMatchSuccess($staticRoute),
|
||||
method: $method,
|
||||
path: $path
|
||||
);
|
||||
}
|
||||
|
||||
$match = $this->matchStatic($method->value, $path)
|
||||
?? $this->matchDynamic($method->value, $path)
|
||||
if($dynamicMatch = $this->matchDynamicOptimized($method, $path)) {
|
||||
return new RouteContext(
|
||||
match: new RouteMatchSuccess($dynamicMatch),
|
||||
method: $method,
|
||||
path: $path
|
||||
);
|
||||
}
|
||||
|
||||
return new RouteContext(
|
||||
match: new NoRouteMatch(),
|
||||
method: $method,
|
||||
path: $path
|
||||
);
|
||||
|
||||
/*
|
||||
$method = $request->method;
|
||||
$path = $request->path;
|
||||
|
||||
if (!$this->routes->has($method)) {
|
||||
return new RouteContext(
|
||||
match: new NoRouteMatch(),
|
||||
method: $method,
|
||||
path: $path
|
||||
);
|
||||
}
|
||||
|
||||
$match = $this->matchStatic($method, $path)
|
||||
?? $this->matchDynamic($method, $path)
|
||||
?? new NoRouteMatch();
|
||||
|
||||
return new RouteContext(
|
||||
match: $match,
|
||||
method: $method->value,
|
||||
method: $method,
|
||||
path: $path
|
||||
);
|
||||
);*/
|
||||
}
|
||||
|
||||
private function matchStatic(string $method, string $path): ?RouteMatch
|
||||
|
||||
private function matchStatic(Method $method, string $path): ?RouteMatch
|
||||
{
|
||||
if (isset($this->routes->getStatic($method)[$path])) {
|
||||
$handler = $this->routes->getStatic($method)[$path];
|
||||
@@ -51,25 +83,100 @@ final readonly class HttpRouter
|
||||
return null;
|
||||
}
|
||||
|
||||
private function matchDynamic(string $method, string $path): ?RouteMatch
|
||||
private function matchDynamic(Method $method, string $path): ?RouteMatch
|
||||
{
|
||||
/* @var $route DynamicRoute */
|
||||
foreach ($this->routes->getDynamic($method) as $route) {
|
||||
if (preg_match($route['regex'], $path, $matches)) {
|
||||
|
||||
if (preg_match($route->regex, $path, $matches)) {
|
||||
|
||||
array_shift($matches); // remove full match
|
||||
$params = array_combine($route['params'], $matches);
|
||||
|
||||
// Überprüfe ob die Anzahl der URL-Parameter stimmt
|
||||
if (count($route->paramNames) !== count($matches)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$paramValues = array_combine($route->paramNames, $matches);
|
||||
|
||||
if (!$paramValues) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$dynamicRoute = new DynamicRoute(
|
||||
$route["regex"],
|
||||
$route["params"],
|
||||
$route['method']['class'],
|
||||
$route['method']['method'],
|
||||
$params
|
||||
$route->regex,
|
||||
$route->paramNames,
|
||||
$route->controller,
|
||||
$route->action,
|
||||
$route->parameters,
|
||||
$paramValues,
|
||||
$route->name,
|
||||
$route->path,
|
||||
);
|
||||
|
||||
return new RouteMatchSuccess($route);
|
||||
return new RouteMatchSuccess($dynamicRoute);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function matchDynamicOptimized(Method $method, string $path): ?DynamicRoute
|
||||
{
|
||||
$pattern = $this->optimizedRoutes->getCompiledPattern($method->value);
|
||||
|
||||
if(!$pattern || !preg_match($pattern->regex, $path, $matches)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$routeIndex = $this->findMatchingRouteIndex($matches, $method);
|
||||
$routeData = $pattern->routes[$routeIndex];
|
||||
$paramValues = $this->extractParameters($matches, $routeData['paramMap']);
|
||||
|
||||
$originalRoute = $routeData['route'];
|
||||
return new DynamicRoute(
|
||||
regex: $originalRoute->regex,
|
||||
paramNames: $originalRoute->paramNames,
|
||||
controller: $originalRoute->controller,
|
||||
action: $originalRoute->action,
|
||||
parameters: $originalRoute->parameters,
|
||||
paramValues: $paramValues,
|
||||
name: $originalRoute->name,
|
||||
path: $originalRoute->path,
|
||||
attributes: $originalRoute->attributes ?? [],
|
||||
);
|
||||
}
|
||||
|
||||
private function findMatchingRouteIndex(array $matches, $method): int
|
||||
{
|
||||
$pattern = $this->optimizedRoutes->getCompiledPattern($method->value);
|
||||
|
||||
foreach($pattern->routes as $index => $routeData) {
|
||||
$routeGroupIndex = $routeData['routeGroupIndex'];
|
||||
|
||||
if (isset($matches[$routeGroupIndex]) && $matches[$routeGroupIndex] !== '') {
|
||||
return $index;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
||||
|
||||
for($i = 1; $i < count($matches); $i++) {
|
||||
if($matches[$i] !== '') {
|
||||
return $i - 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private function extractParameters(array $matches, array $paramMap): array
|
||||
{
|
||||
$params = [];
|
||||
foreach($paramMap as $paramName => $matchIndex) {
|
||||
if (isset($matches[$matchIndex]) && $matches[$matchIndex] !== '') {
|
||||
$params[$paramName] = $matches[$matchIndex];
|
||||
}
|
||||
}
|
||||
return $params;
|
||||
}
|
||||
}
|
||||
|
||||
76
src/Framework/Router/ParameterProcessor.php
Normal file
76
src/Framework/Router/ParameterProcessor.php
Normal file
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Router;
|
||||
|
||||
use App\Framework\DI\DefaultContainer;
|
||||
use App\Framework\Http\ControllerRequest;
|
||||
use App\Framework\Http\Request;
|
||||
use App\Framework\Logging\DefaultLogger;
|
||||
use App\Framework\Router\Exceptions\ParameterResolutionException;
|
||||
use ReflectionClass;
|
||||
use ReflectionException;
|
||||
|
||||
final readonly class ParameterProcessor
|
||||
{
|
||||
public function __construct(
|
||||
private DefaultContainer $container,
|
||||
private ControllerRequestFactory $controllerRequestFactory,
|
||||
private DefaultLogger $logger
|
||||
) {}
|
||||
|
||||
public function prepareParameters(array $params, array $queryParams): array
|
||||
{
|
||||
$parameters = [];
|
||||
|
||||
foreach ($params as $param) {
|
||||
try {
|
||||
if(in_array($param['name'], array_keys($queryParams))) {
|
||||
$parameters[] = $queryParams[$param['name']];
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($param['isBuiltin'] === true) {
|
||||
$parameters[] = $param['default'] ?? null;
|
||||
continue;
|
||||
}
|
||||
|
||||
$reflectionClass = new ReflectionClass($param['type']);
|
||||
|
||||
if ($reflectionClass->implementsInterface(ControllerRequest::class)) {
|
||||
$request = $this->container->get(Request::class);
|
||||
$parsedBody = $request->parsedBody;
|
||||
|
||||
$controllerRequest = $this->controllerRequestFactory
|
||||
->createAndValidate($reflectionClass, $parsedBody);
|
||||
|
||||
$this->container->bind($param['type'], $controllerRequest);
|
||||
$parameters[] = $controllerRequest;
|
||||
continue;
|
||||
}
|
||||
|
||||
$parameters[] = $this->container->get($param['type']);
|
||||
|
||||
} catch (ReflectionException $e) {
|
||||
$this->logger?->error("Parameter reflection failed", [
|
||||
'type' => $param['type'],
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
throw new ParameterResolutionException(
|
||||
"Cannot resolve parameter of type '{$param['type']}'",
|
||||
0,
|
||||
$e
|
||||
);
|
||||
} catch (\Throwable $e) {
|
||||
$this->logger?->error("Parameter preparation failed", [
|
||||
'param' => $param,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
return $parameters;
|
||||
}
|
||||
}
|
||||
51
src/Framework/Router/PropertyValueConverter.php
Normal file
51
src/Framework/Router/PropertyValueConverter.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Router;
|
||||
|
||||
use ReflectionNamedType;
|
||||
use ReflectionProperty;
|
||||
|
||||
final readonly class PropertyValueConverter
|
||||
{
|
||||
/**
|
||||
* Setzt einen Wert für eine Property mit entsprechender Typenkonvertierung
|
||||
*/
|
||||
public function setPropertyValue(ReflectionProperty $property, object $instance, mixed $value): void
|
||||
{
|
||||
if ($property->hasDefaultValue()) {
|
||||
$property->setValue($instance, $property->getDefaultValue());
|
||||
return;
|
||||
}
|
||||
|
||||
if ($value === null) {
|
||||
$property->setValue($instance, null);
|
||||
return;
|
||||
}
|
||||
|
||||
$type = $property->getType();
|
||||
|
||||
if ($type instanceof ReflectionNamedType) {
|
||||
$typeName = $type->getName();
|
||||
$value = $this->convertValueToType($value, $typeName);
|
||||
}
|
||||
|
||||
$property->setValue($instance, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Konvertiert einen Wert in den angegebenen Typ
|
||||
*/
|
||||
private function convertValueToType(mixed $value, string $typeName): mixed
|
||||
{
|
||||
return match ($typeName) {
|
||||
'int' => (int) $value,
|
||||
'float' => (float) $value,
|
||||
'bool' => filter_var($value, FILTER_VALIDATE_BOOLEAN),
|
||||
'string' => (string) $value,
|
||||
'array' => is_array($value) ? $value : [$value],
|
||||
default => $value
|
||||
};
|
||||
}
|
||||
}
|
||||
15
src/Framework/Router/Result/ContentNegotiationResult.php
Normal file
15
src/Framework/Router/Result/ContentNegotiationResult.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\Router\Result;
|
||||
|
||||
use App\Framework\Router\ActionResult;
|
||||
|
||||
final readonly class ContentNegotiationResult implements ActionResult
|
||||
{
|
||||
public function __construct(
|
||||
public array $jsonPayload = [],
|
||||
public ?string $redirectTo = null,
|
||||
public ?string $viewTemplate = null,
|
||||
public array $viewData = [],
|
||||
) {}
|
||||
}
|
||||
14
src/Framework/Router/Result/FileResult.php
Normal file
14
src/Framework/Router/Result/FileResult.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\Router\Result;
|
||||
|
||||
use App\Framework\Router\ActionResult;
|
||||
|
||||
final readonly class FileResult implements ActionResult
|
||||
{
|
||||
public function __construct(
|
||||
public string $filePath,
|
||||
public string $variant = 'original',
|
||||
public string $mimeType = 'application/image',
|
||||
) {}
|
||||
}
|
||||
15
src/Framework/Router/Result/JsonResult.php
Normal file
15
src/Framework/Router/Result/JsonResult.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Router\Result;
|
||||
|
||||
use App\Framework\Http\Status;
|
||||
use App\Framework\Router\ActionResult;
|
||||
|
||||
final class JsonResult implements ActionResult
|
||||
{
|
||||
public function __construct(
|
||||
public readonly array $data,
|
||||
public Status $status = Status::OK,
|
||||
) {}
|
||||
}
|
||||
15
src/Framework/Router/Result/Redirect.php
Normal file
15
src/Framework/Router/Result/Redirect.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Router\Result;
|
||||
|
||||
use App\Framework\Http\Status;
|
||||
use App\Framework\Router\ActionResult;
|
||||
|
||||
final class Redirect implements ActionResult
|
||||
{
|
||||
public Status $status = Status::FOUND;
|
||||
public function __construct(
|
||||
public readonly string $target,
|
||||
) {}
|
||||
}
|
||||
52
src/Framework/Router/Result/SseEvent.php
Normal file
52
src/Framework/Router/Result/SseEvent.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Router\Result;
|
||||
|
||||
/**
|
||||
* Repräsentiert ein einzelnes Server-Sent Event
|
||||
*/
|
||||
final readonly class SseEvent
|
||||
{
|
||||
/**
|
||||
* @param string $data Der Dateninhalt des Events
|
||||
* @param string|null $event Der Event-Typ (optional)
|
||||
* @param string|null $id Die Event-ID (optional)
|
||||
* @param int|null $retry Retry-Intervall in Millisekunden (optional)
|
||||
*/
|
||||
public function __construct(
|
||||
public string $data,
|
||||
public ?string $event = null,
|
||||
public ?string $id = null,
|
||||
public ?int $retry = null
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Formatiert das Event in das SSE-Protokollformat
|
||||
*/
|
||||
public function format(): string
|
||||
{
|
||||
$formatted = [];
|
||||
|
||||
if ($this->id !== null) {
|
||||
$formatted[] = "id: {$this->id}";
|
||||
}
|
||||
|
||||
if ($this->event !== null) {
|
||||
$formatted[] = "event: {$this->event}";
|
||||
}
|
||||
|
||||
if ($this->retry !== null) {
|
||||
$formatted[] = "retry: {$this->retry}";
|
||||
}
|
||||
|
||||
// Mehrzeilige Daten unterstützen
|
||||
foreach (explode("\n", $this->data) as $line) {
|
||||
$formatted[] = "data: {$line}";
|
||||
}
|
||||
|
||||
$formatted[] = ''; // Leere Zeile am Ende
|
||||
|
||||
return implode("\n", $formatted);
|
||||
}
|
||||
}
|
||||
72
src/Framework/Router/Result/SseResult.php
Normal file
72
src/Framework/Router/Result/SseResult.php
Normal file
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Router\Result;
|
||||
|
||||
use App\Framework\Http\Status;
|
||||
use App\Framework\Router\ActionResult;
|
||||
use Closure;
|
||||
|
||||
final class SseResult implements ActionResult
|
||||
{
|
||||
/**
|
||||
* @var array<SseEvent> Enthält die SSE-Events, die beim initialen Verbindungsaufbau gesendet werden
|
||||
*/
|
||||
private array $initialEvents = [];
|
||||
|
||||
/**
|
||||
* @param Status $status HTTP-Status-Code der Antwort
|
||||
* @param int|null $retryInterval Zeit in Millisekunden, nach der der Client bei Verbindungsverlust neu verbinden soll
|
||||
* @param array<string, string> $headers Zusätzliche HTTP-Header für die Response
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly Status $status = Status::OK,
|
||||
public readonly ?int $retryInterval = 3000,
|
||||
public readonly array $headers = [],
|
||||
|
||||
public readonly ?Closure $callback = null,
|
||||
public readonly int $maxDuration = 0,
|
||||
public readonly int $heartbeatInterval = 30,
|
||||
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Fügt ein Event hinzu, das beim initialen Verbindungsaufbau gesendet wird
|
||||
*/
|
||||
public function addEvent(
|
||||
string $data,
|
||||
?string $event = null,
|
||||
?string $id = null,
|
||||
?int $retry = null
|
||||
): self {
|
||||
$this->initialEvents[] = new SseEvent($data, $event, $id, $retry);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fügt ein JSON-formatiertes Event hinzu
|
||||
*/
|
||||
public function addJsonEvent(
|
||||
array $data,
|
||||
?string $event = null,
|
||||
?string $id = null,
|
||||
?int $retry = null
|
||||
): self {
|
||||
return $this->addEvent(json_encode($data), $event, $id, $retry);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt alle initialen Events zurück
|
||||
*
|
||||
* @return array<SseEvent>
|
||||
*/
|
||||
public function getInitialEvents(): array
|
||||
{
|
||||
return $this->initialEvents;
|
||||
}
|
||||
|
||||
public function hasCallback(): bool
|
||||
{
|
||||
return $this->callback !== null;
|
||||
}
|
||||
}
|
||||
92
src/Framework/Router/Result/SseResultWithCallback.php
Normal file
92
src/Framework/Router/Result/SseResultWithCallback.php
Normal file
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Router\Result;
|
||||
|
||||
use App\Framework\Http\SseStream;
|
||||
use App\Framework\Http\Status;
|
||||
use App\Framework\Router\ActionResult;
|
||||
|
||||
/**
|
||||
* Erweiterte SSE-Klasse, die eine Callback-Funktion für Live-Updates unterstützt
|
||||
*/
|
||||
final class SseResultWithCallback implements ActionResult
|
||||
{
|
||||
/**
|
||||
* @var callable|null Callback-Funktion, die während des Streamings ausgeführt wird
|
||||
*/
|
||||
private $callback = null;
|
||||
|
||||
/**
|
||||
* @var int Maximale Streaming-Dauer in Sekunden (0 = unbegrenzt)
|
||||
*/
|
||||
private int $maxDuration = 0;
|
||||
|
||||
/**
|
||||
* @var int Intervall für Heartbeats in Sekunden
|
||||
*/
|
||||
private int $heartbeatInterval = 30;
|
||||
|
||||
public function __construct(
|
||||
public readonly Status $status = Status::OK,
|
||||
public readonly array $headers = []
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Setzt die Callback-Funktion, die während des Streamings aufgerufen wird
|
||||
* Der Callback erhält den SseStream als Parameter
|
||||
*
|
||||
* @param callable $callback Funktion mit Signatur: function(SseStream $stream): void
|
||||
*/
|
||||
public function setCallback(callable $callback): self
|
||||
{
|
||||
$this->callback = $callback;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt die maximale Streaming-Dauer
|
||||
*
|
||||
* @param int $seconds Maximale Dauer in Sekunden (0 = unbegrenzt)
|
||||
*/
|
||||
public function setMaxDuration(int $seconds): self
|
||||
{
|
||||
$this->maxDuration = $seconds;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt das Intervall für Heartbeats
|
||||
*
|
||||
* @param int $seconds Intervall in Sekunden
|
||||
*/
|
||||
public function setHeartbeatInterval(int $seconds): self
|
||||
{
|
||||
$this->heartbeatInterval = $seconds;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt den Callback zurück
|
||||
*/
|
||||
public function getCallback(): ?callable
|
||||
{
|
||||
return $this->callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt die maximale Streaming-Dauer zurück
|
||||
*/
|
||||
public function getMaxDuration(): int
|
||||
{
|
||||
return $this->maxDuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt das Heartbeat-Intervall zurück
|
||||
*/
|
||||
public function getHeartbeatInterval(): int
|
||||
{
|
||||
return $this->heartbeatInterval;
|
||||
}
|
||||
}
|
||||
27
src/Framework/Router/Result/ViewResult.php
Normal file
27
src/Framework/Router/Result/ViewResult.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Router\Result;
|
||||
|
||||
use App\Framework\Http\Status;
|
||||
use App\Framework\Meta\MetaData;
|
||||
use App\Framework\Router\ActionResult;
|
||||
|
||||
final readonly class ViewResult implements ActionResult
|
||||
{
|
||||
public function __construct(
|
||||
public string $template,
|
||||
public MetaData $metaData,
|
||||
public array $data = [],
|
||||
public Status $status = Status::OK,
|
||||
#public string $layout = '',
|
||||
public array $slots = [],
|
||||
public ?string $controllerClass = null,
|
||||
public ?object $model = null,
|
||||
) {}
|
||||
|
||||
/*public static function fromStrings():self
|
||||
{
|
||||
|
||||
}*/
|
||||
}
|
||||
130
src/Framework/Router/Result/WebSocketResult.php
Normal file
130
src/Framework/Router/Result/WebSocketResult.php
Normal file
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Router\Result;
|
||||
|
||||
use App\Framework\Http\Status;
|
||||
use App\Framework\Router\ActionResult;
|
||||
|
||||
final class WebSocketResult implements ActionResult
|
||||
{
|
||||
/**
|
||||
* @var callable|null Handler für neue Verbindungen
|
||||
*/
|
||||
private $onConnect = null;
|
||||
|
||||
/**
|
||||
* @var callable|null Handler für eingehende Nachrichten
|
||||
*/
|
||||
private $onMessage = null;
|
||||
|
||||
/**
|
||||
* @var callable|null Handler für geschlossene Verbindungen
|
||||
*/
|
||||
private $onClose = null;
|
||||
|
||||
/**
|
||||
* @var callable|null Handler für Fehler
|
||||
*/
|
||||
private $onError = null;
|
||||
|
||||
/**
|
||||
* @var array Unterstützte Subprotokolle
|
||||
*/
|
||||
private array $subprotocols = [];
|
||||
|
||||
/**
|
||||
* @var int Maximale Nachrichtengröße in Bytes
|
||||
*/
|
||||
private int $maxMessageSize = 1048576; // 1MB
|
||||
|
||||
/**
|
||||
* @var int Ping-Intervall in Sekunden
|
||||
*/
|
||||
private int $pingInterval = 30;
|
||||
|
||||
public function __construct(
|
||||
public readonly Status $status = Status::SWITCHING_PROTOCOLS,
|
||||
public readonly array $headers = []
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Setzt den Handler für neue Verbindungen
|
||||
*
|
||||
* @param callable $handler function(WebSocketConnection $connection): void
|
||||
*/
|
||||
public function onConnect(callable $handler): self
|
||||
{
|
||||
$this->onConnect = $handler;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt den Handler für eingehende Nachrichten
|
||||
*
|
||||
* @param callable $handler function(WebSocketConnection $connection, string $message): void
|
||||
*/
|
||||
public function onMessage(callable $handler): self
|
||||
{
|
||||
$this->onMessage = $handler;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt den Handler für geschlossene Verbindungen
|
||||
*
|
||||
* @param callable $handler function(WebSocketConnection $connection, int $code, string $reason): void
|
||||
*/
|
||||
public function onClose(callable $handler): self
|
||||
{
|
||||
$this->onClose = $handler;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt den Handler für Fehler
|
||||
*
|
||||
* @param callable $handler function(WebSocketConnection $connection, \Throwable $error): void
|
||||
*/
|
||||
public function onError(callable $handler): self
|
||||
{
|
||||
$this->onError = $handler;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt unterstützte Subprotokolle
|
||||
*/
|
||||
public function withSubprotocols(array $subprotocols): self
|
||||
{
|
||||
$this->subprotocols = $subprotocols;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt die maximale Nachrichtengröße
|
||||
*/
|
||||
public function withMaxMessageSize(int $bytes): self
|
||||
{
|
||||
$this->maxMessageSize = $bytes;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt das Ping-Intervall
|
||||
*/
|
||||
public function withPingInterval(int $seconds): self
|
||||
{
|
||||
$this->pingInterval = $seconds;
|
||||
return $this;
|
||||
}
|
||||
|
||||
// Getter
|
||||
public function getOnConnect(): ?callable { return $this->onConnect; }
|
||||
public function getOnMessage(): ?callable { return $this->onMessage; }
|
||||
public function getOnClose(): ?callable { return $this->onClose; }
|
||||
public function getOnError(): ?callable { return $this->onError; }
|
||||
public function getSubprotocols(): array { return $this->subprotocols; }
|
||||
public function getMaxMessageSize(): int { return $this->maxMessageSize; }
|
||||
public function getPingInterval(): int { return $this->pingInterval; }
|
||||
}
|
||||
@@ -2,24 +2,63 @@
|
||||
|
||||
namespace App\Framework\Router;
|
||||
|
||||
final class RouteCollection
|
||||
use App\Framework\Core\Route;
|
||||
use App\Framework\Http\Method;
|
||||
use Countable;
|
||||
use Generator;
|
||||
|
||||
final readonly class RouteCollection implements Countable
|
||||
{
|
||||
/** @var array<string, array{static: array<string, callable>, dynamic: array<array{regex: string, handler: callable}>}> */
|
||||
private array $routes;
|
||||
/**
|
||||
* @param array<string, array{static: array<string, callable>, dynamic: array<array{regex: string, handler: callable}>}> $routes
|
||||
#* @param array<string, array{path: string, method: Method, handler: callable}> $namedRoutes
|
||||
*/
|
||||
public function __construct(
|
||||
private array $routes,
|
||||
private array $namedRoutes = []
|
||||
) {}
|
||||
|
||||
public function __construct(array $routes) {
|
||||
$this->routes = $routes;
|
||||
public function getStatic(Method $method): array {
|
||||
return $this->routes[$method->value]['static'] ?? [];
|
||||
}
|
||||
|
||||
public function getStatic(string $method): array {
|
||||
return $this->routes[$method]['static'] ?? [];
|
||||
public function getDynamic(Method $method): array {
|
||||
return $this->routes[$method->value]['dynamic'] ?? [];
|
||||
}
|
||||
|
||||
public function getDynamic(string $method): array {
|
||||
return $this->routes[$method]['dynamic'] ?? [];
|
||||
public function has(Method $method): bool {
|
||||
return isset($this->routes[$method->value]);
|
||||
}
|
||||
|
||||
public function has(string $method): bool {
|
||||
return isset($this->routes[$method]);
|
||||
public function getByMethod(Method $method): array {
|
||||
|
||||
return ($this->routes[$method->value]['dynamic'] + $this->routes[$method->value]['static']) ?? [];
|
||||
}
|
||||
|
||||
public function getByName(string $name): ?Route
|
||||
{
|
||||
return $this->namedRoutes[$name] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt alle Named Routes zurück
|
||||
*
|
||||
* @return Generator<string, Route>
|
||||
*/
|
||||
public function getAllNamedRoutes(): Generator
|
||||
{
|
||||
foreach ($this->namedRoutes as $name => $route) {
|
||||
yield $name => $route;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function hasName(string $name): bool {
|
||||
return isset($this->namedRoutes[$name]);
|
||||
}
|
||||
|
||||
public function count(): int
|
||||
{
|
||||
return count($this->routes);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Framework\Router;
|
||||
|
||||
use App\Framework\Http\Method;
|
||||
use Closure;
|
||||
|
||||
final class RouteContext
|
||||
@@ -16,8 +17,8 @@ final class RouteContext
|
||||
|
||||
public function __construct(
|
||||
public readonly RouteMatch $match,
|
||||
public readonly string $method,
|
||||
public readonly string $path
|
||||
public readonly Method $method,
|
||||
public readonly string $path
|
||||
) {}
|
||||
|
||||
public function isSuccess(): bool
|
||||
|
||||
@@ -4,48 +4,58 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Router;
|
||||
|
||||
use App\Framework\Http\HttpResponse;
|
||||
use App\Framework\Core\DynamicRoute;
|
||||
use App\Framework\DI\DefaultContainer;
|
||||
use App\Framework\Http\Response;
|
||||
use App\Framework\Http\Status;
|
||||
use App\Framework\Http\Responses\NotFound;
|
||||
use App\Framework\Router\Result\ContentNegotiationResult;
|
||||
|
||||
class RouteDispatcher
|
||||
final readonly class RouteDispatcher
|
||||
{
|
||||
public function __construct(
|
||||
private DefaultContainer $container,
|
||||
private ParameterProcessor $parameterProcessor
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Verarbeitet eine Route und führt den entsprechenden Controller-Action aus
|
||||
*/
|
||||
public function dispatch(RouteContext $routeContext): ActionResult|Response
|
||||
{
|
||||
$routeMatch = $routeContext->match;
|
||||
|
||||
if ($routeMatch->isMatch()) {
|
||||
$controller = $routeMatch->route->controller;
|
||||
$action = $routeMatch->route->action;
|
||||
$params = $routeMatch->route->params;
|
||||
|
||||
$params = $this->prepareParameters(...$params);
|
||||
|
||||
$obj = new $controller();
|
||||
$result = $obj->$action(...$params);
|
||||
|
||||
// Hier könntest du z. B. Response-Objekte erwarten oder generieren:
|
||||
if ($result instanceof Response || $result instanceof ActionResult) {
|
||||
return $result;
|
||||
}
|
||||
return $this->executeController($routeMatch);
|
||||
}
|
||||
|
||||
// Fehlerbehandlung z.B. 404
|
||||
return new HttpResponse(status: Status::NOT_FOUND, body: 'Nicht gefunden');
|
||||
return new NotFound();
|
||||
}
|
||||
|
||||
public function prepareParameters(...$params): mixed
|
||||
/**
|
||||
* Führt die Controller-Action mit den aufbereiteten Parametern aus
|
||||
*/
|
||||
private function executeController(RouteMatch $routeMatch): ActionResult|Response
|
||||
{
|
||||
$parameters = [];
|
||||
foreach ($params as $param) {
|
||||
if ($param['isBuiltin'] === true) {
|
||||
$parameters[] = $param['default'];
|
||||
} else {
|
||||
#Container!
|
||||
var_dump($param['isBuiltin']);
|
||||
}
|
||||
$controller = $routeMatch->route->controller;
|
||||
$action = $routeMatch->route->action;
|
||||
$params = $routeMatch->route->parameters;
|
||||
|
||||
$queryParams = [];
|
||||
|
||||
if($routeMatch->route instanceof DynamicRoute) {
|
||||
$queryParams = $routeMatch->route->paramValues;
|
||||
}
|
||||
|
||||
return $parameters;
|
||||
$preparedParams = $this->parameterProcessor->prepareParameters($params, $queryParams);
|
||||
|
||||
$controllerInstance = $this->container->get($controller);
|
||||
$result = $controllerInstance->$action(...$preparedParams);
|
||||
|
||||
if ($result instanceof Response || $result instanceof ActionResult) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
// Wenn kein gültiges Result zurückgegeben wird
|
||||
return new NotFound('Ungültige Antwort vom Controller');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,15 +5,13 @@ declare(strict_types=1);
|
||||
namespace App\Framework\Router;
|
||||
|
||||
use App\Framework\Core\DynamicRoute;
|
||||
use App\Framework\Core\Route;
|
||||
use App\Framework\Core\StaticRoute;
|
||||
|
||||
final readonly class RouteMatchSuccess implements RouteMatch
|
||||
{
|
||||
#public string $controller;
|
||||
#public string $action;
|
||||
#public array $params;
|
||||
public function __construct(
|
||||
public DynamicRoute|StaticRoute $route
|
||||
public Route $route
|
||||
) {
|
||||
}
|
||||
|
||||
|
||||
@@ -4,70 +4,99 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Router;
|
||||
|
||||
use App\Framework\Core\PathProvider;
|
||||
use App\Framework\DI\DefaultContainer;
|
||||
use App\Framework\Http\Headers;
|
||||
use App\Framework\Http\HttpResponse;
|
||||
use App\Framework\Http\Response;
|
||||
use App\Framework\View\Engine;
|
||||
use App\Framework\Http\Responses\SseResponse;
|
||||
use App\Framework\Http\Responses\WebSocketResponse;
|
||||
use App\Framework\Router\Result\FileResult;
|
||||
use App\Framework\Router\Result\JsonResult;
|
||||
use App\Framework\Router\Result\Redirect;
|
||||
use App\Framework\Router\Result\SseResult;
|
||||
use App\Framework\Router\Result\ViewResult;
|
||||
use App\Framework\Router\Result\WebSocketResult;
|
||||
use App\Framework\View\RenderContext;
|
||||
use App\Framework\View\Template;
|
||||
use App\Framework\View\TemplateRenderer;
|
||||
use App\Framework\View\TemplateRendererInitializer;
|
||||
|
||||
readonly class RouteResponder
|
||||
final readonly class RouteResponder
|
||||
{
|
||||
public function __construct(
|
||||
private TemplateRenderer $templateRenderer = new Engine()
|
||||
) {
|
||||
}
|
||||
private PathProvider $pathProvider,
|
||||
private DefaultContainer $container,
|
||||
private TemplateRenderer $templateRenderer,
|
||||
) {}
|
||||
|
||||
public function respond(ActionResult $result): Response
|
||||
public function respond(Response|ActionResult $result): Response
|
||||
{
|
||||
$contentType = "text/html";
|
||||
|
||||
switch ($result->resultType) {
|
||||
case ResultType::Html:
|
||||
$body = $this->renderTemplate(
|
||||
$result->template,
|
||||
$result->data,
|
||||
$result->layout ?? null,
|
||||
$result->slots ?? [],
|
||||
$result->controllerClass
|
||||
);
|
||||
|
||||
$contentType = "text/html";
|
||||
|
||||
break;
|
||||
case ResultType::Json:
|
||||
$body = json_encode(
|
||||
$result->data,
|
||||
JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE
|
||||
);
|
||||
$contentType = "application/json";
|
||||
|
||||
break;
|
||||
case ResultType::Plain:
|
||||
$body = $result->data['text'] ?? '';
|
||||
$contentType = "text/plain";
|
||||
break;
|
||||
default:
|
||||
throw new \RuntimeException("Unknown result type: {$result->resultType}");
|
||||
if($result instanceof Response) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
return new HttpResponse(
|
||||
status: $result->status,
|
||||
headers: new Headers()->with('Content-Type', $contentType), //['Content-Type' => $contentType],
|
||||
body: $body
|
||||
);
|
||||
return match(true) {
|
||||
$result instanceof ViewResult => new HttpResponse(
|
||||
status: $result->status,
|
||||
body: $this->renderTemplate(
|
||||
new RenderContext(
|
||||
template: $result->model ? $this->resolveTemplate($result->model) : $result->template,
|
||||
metaData: $result->metaData,
|
||||
data: $result->model ? get_object_vars($result->model) + $result->data : $result->data,
|
||||
layout: '',
|
||||
slots: $result->slots
|
||||
)
|
||||
)
|
||||
),
|
||||
$result instanceof JsonResult => new HttpResponse(
|
||||
status : $result->status,
|
||||
headers: new Headers()->with('Content-Type', 'application/json'),
|
||||
body : json_encode($result->data),
|
||||
),
|
||||
$result instanceof Redirect => new HttpResponse(
|
||||
status: $result->status,
|
||||
headers: new Headers()->with('Location', $result->target)
|
||||
),
|
||||
$result instanceof SseResult => new SseResponse($result, $result->callback),
|
||||
$result instanceof WebSocketResult => $this->createWebSocketResponse($result),
|
||||
$result instanceof FileResult => new HttpResponse(
|
||||
#headers: new Headers()->with('Content-Type', $result->mimeType),
|
||||
body: $result->filePath
|
||||
),
|
||||
|
||||
default => throw new \RuntimeException('Unbekanntes Ergebnisobjekt: ' . get_class($result)),
|
||||
};
|
||||
}
|
||||
|
||||
private function renderTemplate(string $template, array $data, ?string $layout, array $slots = [], ?string $controllerName = null): string
|
||||
private function createWebSocketResponse(WebSocketResult $result): WebSocketResponse
|
||||
{
|
||||
$context = new RenderContext(
|
||||
template: $template,
|
||||
data: $data,
|
||||
layout: $layout,
|
||||
slots: $slots,
|
||||
controllerClass: $controllerName
|
||||
);
|
||||
// WebSocket-Key aus Request-Headers abrufen
|
||||
$websocketKey = $_SERVER['HTTP_SEC_WEBSOCKET_KEY'] ?? '';
|
||||
|
||||
return new WebSocketResponse($result, $websocketKey);
|
||||
}
|
||||
|
||||
private function renderTemplate(RenderContext $context): string
|
||||
{
|
||||
return $this->templateRenderer->render($context);
|
||||
}
|
||||
|
||||
private function resolveTemplate(?object $model):string
|
||||
{
|
||||
if($model === null) {
|
||||
return 'test';
|
||||
}
|
||||
|
||||
$ref = new \ReflectionClass($model);
|
||||
$attrs = $ref->getAttributes(Template::class);
|
||||
if ($attrs === []) {
|
||||
return 'test';
|
||||
#throw new \RuntimeException("Fehlendes #[Template] Attribut in {$ref->getName()}");
|
||||
}
|
||||
|
||||
/** @var Template $attr */
|
||||
$attr = $attrs[0]->newInstance();
|
||||
return $attr->path;
|
||||
}
|
||||
}
|
||||
|
||||
57
src/Framework/Router/RouterSetup.php
Normal file
57
src/Framework/Router/RouterSetup.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Router;
|
||||
|
||||
use App\Framework\Attributes\Route;
|
||||
use App\Framework\Core\PathProvider;
|
||||
use App\Framework\Core\RouteCache;
|
||||
use App\Framework\Core\RouteCompiler;
|
||||
use App\Framework\DI\DefaultContainer;
|
||||
use App\Framework\DI\Initializer;
|
||||
use App\Framework\Discovery\Results\DiscoveryResults;
|
||||
|
||||
/**
|
||||
* Verantwortlich für die Einrichtung des Routers
|
||||
*/
|
||||
final readonly class RouterSetup
|
||||
{
|
||||
public function __construct(
|
||||
private DefaultContainer $container,
|
||||
private PathProvider $pathProvider,
|
||||
private RouteCompiler $routeCompiler,
|
||||
private DiscoveryResults $results,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Richtet den Router basierend auf den verarbeiteten Attributen ein
|
||||
*/
|
||||
#[Initializer]
|
||||
public function __invoke(): HttpRouter
|
||||
{
|
||||
#$routeCache = new RouteCache($this->pathProvider->getCachePath('routes.cache.php'));
|
||||
|
||||
if ($this->results->has(Route::class)) {
|
||||
$discoveryRoutes = $this->results->get(Route::class);
|
||||
|
||||
// DIREKT zu optimierten Routen
|
||||
$optimizedRoutes = $this->routeCompiler->compileOptimized($discoveryRoutes);
|
||||
|
||||
// Cache speichern
|
||||
#$routeCache->save($optimizedRoutes);
|
||||
|
||||
// FULL COMMIT: Nur CompiledRoutes
|
||||
|
||||
} else {
|
||||
$optimizedRoutes = $this->routeCompiler->compileOptimized([]);
|
||||
}
|
||||
|
||||
$router = new HttpRouter($optimizedRoutes);
|
||||
|
||||
$this->container->bind(CompiledRoutes::class, $optimizedRoutes);
|
||||
$this->container->bind(HttpRouter::class, $router);
|
||||
|
||||
return $router;
|
||||
}
|
||||
}
|
||||
173
src/Framework/Router/UrlGenerator.php
Normal file
173
src/Framework/Router/UrlGenerator.php
Normal file
@@ -0,0 +1,173 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Router;
|
||||
|
||||
use App\Framework\Attributes\Singleton;
|
||||
use App\Framework\Core\Route;
|
||||
use App\Framework\Http\Request;
|
||||
use App\Framework\Router\Exception\RouteNotFound;
|
||||
|
||||
/**
|
||||
* Service zur Generierung von URLs basierend auf Named Routes
|
||||
*/
|
||||
#[Singleton]
|
||||
final readonly class UrlGenerator
|
||||
{
|
||||
#private RouteCollection $routes;
|
||||
|
||||
public function __construct(
|
||||
HttpRouter $router,
|
||||
private Request $request,
|
||||
private CompiledRoutes $routes,
|
||||
) {
|
||||
#$this->routes = $router->routes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert eine URL für eine Named Route
|
||||
*
|
||||
* @param string $name Name der Route
|
||||
* @param array<string, mixed> $parameters Parameter für dynamische Routes
|
||||
* @param bool $absolute Ob eine absolute URL generiert werden soll
|
||||
* @return string Generierte URL
|
||||
* @throws RouteNotFound Wenn die Route nicht gefunden wird
|
||||
*/
|
||||
public function route(string $name, array $parameters = [], bool $absolute = false): string
|
||||
{
|
||||
$route = $this->routes->getNamedRoute($name);
|
||||
|
||||
if ($route === null) {
|
||||
throw new RouteNotFound("Route with name '{$name}' not found.");
|
||||
}
|
||||
|
||||
$url = $this->buildUrl($route->path, $parameters);
|
||||
|
||||
if ($absolute) {
|
||||
return $this->makeAbsolute($url);
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert eine absolute URL für eine Named Route
|
||||
*
|
||||
* @param string $name Name der Route
|
||||
* @param array<string, mixed> $parameters Parameter für dynamische Routes
|
||||
* @return string Absolute URL
|
||||
*/
|
||||
public function absoluteRoute(string $name, array $parameters = []): string
|
||||
{
|
||||
return $this->route($name, $parameters, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt eine URL basierend auf dem aktuellen Request mit geänderten Parametern
|
||||
*
|
||||
* @param array<string, mixed> $parameters Query-Parameter
|
||||
* @param bool $absolute Ob eine absolute URL generiert werden soll
|
||||
* @return string Generierte URL
|
||||
*/
|
||||
public function current(array $parameters = [], bool $absolute = false): string
|
||||
{
|
||||
$url = $this->request->path;
|
||||
|
||||
if (!empty($parameters)) {
|
||||
$url .= '?' . http_build_query(array_merge(
|
||||
$this->request->query->all(),
|
||||
$parameters
|
||||
));
|
||||
} elseif (!empty($this->request->query->all())) {
|
||||
$url .= '?' . http_build_query($this->request->query->all());
|
||||
}
|
||||
|
||||
if ($absolute) {
|
||||
return $this->makeAbsolute($url);
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft, ob die aktuelle Route mit dem gegebenen Namen übereinstimmt
|
||||
*/
|
||||
public function isCurrentRoute(string $name): bool
|
||||
{
|
||||
try {
|
||||
$route = $this->routes->getByName($name);
|
||||
return $route !== null && $route->path === $this->request->path;
|
||||
} catch (RouteNotFound) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt eine URL aus einem Pfad-Template und Parametern
|
||||
*
|
||||
* @param string $path Pfad-Template (z.B. "/user/{id}/posts/{slug}")
|
||||
* @param array<string, mixed> $parameters Parameter-Werte
|
||||
* @return string Fertige URL
|
||||
*/
|
||||
private function buildUrl(string $path, array $parameters): string
|
||||
{
|
||||
$url = $path;
|
||||
$queryParams = [];
|
||||
|
||||
// Ersetze Pfad-Parameter
|
||||
foreach ($parameters as $key => $value) {
|
||||
$placeholder = "{{$key}}";
|
||||
if (str_contains($url, $placeholder)) {
|
||||
$url = str_replace($placeholder, (string)$value, $url);
|
||||
unset($parameters[$key]);
|
||||
} else {
|
||||
// Übrige Parameter werden zu Query-Parametern
|
||||
$queryParams[$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
// Prüfe auf nicht ersetzte Platzhalter
|
||||
if (preg_match('/\{[^}]+\}/', $url)) {
|
||||
throw new \InvalidArgumentException(
|
||||
"Missing required parameters for route. URL: {$url}"
|
||||
);
|
||||
}
|
||||
|
||||
// Füge Query-Parameter hinzu
|
||||
if (!empty($queryParams)) {
|
||||
$url .= '?' . http_build_query($queryParams);
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Macht eine relative URL zu einer absoluten URL
|
||||
*/
|
||||
private function makeAbsolute(string $url): string
|
||||
{
|
||||
$scheme = $this->request->server->isHttps() ? 'https' : 'http';
|
||||
$host = $this->request->server->getHttpHost();
|
||||
|
||||
return "{$scheme}://{$host}{$url}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert eine URL für eine Asset-Datei
|
||||
*
|
||||
* @param string $asset Pfad zur Asset-Datei
|
||||
* @param bool $absolute Ob eine absolute URL generiert werden soll
|
||||
* @return string Asset-URL
|
||||
*/
|
||||
public function asset(string $asset, bool $absolute = false): string
|
||||
{
|
||||
$url = '/assets/' . ltrim($asset, '/');
|
||||
|
||||
if ($absolute) {
|
||||
return $this->makeAbsolute($url);
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user