Files
michaelschiemer/src/Framework/Router/RouteResponder.php
Michael Schiemer 5050c7d73a docs: consolidate documentation into organized structure
- 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
2025-10-05 11:05:04 +02:00

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