Files
michaelschiemer/src/Framework/ErrorHandling/ErrorHandler.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);
}
}