{
"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
}
}