Enable Discovery debug logging for production troubleshooting

- Add DISCOVERY_LOG_LEVEL=debug
- Add DISCOVERY_SHOW_PROGRESS=true
- Temporary changes for debugging InitializerProcessor fixes on production
This commit is contained in:
2025-08-11 20:13:26 +02:00
parent 59fd3dd3b1
commit 55a330b223
3683 changed files with 2956207 additions and 16948 deletions

View File

@@ -14,7 +14,8 @@ final readonly class CliErrorHandler
{
public function __construct(
private ?ConsoleOutput $output = null
) {}
) {
}
public function register(): void
{
@@ -25,7 +26,7 @@ final readonly class CliErrorHandler
public function handleError(int $severity, string $message, string $file, int $line): bool
{
if (!(error_reporting() & $severity)) {
if (! (error_reporting() & $severity)) {
return false;
}

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
namespace App\Framework\ErrorHandling;
@@ -18,4 +19,9 @@ final class DummyTemplateRenderer implements TemplateRenderer
// seine eigene Fallback-Methode hat
return '';
}
public function renderPartial(RenderContext $context): string
{
return '';
}
}

View File

@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Framework\ErrorHandling;
use App\Framework\Http\RequestId;
@@ -13,8 +15,7 @@ final readonly class ErrorContext
public ErrorLevel $level,
public RequestId $requestId,
public array $additionalData = []
)
{
) {
$this->stackTrace = new StackTrace($exception);
}
@@ -33,6 +34,7 @@ final readonly class ErrorContext
{
$class = get_class($this->exception);
$parts = explode('\\', $class);
return end($parts);
}

View File

