- Move 12 markdown files from root to docs/ subdirectories - Organize documentation by category: • docs/troubleshooting/ (1 file) - Technical troubleshooting guides • docs/deployment/ (4 files) - Deployment and security documentation • docs/guides/ (3 files) - Feature-specific guides • docs/planning/ (4 files) - Planning and improvement proposals Root directory cleanup: - Reduced from 16 to 4 markdown files in root - Only essential project files remain: • CLAUDE.md (AI instructions) • README.md (Main project readme) • CLEANUP_PLAN.md (Current cleanup plan) • SRC_STRUCTURE_IMPROVEMENTS.md (Structure improvements) This improves: ✅ Documentation discoverability ✅ Logical organization by purpose ✅ Clean root directory ✅ Better maintainability
171 lines
6.6 KiB
PHP
171 lines
6.6 KiB
PHP
<?php
|
|
|
|
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\Request;
|
|
use App\Framework\Http\Response;
|
|
use App\Framework\Http\Responses\JsonResponse;
|
|
use App\Framework\Http\Responses\RedirectResponse;
|
|
use App\Framework\Http\Responses\SseResponse;
|
|
use App\Framework\Http\Responses\WebSocketResponse;
|
|
use App\Framework\Http\Uri;
|
|
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;
|
|
|
|
final readonly class RouteResponder
|
|
{
|
|
public function __construct(
|
|
private PathProvider $pathProvider,
|
|
private DefaultContainer $container,
|
|
private TemplateRenderer $templateRenderer,
|
|
private Request $request,
|
|
) {
|
|
}
|
|
|
|
public function getContext(ViewResult $result): RenderContext
|
|
{
|
|
return 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,
|
|
isPartial: $this->isSpaRequest(),
|
|
);
|
|
}
|
|
|
|
public function respond(Response|ActionResult $result): Response
|
|
{
|
|
if ($result instanceof Response) {
|
|
return $result;
|
|
}
|
|
|
|
if ($this->isSpaRequest() && $result instanceof ViewResult) {
|
|
return $this->createSpaResponse($result);
|
|
}
|
|
|
|
return match(true) {
|
|
$result instanceof ViewResult => new HttpResponse(
|
|
status: $result->status,
|
|
body: $this->renderTemplate($this->getContext($result))
|
|
),
|
|
$result instanceof JsonResult => new JsonResponse(
|
|
body : $result->data,
|
|
status : $result->status,
|
|
),
|
|
$result instanceof Redirect => new RedirectResponse(new Uri($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 createWebSocketResponse(WebSocketResult $result): WebSocketResponse
|
|
{
|
|
// WebSocket-Key aus Request-Headers abrufen
|
|
$websocketKey = $_SERVER['HTTP_SEC_WEBSOCKET_KEY'] ?? '';
|
|
|
|
return new WebSocketResponse($result, $websocketKey);
|
|
}
|
|
|
|
private function renderTemplate(RenderContext $context): string
|
|
{
|
|
// Force log to file to ensure visibility
|
|
file_put_contents('/tmp/debug.log', "RouteResponder::renderTemplate STARTED for template: " . $context->template . "\n", FILE_APPEND);
|
|
file_put_contents('/tmp/debug.log', "Template data keys: " . implode(', ', array_keys($context->data)) . "\n", FILE_APPEND);
|
|
|
|
error_log("RouteResponder::renderTemplate STARTED for template: " . $context->template);
|
|
error_log("Template renderer class: " . get_class($this->templateRenderer));
|
|
|
|
// Log the class before calling render
|
|
error_log("RouteResponder: About to call render on class: " . get_class($this->templateRenderer));
|
|
file_put_contents('/tmp/debug.log', "About to call render on: " . get_class($this->templateRenderer) . "\n", FILE_APPEND);
|
|
|
|
$result = $this->templateRenderer->render($context);
|
|
|
|
file_put_contents('/tmp/debug.log', "RouteResponder::renderTemplate COMPLETED, result length: " . strlen($result) . "\n", FILE_APPEND);
|
|
file_put_contents('/tmp/debug.log', "Result contains {{: " . (str_contains($result, '{{') ? 'YES' : 'NO') . "\n", FILE_APPEND);
|
|
|
|
error_log("RouteResponder::renderTemplate COMPLETED, result length: " . strlen($result));
|
|
error_log("Result contains {{ model.title }}: " . (str_contains($result, '{{ model.title }}') ? 'YES' : 'NO'));
|
|
|
|
return $result;
|
|
}
|
|
|
|
private function isSpaRequest(): bool
|
|
{
|
|
$xmlHttpRequest = $this->request->headers->getFirst('X-Requested-With');
|
|
$hasSpaRequest = $this->request->headers->has('X-SPA-Request');
|
|
|
|
// Fallback: Direkt aus $_SERVER lesen (für Nginx FastCGI)
|
|
if (empty($xmlHttpRequest)) {
|
|
$xmlHttpRequest = $_SERVER['HTTP_X_REQUESTED_WITH'] ?? '';
|
|
}
|
|
|
|
if (! $hasSpaRequest) {
|
|
$hasSpaRequest = ! empty($_SERVER['HTTP_X_SPA_REQUEST']);
|
|
}
|
|
|
|
// DEBUG: Log SPA detection
|
|
error_log("SPA Detection - xmlHttpRequest: '{$xmlHttpRequest}', hasSpaRequest: " . ($hasSpaRequest ? 'true' : 'false'));
|
|
error_log("SPA Detection - Headers: " . json_encode($this->request->headers->toArray()));
|
|
error_log("SPA Detection - SERVER vars: X-Requested-With='" . ($_SERVER['HTTP_X_REQUESTED_WITH'] ?? 'null') . "', X-SPA-Request='" . ($_SERVER['HTTP_X_SPA_REQUEST'] ?? 'null') . "'");
|
|
|
|
$isSpa = $xmlHttpRequest === 'XMLHttpRequest' && $hasSpaRequest;
|
|
error_log("SPA Detection - Final result: " . ($isSpa ? 'TRUE (returning JSON)' : 'FALSE (returning HTML)'));
|
|
|
|
return $isSpa;
|
|
}
|
|
|
|
private function createSpaResponse(ViewResult $result): JsonResponse
|
|
{
|
|
$context = $this->getContext($result);
|
|
|
|
return new JsonResponse([
|
|
'html' => $this->templateRenderer->renderPartial($context),
|
|
'title' => $result->metaData->title ?? '',
|
|
'meta' => [
|
|
'description' => $result->metaData->description ?? '',
|
|
// Add other meta data as needed
|
|
],
|
|
]);
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|