Protokoll #29155

ID29155
Zeitstempel2025-12-29 00:20:02.794846
Clientroot
IP145.224.72.140
Modellclaude-sonnet-4-20250514
Statuscompleted
Tokens10,152 (Input: 2,550, Output: 7,602)
Dauer85 ms
Request-Zeit2025-12-29 00:20:02.794846
Response-Zeit2025-12-29 00:20:02.879538

Request

{
    "event": "PreToolUse",
    "tool_name": "Edit",
    "tool_input": {
        "file_path": "\/var\/www\/dev.campus.systemische-tools.de\/src\/Framework\/Controller.php",
        "old_string": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Framework;\n\n\/\/ @responsibility: Basis-Controller mit CSRF, JSON-Response, View-Rendering\n\nuse Domain\\Constants;\n\nabstract class Controller\n{\n    protected function csrfToken(): string\n    {\n        return CsrfService::getToken();\n    }\n\n    protected function csrfField(): string\n    {\n        return CsrfService::getTokenField();\n    }\n\n    protected function validateCsrf(): bool\n    {\n        $token = $_POST['_csrf_token'] ?? $_SERVER['HTTP_X_CSRF_TOKEN'] ?? null;\n\n        return CsrfService::validateToken($token);\n    }\n\n    protected function requireCsrf(): void\n    {\n        if (!$this->validateCsrf()) {\n            if ($this->isJsonRequest()) {\n                $this->json(['error' => 'CSRF token invalid'], 403);\n            } else {\n                $this->text('CSRF token invalid', 403);\n            }\n            exit;\n        }\n    }\n\n    private function isJsonRequest(): bool\n    {\n        $accept = $_SERVER['HTTP_ACCEPT'] ?? '';\n        $contentType = $_SERVER['CONTENT_TYPE'] ?? '';\n\n        return str_contains($accept, 'application\/json') || str_contains($contentType, 'application\/json');\n    }\n\n    protected function view(string $name, array $data = []): void\n    {\n        $data['csrfField'] = $this->csrfField();\n        $data['csrfToken'] = $this->csrfToken();\n        $data['flashSuccess'] = $this->consumeFlash('success');\n        $data['flashError'] = $this->consumeFlash('error');\n        $__viewPath = VIEW_PATH . '\/' . str_replace('.', '\/', $name) . '.php';\n        extract($data);\n\n        if (file_exists($__viewPath)) {\n            require $__viewPath;\n        } else {\n            throw new \\Exception(\"View not found: {$name}\");\n        }\n    }\n\n    protected function json(mixed $data, int $status = 200): void\n    {\n        http_response_code($status);\n        header('Content-Type: application\/json');\n        echo json_encode($data, JSON_UNESCAPED_UNICODE);\n    }\n\n    protected function redirect(string $url, int $status = 302): never\n    {\n        http_response_code($status);\n        header(\"Location: {$url}\");\n        exit;\n    }\n\n    \/**\n     * Render a partial template (for HTMX responses).\n     *\n     * @param string $name Partial name (e.g., 'chat\/message' or 'content\/version')\n     * @param array<string, mixed> $data Data to pass to partial\n     *\/\n    protected function partial(string $name, array $data = []): void\n    {\n        $data['csrfField'] = $this->csrfField();\n        $data['csrfToken'] = $this->csrfToken();\n        extract($data);\n\n        \/\/ Try module-specific partials first, then global partials\n        $paths = [\n            VIEW_PATH . '\/' . str_replace('.', '\/', $name) . '.php',\n            VIEW_PATH . '\/partials\/' . str_replace('.', '\/', $name) . '.php',\n        ];\n\n        foreach ($paths as $file) {\n            if (file_exists($file)) {\n                require $file;\n\n                return;\n            }\n        }\n\n        throw new \\Exception(\"Partial not found: {$name}\");\n    }\n\n    \/**\n     * Output an HTMX-compatible alert message.\n     *\n     * Always targets #htmx-messages container via HX-Retarget header.\n     * Auto-dismisses after 5 seconds.\n     *\n     * @param string $type Alert type: 'success', 'error', 'warning', 'info'\n     * @param string $message The message to display\n     *\/\n    protected function htmxAlert(string $type, string $message): void\n    {\n        \/\/ Retarget to global message container\n        header('HX-Retarget: #htmx-messages');\n        header('HX-Reswap: beforeend');\n\n        $escapedMessage = htmlspecialchars($message, ENT_QUOTES, 'UTF-8');\n        $id = 'alert-' . uniqid();\n        echo \"<div id=\\\"{$id}\\\" class=\\\"alert alert--{$type}\\\">{$escapedMessage}<\/div>\";\n        echo \"<script>setTimeout(() => document.getElementById('{$id}')?.remove(), 5000)<\/script>\";\n    }\n\n    \/**\n     * Output success alert for HTMX.\n     *\/\n    protected function htmxSuccess(string $message): void\n    {\n        $this->htmxAlert('success', $message);\n    }\n\n    \/**\n     * Output error alert for HTMX.\n     *\/\n    protected function htmxError(string $message): void\n    {\n        $this->htmxAlert('error', $message);\n    }\n\n    \/**\n     * Check if this is an HTMX request.\n     *\/\n    protected function isHtmxRequest(): bool\n    {\n        return isset($_SERVER['HTTP_HX_REQUEST']);\n    }\n\n    \/**\n     * HTMX redirect via header.\n     *\/\n    protected function htmxRedirect(string $url): void\n    {\n        header('HX-Redirect: ' . $url);\n        $this->text('OK');\n    }\n\n    \/**\n     * Output plain text response.\n     *\/\n    protected function text(string $content, int $status = 200): void\n    {\n        http_response_code($status);\n        header('Content-Type: text\/plain; charset=utf-8');\n        print $content;\n    }\n\n    \/**\n     * Output HTML response.\n     *\/\n    protected function html(string $content, int $status = 200): void\n    {\n        http_response_code($status);\n        header('Content-Type: text\/html; charset=utf-8');\n        print $content;\n    }\n\n    \/**\n     * Output file download response.\n     *\/\n    protected function download(string $content, string $filename, string $contentType = 'application\/octet-stream'): void\n    {\n        header('Content-Type: ' . $contentType . '; charset=utf-8');\n        header('Content-Disposition: attachment; filename=\"' . $filename . '\"');\n        header('Content-Length: ' . strlen($content));\n        print $content;\n    }\n\n    \/**\n     * @return array<string, mixed>\n     *\/\n    protected function getJsonInput(): array\n    {\n        $input = file_get_contents('php:\/\/input');\n        if ($input === false || $input === '') {\n            return [];\n        }\n\n        $decoded = json_decode($input, true);\n        if (!is_array($decoded)) {\n            return [];\n        }\n\n        return $decoded;\n    }\n\n    protected function jsonError(string $message, int $status = 500): void\n    {\n        $this->json(['success' => false, 'error' => $message], $status);\n    }\n\n    \/**\n     * Return 404 Not Found response and exit.\n     * Automatically detects JSON requests.\n     *\/\n    protected function notFound(string $message = 'Nicht gefunden'): never\n    {\n        if ($this->isJsonRequest()) {\n            $this->json(['error' => $message], 404);\n        } else {\n            $this->text(\"404 - {$message}\", 404);\n        }\n        exit;\n    }\n\n    \/**\n     * Consume flash message from session.\n     *\/\n    protected function consumeFlash(string $key): ?string\n    {\n        $value = $_SESSION[$key] ?? null;\n        if ($value !== null) {\n            unset($_SESSION[$key]);\n        }\n\n        return $value;\n    }\n\n    \/**\n     * Set flash message in session.\n     *\/\n    protected function flash(string $key, string $message): void\n    {\n        $_SESSION[$key] = $message;\n    }\n\n    \/**\n     * Decode JSON string to array with safe defaults.\n     *\n     * @return array<mixed>\n     *\/\n    protected function decodeJson(?string $json): array\n    {\n        if ($json === null || $json === '') {\n            return [];\n        }\n\n        $decoded = json_decode($json, true);\n\n        return is_array($decoded) ? $decoded : [];\n    }\n\n    \/**\n     * Get input value from GET or POST.\n     *\/\n    protected function getInput(string $key, mixed $default = null): mixed\n    {\n        return $_GET[$key] ?? $_POST[$key] ?? $default;\n    }\n\n    \/**\n     * Get trimmed string input.\n     *\/\n    protected function getString(string $key, string $default = ''): string\n    {\n        $value = $this->getInput($key, $default);\n\n        return is_string($value) ? trim($value) : $default;\n    }\n\n    \/**\n     * Get integer input.\n     *\/\n    protected function getInt(string $key, int $default = 0): int\n    {\n        return (int) ($this->getInput($key, $default));\n    }\n\n    \/**\n     * Get current page number from request.\n     *\/\n    protected function getPage(): int\n    {\n        return max(1, $this->getInt('page', 1));\n    }\n\n    \/**\n     * Get limit with bounds.\n     *\/\n    protected function getLimit(int $max = 50, int $default = 10): int\n    {\n        return min($max, max(1, $this->getInt('limit', $default)));\n    }\n\n    \/**\n     * Calculate offset from page and limit.\n     *\/\n    protected function getOffset(int $limit): int\n    {\n        return ($this->getPage() - 1) * $limit;\n    }\n\n    \/**\n     * Create Pagination from request parameters.\n     * This keeps HTTP-related code in Infrastructure (Controller) layer.\n     *\/\n    protected function getPagination(int $defaultLimit = 50, int $maxLimit = Constants::DEFAULT_LIMIT): \\Domain\\ValueObject\\Pagination\n    {\n        $page = $this->getPage();\n        $limit = $this->getLimit($maxLimit, $defaultLimit);\n\n        return \\Domain\\ValueObject\\Pagination::create($page, $limit, $maxLimit);\n    }\n}",
        "new_string": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Framework;\n\n\/\/ @responsibility: Basis-Controller mit Trait-Komposition\n\nuse Framework\\Http\\CsrfTrait;\nuse Framework\\Http\\HtmxResponseTrait;\nuse Framework\\Http\\InputTrait;\nuse Framework\\Http\\ResponseTrait;\nuse Framework\\Http\\ViewTrait;\n\n\/**\n * Base controller using trait composition for better cohesion.\n *\n * Traits:\n * - CsrfTrait: CSRF token handling\n * - ResponseTrait: json(), text(), html(), download(), redirect(), notFound()\n * - HtmxResponseTrait: htmxAlert(), htmxSuccess(), htmxError(), htmxRedirect()\n * - ViewTrait: view(), partial(), flash(), consumeFlash()\n * - InputTrait: getInput(), getString(), getInt(), getPagination()\n *\/\nabstract class Controller\n{\n    use CsrfTrait;\n    use ResponseTrait;\n    use HtmxResponseTrait;\n    use ViewTrait;\n    use InputTrait;\n}"
    }
}

Response

{
    "tool_response": {
        "filePath": "\/var\/www\/dev.campus.systemische-tools.de\/src\/Framework\/Controller.php",
        "oldString": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Framework;\n\n\/\/ @responsibility: Basis-Controller mit CSRF, JSON-Response, View-Rendering\n\nuse Domain\\Constants;\n\nabstract class Controller\n{\n    protected function csrfToken(): string\n    {\n        return CsrfService::getToken();\n    }\n\n    protected function csrfField(): string\n    {\n        return CsrfService::getTokenField();\n    }\n\n    protected function validateCsrf(): bool\n    {\n        $token = $_POST['_csrf_token'] ?? $_SERVER['HTTP_X_CSRF_TOKEN'] ?? null;\n\n        return CsrfService::validateToken($token);\n    }\n\n    protected function requireCsrf(): void\n    {\n        if (!$this->validateCsrf()) {\n            if ($this->isJsonRequest()) {\n                $this->json(['error' => 'CSRF token invalid'], 403);\n            } else {\n                $this->text('CSRF token invalid', 403);\n            }\n            exit;\n        }\n    }\n\n    private function isJsonRequest(): bool\n    {\n        $accept = $_SERVER['HTTP_ACCEPT'] ?? '';\n        $contentType = $_SERVER['CONTENT_TYPE'] ?? '';\n\n        return str_contains($accept, 'application\/json') || str_contains($contentType, 'application\/json');\n    }\n\n    protected function view(string $name, array $data = []): void\n    {\n        $data['csrfField'] = $this->csrfField();\n        $data['csrfToken'] = $this->csrfToken();\n        $data['flashSuccess'] = $this->consumeFlash('success');\n        $data['flashError'] = $this->consumeFlash('error');\n        $__viewPath = VIEW_PATH . '\/' . str_replace('.', '\/', $name) . '.php';\n        extract($data);\n\n        if (file_exists($__viewPath)) {\n            require $__viewPath;\n        } else {\n            throw new \\Exception(\"View not found: {$name}\");\n        }\n    }\n\n    protected function json(mixed $data, int $status = 200): void\n    {\n        http_response_code($status);\n        header('Content-Type: application\/json');\n        echo json_encode($data, JSON_UNESCAPED_UNICODE);\n    }\n\n    protected function redirect(string $url, int $status = 302): never\n    {\n        http_response_code($status);\n        header(\"Location: {$url}\");\n        exit;\n    }\n\n    \/**\n     * Render a partial template (for HTMX responses).\n     *\n     * @param string $name Partial name (e.g., 'chat\/message' or 'content\/version')\n     * @param array<string, mixed> $data Data to pass to partial\n     *\/\n    protected function partial(string $name, array $data = []): void\n    {\n        $data['csrfField'] = $this->csrfField();\n        $data['csrfToken'] = $this->csrfToken();\n        extract($data);\n\n        \/\/ Try module-specific partials first, then global partials\n        $paths = [\n            VIEW_PATH . '\/' . str_replace('.', '\/', $name) . '.php',\n            VIEW_PATH . '\/partials\/' . str_replace('.', '\/', $name) . '.php',\n        ];\n\n        foreach ($paths as $file) {\n            if (file_exists($file)) {\n                require $file;\n\n                return;\n            }\n        }\n\n        throw new \\Exception(\"Partial not found: {$name}\");\n    }\n\n    \/**\n     * Output an HTMX-compatible alert message.\n     *\n     * Always targets #htmx-messages container via HX-Retarget header.\n     * Auto-dismisses after 5 seconds.\n     *\n     * @param string $type Alert type: 'success', 'error', 'warning', 'info'\n     * @param string $message The message to display\n     *\/\n    protected function htmxAlert(string $type, string $message): void\n    {\n        \/\/ Retarget to global message container\n        header('HX-Retarget: #htmx-messages');\n        header('HX-Reswap: beforeend');\n\n        $escapedMessage = htmlspecialchars($message, ENT_QUOTES, 'UTF-8');\n        $id = 'alert-' . uniqid();\n        echo \"<div id=\\\"{$id}\\\" class=\\\"alert alert--{$type}\\\">{$escapedMessage}<\/div>\";\n        echo \"<script>setTimeout(() => document.getElementById('{$id}')?.remove(), 5000)<\/script>\";\n    }\n\n    \/**\n     * Output success alert for HTMX.\n     *\/\n    protected function htmxSuccess(string $message): void\n    {\n        $this->htmxAlert('success', $message);\n    }\n\n    \/**\n     * Output error alert for HTMX.\n     *\/\n    protected function htmxError(string $message): void\n    {\n        $this->htmxAlert('error', $message);\n    }\n\n    \/**\n     * Check if this is an HTMX request.\n     *\/\n    protected function isHtmxRequest(): bool\n    {\n        return isset($_SERVER['HTTP_HX_REQUEST']);\n    }\n\n    \/**\n     * HTMX redirect via header.\n     *\/\n    protected function htmxRedirect(string $url): void\n    {\n        header('HX-Redirect: ' . $url);\n        $this->text('OK');\n    }\n\n    \/**\n     * Output plain text response.\n     *\/\n    protected function text(string $content, int $status = 200): void\n    {\n        http_response_code($status);\n        header('Content-Type: text\/plain; charset=utf-8');\n        print $content;\n    }\n\n    \/**\n     * Output HTML response.\n     *\/\n    protected function html(string $content, int $status = 200): void\n    {\n        http_response_code($status);\n        header('Content-Type: text\/html; charset=utf-8');\n        print $content;\n    }\n\n    \/**\n     * Output file download response.\n     *\/\n    protected function download(string $content, string $filename, string $contentType = 'application\/octet-stream'): void\n    {\n        header('Content-Type: ' . $contentType . '; charset=utf-8');\n        header('Content-Disposition: attachment; filename=\"' . $filename . '\"');\n        header('Content-Length: ' . strlen($content));\n        print $content;\n    }\n\n    \/**\n     * @return array<string, mixed>\n     *\/\n    protected function getJsonInput(): array\n    {\n        $input = file_get_contents('php:\/\/input');\n        if ($input === false || $input === '') {\n            return [];\n        }\n\n        $decoded = json_decode($input, true);\n        if (!is_array($decoded)) {\n            return [];\n        }\n\n        return $decoded;\n    }\n\n    protected function jsonError(string $message, int $status = 500): void\n    {\n        $this->json(['success' => false, 'error' => $message], $status);\n    }\n\n    \/**\n     * Return 404 Not Found response and exit.\n     * Automatically detects JSON requests.\n     *\/\n    protected function notFound(string $message = 'Nicht gefunden'): never\n    {\n        if ($this->isJsonRequest()) {\n            $this->json(['error' => $message], 404);\n        } else {\n            $this->text(\"404 - {$message}\", 404);\n        }\n        exit;\n    }\n\n    \/**\n     * Consume flash message from session.\n     *\/\n    protected function consumeFlash(string $key): ?string\n    {\n        $value = $_SESSION[$key] ?? null;\n        if ($value !== null) {\n            unset($_SESSION[$key]);\n        }\n\n        return $value;\n    }\n\n    \/**\n     * Set flash message in session.\n     *\/\n    protected function flash(string $key, string $message): void\n    {\n        $_SESSION[$key] = $message;\n    }\n\n    \/**\n     * Decode JSON string to array with safe defaults.\n     *\n     * @return array<mixed>\n     *\/\n    protected function decodeJson(?string $json): array\n    {\n        if ($json === null || $json === '') {\n            return [];\n        }\n\n        $decoded = json_decode($json, true);\n\n        return is_array($decoded) ? $decoded : [];\n    }\n\n    \/**\n     * Get input value from GET or POST.\n     *\/\n    protected function getInput(string $key, mixed $default = null): mixed\n    {\n        return $_GET[$key] ?? $_POST[$key] ?? $default;\n    }\n\n    \/**\n     * Get trimmed string input.\n     *\/\n    protected function getString(string $key, string $default = ''): string\n    {\n        $value = $this->getInput($key, $default);\n\n        return is_string($value) ? trim($value) : $default;\n    }\n\n    \/**\n     * Get integer input.\n     *\/\n    protected function getInt(string $key, int $default = 0): int\n    {\n        return (int) ($this->getInput($key, $default));\n    }\n\n    \/**\n     * Get current page number from request.\n     *\/\n    protected function getPage(): int\n    {\n        return max(1, $this->getInt('page', 1));\n    }\n\n    \/**\n     * Get limit with bounds.\n     *\/\n    protected function getLimit(int $max = 50, int $default = 10): int\n    {\n        return min($max, max(1, $this->getInt('limit', $default)));\n    }\n\n    \/**\n     * Calculate offset from page and limit.\n     *\/\n    protected function getOffset(int $limit): int\n    {\n        return ($this->getPage() - 1) * $limit;\n    }\n\n    \/**\n     * Create Pagination from request parameters.\n     * This keeps HTTP-related code in Infrastructure (Controller) layer.\n     *\/\n    protected function getPagination(int $defaultLimit = 50, int $maxLimit = Constants::DEFAULT_LIMIT): \\Domain\\ValueObject\\Pagination\n    {\n        $page = $this->getPage();\n        $limit = $this->getLimit($maxLimit, $defaultLimit);\n\n        return \\Domain\\ValueObject\\Pagination::create($page, $limit, $maxLimit);\n    }\n}",
        "newString": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Framework;\n\n\/\/ @responsibility: Basis-Controller mit Trait-Komposition\n\nuse Framework\\Http\\CsrfTrait;\nuse Framework\\Http\\HtmxResponseTrait;\nuse Framework\\Http\\InputTrait;\nuse Framework\\Http\\ResponseTrait;\nuse Framework\\Http\\ViewTrait;\n\n\/**\n * Base controller using trait composition for better cohesion.\n *\n * Traits:\n * - CsrfTrait: CSRF token handling\n * - ResponseTrait: json(), text(), html(), download(), redirect(), notFound()\n * - HtmxResponseTrait: htmxAlert(), htmxSuccess(), htmxError(), htmxRedirect()\n * - ViewTrait: view(), partial(), flash(), consumeFlash()\n * - InputTrait: getInput(), getString(), getInt(), getPagination()\n *\/\nabstract class Controller\n{\n    use CsrfTrait;\n    use ResponseTrait;\n    use HtmxResponseTrait;\n    use ViewTrait;\n    use InputTrait;\n}",
        "originalFile": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Framework;\n\n\/\/ @responsibility: Basis-Controller mit CSRF, JSON-Response, View-Rendering\n\nuse Domain\\Constants;\n\nabstract class Controller\n{\n    protected function csrfToken(): string\n    {\n        return CsrfService::getToken();\n    }\n\n    protected function csrfField(): string\n    {\n        return CsrfService::getTokenField();\n    }\n\n    protected function validateCsrf(): bool\n    {\n        $token = $_POST['_csrf_token'] ?? $_SERVER['HTTP_X_CSRF_TOKEN'] ?? null;\n\n        return CsrfService::validateToken($token);\n    }\n\n    protected function requireCsrf(): void\n    {\n        if (!$this->validateCsrf()) {\n            if ($this->isJsonRequest()) {\n                $this->json(['error' => 'CSRF token invalid'], 403);\n            } else {\n                $this->text('CSRF token invalid', 403);\n            }\n            exit;\n        }\n    }\n\n    private function isJsonRequest(): bool\n    {\n        $accept = $_SERVER['HTTP_ACCEPT'] ?? '';\n        $contentType = $_SERVER['CONTENT_TYPE'] ?? '';\n\n        return str_contains($accept, 'application\/json') || str_contains($contentType, 'application\/json');\n    }\n\n    protected function view(string $name, array $data = []): void\n    {\n        $data['csrfField'] = $this->csrfField();\n        $data['csrfToken'] = $this->csrfToken();\n        $data['flashSuccess'] = $this->consumeFlash('success');\n        $data['flashError'] = $this->consumeFlash('error');\n        $__viewPath = VIEW_PATH . '\/' . str_replace('.', '\/', $name) . '.php';\n        extract($data);\n\n        if (file_exists($__viewPath)) {\n            require $__viewPath;\n        } else {\n            throw new \\Exception(\"View not found: {$name}\");\n        }\n    }\n\n    protected function json(mixed $data, int $status = 200): void\n    {\n        http_response_code($status);\n        header('Content-Type: application\/json');\n        echo json_encode($data, JSON_UNESCAPED_UNICODE);\n    }\n\n    protected function redirect(string $url, int $status = 302): never\n    {\n        http_response_code($status);\n        header(\"Location: {$url}\");\n        exit;\n    }\n\n    \/**\n     * Render a partial template (for HTMX responses).\n     *\n     * @param string $name Partial name (e.g., 'chat\/message' or 'content\/version')\n     * @param array<string, mixed> $data Data to pass to partial\n     *\/\n    protected function partial(string $name, array $data = []): void\n    {\n        $data['csrfField'] = $this->csrfField();\n        $data['csrfToken'] = $this->csrfToken();\n        extract($data);\n\n        \/\/ Try module-specific partials first, then global partials\n        $paths = [\n            VIEW_PATH . '\/' . str_replace('.', '\/', $name) . '.php',\n            VIEW_PATH . '\/partials\/' . str_replace('.', '\/', $name) . '.php',\n        ];\n\n        foreach ($paths as $file) {\n            if (file_exists($file)) {\n                require $file;\n\n                return;\n            }\n        }\n\n        throw new \\Exception(\"Partial not found: {$name}\");\n    }\n\n    \/**\n     * Output an HTMX-compatible alert message.\n     *\n     * Always targets #htmx-messages container via HX-Retarget header.\n     * Auto-dismisses after 5 seconds.\n     *\n     * @param string $type Alert type: 'success', 'error', 'warning', 'info'\n     * @param string $message The message to display\n     *\/\n    protected function htmxAlert(string $type, string $message): void\n    {\n        \/\/ Retarget to global message container\n        header('HX-Retarget: #htmx-messages');\n        header('HX-Reswap: beforeend');\n\n        $escapedMessage = htmlspecialchars($message, ENT_QUOTES, 'UTF-8');\n        $id = 'alert-' . uniqid();\n        echo \"<div id=\\\"{$id}\\\" class=\\\"alert alert--{$type}\\\">{$escapedMessage}<\/div>\";\n        echo \"<script>setTimeout(() => document.getElementById('{$id}')?.remove(), 5000)<\/script>\";\n    }\n\n    \/**\n     * Output success alert for HTMX.\n     *\/\n    protected function htmxSuccess(string $message): void\n    {\n        $this->htmxAlert('success', $message);\n    }\n\n    \/**\n     * Output error alert for HTMX.\n     *\/\n    protected function htmxError(string $message): void\n    {\n        $this->htmxAlert('error', $message);\n    }\n\n    \/**\n     * Check if this is an HTMX request.\n     *\/\n    protected function isHtmxRequest(): bool\n    {\n        return isset($_SERVER['HTTP_HX_REQUEST']);\n    }\n\n    \/**\n     * HTMX redirect via header.\n     *\/\n    protected function htmxRedirect(string $url): void\n    {\n        header('HX-Redirect: ' . $url);\n        $this->text('OK');\n    }\n\n    \/**\n     * Output plain text response.\n     *\/\n    protected function text(string $content, int $status = 200): void\n    {\n        http_response_code($status);\n        header('Content-Type: text\/plain; charset=utf-8');\n        print $content;\n    }\n\n    \/**\n     * Output HTML response.\n     *\/\n    protected function html(string $content, int $status = 200): void\n    {\n        http_response_code($status);\n        header('Content-Type: text\/html; charset=utf-8');\n        print $content;\n    }\n\n    \/**\n     * Output file download response.\n     *\/\n    protected function download(string $content, string $filename, string $contentType = 'application\/octet-stream'): void\n    {\n        header('Content-Type: ' . $contentType . '; charset=utf-8');\n        header('Content-Disposition: attachment; filename=\"' . $filename . '\"');\n        header('Content-Length: ' . strlen($content));\n        print $content;\n    }\n\n    \/**\n     * @return array<string, mixed>\n     *\/\n    protected function getJsonInput(): array\n    {\n        $input = file_get_contents('php:\/\/input');\n        if ($input === false || $input === '') {\n            return [];\n        }\n\n        $decoded = json_decode($input, true);\n        if (!is_array($decoded)) {\n            return [];\n        }\n\n        return $decoded;\n    }\n\n    protected function jsonError(string $message, int $status = 500): void\n    {\n        $this->json(['success' => false, 'error' => $message], $status);\n    }\n\n    \/**\n     * Return 404 Not Found response and exit.\n     * Automatically detects JSON requests.\n     *\/\n    protected function notFound(string $message = 'Nicht gefunden'): never\n    {\n        if ($this->isJsonRequest()) {\n            $this->json(['error' => $message], 404);\n        } else {\n            $this->text(\"404 - {$message}\", 404);\n        }\n        exit;\n    }\n\n    \/**\n     * Consume flash message from session.\n     *\/\n    protected function consumeFlash(string $key): ?string\n    {\n        $value = $_SESSION[$key] ?? null;\n        if ($value !== null) {\n            unset($_SESSION[$key]);\n        }\n\n        return $value;\n    }\n\n    \/**\n     * Set flash message in session.\n     *\/\n    protected function flash(string $key, string $message): void\n    {\n        $_SESSION[$key] = $message;\n    }\n\n    \/**\n     * Decode JSON string to array with safe defaults.\n     *\n     * @return array<mixed>\n     *\/\n    protected function decodeJson(?string $json): array\n    {\n        if ($json === null || $json === '') {\n            return [];\n        }\n\n        $decoded = json_decode($json, true);\n\n        return is_array($decoded) ? $decoded : [];\n    }\n\n    \/**\n     * Get input value from GET or POST.\n     *\/\n    protected function getInput(string $key, mixed $default = null): mixed\n    {\n        return $_GET[$key] ?? $_POST[$key] ?? $default;\n    }\n\n    \/**\n     * Get trimmed string input.\n     *\/\n    protected function getString(string $key, string $default = ''): string\n    {\n        $value = $this->getInput($key, $default);\n\n        return is_string($value) ? trim($value) : $default;\n    }\n\n    \/**\n     * Get integer input.\n     *\/\n    protected function getInt(string $key, int $default = 0): int\n    {\n        return (int) ($this->getInput($key, $default));\n    }\n\n    \/**\n     * Get current page number from request.\n     *\/\n    protected function getPage(): int\n    {\n        return max(1, $this->getInt('page', 1));\n    }\n\n    \/**\n     * Get limit with bounds.\n     *\/\n    protected function getLimit(int $max = 50, int $default = 10): int\n    {\n        return min($max, max(1, $this->getInt('limit', $default)));\n    }\n\n    \/**\n     * Calculate offset from page and limit.\n     *\/\n    protected function getOffset(int $limit): int\n    {\n        return ($this->getPage() - 1) * $limit;\n    }\n\n    \/**\n     * Create Pagination from request parameters.\n     * This keeps HTTP-related code in Infrastructure (Controller) layer.\n     *\/\n    protected function getPagination(int $defaultLimit = 50, int $maxLimit = Constants::DEFAULT_LIMIT): \\Domain\\ValueObject\\Pagination\n    {\n        $page = $this->getPage();\n        $limit = $this->getLimit($maxLimit, $defaultLimit);\n\n        return \\Domain\\ValueObject\\Pagination::create($page, $limit, $maxLimit);\n    }\n}\n",
        "structuredPatch": [
            {
                "oldStart": 4,
                "oldLines": 326,
                "newStart": 4,
                "newLines": 29,
                "lines": [
                    " ",
                    " namespace Framework;",
                    " ",
                    "-\/\/ @responsibility: Basis-Controller mit CSRF, JSON-Response, View-Rendering",
                    "+\/\/ @responsibility: Basis-Controller mit Trait-Komposition",
                    " ",
                    "-use Domain\\Constants;",
                    "+use Framework\\Http\\CsrfTrait;",
                    "+use Framework\\Http\\HtmxResponseTrait;",
                    "+use Framework\\Http\\InputTrait;",
                    "+use Framework\\Http\\ResponseTrait;",
                    "+use Framework\\Http\\ViewTrait;",
                    " ",
                    "+\/**",
                    "+ * Base controller using trait composition for better cohesion.",
                    "+ *",
                    "+ * Traits:",
                    "+ * - CsrfTrait: CSRF token handling",
                    "+ * - ResponseTrait: json(), text(), html(), download(), redirect(), notFound()",
                    "+ * - HtmxResponseTrait: htmxAlert(), htmxSuccess(), htmxError(), htmxRedirect()",
                    "+ * - ViewTrait: view(), partial(), flash(), consumeFlash()",
                    "+ * - InputTrait: getInput(), getString(), getInt(), getPagination()",
                    "+ *\/",
                    " abstract class Controller",
                    " {",
                    "-    protected function csrfToken(): string",
                    "-    {",
                    "-        return CsrfService::getToken();",
                    "-    }",
                    "-",
                    "-    protected function csrfField(): string",
                    "-    {",
                    "-        return CsrfService::getTokenField();",
                    "-    }",
                    "-",
                    "-    protected function validateCsrf(): bool",
                    "-    {",
                    "-        $token = $_POST['_csrf_token'] ?? $_SERVER['HTTP_X_CSRF_TOKEN'] ?? null;",
                    "-",
                    "-        return CsrfService::validateToken($token);",
                    "-    }",
                    "-",
                    "-    protected function requireCsrf(): void",
                    "-    {",
                    "-        if (!$this->validateCsrf()) {",
                    "-            if ($this->isJsonRequest()) {",
                    "-                $this->json(['error' => 'CSRF token invalid'], 403);",
                    "-            } else {",
                    "-                $this->text('CSRF token invalid', 403);",
                    "-            }",
                    "-            exit;",
                    "-        }",
                    "-    }",
                    "-",
                    "-    private function isJsonRequest(): bool",
                    "-    {",
                    "-        $accept = $_SERVER['HTTP_ACCEPT'] ?? '';",
                    "-        $contentType = $_SERVER['CONTENT_TYPE'] ?? '';",
                    "-",
                    "-        return str_contains($accept, 'application\/json') || str_contains($contentType, 'application\/json');",
                    "-    }",
                    "-",
                    "-    protected function view(string $name, array $data = []): void",
                    "-    {",
                    "-        $data['csrfField'] = $this->csrfField();",
                    "-        $data['csrfToken'] = $this->csrfToken();",
                    "-        $data['flashSuccess'] = $this->consumeFlash('success');",
                    "-        $data['flashError'] = $this->consumeFlash('error');",
                    "-        $__viewPath = VIEW_PATH . '\/' . str_replace('.', '\/', $name) . '.php';",
                    "-        extract($data);",
                    "-",
                    "-        if (file_exists($__viewPath)) {",
                    "-            require $__viewPath;",
                    "-        } else {",
                    "-            throw new \\Exception(\"View not found: {$name}\");",
                    "-        }",
                    "-    }",
                    "-",
                    "-    protected function json(mixed $data, int $status = 200): void",
                    "-    {",
                    "-        http_response_code($status);",
                    "-        header('Content-Type: application\/json');",
                    "-        echo json_encode($data, JSON_UNESCAPED_UNICODE);",
                    "-    }",
                    "-",
                    "-    protected function redirect(string $url, int $status = 302): never",
                    "-    {",
                    "-        http_response_code($status);",
                    "-        header(\"Location: {$url}\");",
                    "-        exit;",
                    "-    }",
                    "-",
                    "-    \/**",
                    "-     * Render a partial template (for HTMX responses).",
                    "-     *",
                    "-     * @param string $name Partial name (e.g., 'chat\/message' or 'content\/version')",
                    "-     * @param array<string, mixed> $data Data to pass to partial",
                    "-     *\/",
                    "-    protected function partial(string $name, array $data = []): void",
                    "-    {",
                    "-        $data['csrfField'] = $this->csrfField();",
                    "-        $data['csrfToken'] = $this->csrfToken();",
                    "-        extract($data);",
                    "-",
                    "-        \/\/ Try module-specific partials first, then global partials",
                    "-        $paths = [",
                    "-            VIEW_PATH . '\/' . str_replace('.', '\/', $name) . '.php',",
                    "-            VIEW_PATH . '\/partials\/' . str_replace('.', '\/', $name) . '.php',",
                    "-        ];",
                    "-",
                    "-        foreach ($paths as $file) {",
                    "-            if (file_exists($file)) {",
                    "-                require $file;",
                    "-",
                    "-                return;",
                    "-            }",
                    "-        }",
                    "-",
                    "-        throw new \\Exception(\"Partial not found: {$name}\");",
                    "-    }",
                    "-",
                    "-    \/**",
                    "-     * Output an HTMX-compatible alert message.",
                    "-     *",
                    "-     * Always targets #htmx-messages container via HX-Retarget header.",
                    "-     * Auto-dismisses after 5 seconds.",
                    "-     *",
                    "-     * @param string $type Alert type: 'success', 'error', 'warning', 'info'",
                    "-     * @param string $message The message to display",
                    "-     *\/",
                    "-    protected function htmxAlert(string $type, string $message): void",
                    "-    {",
                    "-        \/\/ Retarget to global message container",
                    "-        header('HX-Retarget: #htmx-messages');",
                    "-        header('HX-Reswap: beforeend');",
                    "-",
                    "-        $escapedMessage = htmlspecialchars($message, ENT_QUOTES, 'UTF-8');",
                    "-        $id = 'alert-' . uniqid();",
                    "-        echo \"<div id=\\\"{$id}\\\" class=\\\"alert alert--{$type}\\\">{$escapedMessage}<\/div>\";",
                    "-        echo \"<script>setTimeout(() => document.getElementById('{$id}')?.remove(), 5000)<\/script>\";",
                    "-    }",
                    "-",
                    "-    \/**",
                    "-     * Output success alert for HTMX.",
                    "-     *\/",
                    "-    protected function htmxSuccess(string $message): void",
                    "-    {",
                    "-        $this->htmxAlert('success', $message);",
                    "-    }",
                    "-",
                    "-    \/**",
                    "-     * Output error alert for HTMX.",
                    "-     *\/",
                    "-    protected function htmxError(string $message): void",
                    "-    {",
                    "-        $this->htmxAlert('error', $message);",
                    "-    }",
                    "-",
                    "-    \/**",
                    "-     * Check if this is an HTMX request.",
                    "-     *\/",
                    "-    protected function isHtmxRequest(): bool",
                    "-    {",
                    "-        return isset($_SERVER['HTTP_HX_REQUEST']);",
                    "-    }",
                    "-",
                    "-    \/**",
                    "-     * HTMX redirect via header.",
                    "-     *\/",
                    "-    protected function htmxRedirect(string $url): void",
                    "-    {",
                    "-        header('HX-Redirect: ' . $url);",
                    "-        $this->text('OK');",
                    "-    }",
                    "-",
                    "-    \/**",
                    "-     * Output plain text response.",
                    "-     *\/",
                    "-    protected function text(string $content, int $status = 200): void",
                    "-    {",
                    "-        http_response_code($status);",
                    "-        header('Content-Type: text\/plain; charset=utf-8');",
                    "-        print $content;",
                    "-    }",
                    "-",
                    "-    \/**",
                    "-     * Output HTML response.",
                    "-     *\/",
                    "-    protected function html(string $content, int $status = 200): void",
                    "-    {",
                    "-        http_response_code($status);",
                    "-        header('Content-Type: text\/html; charset=utf-8');",
                    "-        print $content;",
                    "-    }",
                    "-",
                    "-    \/**",
                    "-     * Output file download response.",
                    "-     *\/",
                    "-    protected function download(string $content, string $filename, string $contentType = 'application\/octet-stream'): void",
                    "-    {",
                    "-        header('Content-Type: ' . $contentType . '; charset=utf-8');",
                    "-        header('Content-Disposition: attachment; filename=\"' . $filename . '\"');",
                    "-        header('Content-Length: ' . strlen($content));",
                    "-        print $content;",
                    "-    }",
                    "-",
                    "-    \/**",
                    "-     * @return array<string, mixed>",
                    "-     *\/",
                    "-    protected function getJsonInput(): array",
                    "-    {",
                    "-        $input = file_get_contents('php:\/\/input');",
                    "-        if ($input === false || $input === '') {",
                    "-            return [];",
                    "-        }",
                    "-",
                    "-        $decoded = json_decode($input, true);",
                    "-        if (!is_array($decoded)) {",
                    "-            return [];",
                    "-        }",
                    "-",
                    "-        return $decoded;",
                    "-    }",
                    "-",
                    "-    protected function jsonError(string $message, int $status = 500): void",
                    "-    {",
                    "-        $this->json(['success' => false, 'error' => $message], $status);",
                    "-    }",
                    "-",
                    "-    \/**",
                    "-     * Return 404 Not Found response and exit.",
                    "-     * Automatically detects JSON requests.",
                    "-     *\/",
                    "-    protected function notFound(string $message = 'Nicht gefunden'): never",
                    "-    {",
                    "-        if ($this->isJsonRequest()) {",
                    "-            $this->json(['error' => $message], 404);",
                    "-        } else {",
                    "-            $this->text(\"404 - {$message}\", 404);",
                    "-        }",
                    "-        exit;",
                    "-    }",
                    "-",
                    "-    \/**",
                    "-     * Consume flash message from session.",
                    "-     *\/",
                    "-    protected function consumeFlash(string $key): ?string",
                    "-    {",
                    "-        $value = $_SESSION[$key] ?? null;",
                    "-        if ($value !== null) {",
                    "-            unset($_SESSION[$key]);",
                    "-        }",
                    "-",
                    "-        return $value;",
                    "-    }",
                    "-",
                    "-    \/**",
                    "-     * Set flash message in session.",
                    "-     *\/",
                    "-    protected function flash(string $key, string $message): void",
                    "-    {",
                    "-        $_SESSION[$key] = $message;",
                    "-    }",
                    "-",
                    "-    \/**",
                    "-     * Decode JSON string to array with safe defaults.",
                    "-     *",
                    "-     * @return array<mixed>",
                    "-     *\/",
                    "-    protected function decodeJson(?string $json): array",
                    "-    {",
                    "-        if ($json === null || $json === '') {",
                    "-            return [];",
                    "-        }",
                    "-",
                    "-        $decoded = json_decode($json, true);",
                    "-",
                    "-        return is_array($decoded) ? $decoded : [];",
                    "-    }",
                    "-",
                    "-    \/**",
                    "-     * Get input value from GET or POST.",
                    "-     *\/",
                    "-    protected function getInput(string $key, mixed $default = null): mixed",
                    "-    {",
                    "-        return $_GET[$key] ?? $_POST[$key] ?? $default;",
                    "-    }",
                    "-",
                    "-    \/**",
                    "-     * Get trimmed string input.",
                    "-     *\/",
                    "-    protected function getString(string $key, string $default = ''): string",
                    "-    {",
                    "-        $value = $this->getInput($key, $default);",
                    "-",
                    "-        return is_string($value) ? trim($value) : $default;",
                    "-    }",
                    "-",
                    "-    \/**",
                    "-     * Get integer input.",
                    "-     *\/",
                    "-    protected function getInt(string $key, int $default = 0): int",
                    "-    {",
                    "-        return (int) ($this->getInput($key, $default));",
                    "-    }",
                    "-",
                    "-    \/**",
                    "-     * Get current page number from request.",
                    "-     *\/",
                    "-    protected function getPage(): int",
                    "-    {",
                    "-        return max(1, $this->getInt('page', 1));",
                    "-    }",
                    "-",
                    "-    \/**",
                    "-     * Get limit with bounds.",
                    "-     *\/",
                    "-    protected function getLimit(int $max = 50, int $default = 10): int",
                    "-    {",
                    "-        return min($max, max(1, $this->getInt('limit', $default)));",
                    "-    }",
                    "-",
                    "-    \/**",
                    "-     * Calculate offset from page and limit.",
                    "-     *\/",
                    "-    protected function getOffset(int $limit): int",
                    "-    {",
                    "-        return ($this->getPage() - 1) * $limit;",
                    "-    }",
                    "-",
                    "-    \/**",
                    "-     * Create Pagination from request parameters.",
                    "-     * This keeps HTTP-related code in Infrastructure (Controller) layer.",
                    "-     *\/",
                    "-    protected function getPagination(int $defaultLimit = 50, int $maxLimit = Constants::DEFAULT_LIMIT): \\Domain\\ValueObject\\Pagination",
                    "-    {",
                    "-        $page = $this->getPage();",
                    "-        $limit = $this->getLimit($maxLimit, $defaultLimit);",
                    "-",
                    "-        return \\Domain\\ValueObject\\Pagination::create($page, $limit, $maxLimit);",
                    "-    }",
                    "+    use CsrfTrait;",
                    "+    use ResponseTrait;",
                    "+    use HtmxResponseTrait;",
                    "+    use ViewTrait;",
                    "+    use InputTrait;",
                    " }"
                ]
            }
        ],
        "userModified": false,
        "replaceAll": false
    }
}
← Vorheriger Zur Liste Nächster →