@@ -1,25 +1,40 @@
<?php
declare(strict_types=1);
namespace App\Framework\ErrorHandling;
use App\Framework\Config\EnvKey;
use App\Framework\Config\Environment;
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\ErrorHandlerContext;
use App\Framework\Exception\ExceptionContext;
use App\Framework\Exception\FrameworkException;
use App\Framework\Exception\RequestContext;
use App\Framework\Exception\SecurityException;
use App\Framework\Exception\SecurityLogLevel;
use App\Framework\Http\HttpResponse;
use App\Framework\Exception\SystemContext;
use App\Framework\Http\MiddlewareContext;
use App\Framework\Http\RequestIdGenerator;
use App\Framework\Http\Response;
use App\Framework\Http\ResponseEmitter;
use App\Framework\Http\Responses\RedirectResponse;
use App\Framework\Logging\Logger;
use App\Framework\Performance\MemoryMonitor;
use App\Framework\Validation\Exceptions\ValidationException;
use App\Framework\Validation\ValidationFormHandler;
use App\Framework\View\TemplateRenderer;
use Throwable;
final readonly class ErrorHandler
{
private ErrorLogger $logger;
private SecurityEventHandler $securityHandler;
private bool $isDebugMode;
public function __construct(
@@ -29,8 +44,8 @@ final readonly class ErrorHandler
?Logger $logger = null,
?bool $isDebugMode = null,
?SecurityEventHandler $securityHandler = null
){
$this->isDebugMode = $isDebugMode ?? (bool)($_ENV['APP_DEBUG'] ?? false);
) {
$this->isDebugMode = $isDebugMode ?? $this->getDebugModeFromEnvironment();
$this->logger = new ErrorLogger($logger);
$this->securityHandler = $securityHandler ?? SecurityEventHandler::createDefault($logger);
}
@@ -38,16 +53,23 @@ final readonly class ErrorHandler
public function register(): void
{
set_exception_handler($this->handleException(...));
set_error_handler($this->handleError(...));;
register_shutdown_function($this->handleShutdown(...));;
set_error_handler($this->handleError(...));
register_shutdown_function($this->handleShutdown(...));
}
public function createHttpResponse(\Throwable $e, ?MiddlewareContext $context = null): HttpResponse
public function createHttpResponse(\Throwable $e, ?MiddlewareContext $context = null): Response
{
$errorContext = $this->createErrorContext($e, $context);
// Handle ValidationException with form-specific logic
if ($e instanceof ValidationException) {
return $this->handleValidationException($e, $context);
}
// Standard Error Logging
$this->logger->logError($errorContext);
$errorHandlerContext = $this->createErrorHandlerContext($e, $context);
// Standard Error Logging mit ErrorHandlerContext
$this->logger->logErrorHandlerContext($errorHandlerContext);
// Security Event Logging delegieren
$this->securityHandler->handleIfSecurityException($e, $context);
@@ -56,19 +78,35 @@ final readonly class ErrorHandler
$isApiRequest = $responseFactory->isApiRequest();
return $responseFactory->createResponse(
$errorContext,
return $responseFactory->createResponseFromHandlerContext(
$errorHandlerContext,
$this->isDebugMode,
$isApiRequest
);
}
private function handleValidationException(ValidationException $exception, ?MiddlewareContext $context): Response
{
// Try to get ValidationFormHandler from container
if ($this->container->has(ValidationFormHandler::class)) {
$formHandler = $this->container->get(ValidationFormHandler::class);
return $formHandler->handle($exception, $context);
}
error_log("ErrorHandler: ValidationFormHandler NOT found, using fallback");
// Fallback: create basic redirect response
$refererUrl = $context?->request?->server?->getRefererUri() ?? '/';
return new RedirectResponse($refererUrl);
}
public function handleException(Throwable $e): never
{
$context = $this->createErrorContext($e);
$errorHandlerContext = $this->createErrorHandlerContext($e);
// Logge den Fehler
$this->logger->logError($context);
// Logge den Fehler mit ErrorHandlerContext
$this->logger->logErrorHandlerContext($errorHandlerContext);
// Security Event Logging delegieren
$this->securityHandler->handleIfSecurityException($e);
@@ -79,8 +117,8 @@ final readonly class ErrorHandler
// Bestimme ob es ein API-Request ist
$isApiRequest = $responseFactory->isApiRequest();
// Erstelle eine Response basierend auf dem Kontext
$response = $responseFactory->createResponse($context, $this->isDebugMode, $isApiRequest);
// Erstelle eine Response basierend auf dem ErrorHandlerContext
$response = $responseFactory->createResponseFromHandlerContext($errorHandlerContext, $this->isDebugMode, $isApiRequest);
// Sende die Fehlermeldung
$this->emitter->emit($response);
@@ -113,24 +151,112 @@ final readonly class ErrorHandler
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
private function createErrorHandlerContext(Throwable $exception, ?MiddlewareContext $context = null): ErrorHandlerContext
{
$level = $this->determineErrorLevel($exception);
// ExceptionContext aus der Exception extrahieren (falls FrameworkException)
$exceptionContext = ExceptionContext::empty();
if ($exception instanceof FrameworkException) {
$exceptionContext = $exception->getContext();
$requestId = $context->requestId ?? $this->requestIdGenerator->generate();
// Ensure framework exceptions also have the original exception for stack traces
if (! isset($exceptionContext->data['original_exception'])) {
$exceptionContext = $exceptionContext->withData([
'original_exception' => $exception,
]);
}
} else {
// For non-framework exceptions, add the actual exception message and the original exception
$exceptionContext = $exceptionContext->withData([
'exception_message' => $exception->getMessage(),
'exception_file' => $exception->getFile(),
'exception_line' => $exception->getLine(),
'original_exception' => $exception, // Preserve original exception for stack trace
]);
}
return new ErrorContext(
exception: $exception,
level: $level,
requestId: $requestId,
additionalData: [
'timestamp' => date('c'),
'memory_usage' => memory_get_peak_usage(true),
]
// RequestContext aus MiddlewareContext oder Globals erstellen
$requestContext = $this->createRequestContext($context);
// SystemContext mit aktuellen System-Metriken erstellen
$memoryMonitor = $this->container->has(MemoryMonitor::class)
? $this->container->get(MemoryMonitor::class)
: null;
$systemContext = SystemContext::current($memoryMonitor);
// Metadata für verschiedene Exception-Typen
$metadata = $this->createExceptionMetadata($exception);
return ErrorHandlerContext::create(
$exceptionContext,
$requestContext,
$systemContext,
$metadata
);
}
private function determineErrorLevel(Throwable $exception):ErrorLevel
private function createRequestContext(?MiddlewareContext $context): RequestContext
{
if ($context?->request) {
$request = $context->request;
return RequestContext::create(
clientIp: (string) $request->server->getClientIp(),
userAgent: $request->server->getUserAgent(),
requestMethod: $request->method->value,
requestUri: (string) $request->getUri(),
hostIp: $request->server->get('SERVER_ADDR', 'unknown'),
hostname: $request->server->getHttpHost() ?: 'localhost',
protocol: (string) $request->server->getProtocol()->value,
port: (string) $request->server->getServerPort(),
requestId: $request->id
);
}
return RequestContext::fromGlobals();
}
private function createExceptionMetadata(Throwable $exception): array
{
$metadata = [
'exception_class' => get_class($exception),
'error_level' => $this->determineErrorLevel($exception)->name,
];
// HTTP-Status-Code basierend auf Exception-Typ
$metadata['http_status'] = match (true) {
$exception instanceof \App\Framework\Exception\Authentication\InvalidCredentialsException,
$exception instanceof \App\Framework\Exception\Authentication\TokenExpiredException,
$exception instanceof \App\Framework\Exception\Authentication\SessionTimeoutException => 401,
$exception instanceof \App\Framework\Exception\Authentication\AccountLockedException => 423,
$exception instanceof \App\Framework\Exception\Authentication\InsufficientPrivilegesException => 403,
$exception instanceof \App\Framework\Exception\Http\RouteNotFoundException => 404,
$exception instanceof \App\Framework\Exception\Http\MalformedJsonException,
$exception instanceof \App\Framework\Exception\Http\InvalidContentTypeException,
$exception instanceof \App\Framework\Exception\Security\SqlInjectionAttemptException,
$exception instanceof \App\Framework\Exception\Security\XssAttemptException,
$exception instanceof \App\Framework\Exception\Security\PathTraversalAttemptException => 400,
$exception instanceof \App\Framework\Exception\Http\OversizedRequestException => 413,
$exception instanceof \App\Framework\Exception\Http\RateLimitExceededException => 429,
default => 500,
};
// Zusätzliche Header für spezielle Exceptions
if ($exception instanceof \App\Framework\Exception\Http\RateLimitExceededException) {
$metadata['additional_headers'] = $exception->getRateLimitHeaders();
}
if ($exception instanceof \App\Framework\Exception\Http\InvalidContentTypeException) {
$metadata['additional_headers'] = $exception->getResponseHeaders();
}
return $metadata;
}
private function determineErrorLevel(Throwable $exception): ErrorLevel
{
return match(true) {
$exception instanceof \Error => ErrorLevel::CRITICAL,
@@ -156,7 +282,6 @@ final readonly class ErrorHandler
};
}
/**
* Bestimmt den Fehler-Level basierend auf dem ErrorException-Severity-Level
*/
@@ -176,6 +301,20 @@ final readonly class ErrorHandler
* Erstellt eine ErrorResponseFactory mit Renderern
* Versucht den TemplateRenderer aus dem Container zu laden, falls vorhanden
*/
private function getDebugModeFromEnvironment(): bool
{
try {
if ($this->container->has(Environment::class)) {
$environment = $this->container->get(Environment::class);
return $environment->getBool(EnvKey::APP_DEBUG, false);
}
} catch (Throwable $e) {
// Sicherer Fallback für Production
}
return false;
}
private function createResponseFactory(): ErrorResponseFactory
{
$templateRenderer = null;

View File

@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Framework\ErrorHandling;
enum ErrorLevel

View File

@@ -1,8 +1,10 @@
<?php
declare(strict_types=1);
namespace App\Framework\ErrorHandling;
use App\Framework\Exception\ErrorHandlerContext;
use App\Framework\Logging\Logger;
use App\Framework\Logging\LogLevel;
@@ -13,10 +15,11 @@ final readonly class ErrorLogger
{
public function __construct(
private ?Logger $logger = null
) {}
) {
}
/**
* Loggt einen Fehler basierend auf dem ErrorContext
* Loggt einen Fehler basierend auf dem ErrorContext (Legacy)
*/
public function logError(ErrorContext $context): void
{
@@ -34,7 +37,7 @@ final readonly class ErrorLogger
'exception' => $exception,
'trace' => $exception->getTraceAsString(),
'requestId' => $context->requestId,
'additionalData' => $context->additionalData
'additionalData' => $context->additionalData,
];
if ($this->logger !== null) {
@@ -46,9 +49,134 @@ final readonly class ErrorLogger
}
}
/**
* Loggt einen Fehler basierend auf dem ErrorHandlerContext
*/
public function logErrorHandlerContext(ErrorHandlerContext $context): void
{
// Structured Logging mit allen Context-Informationen
$logData = $context->forLogging();
// Security Event Logging falls Security-Event
if ($context->exception->metadata['security_event'] ?? false) {
$this->logSecurityEvent($context);
}
// Standard Error Logging
$message = sprintf(
'[%s] %s: %s',
$context->request->requestId?->toString() ?? 'NO-REQUEST-ID',
$context->exception->component ?? 'Application',
$this->extractExceptionMessage($context)
);
// Debug: Log the actual exception class
if (isset($context->metadata['exception_class'])) {
error_log("DEBUG: Exception class: " . $context->metadata['exception_class']);
}
if ($this->logger !== null) {
$logLevel = $this->determineLogLevel($context);
$this->logger->log($logLevel, $message, $logData);
} else {
// Fallback auf error_log
$this->logHandlerContextToErrorLog($context);
}
}
/**
* Fallback-Methode für Logging ohne Framework Logger
* Loggt Security Events im OWASP-Format
*/
private function logSecurityEvent(ErrorHandlerContext $context): void
{
$securityLog = $context->toSecurityEventFormat($_ENV['APP_NAME'] ?? 'app');
if ($this->logger !== null) {
$this->logger->log(LogLevel::WARNING, 'Security Event', $securityLog);
} else {
error_log('SECURITY_EVENT: ' . json_encode($securityLog, JSON_UNESCAPED_SLASHES));
}
}
/**
* Extrahiert Exception-Message aus ErrorHandlerContext
*/
private function extractExceptionMessage(ErrorHandlerContext $context): string
{
// Versuche Exception aus context.exception.data zu extrahieren
$exceptionData = $context->exception->data;
if (isset($exceptionData['exception_message'])) {
return $exceptionData['exception_message'];
}
// Debug: Log what data we have
error_log("DEBUG: Exception data: " . json_encode($exceptionData));
// Fallback: Operation und Component verwenden
$operation = $context->exception->operation ?? 'unknown_operation';
$component = $context->exception->component ?? 'unknown_component';
return "Error in {$component} during {$operation}";
}
/**
* Bestimmt Log-Level aus ErrorHandlerContext
*/
private function determineLogLevel(ErrorHandlerContext $context): LogLevel
{
// Security Events haben höhere Priorität
if ($context->exception->metadata['security_event'] ?? false) {
$securityLevel = $context->exception->metadata['security_level'] ?? 'ERROR';
return match ($securityLevel) {
'DEBUG' => LogLevel::DEBUG,
'INFO' => LogLevel::INFO,
'WARN' => LogLevel::WARNING,
'ERROR' => LogLevel::ERROR,
'FATAL' => LogLevel::CRITICAL,
default => LogLevel::ERROR,
};
}
// Standard Log-Level aus Metadata
$errorLevel = $context->exception->metadata['error_level'] ?? 'ERROR';
return match ($errorLevel) {
'CRITICAL' => LogLevel::CRITICAL,
'ERROR' => LogLevel::ERROR,
'WARNING' => LogLevel::WARNING,
'NOTICE' => LogLevel::NOTICE,
'INFO' => LogLevel::INFO,
'DEBUG' => LogLevel::DEBUG,
default => LogLevel::ERROR,
};
}
/**
* Fallback-Methode für ErrorHandlerContext ohne Framework Logger
*/
private function logHandlerContextToErrorLog(ErrorHandlerContext $context): void
{
$message = sprintf(
'[%s] [%s] %s: %s',
date('Y-m-d H:i:s'),
$context->exception->metadata['error_level'] ?? 'ERROR',
$context->exception->component ?? 'Application',
$this->extractExceptionMessage($context)
);
error_log($message);
// Security Events auch separat loggen
if ($context->exception->metadata['security_event'] ?? false) {
$securityLog = $context->toSecurityEventJson($_ENV['APP_NAME'] ?? 'app');
error_log('SECURITY_EVENT: ' . $securityLog);
}
}
/**
* Fallback-Methode für Logging ohne Framework Logger (Legacy)
*/
private function logToErrorLog(ErrorContext $context): void
{

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
namespace App\Framework\ErrorHandling;

View File

@@ -4,8 +4,8 @@ declare(strict_types=1);
namespace App\Framework\ErrorHandling;
use App\Framework\Exception\SecurityException;
use App\Framework\Exception\ErrorHandlerContext;
use App\Framework\Exception\SecurityException;
/**
* Manager für Security-Alerts - Integration zu externen Alert-Systemen
@@ -14,7 +14,8 @@ final readonly class SecurityAlertManager
{
public function __construct(
private array $alertChannels = []
) {}
) {
}
/**
* Sendet Alert über alle konfigurierten Kanäle
@@ -57,10 +58,10 @@ final readonly class SecurityAlertManager
'user_agent' => $context->request->userAgent,
'request_uri' => $context->request->requestUri,
'timestamp' => date('c'),
'request_id' => $context->request->requestId
'request_id' => $context->request->requestId,
],
'event_data' => $securityEvent->toArray(),
'context' => $context->forLogging()
'context' => $context->forLogging(),
];
}

View File

@@ -4,8 +4,8 @@ declare(strict_types=1);
namespace App\Framework\ErrorHandling;
use App\Framework\Exception\SecurityException;
use App\Framework\Exception\ErrorHandlerContext;
use App\Framework\Exception\SecurityException;
use App\Framework\Http\MiddlewareContext;
use App\Framework\Logging\Logger;
use Throwable;
@@ -18,7 +18,8 @@ final readonly class SecurityEventHandler
public function __construct(
private SecurityEventLogger $securityLogger,
private ?SecurityAlertManager $alertManager = null
) {}
) {
}
/**
* Behandelt Security-Exception und führt entsprechendes Logging durch
@@ -29,7 +30,7 @@ final readonly class SecurityEventHandler
): void {
try {
// Erstelle ErrorHandlerContext für OWASP-Format
$errorHandlerContext = ErrorHandlerContext::fromException($exception, $context);
$errorHandlerContext = $this->createErrorHandlerContext($exception, $context);
// Führe Security-Logging durch
$this->securityLogger->logSecurityEvent($exception, $errorHandlerContext);
@@ -52,11 +53,12 @@ final readonly class SecurityEventHandler
Throwable $exception,
?MiddlewareContext $context = null
): bool {
if (!$exception instanceof SecurityException) {
if (! $exception instanceof SecurityException) {
return false;
}
$this->handleSecurityException($exception, $context);
return true;
}
@@ -99,7 +101,7 @@ final readonly class SecurityEventHandler
'description' => $originalException->getSecurityEvent()->getDescription(),
'level' => 'FATAL',
'audit_failure' => true,
'original_error' => $loggingError->getMessage()
'original_error' => $loggingError->getMessage(),
];
error_log('SECURITY_EVENT_FALLBACK: ' . json_encode($minimalLog));
@@ -110,6 +112,26 @@ final readonly class SecurityEventHandler
}
}
/**
* Erstellt ErrorHandlerContext aus Exception und MiddlewareContext
*/
private function createErrorHandlerContext(
SecurityException $exception,
?MiddlewareContext $context = null
): ErrorHandlerContext {
// Extrahiere Metadata aus dem MiddlewareContext
$metadata = [];
if ($context) {
$metadata = [
'request_id' => $context->request->id->toString(),
'middleware_context' => true,
];
}
// Verwende die bestehende fromException Methode mit korrekten Metadata
return ErrorHandlerContext::fromException($exception, $metadata);
}
/**
* Factory-Methode mit Standard-Konfiguration
*/

View File

@@ -4,8 +4,8 @@ declare(strict_types=1);
namespace App\Framework\ErrorHandling;
use App\Framework\Exception\SecurityException;
use App\Framework\Exception\ErrorHandlerContext;
use App\Framework\Exception\SecurityException;
use App\Framework\Exception\SecurityLogLevel;
use App\Framework\Logging\Logger;
use App\Framework\Logging\LogLevel;
@@ -18,7 +18,8 @@ final readonly class SecurityEventLogger
public function __construct(
private ?Logger $logger = null,
private string $applicationId = 'app'
) {}
) {
}
/**
* Loggt Security-Event im OWASP-Format
@@ -45,7 +46,7 @@ final readonly class SecurityEventLogger
'requires_alert' => $securityEvent->requiresAlert(),
'owasp_format' => $owaspLog,
'event_data' => $securityEvent->toArray(),
'context_data' => $context->forLogging()
'context_data' => $context->forLogging(),
]
);
} else {
@@ -100,7 +101,7 @@ final readonly class SecurityEventLogger
'region' => $_ENV['AWS_REGION'] ?? 'unknown',
'geo' => $_ENV['GEO_LOCATION'] ?? 'unknown',
'category' => $securityEvent->getCategory(),
'requires_alert' => $securityEvent->requiresAlert()
'requires_alert' => $securityEvent->requiresAlert(),
];
}
@@ -122,7 +123,7 @@ final readonly class SecurityEventLogger
'request_method' => $context->request->requestMethod,
'timestamp' => date('c'),
'request_id' => $context->request->requestId,
'event_data' => $exception->getSecurityEvent()->toArray()
'event_data' => $exception->getSecurityEvent()->toArray(),
];
}

View File

@@ -1,11 +1,13 @@
<?php
declare(strict_types=1);
namespace App\Framework\ErrorHandling;
use ArrayAccess;
use ArrayIterator;
use Countable;
use IteratorAggregate;
use ArrayIterator;
use Traversable;
/**
@@ -15,7 +17,7 @@ use Traversable;
final class StackTrace implements ArrayAccess, IteratorAggregate, Countable
{
/** @var TraceItem[] */
private(set) array $items = [];
public private(set) array $items = [];
/**
* Erstellt ein neues StackTrace-Objekt aus einer Exception
@@ -147,6 +149,7 @@ final class StackTrace implements ArrayAccess, IteratorAggregate, Countable
foreach ($this->items as $index => $item) {
$result .= "#{$index} {$item}\n";
}
return $result;
}
}

View File

@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Framework\ErrorHandling;
/**
@@ -15,7 +17,8 @@ final readonly class TraceItem
public ?string $type,
public array $args,
public bool $isExceptionOrigin
) {}
) {
}
/**
* Gibt den Dateipfad relativ zum Projekt-Root zurück
@@ -33,11 +36,12 @@ final readonly class TraceItem
*/
public function getShortClass(): ?string
{
if (!$this->class) {
if (! $this->class) {
return null;
}
$parts = explode('\\', $this->class);
return end($parts);
}
@@ -46,11 +50,12 @@ final readonly class TraceItem
*/
public function getNamespace(): ?string
{
if (!$this->class) {
if (! $this->class) {
return null;
}
$lastSlash = strrpos($this->class, '\\');
return $lastSlash ? substr($this->class, 0, $lastSlash) : null;
}
@@ -126,6 +131,7 @@ final readonly class TraceItem
private function removeNamespace(string $class): string
{
$lastSlash = strrpos($class, '\\');
return $lastSlash ? substr($class, $lastSlash + 1) : $class;
}

View File

@@ -1,10 +1,12 @@
<?php
declare(strict_types=1);
namespace App\Framework\ErrorHandling\View;
use App\Framework\ErrorHandling\ErrorContext;
use App\Framework\ErrorHandling\ExceptionConverter;
use App\Framework\Exception\ErrorHandlerContext;
/**
* Renderer für API-Fehler im JSON-Format
@@ -12,7 +14,72 @@ use App\Framework\ErrorHandling\ExceptionConverter;
final readonly class ApiErrorRenderer implements ErrorViewRendererInterface
{
/**
* Rendert einen API-Fehler als JSON
* Rendert einen API-Fehler als JSON basierend auf ErrorHandlerContext
*/
public function renderFromHandlerContext(ErrorHandlerContext $context, bool $isDebug = false): string
{
$responseData = [
'error' => true,
'code' => $context->metadata['http_status'] ?? 500,
'message' => $this->getUserMessage($context, $isDebug),
'requestId' => $context->request->requestId,
'timestamp' => date('c'),
];
// Exception-spezifische Daten hinzufügen
if ($errorCode = $this->getErrorCode($context)) {
$responseData['errorCode'] = $errorCode;
}
// Security Event Informationen (nur für interne Zwecke)
if ($context->exception->metadata['security_event'] ?? false) {
$responseData['type'] = 'security_event';
if (! $isDebug) {
// In Production keine Details von Security Events preisgeben
$responseData['message'] = 'Invalid request. Please check your input and try again.';
}
}
// Rate Limit spezifische Informationen
if (isset($context->metadata['additional_headers']['X-RateLimit-Limit'])) {
$responseData['rateLimit'] = [
'limit' => (int) $context->metadata['additional_headers']['X-RateLimit-Limit'],
'remaining' => (int) $context->metadata['additional_headers']['X-RateLimit-Remaining'],
'reset' => (int) $context->metadata['additional_headers']['X-RateLimit-Reset'],
'retryAfter' => (int) ($context->metadata['additional_headers']['Retry-After'] ?? 0),
];
}
// Debug-Informationen
if ($isDebug) {
$responseData['debug'] = [
'operation' => $context->exception->operation,
'component' => $context->exception->component,
'exception_class' => $context->metadata['exception_class'],
'client_ip' => $context->request->clientIp,
'request_method' => $context->request->requestMethod,
'request_uri' => $context->request->requestUri,
'user_agent' => $context->request->userAgent,
'memory_usage' => $context->system->memoryUsage ?? 0,
'execution_time' => $context->system->executionTime ?? 0,
];
// Exception-spezifische Debug-Daten
if (! empty($context->exception->data)) {
$responseData['debug']['exception_data'] = $context->exception->data;
}
if (! empty($context->exception->debug)) {
$responseData['debug']['exception_debug'] = $context->exception->debug;
}
}
return json_encode($responseData, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
}
/**
* Rendert einen API-Fehler als JSON (Legacy)
*/
public function render(ErrorContext $context, bool $isDebug = false): string
{
@@ -36,4 +103,52 @@ final readonly class ApiErrorRenderer implements ErrorViewRendererInterface
return json_encode($responseData, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
}
/**
* Extrahiert benutzerfreundliche Nachricht aus ErrorHandlerContext
*/
private function getUserMessage(ErrorHandlerContext $context, bool $isDebug): string
{
// Bei Debug-Modus die technische Nachricht verwenden
if ($isDebug && isset($context->exception->data['exception_message'])) {
return $context->exception->data['exception_message'];
}
// Exception-spezifische Nachrichten verwenden falls verfügbar
if (isset($context->exception->data['user_message'])) {
return $context->exception->data['user_message'];
}
// Fallback auf generische Nachrichten basierend auf HTTP-Status
return match ($context->metadata['http_status'] ?? 500) {
400 => 'Bad Request: Invalid input provided.',
401 => 'Unauthorized: Authentication required.',
403 => 'Forbidden: Insufficient permissions.',
404 => 'Not Found: The requested resource was not found.',
413 => 'Payload Too Large: Request size exceeds limit.',
415 => 'Unsupported Media Type: Invalid content type.',
423 => 'Locked: Account or resource is temporarily locked.',
429 => 'Too Many Requests: Rate limit exceeded.',
500 => 'Internal Server Error: An unexpected error occurred.',
default => 'An error occurred while processing your request.',
};
}
/**
* Extrahiert Error-Code aus ErrorHandlerContext
*/
private function getErrorCode(ErrorHandlerContext $context): ?string
{
// Aus Exception-Daten extrahieren
if (isset($context->exception->data['error_code'])) {
return $context->exception->data['error_code'];
}
// Aus Metadata extrahieren
if (isset($context->exception->metadata['error_code'])) {
return $context->exception->metadata['error_code'];
}
return null;
}
}

View File

@@ -1,10 +1,12 @@
<?php
declare(strict_types=1);
namespace App\Framework\ErrorHandling\View;
use App\Framework\ErrorHandling\ErrorContext;
use App\Framework\ErrorHandling\ExceptionConverter;
use App\Framework\Exception\ErrorHandlerContext;
use App\Framework\Http\Headers;
use App\Framework\Http\HttpResponse;
use App\Framework\Http\Status;
@@ -17,10 +19,59 @@ final readonly class ErrorResponseFactory
public function __construct(
private ErrorViewRendererInterface $htmlRenderer,
private ErrorViewRendererInterface $apiRenderer
) {}
) {
}
/**
* Erstellt eine HTTP-Response basierend auf dem ErrorContext
* Erstellt eine HTTP-Response basierend auf dem ErrorHandlerContext
*/
public function createResponseFromHandlerContext(ErrorHandlerContext $context, bool $isDebug = false, bool $isApiRequest = false): HttpResponse
{
// HTTP-Status aus Metadata extrahieren
$status = $context->metadata['http_status'] ?? 500;
// Headers aufbauen
$headers = new Headers();
// Content-Type basierend auf Request-Typ
if ($isApiRequest) {
$headers = $headers->with('Content-Type', 'application/json; charset=utf-8');
$body = $this->apiRenderer->renderFromHandlerContext($context, $isDebug);
} else {
$headers = $headers->with('Content-Type', 'text/html; charset=utf-8');
$body = $this->htmlRenderer->renderFromHandlerContext($context, $isDebug);
}
// Exception-spezifische Header hinzufügen
if (isset($context->metadata['additional_headers'])) {
foreach ($context->metadata['additional_headers'] as $name => $value) {
$headers = $headers->with($name, $value);
}
}
// Standard Security Header
$headers = $headers
->with('Cache-Control', 'no-store, no-cache, must-revalidate')
->with('X-Content-Type-Options', 'nosniff')
->with('X-Frame-Options', 'DENY');
// Security Event Header für Security-Exceptions
if ($context->exception->metadata['security_event'] ?? false) {
$headers = $headers->with('X-Security-Event', 'true');
// Request-ID für Security-Tracking
$headers = $headers->with('X-Request-ID', $context->request->requestId?->toString() ?? 'unknown');
}
return new HttpResponse(
status: Status::from($status),
headers: $headers,
body: $body
);
}
/**
* Erstellt eine HTTP-Response basierend auf dem ErrorContext (Legacy)
*/
public function createResponse(ErrorContext $context, bool $isDebug = false, bool $isApiRequest = false): HttpResponse
{
@@ -39,7 +90,7 @@ final readonly class ErrorResponseFactory
$headers = $headers->with('Cache-Control', 'no-store, no-cache, must-revalidate');
return new HttpResponse(
status: $status,
status: Status::from($status),
headers: $headers,
body: $body
);

View File

@@ -1,12 +1,17 @@
<?php
declare(strict_types=1);
namespace App\Framework\ErrorHandling\View;
use App\Framework\ErrorHandling\ErrorContext;
use App\Framework\ErrorHandling\StackTrace;
use App\Framework\ErrorHandling\TraceItem;
use App\Framework\Exception\ErrorHandlerContext;
use App\Framework\Meta\MetaData;
use App\Framework\Meta\OpenGraphTypeWebsite;
use App\Framework\SyntaxHighlighter\FileHighlighter;
use App\Framework\View\RawHtml;
use App\Framework\View\RenderContext;
use App\Framework\View\TemplateRenderer;
use Throwable;
@@ -20,10 +25,149 @@ final readonly class ErrorTemplateRenderer implements ErrorViewRendererInterface
private TemplateRenderer $renderer,
private string $debugTemplate = 'debug',
private string $productionTemplate = 'production'
) {}
) {
}
/**
* Rendert eine Fehlerseite basierend auf dem ErrorContext
* Rendert eine Fehlerseite basierend auf dem ErrorHandlerContext
*/
public function renderFromHandlerContext(ErrorHandlerContext $context, bool $isDebug = false): string
{
$template = $isDebug ? $this->debugTemplate : $this->productionTemplate;
try {
// Sichere Datenaufbereitung für ErrorHandlerContext
$safeData = [
'errorClass' => $context->metadata['exception_class'] ?? 'Unknown',
'errorMessage' => $this->extractErrorMessage($context),
'error' => [
'message' => $this->extractErrorMessage($context),
'operation' => $context->exception->operation ?? 'unknown',
'component' => $context->exception->component ?? 'Application',
'class' => $context->metadata['exception_class'] ?? 'Unknown',
],
'requestId' => $context->request->requestId ?? 'NO-REQUEST-ID',
'timestamp' => date('c'),
'level' => $context->metadata['error_level'] ?? 'ERROR',
'memory' => $context->system->memoryUsage ?? 0,
'httpStatus' => $context->metadata['http_status'] ?? 500,
'clientIp' => $context->request->clientIp,
'requestUri' => $context->request->requestUri,
'userAgent' => (string) $context->request->userAgent,
];
// Security Event spezifische Daten
if ($context->exception->metadata['security_event'] ?? false) {
$safeData['securityEvent'] = [
'type' => $context->exception->metadata['attack_type'] ?? 'unknown',
'category' => $context->exception->metadata['category'] ?? 'security',
'requiresAlert' => $context->exception->metadata['requires_alert'] ?? false,
];
// In Production keine Security Details anzeigen
if (! $isDebug) {
$safeData['errorMessage'] = 'Security violation detected. Access denied.';
$safeData['error']['message'] = 'Security violation detected. Access denied.';
}
}
// Debug-spezifische Daten
if ($isDebug) {
$safeData['debug'] = [
'exception_data' => $context->exception->data,
'exception_debug' => $context->exception->debug,
'system_data' => $context->system->data ?? [],
'execution_time' => $context->system->executionTime ?? 0,
];
// Dependency Injection spezifische Informationen
if (isset($context->exception->data['dependencyChain'])) {
$chainDisplay = implode(' → ', $context->exception->data['dependencyChain']) . ' → ' . ($context->exception->data['class'] ?? 'Unknown');
$targetClass = $context->exception->data['class'] ?? 'Unknown';
$dependencyHtml = '<div class="dependency-info" style="margin-top: 20px; padding: 15px; background: #fff3cd; border-left: 4px solid #ffc107; border-radius: 4px;">' .
'<h3 style="margin: 0 0 10px 0; color: #856404;">🔄 Zyklische Abhängigkeit</h3>' .
'<p style="margin: 0 0 10px 0; font-family: monospace; font-size: 0.9rem; color: #856404;">' .
'<strong>Abhängigkeitskette:</strong><br>' .
htmlspecialchars($chainDisplay, ENT_QUOTES, 'UTF-8') .
'</p>' .
'<p style="margin: 0; font-size: 0.85rem; color: #856404;">' .
'Die Klasse <code>' . htmlspecialchars($targetClass, ENT_QUOTES, 'UTF-8') . '</code> kann nicht instanziiert werden, ' .
'da sie Teil einer zyklischen Abhängigkeit ist.' .
'</p>' .
'</div>';
$safeData['dependencyInfo'] = RawHtml::from($dependencyHtml);
}
// Code-Highlighter für Debug-Template
$file = $context->exception->data['exception_file'] ?? null;
$line = $context->exception->data['exception_line'] ?? null;
if ($file && $line && is_numeric($line)) {
$errorLine = (int)$line;
$codeHtml = new FileHighlighter()($file, max(1, $errorLine - 10), 20, $errorLine);
$safeData['code'] = RawHtml::from($codeHtml);
} else {
$safeData['code'] = RawHtml::from('<div style="padding: 20px; background: #f8f9fa; border-radius: 4px; color: #6c757d;">Code-Vorschau nicht verfügbar</div>');
}
// Stack Trace für Debug-Template
// Prüfe zuerst, ob eine ursprüngliche Exception verfügbar ist
if (isset($context->exception->data['original_exception']) && $context->exception->data['original_exception'] instanceof \Throwable) {
// Verwende die ursprüngliche Exception für den Stack Trace
$originalException = $context->exception->data['original_exception'];
$stackTrace = new StackTrace($originalException);
$safeData['trace'] = $stackTrace->getItems();
} elseif (isset($context->exception->data['previous_exception']) && $context->exception->data['previous_exception'] instanceof \Throwable) {
// Alternative: Prüfe auf previous_exception
$stackTrace = new StackTrace($context->exception->data['previous_exception']);
$safeData['trace'] = $stackTrace->getItems();
} else {
// Fallback: Erstelle minimalen Trace aus verfügbaren Daten
$safeData['trace'] = [];
// Suche nach verfügbaren Trace-Informationen
$file = $context->exception->data['exception_file'] ?? null;
$line = $context->exception->data['exception_line'] ?? null;
if ($file && $line) {
$safeData['trace'] = [
new TraceItem(
file: $file,
line: (int)$line,
function: null,
class: null,
type: null,
args: [],
isExceptionOrigin: true
),
];
}
}
}
$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->createFallbackErrorPageFromHandlerContext($context, $isDebug, $e);
}
}
/**
* Rendert eine Fehlerseite basierend auf dem ErrorContext (Legacy)
*/
public function render(ErrorContext $context, bool $isDebug = false): string
{
@@ -31,8 +175,10 @@ final readonly class ErrorTemplateRenderer implements ErrorViewRendererInterface
try {
// Sichere Datenaufbereitung ohne Closures oder komplexe Objekte
$errorLine = $context->exception->getLine();
$codeHtml = new FileHighlighter()($context->exception->getFile(), $errorLine - 10, 10, $errorLine);
$safeData = [
'code' => new FileHighlighter()($context->exception->getFile(), $context->exception->getLine() -10, 10),
'code' => RawHtml::from($codeHtml),
'errorClass' => $context->exception::class,
'errorMessage' => $context->exception->getMessage(),
'error' => [
@@ -45,18 +191,19 @@ final readonly class ErrorTemplateRenderer implements ErrorViewRendererInterface
'requestId' => $context->requestId->toString(),
'timestamp' => $context->additionalData['timestamp'] ?? date('c'),
'level' => $context->level->name,
'memory' => $context->additionalData['memory_usage'] ?? 0
'memory' => $context->additionalData['memory_usage'] ?? 0,
'trace' => (new StackTrace($context->exception))->getItems(),
];
$renderContext = new RenderContext(
template: $template,
metaData: new MetaData('', '', new OpenGraphTypeWebsite),
metaData: new MetaData('', '', new OpenGraphTypeWebsite()),
data: $safeData
);
$renderedContent = $this->renderer->render($renderContext);
if($renderedContent === ""){
if ($renderedContent === "") {
throw new \Exception("Template Renderer returned empty string");
}
@@ -115,7 +262,7 @@ final readonly class ErrorTemplateRenderer implements ErrorViewRendererInterface
</body>
</html>',
htmlspecialchars($exception->getMessage()),
htmlspecialchars($context->requestId->toString()),
htmlspecialchars($context->requestId?->toString() ?? 'unknown'),
htmlspecialchars($context->additionalData['timestamp'] ?? date('c')),
htmlspecialchars($context->level->name),
htmlspecialchars($exception->getFile()),
@@ -147,7 +294,144 @@ final readonly class ErrorTemplateRenderer implements ErrorViewRendererInterface
</div>
</body>
</html>',
htmlspecialchars($context->requestId->toString())
htmlspecialchars($context->requestId?->toString() ?? 'unknown')
);
}
return $content;
}
/**
* Extrahiert Fehlermeldung aus ErrorHandlerContext
*/
private function extractErrorMessage(ErrorHandlerContext $context): string
{
// Aus Exception-Daten extrahieren
if (isset($context->exception->data['exception_message'])) {
return $context->exception->data['exception_message'];
}
// Aus Exception-Daten (message)
if (isset($context->exception->data['message'])) {
return $context->exception->data['message'];
}
// Aus Exception-Daten (user_message)
if (isset($context->exception->data['user_message'])) {
return $context->exception->data['user_message'];
}
// Fallback: Operation und Component verwenden
$operation = $context->exception->operation ?? 'unknown_operation';
$component = $context->exception->component ?? 'Application';
return "Error in {$component} during {$operation}";
}
/**
* Erstellt eine einfache Fehlerseite als Fallback für ErrorHandlerContext
*/
private function createFallbackErrorPageFromHandlerContext(ErrorHandlerContext $context, bool $isDebug, ?Throwable $renderException = null): string
{
$errorMessage = $this->extractErrorMessage($context);
$isSecurityEvent = $context->exception->metadata['security_event'] ?? false;
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; }
.security-warning { background: #fff3cd; border: 1px solid #ffeaa7; padding: 15px; border-radius: 4px; margin: 15px 0; }
.debug-section { margin-top: 20px; padding: 15px; background: #f8f9fa; border-radius: 4px; }
</style>
</head>
<body>
<div class="error-container">
<h1 class="error-title">%s: %s</h1>
%s
<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<br>
<strong>HTTP-Status:</strong> %d<br>
<strong>Component:</strong> %s<br>
<strong>Operation:</strong> %s
</div>
<div class="error-info">
<p><strong>Client-IP:</strong> %s</p>
<p><strong>Request-URI:</strong> %s</p>
<p><strong>User-Agent:</strong> %s</p>
</div>
<div class="debug-section">
<h3>Exception Data:</h3>
<pre>%s</pre>
</div>
%s
</div>
</body>
</html>',
htmlspecialchars($context->exception->component ?? 'Application'),
htmlspecialchars($errorMessage),
$isSecurityEvent ? '<div class="security-warning"><strong>⚠️ Security Event:</strong> This error indicates a potential security issue.</div>' : '',
htmlspecialchars($context->request->requestId?->toString() ?? 'unknown'),
htmlspecialchars(date('c')),
htmlspecialchars($context->metadata['error_level'] ?? 'ERROR'),
$context->metadata['http_status'] ?? 500,
htmlspecialchars($context->exception->component ?? 'Application'),
htmlspecialchars($context->exception->operation ?? 'unknown'),
htmlspecialchars($context->request->clientIp ?? 'unknown'),
htmlspecialchars($context->request->requestUri ?? '/'),
htmlspecialchars(substr((string)$context->request->userAgent ?? '', 0, 100)),
htmlspecialchars(json_encode($context->exception->data, JSON_PRETTY_PRINT)),
$renderException ? '<div class="render-error"><h3>Fehler beim Rendern der Fehlerseite:</h3><p>' .
htmlspecialchars($renderException->getMessage()) . '</p></div>' : ''
);
} else {
// Produktions-Fallback mit minimalen Informationen
$userMessage = $isSecurityEvent ? 'Access denied.' : 'Es ist ein Fehler aufgetreten.';
$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; }
.status-code { font-size: 4em; font-weight: bold; color: #dc3545; margin: 20px 0; }
</style>
</head>
<body>
<div class="error-container">
<div class="status-code">%d</div>
<h1 class="error-title">%s</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>',
$context->metadata['http_status'] ?? 500,
htmlspecialchars($userMessage),
htmlspecialchars($context->request->requestId?->toString() ?? 'unknown')
);
}

View File

@@ -1,9 +1,11 @@
<?php
declare(strict_types=1);
namespace App\Framework\ErrorHandling\View;
use App\Framework\ErrorHandling\ErrorContext;
use App\Framework\Exception\ErrorHandlerContext;
/**
* Interface für alle Renderer von Fehlerseiten
@@ -11,7 +13,12 @@ use App\Framework\ErrorHandling\ErrorContext;
interface ErrorViewRendererInterface
{
/**
* Rendert eine Fehlerseite basierend auf dem ErrorContext
* Rendert eine Fehlerseite basierend auf dem ErrorHandlerContext
*/
public function renderFromHandlerContext(ErrorHandlerContext $context, bool $isDebug = false): string;
/**
* Rendert eine Fehlerseite basierend auf dem ErrorContext (Legacy)
*/
public function render(ErrorContext $context, bool $isDebug = false): string;
}

View File

@@ -175,13 +175,13 @@
<span>Request ID: {{ requestId }}</span>
<span>Zeit: {{ timestamp }}</span>
<span>Level: {{ level }}</span>
<span>Speicherverbrauch: {{ format_filesize($memory) }}</span>
<span>Speicherverbrauch: {{ memory }}</span>
</div>
{{ dependencyInfo }}
</div>
</div>
<?php echo "Hallo" ?>
{{ code }}
<div class="trace-container">
@@ -191,17 +191,15 @@
<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-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>