202 lines
7.0 KiB
PHP
202 lines
7.0 KiB
PHP
<?php
|
|
|
|
namespace App\Framework\ErrorHandling;
|
|
|
|
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\Logging\Logger;
|
|
use App\Framework\View\TemplateRenderer;
|
|
use Throwable;
|
|
|
|
final readonly class ErrorHandler
|
|
{
|
|
private ErrorLogger $logger;
|
|
private SecurityEventHandler $securityHandler;
|
|
private bool $isDebugMode;
|
|
|
|
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);
|
|
}
|
|
|
|
public function register(): void
|
|
{
|
|
set_exception_handler($this->handleException(...));
|
|
set_error_handler($this->handleError(...));;
|
|
register_shutdown_function($this->handleShutdown(...));;
|
|
}
|
|
|
|
public function createHttpResponse(\Throwable $e, ?MiddlewareContext $context = null): HttpResponse
|
|
{
|
|
$errorContext = $this->createErrorContext($e, $context);
|
|
|
|
// Standard Error Logging
|
|
$this->logger->logError($errorContext);
|
|
|
|
// Security Event Logging delegieren
|
|
$this->securityHandler->handleIfSecurityException($e, $context);
|
|
|
|
$responseFactory = $this->createResponseFactory();
|
|
|
|
$isApiRequest = $responseFactory->isApiRequest();
|
|
|
|
return $responseFactory->createResponse(
|
|
$errorContext,
|
|
$this->isDebugMode,
|
|
$isApiRequest
|
|
);
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|