chore: complete update
This commit is contained in:
99
src/Framework/ErrorHandling/CliErrorHandler.php
Normal file
99
src/Framework/ErrorHandling/CliErrorHandler.php
Normal file
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\ErrorHandling;
|
||||
|
||||
use App\Framework\Console\ConsoleColor;
|
||||
use App\Framework\Console\ConsoleOutput;
|
||||
|
||||
/**
|
||||
* CLI-spezifischer Error Handler für Worker und Console
|
||||
*/
|
||||
final readonly class CliErrorHandler
|
||||
{
|
||||
public function __construct(
|
||||
private ?ConsoleOutput $output = null
|
||||
) {}
|
||||
|
||||
public function register(): void
|
||||
{
|
||||
set_error_handler([$this, 'handleError']);
|
||||
set_exception_handler([$this, 'handleException']);
|
||||
register_shutdown_function([$this, 'handleShutdown']);
|
||||
}
|
||||
|
||||
public function handleError(int $severity, string $message, string $file, int $line): bool
|
||||
{
|
||||
if (!(error_reporting() & $severity)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$errorType = $this->getErrorType($severity);
|
||||
$color = $this->getErrorColor($severity);
|
||||
|
||||
$output = $this->output ?? new ConsoleOutput();
|
||||
$output->writeLine("[$errorType] $message", $color);
|
||||
$output->writeLine(" in $file:$line", ConsoleColor::GRAY);
|
||||
|
||||
if (in_array($severity, [E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR])) {
|
||||
exit(1);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function handleException(\Throwable $exception): void
|
||||
{
|
||||
$output = $this->output ?? new ConsoleOutput();
|
||||
|
||||
$output->writeLine("❌ Uncaught " . get_class($exception) . ": " . $exception->getMessage(), ConsoleColor::BRIGHT_RED);
|
||||
$output->writeLine(" File: " . $exception->getFile() . ":" . $exception->getLine(), ConsoleColor::RED);
|
||||
|
||||
if ($exception->getPrevious()) {
|
||||
$output->writeLine(" Caused by: " . $exception->getPrevious()->getMessage(), ConsoleColor::YELLOW);
|
||||
}
|
||||
|
||||
$output->writeLine(" Stack trace:", ConsoleColor::GRAY);
|
||||
foreach (explode("\n", $exception->getTraceAsString()) as $line) {
|
||||
$output->writeLine(" " . $line, ConsoleColor::GRAY);
|
||||
}
|
||||
|
||||
exit(1);
|
||||
}
|
||||
|
||||
public function handleShutdown(): void
|
||||
{
|
||||
$error = error_get_last();
|
||||
|
||||
if ($error && in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR])) {
|
||||
$output = $this->output ?? new ConsoleOutput();
|
||||
$output->writeLine("💥 Fatal Error: " . $error['message'], ConsoleColor::BRIGHT_RED);
|
||||
$output->writeLine(" File: " . $error['file'] . ":" . $error['line'], ConsoleColor::RED);
|
||||
}
|
||||
}
|
||||
|
||||
private function getErrorType(int $severity): string
|
||||
{
|
||||
return match ($severity) {
|
||||
E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR => 'ERROR',
|
||||
E_WARNING, E_CORE_WARNING, E_COMPILE_WARNING, E_USER_WARNING => 'WARNING',
|
||||
E_NOTICE, E_USER_NOTICE => 'NOTICE',
|
||||
E_STRICT => 'STRICT',
|
||||
E_RECOVERABLE_ERROR => 'RECOVERABLE_ERROR',
|
||||
E_DEPRECATED, E_USER_DEPRECATED => 'DEPRECATED',
|
||||
default => 'UNKNOWN'
|
||||
};
|
||||
}
|
||||
|
||||
private function getErrorColor(int $severity): ConsoleColor
|
||||
{
|
||||
return match ($severity) {
|
||||
E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR => ConsoleColor::BRIGHT_RED,
|
||||
E_WARNING, E_CORE_WARNING, E_COMPILE_WARNING, E_USER_WARNING => ConsoleColor::YELLOW,
|
||||
E_NOTICE, E_USER_NOTICE => ConsoleColor::CYAN,
|
||||
E_DEPRECATED, E_USER_DEPRECATED => ConsoleColor::MAGENTA,
|
||||
default => ConsoleColor::WHITE
|
||||
};
|
||||
}
|
||||
}
|
||||
21
src/Framework/ErrorHandling/DummyTemplateRenderer.php
Normal file
21
src/Framework/ErrorHandling/DummyTemplateRenderer.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\ErrorHandling;
|
||||
|
||||
use App\Framework\View\RenderContext;
|
||||
use App\Framework\View\TemplateRenderer;
|
||||
|
||||
/**
|
||||
* Dummy-Implementierung des TemplateRenderer als Fallback
|
||||
*/
|
||||
final class DummyTemplateRenderer implements TemplateRenderer
|
||||
{
|
||||
public function render(RenderContext $context): string
|
||||
{
|
||||
// Dieser Renderer gibt immer einen leeren String zurück,
|
||||
// da er nur als Fallback dient und der ErrorTemplateRenderer
|
||||
// seine eigene Fallback-Methode hat
|
||||
return '';
|
||||
}
|
||||
}
|
||||
46
src/Framework/ErrorHandling/ErrorContext.php
Normal file
46
src/Framework/ErrorHandling/ErrorContext.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\ErrorHandling;
|
||||
|
||||
use App\Framework\Http\RequestId;
|
||||
|
||||
final readonly class ErrorContext
|
||||
{
|
||||
public StackTrace $stackTrace;
|
||||
|
||||
public function __construct(
|
||||
public \Throwable $exception,
|
||||
public ErrorLevel $level,
|
||||
public RequestId $requestId,
|
||||
public array $additionalData = []
|
||||
)
|
||||
{
|
||||
$this->stackTrace = new StackTrace($exception);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt eine HTML-formatierte Version der Fehlermeldung zurück
|
||||
*/
|
||||
public function getFormattedMessage(): string
|
||||
{
|
||||
return nl2br(htmlspecialchars($this->exception->getMessage(), ENT_QUOTES, 'UTF-8'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt die Fehlerklasse zurück (ohne Namespace)
|
||||
*/
|
||||
public function getErrorClass(): string
|
||||
{
|
||||
$class = get_class($this->exception);
|
||||
$parts = explode('\\', $class);
|
||||
return end($parts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt den vollständigen Klassennamen zurück
|
||||
*/
|
||||
public function getFullErrorClass(): string
|
||||
{
|
||||
return get_class($this->exception);
|
||||
}
|
||||
}
|
||||
@@ -2,62 +2,200 @@
|
||||
|
||||
namespace App\Framework\ErrorHandling;
|
||||
|
||||
use App\Framework\Http\Headers;
|
||||
use App\Framework\DI\Container;
|
||||
use App\Framework\ErrorHandling\View\ApiErrorRenderer;
|
||||
use App\Framework\ErrorHandling\View\ErrorResponseFactory;
|
||||
use App\Framework\ErrorHandling\View\ErrorTemplateRenderer;
|
||||
use App\Framework\Exception\SecurityException;
|
||||
use App\Framework\Exception\SecurityLogLevel;
|
||||
use App\Framework\Http\HttpResponse;
|
||||
use App\Framework\Http\MiddlewareContext;
|
||||
use App\Framework\Http\RequestIdGenerator;
|
||||
use App\Framework\Http\ResponseEmitter;
|
||||
use App\Framework\Http\Status;
|
||||
use JetBrains\PhpStorm\NoReturn;
|
||||
use App\Framework\Logging\Logger;
|
||||
use App\Framework\View\TemplateRenderer;
|
||||
use Throwable;
|
||||
|
||||
final class ErrorHandler
|
||||
final readonly class ErrorHandler
|
||||
{
|
||||
public static function register(ResponseEmitter $emitter): void
|
||||
{
|
||||
set_exception_handler(function (Throwable $exception) use ($emitter) {
|
||||
self::handleException($exception, $emitter);
|
||||
});
|
||||
private ErrorLogger $logger;
|
||||
private SecurityEventHandler $securityHandler;
|
||||
private bool $isDebugMode;
|
||||
|
||||
set_error_handler(function (int $errno, string $errstr, string $errfile, int $errline) use ($emitter) {
|
||||
$exception = new \ErrorException($errstr, 0, $errno, $errfile, $errline);
|
||||
self::handleException($exception, $emitter);
|
||||
});
|
||||
|
||||
register_shutdown_function(function () use ($emitter) {
|
||||
$error = error_get_last();
|
||||
if ($error) {
|
||||
$exception = new \ErrorException(
|
||||
$error['message'],
|
||||
0,
|
||||
$error['type'],
|
||||
$error['file'],
|
||||
$error['line']
|
||||
);
|
||||
self::handleException($exception, $emitter);
|
||||
}
|
||||
});
|
||||
public function __construct(
|
||||
private ResponseEmitter $emitter,
|
||||
private Container $container,
|
||||
private RequestIdGenerator $requestIdGenerator,
|
||||
?Logger $logger = null,
|
||||
?bool $isDebugMode = null,
|
||||
?SecurityEventHandler $securityHandler = null
|
||||
){
|
||||
$this->isDebugMode = $isDebugMode ?? (bool)($_ENV['APP_DEBUG'] ?? false);
|
||||
$this->logger = new ErrorLogger($logger);
|
||||
$this->securityHandler = $securityHandler ?? SecurityEventHandler::createDefault($logger);
|
||||
}
|
||||
|
||||
#[NoReturn]
|
||||
private static function handleException(Throwable $e, ResponseEmitter $emitter): void
|
||||
public function register(): void
|
||||
{
|
||||
$isDebug = $_ENV['APP_DEBUG'] ?? false;
|
||||
set_exception_handler($this->handleException(...));
|
||||
set_error_handler($this->handleError(...));;
|
||||
register_shutdown_function($this->handleShutdown(...));;
|
||||
}
|
||||
|
||||
$status = Status::INTERNAL_SERVER_ERROR ?? 500;
|
||||
$message = $isDebug
|
||||
? sprintf("Fehler: %s in %s:%d\n\n%s", $e->getMessage(), $e->getFile(), $e->getLine(), $e->getTraceAsString())
|
||||
: "Es ist ein interner Fehler aufgetreten.";
|
||||
public function createHttpResponse(\Throwable $e, ?MiddlewareContext $context = null): HttpResponse
|
||||
{
|
||||
$errorContext = $this->createErrorContext($e, $context);
|
||||
|
||||
$headers = new Headers()->with('Content-Type', 'text/plain; charset=utf-8');
|
||||
// Standard Error Logging
|
||||
$this->logger->logError($errorContext);
|
||||
|
||||
$response = new HttpResponse(
|
||||
status: $status,
|
||||
headers: $headers,
|
||||
body: $message
|
||||
// Security Event Logging delegieren
|
||||
$this->securityHandler->handleIfSecurityException($e, $context);
|
||||
|
||||
$responseFactory = $this->createResponseFactory();
|
||||
|
||||
$isApiRequest = $responseFactory->isApiRequest();
|
||||
|
||||
return $responseFactory->createResponse(
|
||||
$errorContext,
|
||||
$this->isDebugMode,
|
||||
$isApiRequest
|
||||
);
|
||||
}
|
||||
|
||||
// Sende die Fehlermeldung (so früh wie möglich!)
|
||||
$emitter->emit($response);
|
||||
public function handleException(Throwable $e): never
|
||||
{
|
||||
$context = $this->createErrorContext($e);
|
||||
|
||||
// Logge den Fehler
|
||||
$this->logger->logError($context);
|
||||
|
||||
// Security Event Logging delegieren
|
||||
$this->securityHandler->handleIfSecurityException($e);
|
||||
|
||||
// Erstelle ResponseFactory mit dem TemplateRenderer
|
||||
$responseFactory = $this->createResponseFactory();
|
||||
|
||||
// Bestimme ob es ein API-Request ist
|
||||
$isApiRequest = $responseFactory->isApiRequest();
|
||||
|
||||
// Erstelle eine Response basierend auf dem Kontext
|
||||
$response = $responseFactory->createResponse($context, $this->isDebugMode, $isApiRequest);
|
||||
|
||||
// Sende die Fehlermeldung
|
||||
$this->emitter->emit($response);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
public function handleError(int $errno, string $errstr, string $errfile, int $errline): void
|
||||
{
|
||||
$exception = new \ErrorException($errstr, 0, $errno, $errfile, $errline);
|
||||
$this->handleException($exception);
|
||||
}
|
||||
|
||||
public function handleShutdown(): void
|
||||
{
|
||||
$error = error_get_last();
|
||||
if ($error && $this->isFatalError($error['type'])) {
|
||||
$exception = new \ErrorException(
|
||||
$error['message'],
|
||||
0,
|
||||
$error['type'],
|
||||
$error['file'],
|
||||
$error['line']
|
||||
);
|
||||
$this->handleException($exception);
|
||||
}
|
||||
}
|
||||
|
||||
private function isFatalError(int $errno): bool
|
||||
{
|
||||
return in_array($errno, [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR, E_RECOVERABLE_ERROR], true);
|
||||
}
|
||||
|
||||
private function createErrorContext(Throwable $exception, ?MiddlewareContext $context = null):ErrorContext
|
||||
{
|
||||
$level = $this->determineErrorLevel($exception);
|
||||
|
||||
$requestId = $context->requestId ?? $this->requestIdGenerator->generate();
|
||||
|
||||
return new ErrorContext(
|
||||
exception: $exception,
|
||||
level: $level,
|
||||
requestId: $requestId,
|
||||
additionalData: [
|
||||
'timestamp' => date('c'),
|
||||
'memory_usage' => memory_get_peak_usage(true),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
private function determineErrorLevel(Throwable $exception):ErrorLevel
|
||||
{
|
||||
return match(true) {
|
||||
$exception instanceof \Error => ErrorLevel::CRITICAL,
|
||||
$exception instanceof \ErrorException => $this->determineErrorExceptionLevel($exception),
|
||||
$exception instanceof SecurityException => $this->determineSecurityErrorLevel($exception),
|
||||
$exception instanceof \RuntimeException => ErrorLevel::ERROR,
|
||||
$exception instanceof \LogicException => ErrorLevel::WARNING,
|
||||
default => ErrorLevel::ERROR,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Bestimmt den Error-Level für SecurityExceptions basierend auf dem Security-Level
|
||||
*/
|
||||
private function determineSecurityErrorLevel(SecurityException $exception): ErrorLevel
|
||||
{
|
||||
return match($exception->getSecurityLevel()) {
|
||||
SecurityLogLevel::DEBUG => ErrorLevel::DEBUG,
|
||||
SecurityLogLevel::INFO => ErrorLevel::INFO,
|
||||
SecurityLogLevel::WARN => ErrorLevel::WARNING,
|
||||
SecurityLogLevel::ERROR => ErrorLevel::ERROR,
|
||||
SecurityLogLevel::FATAL => ErrorLevel::CRITICAL,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Bestimmt den Fehler-Level basierend auf dem ErrorException-Severity-Level
|
||||
*/
|
||||
private function determineErrorExceptionLevel(\ErrorException $exception): ErrorLevel
|
||||
{
|
||||
return match($exception->getSeverity()) {
|
||||
E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR, E_RECOVERABLE_ERROR => ErrorLevel::CRITICAL,
|
||||
E_WARNING, E_CORE_WARNING, E_COMPILE_WARNING, E_USER_WARNING => ErrorLevel::WARNING,
|
||||
E_NOTICE, E_USER_NOTICE => ErrorLevel::NOTICE,
|
||||
E_DEPRECATED, E_USER_DEPRECATED => ErrorLevel::INFO,
|
||||
E_STRICT => ErrorLevel::DEBUG,
|
||||
default => ErrorLevel::ERROR,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt eine ErrorResponseFactory mit Renderern
|
||||
* Versucht den TemplateRenderer aus dem Container zu laden, falls vorhanden
|
||||
*/
|
||||
private function createResponseFactory(): ErrorResponseFactory
|
||||
{
|
||||
$templateRenderer = null;
|
||||
|
||||
// Versuche TemplateRenderer aus Container zu laden
|
||||
if ($this->container && $this->container->has(TemplateRenderer::class)) {
|
||||
try {
|
||||
$templateRenderer = $this->container->get(TemplateRenderer::class);
|
||||
} catch (Throwable $e) {
|
||||
// Falls ein Fehler beim Laden auftritt, verwende Fallback
|
||||
$templateRenderer = null;
|
||||
}
|
||||
}
|
||||
|
||||
$htmlRenderer = $templateRenderer ?
|
||||
new ErrorTemplateRenderer($templateRenderer) :
|
||||
new ErrorTemplateRenderer(new DummyTemplateRenderer());
|
||||
|
||||
$apiRenderer = new ApiErrorRenderer();
|
||||
|
||||
return new ErrorResponseFactory($htmlRenderer, $apiRenderer);
|
||||
}
|
||||
}
|
||||
|
||||
13
src/Framework/ErrorHandling/ErrorLevel.php
Normal file
13
src/Framework/ErrorHandling/ErrorLevel.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\ErrorHandling;
|
||||
|
||||
enum ErrorLevel
|
||||
{
|
||||
case CRITICAL;
|
||||
case ERROR;
|
||||
case WARNING;
|
||||
case NOTICE;
|
||||
case INFO;
|
||||
case DEBUG;
|
||||
}
|
||||
88
src/Framework/ErrorHandling/ErrorLogger.php
Normal file
88
src/Framework/ErrorHandling/ErrorLogger.php
Normal file
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\ErrorHandling;
|
||||
|
||||
use App\Framework\Logging\Logger;
|
||||
use App\Framework\Logging\LogLevel;
|
||||
|
||||
/**
|
||||
* Logger für Fehler und Exceptions - nutzt Framework Logger
|
||||
*/
|
||||
final readonly class ErrorLogger
|
||||
{
|
||||
public function __construct(
|
||||
private ?Logger $logger = null
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Loggt einen Fehler basierend auf dem ErrorContext
|
||||
*/
|
||||
public function logError(ErrorContext $context): void
|
||||
{
|
||||
$exception = $context->exception;
|
||||
$message = sprintf(
|
||||
'[%s] %s: %s in %s:%d',
|
||||
$context->requestId,
|
||||
get_class($exception),
|
||||
$exception->getMessage(),
|
||||
$exception->getFile(),
|
||||
$exception->getLine()
|
||||
);
|
||||
|
||||
$contextData = [
|
||||
'exception' => $exception,
|
||||
'trace' => $exception->getTraceAsString(),
|
||||
'requestId' => $context->requestId,
|
||||
'additionalData' => $context->additionalData
|
||||
];
|
||||
|
||||
if ($this->logger !== null) {
|
||||
$logLevel = $this->mapErrorLevelToFrameworkLevel($context->level);
|
||||
$this->logger->log($logLevel, $message, $contextData);
|
||||
} else {
|
||||
// Fallback auf error_log
|
||||
$this->logToErrorLog($context);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Fallback-Methode für Logging ohne Framework Logger
|
||||
*/
|
||||
private function logToErrorLog(ErrorContext $context): void
|
||||
{
|
||||
$exception = $context->exception;
|
||||
$message = sprintf(
|
||||
'[%s] [%s] %s: %s in %s:%d',
|
||||
date('Y-m-d H:i:s'),
|
||||
$context->level->name,
|
||||
get_class($exception),
|
||||
$exception->getMessage(),
|
||||
$exception->getFile(),
|
||||
$exception->getLine()
|
||||
);
|
||||
|
||||
error_log($message);
|
||||
|
||||
// Bei kritischen Fehlern auch den Stacktrace loggen
|
||||
if ($context->level === ErrorLevel::CRITICAL) {
|
||||
error_log("Stack trace: \n" . $exception->getTraceAsString());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mappt interne ErrorLevel auf Framework LogLevel
|
||||
*/
|
||||
private function mapErrorLevelToFrameworkLevel(ErrorLevel $level): LogLevel
|
||||
{
|
||||
return match($level) {
|
||||
ErrorLevel::CRITICAL => LogLevel::CRITICAL,
|
||||
ErrorLevel::ERROR => LogLevel::ERROR,
|
||||
ErrorLevel::WARNING => LogLevel::WARNING,
|
||||
ErrorLevel::NOTICE => LogLevel::NOTICE,
|
||||
ErrorLevel::INFO => LogLevel::INFO,
|
||||
ErrorLevel::DEBUG => LogLevel::DEBUG,
|
||||
};
|
||||
}
|
||||
}
|
||||
75
src/Framework/ErrorHandling/ExceptionConverter.php
Normal file
75
src/Framework/ErrorHandling/ExceptionConverter.php
Normal file
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\ErrorHandling;
|
||||
|
||||
use App\Framework\Http\Status;
|
||||
|
||||
/**
|
||||
* Hilfsklasse zur Konvertierung von Exceptions in HTTP-Status-Codes.
|
||||
*/
|
||||
final class ExceptionConverter
|
||||
{
|
||||
/**
|
||||
* Bestimmt den HTTP-Status basierend auf dem Exception-Typ
|
||||
*/
|
||||
public static function getStatusFromException(\Throwable $e): Status
|
||||
{
|
||||
return match (true) {
|
||||
// Routing-Fehler
|
||||
$e instanceof \App\Framework\Router\Exception\RouteNotFound => Status::NOT_FOUND,
|
||||
$e instanceof \App\Framework\Router\Exception\MethodNotAllowed => Status::METHOD_NOT_ALLOWED,
|
||||
|
||||
// Validierungs-Fehler
|
||||
$e instanceof \App\Framework\Validation\Exceptions\ValidationException => Status::BAD_REQUEST,
|
||||
|
||||
// API-Fehler
|
||||
$e instanceof \App\Framework\Api\ApiException => Status::BAD_GATEWAY,
|
||||
|
||||
// Datenbank-Fehler
|
||||
$e instanceof \PDOException => Status::INTERNAL_SERVER_ERROR,
|
||||
|
||||
// Standard
|
||||
default => Status::INTERNAL_SERVER_ERROR,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt den Response-Body basierend auf der Exception
|
||||
*/
|
||||
public static function getResponseBody(\Throwable $e, bool $debug = false, ?string $requestId = null): array
|
||||
{
|
||||
if ($debug) {
|
||||
// Im Debug-Modus detaillierte Informationen zurückgeben
|
||||
return [
|
||||
'error' => true,
|
||||
'message' => $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'trace' => explode("\n", $e->getTraceAsString()),
|
||||
'request_id' => $requestId,
|
||||
];
|
||||
}
|
||||
|
||||
// Im Produktionsmodus nur minimale Informationen
|
||||
return [
|
||||
'error' => true,
|
||||
'message' => self::getUserFriendlyMessage($e),
|
||||
'request_id' => $requestId,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt eine benutzerfreundliche Fehlermeldung basierend auf dem Exception-Typ zurück
|
||||
*/
|
||||
public static function getUserFriendlyMessage(\Throwable $e): string
|
||||
{
|
||||
return match (true) {
|
||||
$e instanceof \App\Framework\Router\Exception\RouteNotFound => 'Die angeforderte Seite wurde nicht gefunden.',
|
||||
$e instanceof \App\Framework\Router\Exception\MethodNotAllowed => 'Diese Anfragemethode ist nicht erlaubt.',
|
||||
$e instanceof \App\Framework\Validation\Exceptions\ValidationException => 'Die eingegebenen Daten sind ungültig.',
|
||||
$e instanceof \App\Framework\Api\ApiException => 'Es ist ein Fehler bei der Kommunikation mit einem externen Dienst aufgetreten.',
|
||||
default => 'Es ist ein interner Fehler aufgetreten.',
|
||||
};
|
||||
}
|
||||
}
|
||||
88
src/Framework/ErrorHandling/SecurityAlertManager.php
Normal file
88
src/Framework/ErrorHandling/SecurityAlertManager.php
Normal file
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\ErrorHandling;
|
||||
|
||||
use App\Framework\Exception\SecurityException;
|
||||
use App\Framework\Exception\ErrorHandlerContext;
|
||||
|
||||
/**
|
||||
* Manager für Security-Alerts - Integration zu externen Alert-Systemen
|
||||
*/
|
||||
final readonly class SecurityAlertManager
|
||||
{
|
||||
public function __construct(
|
||||
private array $alertChannels = []
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Sendet Alert über alle konfigurierten Kanäle
|
||||
*/
|
||||
public function sendAlert(
|
||||
SecurityException $exception,
|
||||
ErrorHandlerContext $context
|
||||
): void {
|
||||
$alertData = $this->createAlertPayload($exception, $context);
|
||||
|
||||
foreach ($this->alertChannels as $channel) {
|
||||
try {
|
||||
$channel->sendAlert($alertData);
|
||||
} catch (\Throwable $e) {
|
||||
// Alert-Fehler nicht propagieren, nur loggen
|
||||
error_log("Alert channel failed: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt standardisierte Alert-Payload
|
||||
*/
|
||||
private function createAlertPayload(
|
||||
SecurityException $exception,
|
||||
ErrorHandlerContext $context
|
||||
): array {
|
||||
$securityEvent = $exception->getSecurityEvent();
|
||||
|
||||
return [
|
||||
'type' => 'security_alert',
|
||||
'severity' => $this->mapSeverity($exception->getSecurityLevel()),
|
||||
'event_id' => $securityEvent->getEventIdentifier(),
|
||||
'title' => 'Security Event: ' . $securityEvent->getCategory(),
|
||||
'description' => $securityEvent->getDescription(),
|
||||
'details' => [
|
||||
'category' => $securityEvent->getCategory(),
|
||||
'level' => $exception->getSecurityLevel()->value,
|
||||
'client_ip' => $context->request->clientIp,
|
||||
'user_agent' => $context->request->userAgent,
|
||||
'request_uri' => $context->request->requestUri,
|
||||
'timestamp' => date('c'),
|
||||
'request_id' => $context->request->requestId
|
||||
],
|
||||
'event_data' => $securityEvent->toArray(),
|
||||
'context' => $context->forLogging()
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Mappt Security-Level auf Alert-Severity
|
||||
*/
|
||||
private function mapSeverity(\App\Framework\Exception\SecurityLogLevel $level): string
|
||||
{
|
||||
return match($level) {
|
||||
\App\Framework\Exception\SecurityLogLevel::FATAL => 'critical',
|
||||
\App\Framework\Exception\SecurityLogLevel::ERROR => 'high',
|
||||
\App\Framework\Exception\SecurityLogLevel::WARN => 'medium',
|
||||
\App\Framework\Exception\SecurityLogLevel::INFO => 'low',
|
||||
\App\Framework\Exception\SecurityLogLevel::DEBUG => 'info',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory mit Standard-Alert-Kanälen
|
||||
*/
|
||||
public static function createWithChannels(array $channels): self
|
||||
{
|
||||
return new self($channels);
|
||||
}
|
||||
}
|
||||
123
src/Framework/ErrorHandling/SecurityEventHandler.php
Normal file
123
src/Framework/ErrorHandling/SecurityEventHandler.php
Normal file
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\ErrorHandling;
|
||||
|
||||
use App\Framework\Exception\SecurityException;
|
||||
use App\Framework\Exception\ErrorHandlerContext;
|
||||
use App\Framework\Http\MiddlewareContext;
|
||||
use App\Framework\Logging\Logger;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Spezialisierte Klasse für Security-Event-Handling und OWASP-konformes Logging
|
||||
*/
|
||||
final readonly class SecurityEventHandler
|
||||
{
|
||||
public function __construct(
|
||||
private SecurityEventLogger $securityLogger,
|
||||
private ?SecurityAlertManager $alertManager = null
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Behandelt Security-Exception und führt entsprechendes Logging durch
|
||||
*/
|
||||
public function handleSecurityException(
|
||||
SecurityException $exception,
|
||||
?MiddlewareContext $context = null
|
||||
): void {
|
||||
try {
|
||||
// Erstelle ErrorHandlerContext für OWASP-Format
|
||||
$errorHandlerContext = ErrorHandlerContext::fromException($exception, $context);
|
||||
|
||||
// Führe Security-Logging durch
|
||||
$this->securityLogger->logSecurityEvent($exception, $errorHandlerContext);
|
||||
|
||||
// Sende Alert falls erforderlich
|
||||
if ($exception->requiresAlert()) {
|
||||
$this->handleSecurityAlert($exception, $errorHandlerContext);
|
||||
}
|
||||
|
||||
} catch (Throwable $loggingError) {
|
||||
// Fallback-Logging: Niemals Security-Events verlieren
|
||||
$this->handleLoggingFailure($exception, $loggingError);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft ob Exception eine SecurityException ist und behandelt sie entsprechend
|
||||
*/
|
||||
public function handleIfSecurityException(
|
||||
Throwable $exception,
|
||||
?MiddlewareContext $context = null
|
||||
): bool {
|
||||
if (!$exception instanceof SecurityException) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->handleSecurityException($exception, $context);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Behandelt Security-Alerts
|
||||
*/
|
||||
private function handleSecurityAlert(
|
||||
SecurityException $exception,
|
||||
ErrorHandlerContext $context
|
||||
): void {
|
||||
if ($this->alertManager) {
|
||||
$this->alertManager->sendAlert($exception, $context);
|
||||
} else {
|
||||
// Fallback: Logge als kritisches Event
|
||||
$this->securityLogger->logCriticalAlert($exception, $context);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Behandelt Fehler beim Security-Logging (Fallback)
|
||||
*/
|
||||
private function handleLoggingFailure(
|
||||
SecurityException $originalException,
|
||||
Throwable $loggingError
|
||||
): void {
|
||||
// Niemals Security-Events verlieren - verwende error_log als Fallback
|
||||
$fallbackMessage = sprintf(
|
||||
'SECURITY_AUDIT_FAILURE: Failed to log security event %s - %s',
|
||||
$originalException->getSecurityEvent()->getEventIdentifier(),
|
||||
$loggingError->getMessage()
|
||||
);
|
||||
|
||||
error_log($fallbackMessage);
|
||||
|
||||
// Versuche minimales Security-Log
|
||||
try {
|
||||
$minimalLog = [
|
||||
'datetime' => date('c'),
|
||||
'event' => $originalException->getSecurityEvent()->getEventIdentifier(),
|
||||
'description' => $originalException->getSecurityEvent()->getDescription(),
|
||||
'level' => 'FATAL',
|
||||
'audit_failure' => true,
|
||||
'original_error' => $loggingError->getMessage()
|
||||
];
|
||||
|
||||
error_log('SECURITY_EVENT_FALLBACK: ' . json_encode($minimalLog));
|
||||
|
||||
} catch (Throwable $criticalError) {
|
||||
// Absoluter Fallback
|
||||
error_log('CRITICAL_SECURITY_AUDIT_FAILURE: Unable to log security event');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory-Methode mit Standard-Konfiguration
|
||||
*/
|
||||
public static function createDefault(?Logger $logger = null): self
|
||||
{
|
||||
$securityLogger = new SecurityEventLogger($logger);
|
||||
$alertManager = null; // Kann später konfiguriert werden
|
||||
|
||||
return new self($securityLogger, $alertManager);
|
||||
}
|
||||
}
|
||||
163
src/Framework/ErrorHandling/SecurityEventLogger.php
Normal file
163
src/Framework/ErrorHandling/SecurityEventLogger.php
Normal file
@@ -0,0 +1,163 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\ErrorHandling;
|
||||
|
||||
use App\Framework\Exception\SecurityException;
|
||||
use App\Framework\Exception\ErrorHandlerContext;
|
||||
use App\Framework\Exception\SecurityLogLevel;
|
||||
use App\Framework\Logging\Logger;
|
||||
use App\Framework\Logging\LogLevel;
|
||||
|
||||
/**
|
||||
* Spezialisierter Logger für Security-Events im OWASP-Format
|
||||
*/
|
||||
final readonly class SecurityEventLogger
|
||||
{
|
||||
public function __construct(
|
||||
private ?Logger $logger = null,
|
||||
private string $applicationId = 'app'
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Loggt Security-Event im OWASP-Format
|
||||
*/
|
||||
public function logSecurityEvent(
|
||||
SecurityException $exception,
|
||||
ErrorHandlerContext $context
|
||||
): void {
|
||||
$securityEvent = $exception->getSecurityEvent();
|
||||
|
||||
// OWASP-konformes Log generieren
|
||||
$owaspLog = $this->createOWASPLog($exception, $context);
|
||||
|
||||
if ($this->logger) {
|
||||
// Strukturiertes Logging über Framework Logger
|
||||
$frameworkLogLevel = $this->mapSecurityLevelToFrameworkLevel($securityEvent->getLogLevel());
|
||||
|
||||
$this->logger->log(
|
||||
$frameworkLogLevel,
|
||||
$securityEvent->getDescription(),
|
||||
[
|
||||
'security_event' => $securityEvent->getEventIdentifier(),
|
||||
'security_category' => $securityEvent->getCategory(),
|
||||
'requires_alert' => $securityEvent->requiresAlert(),
|
||||
'owasp_format' => $owaspLog,
|
||||
'event_data' => $securityEvent->toArray(),
|
||||
'context_data' => $context->forLogging()
|
||||
]
|
||||
);
|
||||
} else {
|
||||
// Fallback auf error_log
|
||||
$this->logToErrorLog($owaspLog);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loggt kritische Security-Alerts
|
||||
*/
|
||||
public function logCriticalAlert(
|
||||
SecurityException $exception,
|
||||
ErrorHandlerContext $context
|
||||
): void {
|
||||
$alertData = $this->createAlertData($exception, $context);
|
||||
|
||||
if ($this->logger) {
|
||||
$this->logger->log(
|
||||
LogLevel::CRITICAL,
|
||||
'SECURITY_ALERT: ' . $exception->getSecurityEvent()->getEventIdentifier(),
|
||||
$alertData
|
||||
);
|
||||
} else {
|
||||
error_log('SECURITY_ALERT: ' . json_encode($alertData));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt OWASP-konformes Log-Format
|
||||
*/
|
||||
private function createOWASPLog(
|
||||
SecurityException $exception,
|
||||
ErrorHandlerContext $context
|
||||
): array {
|
||||
$securityEvent = $exception->getSecurityEvent();
|
||||
|
||||
return [
|
||||
'datetime' => date('c'),
|
||||
'appid' => $this->applicationId,
|
||||
'event' => $securityEvent->getEventIdentifier(),
|
||||
'level' => $securityEvent->getLogLevel()->value,
|
||||
'description' => $securityEvent->getDescription(),
|
||||
'useragent' => $context->request->userAgent,
|
||||
'source_ip' => $context->request->clientIp,
|
||||
'host_ip' => $context->request->hostIp,
|
||||
'hostname' => $context->request->hostname,
|
||||
'protocol' => $context->request->protocol,
|
||||
'port' => $context->request->port,
|
||||
'request_uri' => $context->request->requestUri,
|
||||
'request_method' => $context->request->requestMethod,
|
||||
'region' => $_ENV['AWS_REGION'] ?? 'unknown',
|
||||
'geo' => $_ENV['GEO_LOCATION'] ?? 'unknown',
|
||||
'category' => $securityEvent->getCategory(),
|
||||
'requires_alert' => $securityEvent->requiresAlert()
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt Alert-Daten für kritische Events
|
||||
*/
|
||||
private function createAlertData(
|
||||
SecurityException $exception,
|
||||
ErrorHandlerContext $context
|
||||
): array {
|
||||
return [
|
||||
'event' => $exception->getSecurityEvent()->getEventIdentifier(),
|
||||
'description' => $exception->getSecurityEvent()->getDescription(),
|
||||
'level' => $exception->getSecurityLevel()->value,
|
||||
'category' => $exception->getSecurityEvent()->getCategory(),
|
||||
'client_ip' => $context->request->clientIp,
|
||||
'user_agent' => $context->request->userAgent,
|
||||
'request_uri' => $context->request->requestUri,
|
||||
'request_method' => $context->request->requestMethod,
|
||||
'timestamp' => date('c'),
|
||||
'request_id' => $context->request->requestId,
|
||||
'event_data' => $exception->getSecurityEvent()->toArray()
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Fallback-Logging über error_log
|
||||
*/
|
||||
private function logToErrorLog(array $owaspLog): void
|
||||
{
|
||||
$logMessage = 'SECURITY_EVENT: ' . json_encode($owaspLog, JSON_UNESCAPED_SLASHES);
|
||||
error_log($logMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mappt SecurityLogLevel auf Framework LogLevel
|
||||
*/
|
||||
private function mapSecurityLevelToFrameworkLevel(
|
||||
SecurityLogLevel $securityLevel
|
||||
): LogLevel {
|
||||
return match($securityLevel) {
|
||||
SecurityLogLevel::DEBUG => LogLevel::DEBUG,
|
||||
SecurityLogLevel::INFO => LogLevel::INFO,
|
||||
SecurityLogLevel::WARN => LogLevel::WARNING,
|
||||
SecurityLogLevel::ERROR => LogLevel::ERROR,
|
||||
SecurityLogLevel::FATAL => LogLevel::CRITICAL,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory-Methode für Standard-Konfiguration
|
||||
*/
|
||||
public static function create(?Logger $logger = null): self
|
||||
{
|
||||
return new self(
|
||||
$logger,
|
||||
$_ENV['APP_NAME'] ?? $_ENV['APP_ID'] ?? 'app'
|
||||
);
|
||||
}
|
||||
}
|
||||
152
src/Framework/ErrorHandling/StackTrace.php
Normal file
152
src/Framework/ErrorHandling/StackTrace.php
Normal file
@@ -0,0 +1,152 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\ErrorHandling;
|
||||
|
||||
use ArrayAccess;
|
||||
use Countable;
|
||||
use IteratorAggregate;
|
||||
use ArrayIterator;
|
||||
use Traversable;
|
||||
|
||||
/**
|
||||
* Stacktrace-Klasse zum Aufbereiten und Darstellen von Exception-Traces
|
||||
* Implementiert ArrayAccess, IteratorAggregate und Countable für einfachen Zugriff in Templates
|
||||
*/
|
||||
final class StackTrace implements ArrayAccess, IteratorAggregate, Countable
|
||||
{
|
||||
/** @var TraceItem[] */
|
||||
private(set) array $items = [];
|
||||
|
||||
/**
|
||||
* Erstellt ein neues StackTrace-Objekt aus einer Exception
|
||||
*/
|
||||
public function __construct(private readonly \Throwable $exception)
|
||||
{
|
||||
$this->processTrace($exception->getTrace());
|
||||
}
|
||||
|
||||
/**
|
||||
* Verarbeitet die rohen Trace-Daten in strukturierte TraceItems
|
||||
*/
|
||||
private function processTrace(array $trace): void
|
||||
{
|
||||
// Füge den Ursprung der Exception hinzu (file/line aus Exception selbst)
|
||||
$this->items[] = new TraceItem(
|
||||
file: $this->exception->getFile(),
|
||||
line: $this->exception->getLine(),
|
||||
function: null,
|
||||
class: null,
|
||||
type: null,
|
||||
args: [],
|
||||
isExceptionOrigin: true
|
||||
);
|
||||
|
||||
// Verarbeite den Rest des Stacktraces
|
||||
foreach ($trace as $index => $frame) {
|
||||
$this->items[] = new TraceItem(
|
||||
file: $frame['file'] ?? '(internal function)',
|
||||
line: $frame['line'] ?? 0,
|
||||
function: $frame['function'] ?? null,
|
||||
class: $frame['class'] ?? null,
|
||||
type: $frame['type'] ?? null,
|
||||
args: $frame['args'] ?? [],
|
||||
isExceptionOrigin: false
|
||||
);
|
||||
}
|
||||
|
||||
$this->items = array_reverse($this->items);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt alle Trace-Items zurück
|
||||
*
|
||||
* @return TraceItem[]
|
||||
*/
|
||||
public function getItems(): array
|
||||
{
|
||||
return $this->items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt den Ursprung der Exception zurück (erstes Item)
|
||||
*/
|
||||
public function getOrigin(): ?TraceItem
|
||||
{
|
||||
return $this->items[0] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt die ersten N Elemente des Stacktraces zurück
|
||||
*/
|
||||
public function getFirst(int $count): array
|
||||
{
|
||||
return array_slice($this->items, 0, $count);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt die letzten N Elemente des Stacktraces zurück
|
||||
*/
|
||||
public function getLast(int $count): array
|
||||
{
|
||||
return array_slice($this->items, -$count);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filtert den Stacktrace nach bestimmten Kriterien
|
||||
*/
|
||||
public function filter(callable $callback): array
|
||||
{
|
||||
return array_filter($this->items, $callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementierung von ArrayAccess
|
||||
*/
|
||||
public function offsetExists(mixed $offset): bool
|
||||
{
|
||||
return isset($this->items[$offset]);
|
||||
}
|
||||
|
||||
public function offsetGet(mixed $offset): mixed
|
||||
{
|
||||
return $this->items[$offset] ?? null;
|
||||
}
|
||||
|
||||
public function offsetSet(mixed $offset, mixed $value): void
|
||||
{
|
||||
// Read-only
|
||||
}
|
||||
|
||||
public function offsetUnset(mixed $offset): void
|
||||
{
|
||||
// Read-only
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementierung von IteratorAggregate
|
||||
*/
|
||||
public function getIterator(): Traversable
|
||||
{
|
||||
return new ArrayIterator($this->items);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementierung von Countable
|
||||
*/
|
||||
public function count(): int
|
||||
{
|
||||
return count($this->items);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formatiert den gesamten Stacktrace als String
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
$result = "";
|
||||
foreach ($this->items as $index => $item) {
|
||||
$result .= "#{$index} {$item}\n";
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
144
src/Framework/ErrorHandling/TraceItem.php
Normal file
144
src/Framework/ErrorHandling/TraceItem.php
Normal file
@@ -0,0 +1,144 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\ErrorHandling;
|
||||
|
||||
/**
|
||||
* Repräsentiert einen einzelnen Eintrag im Stacktrace
|
||||
*/
|
||||
final readonly class TraceItem
|
||||
{
|
||||
public function __construct(
|
||||
public string $file,
|
||||
public int $line,
|
||||
public ?string $function,
|
||||
public ?string $class,
|
||||
public ?string $type,
|
||||
public array $args,
|
||||
public bool $isExceptionOrigin
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Gibt den Dateipfad relativ zum Projekt-Root zurück
|
||||
*/
|
||||
public function getRelativeFile(bool $namespace = true): string
|
||||
{
|
||||
$projectRoot = dirname(__DIR__, 3); // Annahme: 3 Verzeichnisse zurück zum Projekt-Root
|
||||
$name = str_replace($projectRoot, '', $this->file);
|
||||
|
||||
return $namespace ? $name : $this->removeNamespace($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt den Klassennamen ohne Namespace zurück
|
||||
*/
|
||||
public function getShortClass(): ?string
|
||||
{
|
||||
if (!$this->class) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$parts = explode('\\', $this->class);
|
||||
return end($parts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt den Namespace zurück
|
||||
*/
|
||||
public function getNamespace(): ?string
|
||||
{
|
||||
if (!$this->class) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$lastSlash = strrpos($this->class, '\\');
|
||||
return $lastSlash ? substr($this->class, 0, $lastSlash) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt zurück, ob es sich um einen Methodenaufruf handelt
|
||||
*/
|
||||
public function isMethod(): bool
|
||||
{
|
||||
return $this->class !== null && $this->function !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt zurück, ob es sich um einen Funktionsaufruf handelt
|
||||
*/
|
||||
public function isFunction(): bool
|
||||
{
|
||||
return $this->class === null && $this->function !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt den Aufruf-String zurück (z.B. "Namespace\Class->method()")
|
||||
*/
|
||||
public function getCallString(): string
|
||||
{
|
||||
if ($this->isExceptionOrigin) {
|
||||
return "Exception origin: >>" . $this->file . '<< in Line: ' . $this->line;
|
||||
}
|
||||
|
||||
if ($this->isMethod()) {
|
||||
return $this->getShortClass() . $this->type . $this->function . '(' . $this->formatArgs() . ')';
|
||||
}
|
||||
|
||||
if ($this->isFunction()) {
|
||||
return $this->function . '(' . $this->formatArgs() . ')';
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Formatiert die Argumente für die Anzeige
|
||||
*/
|
||||
private function formatArgs(): string
|
||||
{
|
||||
if (empty($this->args)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$formatted = [];
|
||||
|
||||
foreach ($this->args as $arg) {
|
||||
$formatted[] = $this->formatArg($arg);
|
||||
}
|
||||
|
||||
return implode(', ', $formatted);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formatiert ein einzelnes Argument für die Anzeige
|
||||
*/
|
||||
private function formatArg(mixed $arg): string
|
||||
{
|
||||
return match(true) {
|
||||
is_string($arg) => '"' . (strlen($arg) > 20 ? substr($arg, 0, 20) . '...' : $arg) . '"',
|
||||
is_array($arg) => 'array(' . count($arg) . ')',
|
||||
is_object($arg) => $this->removeNamespace(get_class($arg)),
|
||||
is_bool($arg) => $arg ? 'true' : 'false',
|
||||
is_null($arg) => 'null',
|
||||
default => (string)$arg
|
||||
};
|
||||
}
|
||||
|
||||
private function removeNamespace(string $class): string
|
||||
{
|
||||
$lastSlash = strrpos($class, '\\');
|
||||
return $lastSlash ? substr($class, $lastSlash + 1) : $class;
|
||||
}
|
||||
|
||||
/**
|
||||
* String-Repräsentation dieses TraceItems
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
$locationInfo = $this->file . ':' . $this->line;
|
||||
if ($this->isExceptionOrigin) {
|
||||
return "Exception origin: {$locationInfo}";
|
||||
}
|
||||
|
||||
return $this->getCallString() . " at {$locationInfo}";
|
||||
}
|
||||
}
|
||||
39
src/Framework/ErrorHandling/View/ApiErrorRenderer.php
Normal file
39
src/Framework/ErrorHandling/View/ApiErrorRenderer.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\ErrorHandling\View;
|
||||
|
||||
use App\Framework\ErrorHandling\ErrorContext;
|
||||
use App\Framework\ErrorHandling\ExceptionConverter;
|
||||
|
||||
/**
|
||||
* Renderer für API-Fehler im JSON-Format
|
||||
*/
|
||||
final readonly class ApiErrorRenderer implements ErrorViewRendererInterface
|
||||
{
|
||||
/**
|
||||
* Rendert einen API-Fehler als JSON
|
||||
*/
|
||||
public function render(ErrorContext $context, bool $isDebug = false): string
|
||||
{
|
||||
$responseData = [
|
||||
'error' => true,
|
||||
'message' => $isDebug ? $context->exception->getMessage() : ExceptionConverter::getUserFriendlyMessage($context->exception),
|
||||
'requestId' => $context->requestId,
|
||||
'timestamp' => $context->additionalData['timestamp'] ?? date('c'),
|
||||
];
|
||||
|
||||
// Im Debug-Modus zusätzliche Informationen hinzufügen
|
||||
if ($isDebug) {
|
||||
$responseData['details'] = [
|
||||
'exception' => get_class($context->exception),
|
||||
'file' => $context->exception->getFile(),
|
||||
'line' => $context->exception->getLine(),
|
||||
'trace' => explode("\n", $context->exception->getTraceAsString()),
|
||||
'level' => $context->level->name,
|
||||
];
|
||||
}
|
||||
|
||||
return json_encode($responseData, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
}
|
||||
79
src/Framework/ErrorHandling/View/ErrorResponseFactory.php
Normal file
79
src/Framework/ErrorHandling/View/ErrorResponseFactory.php
Normal file
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\ErrorHandling\View;
|
||||
|
||||
use App\Framework\ErrorHandling\ErrorContext;
|
||||
use App\Framework\ErrorHandling\ExceptionConverter;
|
||||
use App\Framework\Http\Headers;
|
||||
use App\Framework\Http\HttpResponse;
|
||||
use App\Framework\Http\Status;
|
||||
|
||||
/**
|
||||
* Factory für HTTP-Fehlermeldungen
|
||||
*/
|
||||
final readonly class ErrorResponseFactory
|
||||
{
|
||||
public function __construct(
|
||||
private ErrorViewRendererInterface $htmlRenderer,
|
||||
private ErrorViewRendererInterface $apiRenderer
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Erstellt eine HTTP-Response basierend auf dem ErrorContext
|
||||
*/
|
||||
public function createResponse(ErrorContext $context, bool $isDebug = false, bool $isApiRequest = false): HttpResponse
|
||||
{
|
||||
$status = ExceptionConverter::getStatusFromException($context->exception);
|
||||
$headers = new Headers();
|
||||
|
||||
if ($isApiRequest) {
|
||||
$headers = $headers->with('Content-Type', 'application/json; charset=utf-8');
|
||||
$body = $this->apiRenderer->render($context, $isDebug);
|
||||
} else {
|
||||
$headers = $headers->with('Content-Type', 'text/html; charset=utf-8');
|
||||
$body = $this->htmlRenderer->render($context, $isDebug);
|
||||
}
|
||||
|
||||
// Cache-Control hinzufügen (Fehlerseiten sollten nicht gecacht werden)
|
||||
$headers = $headers->with('Cache-Control', 'no-store, no-cache, must-revalidate');
|
||||
|
||||
return new HttpResponse(
|
||||
status: $status,
|
||||
headers: $headers,
|
||||
body: $body
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bestimmt, ob es sich um eine API-Anfrage handelt
|
||||
*/
|
||||
public function isApiRequest(): bool
|
||||
{
|
||||
// Accept-Header prüfen
|
||||
$acceptHeader = $_SERVER['HTTP_ACCEPT'] ?? '';
|
||||
if (str_contains($acceptHeader, 'application/json')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// URL-Pfad prüfen (z.B. /api/...)
|
||||
$requestUri = $_SERVER['REQUEST_URI'] ?? '';
|
||||
if (str_starts_with($requestUri, '/api/')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Content-Type prüfen
|
||||
$contentType = $_SERVER['CONTENT_TYPE'] ?? '';
|
||||
if (str_contains($contentType, 'application/json')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// X-Requested-With Header prüfen (für AJAX)
|
||||
$requestedWith = $_SERVER['HTTP_X_REQUESTED_WITH'] ?? '';
|
||||
if (strtolower($requestedWith) === 'xmlhttprequest') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
156
src/Framework/ErrorHandling/View/ErrorTemplateRenderer.php
Normal file
156
src/Framework/ErrorHandling/View/ErrorTemplateRenderer.php
Normal file
@@ -0,0 +1,156 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\ErrorHandling\View;
|
||||
|
||||
use App\Framework\ErrorHandling\ErrorContext;
|
||||
use App\Framework\Meta\MetaData;
|
||||
use App\Framework\Meta\OpenGraphTypeWebsite;
|
||||
use App\Framework\SyntaxHighlighter\FileHighlighter;
|
||||
use App\Framework\View\RenderContext;
|
||||
use App\Framework\View\TemplateRenderer;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Renderer für Fehlerseiten unter Verwendung des View-Systems
|
||||
*/
|
||||
final readonly class ErrorTemplateRenderer implements ErrorViewRendererInterface
|
||||
{
|
||||
public function __construct(
|
||||
private TemplateRenderer $renderer,
|
||||
private string $debugTemplate = 'debug',
|
||||
private string $productionTemplate = 'production'
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Rendert eine Fehlerseite basierend auf dem ErrorContext
|
||||
*/
|
||||
public function render(ErrorContext $context, bool $isDebug = false): string
|
||||
{
|
||||
$template = $isDebug ? $this->debugTemplate : $this->productionTemplate;
|
||||
|
||||
try {
|
||||
// Sichere Datenaufbereitung ohne Closures oder komplexe Objekte
|
||||
$safeData = [
|
||||
'code' => new FileHighlighter()($context->exception->getFile(), $context->exception->getLine() -10, 10),
|
||||
'errorClass' => $context->exception::class,
|
||||
'errorMessage' => $context->exception->getMessage(),
|
||||
'error' => [
|
||||
'message' => $context->exception->getMessage(),
|
||||
'file' => $context->exception->getFile(),
|
||||
'line' => $context->exception->getLine(),
|
||||
'trace' => $context->exception->getTraceAsString(),
|
||||
'class' => get_class($context->exception),
|
||||
],
|
||||
'requestId' => $context->requestId->toString(),
|
||||
'timestamp' => $context->additionalData['timestamp'] ?? date('c'),
|
||||
'level' => $context->level->name,
|
||||
'memory' => $context->additionalData['memory_usage'] ?? 0
|
||||
];
|
||||
|
||||
$renderContext = new RenderContext(
|
||||
template: $template,
|
||||
metaData: new MetaData('', '', new OpenGraphTypeWebsite),
|
||||
data: $safeData
|
||||
);
|
||||
|
||||
$renderedContent = $this->renderer->render($renderContext);
|
||||
|
||||
if($renderedContent === ""){
|
||||
throw new \Exception("Template Renderer returned empty string");
|
||||
}
|
||||
|
||||
return $renderedContent;
|
||||
} catch (Throwable $e) {
|
||||
// Fallback falls das Template-Rendering fehlschlägt
|
||||
return $this->createFallbackErrorPage($context, $isDebug, $e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt eine einfache Fehlerseite als Fallback
|
||||
*/
|
||||
private function createFallbackErrorPage(ErrorContext $context, bool $isDebug, ?Throwable $renderException = null): string
|
||||
{
|
||||
$exception = $context->exception;
|
||||
|
||||
if ($isDebug) {
|
||||
$content = sprintf(
|
||||
'<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Anwendungsfehler</title>
|
||||
<style>
|
||||
body { font-family: sans-serif; line-height: 1.6; color: #333; max-width: 1200px; margin: 0 auto; padding: 20px; }
|
||||
.error-container { background: #f8f9fa; border: 1px solid #dee2e6; border-radius: 4px; padding: 20px; margin: 20px 0; }
|
||||
.error-title { color: #dc3545; margin-top: 0; }
|
||||
.error-meta { background: #e9ecef; padding: 10px; border-radius: 4px; font-size: 0.9em; margin: 10px 0; }
|
||||
.error-trace { background: #f1f3f5; padding: 15px; border-radius: 4px; overflow-x: auto; font-family: monospace; font-size: 0.9em; }
|
||||
.error-info { margin-bottom: 20px; }
|
||||
.request-id { font-family: monospace; color: #6c757d; }
|
||||
.render-error { margin-top: 30px; padding-top: 20px; border-top: 1px dashed #dee2e6; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="error-container">
|
||||
<h1 class="error-title">Anwendungsfehler: %s</h1>
|
||||
|
||||
<div class="error-meta">
|
||||
<strong>Request-ID:</strong> <span class="request-id">%s</span><br>
|
||||
<strong>Zeit:</strong> %s<br>
|
||||
<strong>Fehler-Level:</strong> %s
|
||||
</div>
|
||||
|
||||
<div class="error-info">
|
||||
<p><strong>Datei:</strong> %s</p>
|
||||
<p><strong>Zeile:</strong> %d</p>
|
||||
</div>
|
||||
|
||||
<h3>Stack Trace:</h3>
|
||||
<div class="error-trace">%s</div>
|
||||
|
||||
%s
|
||||
</div>
|
||||
</body>
|
||||
</html>',
|
||||
htmlspecialchars($exception->getMessage()),
|
||||
htmlspecialchars($context->requestId->toString()),
|
||||
htmlspecialchars($context->additionalData['timestamp'] ?? date('c')),
|
||||
htmlspecialchars($context->level->name),
|
||||
htmlspecialchars($exception->getFile()),
|
||||
$exception->getLine(),
|
||||
nl2br(htmlspecialchars($exception->getTraceAsString())),
|
||||
$renderException ? '<div class="render-error"><h3>Fehler beim Rendern der Fehlerseite:</h3><p>' .
|
||||
htmlspecialchars($renderException->getMessage()) . '</p></div>' : ''
|
||||
);
|
||||
} else {
|
||||
// Produktions-Fallback mit minimalen Informationen
|
||||
$content = sprintf(
|
||||
'<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Fehler</title>
|
||||
<style>
|
||||
body { font-family: sans-serif; line-height: 1.6; color: #333; max-width: 800px; margin: 0 auto; padding: 40px 20px; text-align: center; }
|
||||
.error-container { background: #f8f9fa; border: 1px solid #dee2e6; border-radius: 4px; padding: 40px 20px; }
|
||||
.error-title { color: #dc3545; margin-top: 0; }
|
||||
.request-id { font-family: monospace; color: #6c757d; font-size: 0.8em; margin-top: 30px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="error-container">
|
||||
<h1 class="error-title">Es ist ein Fehler aufgetreten</h1>
|
||||
<p>Bitte versuchen Sie es später erneut oder kontaktieren Sie den Support.</p>
|
||||
<p class="request-id">Request-ID: %s</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>',
|
||||
htmlspecialchars($context->requestId->toString())
|
||||
);
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\ErrorHandling\View;
|
||||
|
||||
use App\Framework\ErrorHandling\ErrorContext;
|
||||
|
||||
/**
|
||||
* Interface für alle Renderer von Fehlerseiten
|
||||
*/
|
||||
interface ErrorViewRendererInterface
|
||||
{
|
||||
/**
|
||||
* Rendert eine Fehlerseite basierend auf dem ErrorContext
|
||||
*/
|
||||
public function render(ErrorContext $context, bool $isDebug = false): string;
|
||||
}
|
||||
209
src/Framework/ErrorHandling/templates/debug.view.php
Normal file
209
src/Framework/ErrorHandling/templates/debug.view.php
Normal file
@@ -0,0 +1,209 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Fehler: $errorClass</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg-color: #f8f9fa;
|
||||
--text-color: #212529;
|
||||
--border-color: #dee2e6;
|
||||
--error-color: #dc3545;
|
||||
--header-bg: #e9ecef;
|
||||
--code-bg: #f5f5f5;
|
||||
--link-color: #0d6efd;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--bg-color: #212529;
|
||||
--text-color: #f8f9fa;
|
||||
--border-color: #495057;
|
||||
--error-color: #f8d7da;
|
||||
--header-bg: #343a40;
|
||||
--code-bg: #2b3035;
|
||||
--link-color: #6ea8fe;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: var(--text-color);
|
||||
background: var(--bg-color);
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.error-container {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.error-header {
|
||||
background: var(--header-bg);
|
||||
padding: 15px 20px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.error-title {
|
||||
margin: 0;
|
||||
color: var(--error-color);
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.error-body {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
background: #fff5f5;
|
||||
border-left: 4px solid var(--error-color);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.error-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
font-size: 0.9rem;
|
||||
color: #6c757d;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.error-meta span {
|
||||
background: var(--header-bg);
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.trace-container {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.trace-header {
|
||||
background: var(--header-bg);
|
||||
padding: 5px 10px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.trace-title {
|
||||
margin: 0;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.trace-body {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.trace-item {
|
||||
padding: 2px 4px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.trace-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.trace-location {
|
||||
font-family: monospace;
|
||||
background: var(--code-bg);
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.trace-call {
|
||||
margin-top: 5px;
|
||||
font-family: monospace;
|
||||
color: #6c757d;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.trace-origin {
|
||||
background-color: #fff5f5;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: monospace;
|
||||
background: var(--code-bg);
|
||||
padding: 2px 4px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.error-container, .trace-container {
|
||||
background: #343a40;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
background: #3a2a2a;
|
||||
}
|
||||
|
||||
.trace-origin {
|
||||
background-color: #3a2a2a;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="error-container">
|
||||
<div class="error-header">
|
||||
<h1 class="error-title">{{ errorClass }}</h1>
|
||||
</div>
|
||||
|
||||
<div class="error-body">
|
||||
<div class="error-message">{{ errorMessage }}</div>
|
||||
|
||||
<div class="error-meta">
|
||||
<span>Request ID: {{ requestId }}</span>
|
||||
<span>Zeit: {{ timestamp }}</span>
|
||||
<span>Level: {{ level }}</span>
|
||||
<span>Speicherverbrauch: {{ format_filesize($memory) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php echo "Hallo" ?>
|
||||
|
||||
{{ code }}
|
||||
|
||||
<div class="trace-container">
|
||||
<div class="trace-header">
|
||||
<h2 class="trace-title">Stack Trace</h2>
|
||||
</div>
|
||||
|
||||
<div class="trace-body" style="font-size: 0.8rem;">
|
||||
<for var="item" in="trace">
|
||||
<div class="trace-item {{ item->isExceptionOrigin() }} 'trace-origin' : ''">
|
||||
<div class="trace-location">
|
||||
{{ item.getRelativeFile() }} <i>Line {{ item.line }}</i>
|
||||
<!--#$i $item->getRelativeFile():$item->line-->
|
||||
</div>
|
||||
<div class="trace-call" style="font-size: 1.25rem; background-color: #81a5ed; color: black; padding: 0.25em;">
|
||||
{{ item.getCallString() }}
|
||||
</div>
|
||||
</div>
|
||||
</for>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
106
src/Framework/ErrorHandling/templates/production.view.php
Normal file
106
src/Framework/ErrorHandling/templates/production.view.php
Normal file
@@ -0,0 +1,106 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Fehler</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg-color: #f8f9fa;
|
||||
--text-color: #212529;
|
||||
--border-color: #dee2e6;
|
||||
--error-color: #dc3545;
|
||||
--header-bg: #e9ecef;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--bg-color: #212529;
|
||||
--text-color: #f8f9fa;
|
||||
--border-color: #495057;
|
||||
--error-color: #f8d7da;
|
||||
--header-bg: #343a40;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: var(--text-color);
|
||||
background: var(--bg-color);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 600px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.error-container {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.error-header {
|
||||
background: var(--header-bg);
|
||||
padding: 15px 20px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.error-title {
|
||||
margin: 0;
|
||||
color: var(--error-color);
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.error-body {
|
||||
padding: 30px 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.error-meta {
|
||||
font-size: 0.9rem;
|
||||
color: #6c757d;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.error-container {
|
||||
background: #343a40;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="error-container">
|
||||
<div class="error-header">
|
||||
<h1 class="error-title">Ein Fehler ist aufgetreten</h1>
|
||||
</div>
|
||||
|
||||
<div class="error-body">
|
||||
<div class="error-message">
|
||||
Es tut uns leid, aber es ist ein Fehler aufgetreten.
|
||||
Unser Team wurde benachrichtigt und arbeitet an einer Lösung.
|
||||
</div>
|
||||
|
||||
<div class="error-meta">
|
||||
Request ID: $requestId
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user