feat: CI/CD pipeline setup complete - Ansible playbooks updated, secrets configured, workflow ready

This commit is contained in:
2025-10-31 01:39:24 +01:00
parent 55c04e4fd0
commit e26eb2aa12
601 changed files with 44184 additions and 32477 deletions

View File

@@ -8,6 +8,7 @@ use App\Framework\Config\Environment;
use App\Framework\Config\EnvKey;
use App\Framework\DI\Container;
use App\Framework\ErrorAggregation\ErrorAggregatorInterface;
use App\Framework\ErrorHandling\ValueObjects\ErrorMetadata;
use App\Framework\ErrorHandling\View\ApiErrorRenderer;
use App\Framework\ErrorHandling\View\ErrorResponseFactory;
use App\Framework\ErrorHandling\View\ErrorTemplateRenderer;
@@ -46,6 +47,7 @@ final readonly class ErrorHandler
private RequestIdGenerator $requestIdGenerator,
private ErrorAggregatorInterface $errorAggregator,
private ErrorReporterInterface $errorReporter,
private ErrorHandlerManager $handlerManager,
?Logger $logger = null,
?bool $isDebugMode = null,
?SecurityEventHandler $securityHandler = null
@@ -57,19 +59,60 @@ final readonly class ErrorHandler
$appConfig = $container->get(\App\Framework\Config\AppConfig::class);
$this->logger = new ErrorLogger($logger, $appConfig);
$this->securityHandler = $securityHandler ?? SecurityEventHandler::createDefault($logger, $appConfig);
$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): Response
{
// Try specialized handlers first
$handlingResult = $this->handlerManager->handle($e);
if ($handlingResult->handled) {
// Handler chain processed the exception
$firstResult = $handlingResult->getFirstResult();
if ($firstResult !== null) {
return $this->createResponseFromHandlerResult($firstResult, $e);
}
}
// Fallback to legacy error handling
return $this->createLegacyHttpResponse($e, $context);
}
private function createResponseFromHandlerResult(
Handlers\HandlerResult $result,
\Throwable $exception
): Response {
$statusCode = $result->statusCode ?? 500;
$responseFactory = $this->createResponseFactory();
$isApiRequest = $responseFactory->isApiRequest();
if ($isApiRequest) {
return new \App\Framework\Http\Responses\JsonResponse([
'error' => $result->message,
'data' => $result->data
], $statusCode);
}
// For HTML requests, still use legacy rendering
$errorHandlerContext = $this->createErrorHandlerContext($exception);
return $responseFactory->createResponseFromHandlerContext(
$errorHandlerContext,
$this->isDebugMode,
false
);
}
private function createLegacyHttpResponse(\Throwable $e, ?MiddlewareContext $context = null): Response
{
// Handle ValidationException with form-specific logic
if ($e instanceof ValidationException) {
@@ -122,7 +165,7 @@ final readonly class ErrorHandler
return new RedirectResponse($refererUrl);
}
public function handleException(Throwable $e): never
public function handleException(Throwable $e): void
{
$errorHandlerContext = $this->createErrorHandlerContext($e);
@@ -149,7 +192,7 @@ final readonly class ErrorHandler
// Sende die Fehlermeldung
$this->emitter->emit($response);
exit(1);
// Let PHP invoke shutdown functions naturally - do not call exit(1)
}
public function handleError(int $errno, string $errstr, string $errfile, int $errline): void
@@ -158,25 +201,6 @@ final readonly class ErrorHandler
$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 createErrorHandlerContext(Throwable $exception, ?MiddlewareContext $context = null): ErrorHandlerContext
{
@@ -259,55 +283,65 @@ final readonly class ErrorHandler
return RequestContext::fromGlobals();
}
/**
* @return array<string, mixed>
*/
private function createExceptionMetadata(Throwable $exception): array
private function createExceptionMetadata(Throwable $exception): ErrorMetadata
{
$metadata = [
'exception_class' => get_class($exception),
'error_level' => $this->determineErrorSeverity($exception)->name,
];
$exceptionClass = get_class($exception);
$errorLevel = $this->determineErrorSeverity($exception);
$httpStatus = $this->determineHttpStatus($exception);
// Enhanced: Add ErrorCode metadata if FrameworkException
// Enhanced metadata for FrameworkException with ErrorCode
if ($exception instanceof FrameworkException) {
$errorCode = $exception->getErrorCode();
if ($errorCode !== null) {
$metadata['error_code'] = $errorCode->getValue();
$metadata['error_category'] = $errorCode->getCategory();
$metadata['error_severity'] = $errorCode->getSeverity()->value;
$metadata['is_recoverable'] = $errorCode->isRecoverable();
$recoveryHint = $this->isDebugMode ? $errorCode->getRecoveryHint() : null;
// Add recovery hint for debug mode or API responses
if ($this->isDebugMode) {
$metadata['recovery_hint'] = $errorCode->getRecoveryHint();
}
$metadata = ErrorMetadata::enhanced(
exceptionClass: $exceptionClass,
errorLevel: $errorLevel,
httpStatus: $httpStatus,
errorCode: $errorCode->getValue(),
errorCategory: $errorCode->getCategory(),
errorSeverity: $errorCode->getSeverity()->value,
isRecoverable: $errorCode->isRecoverable(),
recoveryHint: $recoveryHint
);
// Add Retry-After header if applicable
$retryAfter = $errorCode->getRetryAfterSeconds();
if ($retryAfter !== null) {
$metadata['additional_headers'] = array_merge(
$metadata['additional_headers'] ?? [],
['Retry-After' => (string) $retryAfter]
);
$metadata = $metadata->withAdditionalHeaders([
'Retry-After' => (string) $retryAfter
]);
}
return $this->addExceptionSpecificHeaders($exception, $metadata);
}
}
// HTTP-Status-Code: ErrorCode-based first, then fallback to exception-type mapping
$metadata['http_status'] = $this->determineHttpStatus($exception);
// Basic metadata for non-FrameworkException or FrameworkException without ErrorCode
$metadata = ErrorMetadata::basic(
exceptionClass: $exceptionClass,
errorLevel: $errorLevel,
httpStatus: $httpStatus
);
// Zusätzliche Header für spezielle Exceptions
return $this->addExceptionSpecificHeaders($exception, $metadata);
}
private function addExceptionSpecificHeaders(
Throwable $exception,
ErrorMetadata $metadata
): ErrorMetadata {
// RateLimitExceededException headers
if ($exception instanceof \App\Framework\Exception\Http\RateLimitExceededException) {
$metadata['additional_headers'] = array_merge(
$metadata['additional_headers'] ?? [],
$metadata = $metadata->withAdditionalHeaders(
$exception->getRateLimitHeaders()
);
}
// InvalidContentTypeException headers
if ($exception instanceof \App\Framework\Exception\Http\InvalidContentTypeException) {
$metadata['additional_headers'] = array_merge(
$metadata['additional_headers'] ?? [],
$metadata = $metadata->withAdditionalHeaders(
$exception->getResponseHeaders()
);
}

View File

@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace App\Framework\ErrorHandling;
use App\Framework\Attributes\Initializer;
use App\Framework\DI\Container;
use App\Framework\ErrorHandling\Handlers\DatabaseErrorHandler;
use App\Framework\ErrorHandling\Handlers\FallbackErrorHandler;
use App\Framework\ErrorHandling\Handlers\HttpErrorHandler;
use App\Framework\ErrorHandling\Handlers\ValidationErrorHandler;
use App\Framework\Logging\Logger;
/**
* Initializes the error handler system
*
* Registers all error handlers in priority order
*/
final readonly class ErrorHandlerInitializer
{
#[Initializer]
public function initialize(Container $container): ErrorHandlerManager
{
$logger = $container->get(Logger::class);
$registry = new ErrorHandlerRegistry();
$manager = new ErrorHandlerManager($registry);
// Register all handlers at once (will be sorted by priority internally)
return $manager->register(
new ValidationErrorHandler(), // CRITICAL priority
new DatabaseErrorHandler($logger), // HIGH priority
new HttpErrorHandler(), // NORMAL priority
new FallbackErrorHandler($logger) // LOWEST priority
);
}
}

View File

@@ -0,0 +1,174 @@
<?php
declare(strict_types=1);
namespace App\Framework\ErrorHandling;
use App\Framework\ErrorHandling\Handlers\ErrorHandlerInterface;
use App\Framework\ErrorHandling\Handlers\ErrorHandlerPriority;
use App\Framework\ErrorHandling\Handlers\HandlerRegistration;
/**
* Manager for error handler chain
*
* Orchestrates multiple error handlers with priority-based execution
*
* Immutable: All modification methods return new instances
*/
final readonly class ErrorHandlerManager
{
/**
* @param HandlerRegistration[] $handlers
*/
public function __construct(
private ErrorHandlerRegistry $registry,
private array $handlers = []
) {}
/**
* Register one or more error handlers (immutable)
*
* @param ErrorHandlerInterface ...$handlers One or more handlers to register
* @return self New instance with handlers registered
*/
public function register(ErrorHandlerInterface ...$handlers): self
{
$newHandlers = $this->handlers;
$newRegistry = $this->registry;
foreach ($handlers as $handler) {
$priority = $handler->getPriority();
$registration = new HandlerRegistration($handler, $priority);
$newHandlers[] = $registration;
// Register in registry
$newRegistry = $newRegistry->register($handler->getName(), $handler);
}
// Sort by priority (highest first)
usort($newHandlers, fn(HandlerRegistration $a, HandlerRegistration $b) =>
$a->comparePriority($b)
);
return new self($newRegistry, $newHandlers);
}
/**
* Handle an exception through the handler chain
*/
public function handle(\Throwable $exception): ErrorHandlingResult
{
$results = [];
$handled = false;
foreach ($this->handlers as $registration) {
// Check if handler can handle this exception
if (!$registration->canHandle($exception)) {
continue;
}
try {
// Handle exception
$result = $registration->handle($exception);
$results[] = $result;
// Mark as handled if handler handled it
if ($result->handled) {
$handled = true;
}
// Stop if handler marked as final
if ($result->isFinal) {
break;
}
} catch (\Throwable $handlerException) {
// Handler itself failed - log but continue
error_log(sprintf(
'[ErrorHandlerManager] Handler %s failed: %s',
$registration->getHandlerName(),
$handlerException->getMessage()
));
}
}
return new ErrorHandlingResult(
handled: $handled,
results: $results,
exception: $exception
);
}
/**
* Get all registered handlers
*
* @return ErrorHandlerInterface[]
*/
public function getHandlers(): array
{
return array_map(
fn(HandlerRegistration $registration) => $registration->handler,
$this->handlers
);
}
/**
* Remove a handler (immutable)
*
* @return self New instance without the specified handler
*/
public function unregister(ErrorHandlerInterface $handler): self
{
$newHandlers = array_filter(
$this->handlers,
fn(HandlerRegistration $registration) => $registration->handler !== $handler
);
// Remove from registry (registry handles its own immutability)
$newRegistry = $this->registry->unregister($handler->getName());
return new self($newRegistry, array_values($newHandlers));
}
/**
* Clear all handlers (immutable)
*
* @return self New instance with no handlers
*/
public function clear(): self
{
$newRegistry = $this->registry->clear();
return new self($newRegistry, []);
}
/**
* Get handler statistics
*/
public function getStatistics(): array
{
$stats = [
'total_handlers' => count($this->handlers),
'handlers_by_priority' => [],
'handlers' => []
];
foreach ($this->handlers as $registration) {
$priorityValue = $registration->priority->value;
// Count by priority
if (!isset($stats['handlers_by_priority'][$priorityValue])) {
$stats['handlers_by_priority'][$priorityValue] = 0;
}
$stats['handlers_by_priority'][$priorityValue]++;
// Handler details
$stats['handlers'][] = [
'name' => $registration->getHandlerName(),
'class' => get_class($registration->handler),
'priority' => $priorityValue
];
}
return $stats;
}
}

View File

@@ -0,0 +1,86 @@
<?php
declare(strict_types=1);
namespace App\Framework\ErrorHandling;
use App\Framework\ErrorHandling\Handlers\ErrorHandlerInterface;
/**
* Registry for named error handlers
*
* Allows handlers to be registered and retrieved by name
*
* Immutable: All modification methods return new instances
*/
final readonly class ErrorHandlerRegistry
{
/**
* @param array<string, ErrorHandlerInterface> $handlers
*/
public function __construct(
private array $handlers = []
) {}
/**
* Register a handler with a name (immutable)
*
* @return self New instance with handler registered
*/
public function register(string $name, ErrorHandlerInterface $handler): self
{
$newHandlers = [...$this->handlers];
$newHandlers[$name] = $handler;
return new self($newHandlers);
}
/**
* Get a handler by name
*/
public function get(string $name): ?ErrorHandlerInterface
{
return $this->handlers[$name] ?? null;
}
/**
* Check if a handler is registered
*/
public function has(string $name): bool
{
return isset($this->handlers[$name]);
}
/**
* Get all registered handlers
*
* @return array<string, ErrorHandlerInterface>
*/
public function all(): array
{
return $this->handlers;
}
/**
* Remove a handler by name (immutable)
*
* @return self New instance without the specified handler
*/
public function unregister(string $name): self
{
$newHandlers = $this->handlers;
unset($newHandlers[$name]);
return new self($newHandlers);
}
/**
* Clear all handlers (immutable)
*
* @return self New instance with no handlers
*/
public function clear(): self
{
return new self([]);
}
}

View File

@@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace App\Framework\ErrorHandling;
use App\Framework\ErrorHandling\Handlers\HandlerResult;
/**
* Result of processing an exception through the error handler chain
*/
final readonly class ErrorHandlingResult
{
/**
* @param bool $handled Whether the exception was handled by any handler
* @param HandlerResult[] $results Results from all handlers that processed the exception
* @param \Throwable $exception The original exception
*/
public function __construct(
public bool $handled,
public array $results,
public \Throwable $exception
) {}
/**
* Get the first handler result (usually the most important)
*/
public function getFirstResult(): ?HandlerResult
{
return $this->results[0] ?? null;
}
/**
* Get all handler messages
*
* @return string[]
*/
public function getMessages(): array
{
return array_map(
fn(HandlerResult $result) => $result->message,
$this->results
);
}
/**
* Get combined data from all handlers
*/
public function getCombinedData(): array
{
return array_merge(
...array_map(
fn(HandlerResult $result) => $result->data,
$this->results
)
);
}
}

View File

@@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace App\Framework\ErrorHandling\Handlers;
use App\Framework\Database\Exception\DatabaseException;
use App\Framework\Logging\Logger;
use App\Framework\Logging\ValueObjects\LogContext;
/**
* Handler for database-related errors
*
* Priority: HIGH - Database errors need immediate attention
*/
final readonly class DatabaseErrorHandler implements ErrorHandlerInterface
{
public function __construct(
private Logger $logger
) {}
public function canHandle(\Throwable $exception): bool
{
return $exception instanceof DatabaseException
|| $exception instanceof \PDOException;
}
public function handle(\Throwable $exception): HandlerResult
{
// Log database error with context
$this->logger->error('Database error occurred', LogContext::withData([
'exception_class' => get_class($exception),
'message' => $exception->getMessage(),
'code' => $exception->getCode()
]));
return HandlerResult::create(
handled: true,
message: 'A database error occurred',
data: [
'error_type' => 'database',
'retry_after' => 60 // Suggest retry after 60 seconds
],
statusCode: 500
);
}
public function getName(): string
{
return 'database_error_handler';
}
public function getPriority(): ErrorHandlerPriority
{
return ErrorHandlerPriority::HIGH;
}
}

View File

@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace App\Framework\ErrorHandling\Handlers;
/**
* Interface for specialized error handlers in the error handling chain
*/
interface ErrorHandlerInterface
{
/**
* Check if this handler can handle the given exception
*/
public function canHandle(\Throwable $exception): bool;
/**
* Handle the exception and return a result
*/
public function handle(\Throwable $exception): HandlerResult;
/**
* Get handler name for logging/debugging
*/
public function getName(): string;
/**
* Get handler priority
*/
public function getPriority(): ErrorHandlerPriority;
}

View File

@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace App\Framework\ErrorHandling\Handlers;
/**
* Priority levels for error handlers
*
* Handlers with higher priority execute first in the chain
*/
enum ErrorHandlerPriority: int
{
/**
* Critical handlers (Security, Authentication)
* Execute first to prevent security incidents
*/
case CRITICAL = 1000;
/**
* High priority handlers (Database, Validation)
* Execute early for data integrity
*/
case HIGH = 750;
/**
* Normal priority handlers (HTTP, Business Logic)
* Standard application errors
*/
case NORMAL = 500;
/**
* Low priority handlers (Logging, Monitoring)
* Non-critical error handling
*/
case LOW = 250;
/**
* Lowest priority handlers (Fallback)
* Catch-all handlers that execute last
*/
case LOWEST = 100;
}

View File

@@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace App\Framework\ErrorHandling\Handlers;
use App\Framework\Logging\Logger;
use App\Framework\Logging\ValueObjects\LogContext;
/**
* Fallback handler for all unhandled exceptions
*
* Priority: LOWEST - Catches everything that other handlers missed
*/
final readonly class FallbackErrorHandler implements ErrorHandlerInterface
{
public function __construct(
private Logger $logger
) {}
public function canHandle(\Throwable $exception): bool
{
return true; // Handles everything
}
public function handle(\Throwable $exception): HandlerResult
{
// Log unhandled exception with full context
$this->logger->error('Unhandled exception', LogContext::withData([
'exception_class' => get_class($exception),
'message' => $exception->getMessage(),
'file' => $exception->getFile(),
'line' => $exception->getLine(),
'trace' => $exception->getTraceAsString()
]));
return HandlerResult::create(
handled: true,
message: 'An unexpected error occurred',
data: [
'error_type' => 'unhandled',
'exception_class' => get_class($exception)
],
isFinal: true, // Stop chain - this is the last resort
statusCode: 500
);
}
public function getName(): string
{
return 'fallback_error_handler';
}
public function getPriority(): ErrorHandlerPriority
{
return ErrorHandlerPriority::LOWEST;
}
}

View File

@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace App\Framework\ErrorHandling\Handlers;
/**
* Value Object representing a registered error handler with its priority.
*
* Replaces primitive array{handler: ErrorHandlerInterface, priority: int} pattern
* with type-safe, immutable Value Object following framework principles.
*/
final readonly class HandlerRegistration
{
public function __construct(
public ErrorHandlerInterface $handler,
public ErrorHandlerPriority $priority
) {}
/**
* Check if this handler can handle the given exception.
*/
public function canHandle(\Throwable $exception): bool
{
return $this->handler->canHandle($exception);
}
/**
* Execute the handler with the given exception.
*/
public function handle(\Throwable $exception): HandlerResult
{
return $this->handler->handle($exception);
}
/**
* Get handler name for debugging/logging.
*/
public function getHandlerName(): string
{
return $this->handler->getName();
}
/**
* Compare priority with another registration for sorting.
*/
public function comparePriority(self $other): int
{
// Higher priority value = execute first (descending sort)
return $other->priority->value <=> $this->priority->value;
}
}

View File

@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace App\Framework\ErrorHandling\Handlers;
/**
* Result returned by error handlers after processing an exception
*/
final readonly class HandlerResult
{
public function __construct(
public bool $handled,
public string $message,
public array $data = [],
public bool $isFinal = false,
public ?int $statusCode = null
) {}
/**
* Factory method for creating handler results
*/
public static function create(
bool $handled,
string $message,
array $data = [],
bool $isFinal = false,
?int $statusCode = null
): self {
return new self($handled, $message, $data, $isFinal, $statusCode);
}
}

View File

@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace App\Framework\ErrorHandling\Handlers;
use App\Framework\Http\Exception\HttpException;
/**
* Handler for HTTP-specific errors
*
* Priority: NORMAL - Standard HTTP error handling
*/
final readonly class HttpErrorHandler implements ErrorHandlerInterface
{
public function canHandle(\Throwable $exception): bool
{
return $exception instanceof HttpException;
}
public function handle(\Throwable $exception): HandlerResult
{
assert($exception instanceof HttpException);
return HandlerResult::create(
handled: true,
message: $exception->getMessage(),
data: [
'error_type' => 'http',
'headers' => $exception->headers ?? []
],
statusCode: $exception->statusCode
);
}
public function getName(): string
{
return 'http_error_handler';
}
public function getPriority(): ErrorHandlerPriority
{
return ErrorHandlerPriority::NORMAL;
}
}

View File

@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace App\Framework\ErrorHandling\Handlers;
use App\Framework\Exception\ValidationException;
/**
* Handler for validation errors
*
* Priority: CRITICAL - Validates before any other processing
*/
final readonly class ValidationErrorHandler implements ErrorHandlerInterface
{
public function canHandle(\Throwable $exception): bool
{
return $exception instanceof ValidationException;
}
public function handle(\Throwable $exception): HandlerResult
{
assert($exception instanceof ValidationException);
return HandlerResult::create(
handled: true,
message: 'Validation failed',
data: [
'errors' => $exception->getErrors(),
'validation_context' => $exception->context->data
],
isFinal: true, // Stop handler chain for validation errors
statusCode: 422
);
}
public function getName(): string
{
return 'validation_error_handler';
}
public function getPriority(): ErrorHandlerPriority
{
return ErrorHandlerPriority::CRITICAL;
}
}

View File

@@ -8,6 +8,9 @@ use App\Framework\Exception\ErrorHandlerContext;
use App\Framework\Exception\SecurityException;
use App\Framework\Http\MiddlewareContext;
use App\Framework\Logging\Logger;
use App\Framework\Logging\Processors\SecurityEventProcessor;
use App\Framework\Logging\ValueObjects\LogContext;
use App\Framework\Logging\ValueObjects\SecurityContext;
use Throwable;
/**
@@ -16,7 +19,8 @@ use Throwable;
final readonly class SecurityEventHandler
{
public function __construct(
private ?SecurityEventLogger $securityLogger,
private Logger $logger,
private SecurityEventProcessor $processor,
private ?SecurityAlertManager $alertManager = null
) {
}
@@ -28,21 +32,78 @@ final readonly class SecurityEventHandler
SecurityException $exception,
?MiddlewareContext $context = null
): void {
// Skip if no logger available
if ($this->securityLogger === null) {
return;
}
try {
// Erstelle ErrorHandlerContext für OWASP-Format
$errorHandlerContext = $this->createErrorHandlerContext($exception, $context);
// Extract SecurityEvent from Exception
$securityEvent = $exception->getSecurityEvent();
// Führe Security-Logging durch
$this->securityLogger->logSecurityEvent($exception, $errorHandlerContext);
// Create SecurityContext based on event category
$securityContext = match ($securityEvent->getCategory()) {
'authentication' => SecurityContext::forAuthentication(
eventId: $securityEvent->getEventIdentifier(),
description: $securityEvent->getDescription(),
level: $securityEvent->getLogLevel(),
requiresAlert: $exception->requiresAlert(),
eventData: $securityEvent->toArray()
),
'authorization' => SecurityContext::forAuthorization(
eventId: $securityEvent->getEventIdentifier(),
description: $securityEvent->getDescription(),
level: $securityEvent->getLogLevel(),
requiresAlert: $exception->requiresAlert(),
eventData: $securityEvent->toArray()
),
'input_validation' => SecurityContext::forInputValidation(
eventId: $securityEvent->getEventIdentifier(),
description: $securityEvent->getDescription(),
level: $securityEvent->getLogLevel(),
requiresAlert: $exception->requiresAlert(),
eventData: $securityEvent->toArray()
),
'session' => SecurityContext::forSession(
eventId: $securityEvent->getEventIdentifier(),
description: $securityEvent->getDescription(),
level: $securityEvent->getLogLevel(),
requiresAlert: $exception->requiresAlert(),
eventData: $securityEvent->toArray()
),
'intrusion_detection' => SecurityContext::forIntrusion(
eventId: $securityEvent->getEventIdentifier(),
description: $securityEvent->getDescription(),
level: $securityEvent->getLogLevel(),
requiresAlert: $exception->requiresAlert(),
eventData: $securityEvent->toArray()
),
default => new SecurityContext(
eventId: $securityEvent->getEventIdentifier(),
level: $securityEvent->getLogLevel(),
description: $securityEvent->getDescription(),
category: $securityEvent->getCategory(),
requiresAlert: $exception->requiresAlert(),
eventData: $securityEvent->toArray()
),
};
// Add request info from MiddlewareContext if available
if ($context !== null) {
$securityContext = $securityContext->withRequestInfo(
sourceIp: (string) $context->request->server->getClientIp(),
userAgent: $context->request->server->getUserAgent()
);
}
// Map SecurityLogLevel to Framework LogLevel
$logLevel = $this->processor->mapSecurityLevelToLogLevel($securityEvent->getLogLevel());
// Log directly via Logger with SecurityContext
$this->logger->log(
$logLevel,
$securityEvent->getDescription(),
LogContext::empty()->withSecurityContext($securityContext)
);
// Sende Alert falls erforderlich
if ($exception->requiresAlert()) {
$this->handleSecurityAlert($exception, $errorHandlerContext);
$this->handleSecurityAlert($exception, $securityContext);
}
} catch (Throwable $loggingError) {
@@ -72,13 +133,17 @@ final readonly class SecurityEventHandler
*/
private function handleSecurityAlert(
SecurityException $exception,
ErrorHandlerContext $context
SecurityContext $securityContext
): void {
if ($this->alertManager) {
$this->alertManager->sendAlert($exception, $context);
} elseif ($this->securityLogger !== null) {
// Fallback: Logge als kritisches Event
$this->securityLogger->logCriticalAlert($exception, $context);
$this->alertManager->sendAlert($exception, $securityContext);
} else {
// Fallback: Logge als kritisches Event direkt mit Logger
$this->logger->log(
\App\Framework\Logging\LogLevel::CRITICAL,
'Security Alert: ' . $exception->getSecurityEvent()->getDescription(),
LogContext::empty()->withSecurityContext($securityContext)
);
}
}
@@ -117,41 +182,19 @@ 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
*/
public static function createDefault(?Logger $logger = null, ?\App\Framework\Config\AppConfig $appConfig = null): self
public static function createDefault(?Logger $logger = null): self
{
// If AppConfig not provided, we can't create SecurityEventLogger properly
// This is a temporary solution - ideally AppConfig should always be provided
if ($logger === null || $appConfig === null) {
// Return a minimal handler without SecurityEventLogger
return new self(null, null);
// If Logger not provided, cannot create handler
if ($logger === null) {
throw new \InvalidArgumentException('Logger is required for SecurityEventHandler');
}
$securityLogger = new SecurityEventLogger($logger, $appConfig);
$processor = new SecurityEventProcessor();
$alertManager = null; // Kann später konfiguriert werden
return new self($securityLogger, $alertManager);
return new self($logger, $processor, $alertManager);
}
}

View File

@@ -13,27 +13,33 @@ use Traversable;
/**
* Stacktrace-Klasse zum Aufbereiten und Darstellen von Exception-Traces
* Implementiert ArrayAccess, IteratorAggregate und Countable für einfachen Zugriff in Templates
*
* Immutable: All properties are readonly, items array is built during construction
*/
final class StackTrace implements ArrayAccess, IteratorAggregate, Countable
final readonly class StackTrace implements ArrayAccess, IteratorAggregate, Countable
{
/** @var TraceItem[] */
public private(set) array $items = [];
public array $items;
/**
* Erstellt ein neues StackTrace-Objekt aus einer Exception
*/
public function __construct(private readonly \Throwable $exception)
public function __construct(private \Throwable $exception)
{
$this->processTrace($exception->getTrace());
$this->items = $this->processTrace($exception->getTrace());
}
/**
* Verarbeitet die rohen Trace-Daten in strukturierte TraceItems
*
* @return TraceItem[]
*/
private function processTrace(array $trace): void
private function processTrace(array $trace): array
{
$items = [];
// Füge den Ursprung der Exception hinzu (file/line aus Exception selbst)
$this->items[] = new TraceItem(
$items[] = new TraceItem(
file: $this->exception->getFile(),
line: $this->exception->getLine(),
function: null,
@@ -45,7 +51,7 @@ final class StackTrace implements ArrayAccess, IteratorAggregate, Countable
// Verarbeite den Rest des Stacktraces
foreach ($trace as $index => $frame) {
$this->items[] = new TraceItem(
$items[] = new TraceItem(
file: $frame['file'] ?? '(internal function)',
line: $frame['line'] ?? 0,
function: $frame['function'] ?? null,
@@ -56,7 +62,7 @@ final class StackTrace implements ArrayAccess, IteratorAggregate, Countable
);
}
$this->items = array_reverse($this->items);
return array_reverse($items);
}
/**

View File

@@ -0,0 +1,571 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ $errorClass }}: {{ $errorMessage }}</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background: #1a202c;
color: #e2e8f0;
line-height: 1.6;
padding: 20px;
}
.debug-container {
max-width: 1400px;
margin: 0 auto;
}
.error-header {
background: linear-gradient(135deg, #dc2626 0%, #991b1b 100%);
border-radius: 12px;
padding: 32px;
margin-bottom: 24px;
box-shadow: 0 4px 20px rgba(220, 38, 38, 0.3);
position: relative;
overflow: hidden;
}
.error-header::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.1) 0%, transparent 100%);
pointer-events: none;
}
.error-class {
font-size: 28px;
font-weight: 700;
color: white;
margin-bottom: 12px;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
position: relative;
}
.error-message {
font-size: 18px;
color: #fef3c7;
margin-bottom: 16px;
font-weight: 500;
position: relative;
}
.error-location {
font-family: 'Fira Code', 'Courier New', Courier, monospace;
font-size: 14px;
color: #fecaca;
background: rgba(0, 0, 0, 0.3);
padding: 12px 16px;
border-radius: 8px;
border-left: 4px solid #fbbf24;
position: relative;
}
.error-location strong {
color: #fbbf24;
font-weight: 600;
}
.section {
background: #2d3748;
border-radius: 12px;
padding: 24px;
margin-bottom: 20px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
}
.section-title {
font-size: 18px;
font-weight: 600;
color: #f7fafc;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 2px solid #4a5568;
display: flex;
justify-content: space-between;
align-items: center;
}
.section-title.collapsible {
cursor: pointer;
user-select: none;
}
.section-title.collapsible:hover {
color: #667eea;
}
.collapse-icon {
font-size: 20px;
transition: transform 0.3s;
color: #a0aec0;
}
.section-title.collapsed .collapse-icon {
transform: rotate(-90deg);
}
.section-content {
max-height: 2000px;
overflow: hidden;
transition: max-height 0.3s ease-out, opacity 0.3s ease-out;
opacity: 1;
}
.section-content.collapsed {
max-height: 0;
opacity: 0;
}
.meta-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 16px;
}
.meta-item {
background: #1a202c;
padding: 12px;
border-radius: 6px;
border-left: 3px solid #667eea;
}
.meta-label {
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.5px;
color: #a0aec0;
margin-bottom: 4px;
}
.meta-value {
font-family: 'Courier New', Courier, monospace;
color: #e2e8f0;
font-size: 14px;
word-break: break-all;
}
.code-preview {
position: relative;
margin-bottom: 20px;
}
/* Stelle sicher, dass Highlighter-Output nicht überschrieben wird */
.code-preview > * {
margin: 0;
}
/* Fallback für Code-Darstellung wenn Highlighter fehlt */
.code-preview pre {
background: #2b2b2b;
color: #a9b7c6;
padding: 16px;
border-radius: 8px;
overflow-x: auto;
font-family: 'Fira Code', 'Consolas', monospace;
font-size: 14px;
line-height: 1.5;
white-space: pre;
tab-size: 4;
}
.code-preview code {
font-family: 'Fira Code', 'Consolas', monospace;
white-space: pre;
}
/* Copy Button für Code */
.copy-button {
position: absolute;
top: 12px;
right: 12px;
background: #4a5568;
color: white;
border: none;
padding: 8px 16px;
border-radius: 6px;
font-size: 12px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
z-index: 10;
}
.copy-button:hover {
background: #667eea;
transform: translateY(-2px);
}
.copy-button:active {
transform: translateY(0);
}
.copy-button.copied {
background: #48bb78;
}
.copy-button.copied::after {
content: ' ✓';
}
.trace-item {
background: #1a202c;
padding: 16px;
margin-bottom: 8px;
border-radius: 6px;
border-left: 3px solid #4a5568;
transition: all 0.2s;
}
.trace-item:hover {
background: #283141;
border-left-color: #667eea;
}
.trace-item.origin {
border-left-color: #dc2626;
border-left-width: 4px;
background: linear-gradient(90deg, rgba(220, 38, 38, 0.15) 0%, #2d1f1f 100%);
box-shadow: 0 0 20px rgba(220, 38, 38, 0.2);
}
.trace-item.origin::before {
content: '⚠️';
position: absolute;
left: -12px;
top: 50%;
transform: translateY(-50%);
font-size: 20px;
}
.trace-index {
display: inline-block;
background: #4a5568;
color: white;
padding: 4px 10px;
border-radius: 4px;
font-size: 12px;
font-weight: 600;
margin-right: 8px;
}
.trace-item.origin .trace-index {
background: linear-gradient(135deg, #dc2626 0%, #991b1b 100%);
box-shadow: 0 2px 8px rgba(220, 38, 38, 0.3);
}
.trace-function {
font-family: 'Courier New', Courier, monospace;
color: #9ae6b4;
font-size: 14px;
margin-bottom: 4px;
}
.trace-location {
font-family: 'Courier New', Courier, monospace;
color: #90cdf4;
font-size: 13px;
}
.trace-filter {
margin-bottom: 16px;
display: flex;
gap: 12px;
align-items: center;
}
.trace-filter input {
flex: 1;
background: #1a202c;
border: 2px solid #4a5568;
color: #e2e8f0;
padding: 10px 16px;
border-radius: 8px;
font-size: 14px;
font-family: 'Courier New', Courier, monospace;
transition: border-color 0.2s;
}
.trace-filter input:focus {
outline: none;
border-color: #667eea;
}
.trace-filter-label {
color: #a0aec0;
font-size: 14px;
font-weight: 500;
}
.trace-stats {
color: #a0aec0;
font-size: 13px;
padding: 8px 12px;
background: #1a202c;
border-radius: 6px;
}
.dependency-info {
background: #78350f;
border: 2px solid #fbbf24;
border-radius: 8px;
padding: 16px;
margin-bottom: 20px;
}
.dependency-info h3 {
color: #fbbf24;
margin-bottom: 8px;
}
@media (max-width: 768px) {
.meta-grid {
grid-template-columns: 1fr;
}
.error-header {
padding: 20px;
}
.section {
padding: 16px;
}
}
</style>
</head>
<body>
<div class="debug-container">
<!-- Error Header -->
<div class="error-header">
<div class="error-class">⚠️ {{ $errorClass }}</div>
<div class="error-message">{{ $errorMessage }}</div>
<div class="error-location">
<strong>📁 File:</strong> {{ $errorFile }}<br>
<strong>📍 Line:</strong> {{ $errorLine }}
</div>
</div>
<!-- Dependency Info (if available) -->
<div if="{{ $dependencyInfo }}">
{{ $dependencyInfo }}
</div>
<!-- Request Information -->
<div class="section">
<h2 class="section-title collapsible collapsed" onclick="toggleSection('request-info')">
<span>📋 Request Information</span>
<span class="collapse-icon"></span>
</h2>
<div class="section-content collapsed" id="request-info-content">
<div class="meta-grid">
<div class="meta-item">
<div class="meta-label">Request ID</div>
<div class="meta-value">{{ $requestId }}</div>
</div>
<div class="meta-item">
<div class="meta-label">HTTP Status</div>
<div class="meta-value">{{ $httpStatus }}</div>
</div>
<div class="meta-item">
<div class="meta-label">Method</div>
<div class="meta-value">{{ $requestMethod }}</div>
</div>
<div class="meta-item">
<div class="meta-label">URI</div>
<div class="meta-value">{{ $requestUri }}</div>
</div>
<div class="meta-item">
<div class="meta-label">Client IP</div>
<div class="meta-value">{{ $clientIp }}</div>
</div>
<div class="meta-item">
<div class="meta-label">Timestamp</div>
<div class="meta-value">{{ $timestamp }}</div>
</div>
</div>
</div>
</div>
<!-- Code Preview -->
<div class="section">
<h2 class="section-title">💻 Code Context</h2>
<div class="code-preview" id="code-container">
<button class="copy-button" onclick="copyCode()" id="copy-code-btn">Copy Code</button>
{{ $code }}
</div>
</div>
<!-- Stack Trace -->
<div class="section" if="{{ $traceCount > 0 }}">
<h2 class="section-title collapsible" onclick="toggleSection('stack-trace')">
<span>📚 Stack Trace (<span id="trace-count">{{ $traceCount }}</span> frames)</span>
<span class="collapse-icon"></span>
</h2>
<div class="section-content" id="stack-trace-content">
<div class="trace-filter">
<span class="trace-filter-label">🔍 Filter:</span>
<input
type="text"
id="trace-filter"
placeholder="Search by class, method, or file path..."
oninput="filterStackTrace()"
>
<button class="copy-button" onclick="copyStackTrace()" id="copy-trace-btn" style="position: static; margin: 0;">
Copy Trace
</button>
</div>
<div foreach="$trace as $item">
<div class="trace-item" if="{{ $item->isOrigin }}">
<span class="trace-index">#{{ $item->index }}</span>
<div class="trace-function">🎯 {{ $item->function }}</div>
<div class="trace-location">{{ $item->file }}:{{ $item->line }}</div>
</div>
<div class="trace-item" if="!{{ $item->isOrigin }}">
<span class="trace-index">#{{ $item->index }}</span>
<div class="trace-function">{{ $item->function }}</div>
<div class="trace-location">{{ $item->file }}:{{ $item->line }}</div>
</div>
</div>
</div>
</div>
<!-- System Information -->
<div class="section">
<h2 class="section-title collapsible collapsed" onclick="toggleSection('system-info')">
<span>⚙️ System Information</span>
<span class="collapse-icon"></span>
</h2>
<div class="section-content collapsed" id="system-info-content">
<div class="meta-grid">
<div class="meta-item">
<div class="meta-label">Environment</div>
<div class="meta-value">{{ $environment }}</div>
</div>
<div class="meta-item">
<div class="meta-label">Debug Mode</div>
<div class="meta-value">{{ $debugMode }}</div>
</div>
<div class="meta-item">
<div class="meta-label">PHP Version</div>
<div class="meta-value">{{ $phpVersion }}</div>
</div>
<div class="meta-item">
<div class="meta-label">Memory Usage</div>
<div class="meta-value">{{ $memory }}</div>
</div>
<div class="meta-item">
<div class="meta-label">Execution Time</div>
<div class="meta-value">{{ $executionTime }}</div>
</div>
<div class="meta-item">
<div class="meta-label">Memory Limit</div>
<div class="meta-value">{{ $memoryLimit }}</div>
</div>
</div>
</div>
</div>
</div>
<script>
// Toggle collapsible sections
function toggleSection(sectionId) {
const content = document.getElementById(sectionId + '-content');
const title = content.previousElementSibling;
content.classList.toggle('collapsed');
title.classList.toggle('collapsed');
}
// Copy code to clipboard
function copyCode() {
const codeContainer = document.getElementById('code-container');
const codeElement = codeContainer.querySelector('pre') || codeContainer.querySelector('code');
if (!codeElement) {
return;
}
const text = codeElement.textContent || codeElement.innerText;
navigator.clipboard.writeText(text).then(() => {
const button = document.getElementById('copy-code-btn');
button.classList.add('copied');
button.textContent = 'Copied!';
setTimeout(() => {
button.classList.remove('copied');
button.textContent = 'Copy Code';
}, 2000);
}).catch(err => {
console.error('Failed to copy:', err);
});
}
// Copy stack trace
function copyStackTrace() {
const traceContainer = document.getElementById('stack-trace-content');
const traceItems = traceContainer.querySelectorAll('.trace-item');
let text = 'Stack Trace:\n\n';
traceItems.forEach(item => {
const func = item.querySelector('.trace-function').textContent;
const location = item.querySelector('.trace-location').textContent;
text += `${func}\n at ${location}\n\n`;
});
navigator.clipboard.writeText(text).then(() => {
const button = document.getElementById('copy-trace-btn');
button.classList.add('copied');
button.textContent = 'Copied!';
setTimeout(() => {
button.classList.remove('copied');
button.textContent = 'Copy Trace';
}, 2000);
}).catch(err => {
console.error('Failed to copy:', err);
});
}
// Filter stack trace
function filterStackTrace() {
const input = document.getElementById('trace-filter');
const filter = input.value.toLowerCase();
const items = document.querySelectorAll('.trace-item');
let visibleCount = 0;
items.forEach(item => {
const text = item.textContent.toLowerCase();
if (text.includes(filter)) {
item.style.display = '';
visibleCount++;
} else {
item.style.display = 'none';
}
});
// Update trace count
const traceCount = document.getElementById('trace-count');
if (traceCount) {
if (filter) {
traceCount.textContent = `${visibleCount}/${items.length}`;
} else {
traceCount.textContent = items.length;
}
}
}
</script>
</body>
</html>

View File

@@ -0,0 +1,141 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Error {{ $httpStatus }}</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background: #1a202c;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
color: #e2e8f0;
}
.error-container {
background: #2d3748;
border-radius: 16px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
max-width: 600px;
width: 100%;
padding: 48px 32px;
text-align: center;
}
.status-code {
font-size: 96px;
font-weight: 700;
color: #667eea;
margin-bottom: 24px;
line-height: 1;
}
.error-title {
font-size: 28px;
margin-bottom: 16px;
color: #f7fafc;
font-weight: 600;
}
.error-message {
color: #cbd5e0;
line-height: 1.6;
margin-bottom: 32px;
font-size: 16px;
}
.error-meta {
background: #1a202c;
padding: 20px;
border-radius: 12px;
border: 1px solid #4a5568;
}
.meta-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
border-bottom: 1px solid #4a5568;
}
.meta-item:last-child {
border-bottom: none;
}
.meta-label {
font-weight: 600;
color: #a0aec0;
font-size: 13px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.meta-value {
font-family: 'Courier New', Courier, monospace;
color: #e2e8f0;
font-size: 13px;
word-break: break-all;
max-width: 60%;
text-align: right;
}
@media (max-width: 600px) {
.error-container {
padding: 32px 24px;
}
.status-code {
font-size: 72px;
}
.error-title {
font-size: 22px;
}
.meta-item {
flex-direction: column;
align-items: flex-start;
gap: 4px;
}
.meta-value {
max-width: 100%;
text-align: left;
}
}
</style>
</head>
<body>
<div class="error-container">
<div class="status-code">{{ $httpStatus }}</div>
<h1 class="error-title">{{ $error->message }}</h1>
<p class="error-message">
Bitte versuchen Sie es später erneut oder kontaktieren Sie den Support, falls das Problem bestehen bleibt.
</p>
<div class="error-meta">
<div class="meta-item">
<span class="meta-label">Request ID</span>
<span class="meta-value">{{ $requestId }}</span>
</div>
<div class="meta-item">
<span class="meta-label">Zeitpunkt</span>
<span class="meta-value">{{ $timestamp }}</span>
</div>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,118 @@
<layout src="main" />
<div class="error-container">
<div class="status-code">{{ $httpStatus }}</div>
<h1 class="error-title">{{ $error->message }}</h1>
<p class="error-message">
Bitte versuchen Sie es später erneut oder kontaktieren Sie den Support, falls das Problem bestehen bleibt.
</p>
<div class="error-meta">
<div class="meta-item">
<span class="meta-label">Request ID</span>
<span class="meta-value">{{ $requestId }}</span>
</div>
<div class="meta-item">
<span class="meta-label">Zeitpunkt</span>
<span class="meta-value">{{ $timestamp }}</span>
</div>
</div>
</div>
<style>
.error-container {
max-width: 600px;
margin: 80px auto;
padding: 48px 32px;
text-align: center;
background: #2d3748;
border-radius: 16px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
}
.status-code {
font-size: 96px;
font-weight: 700;
color: #667eea;
margin-bottom: 24px;
line-height: 1;
}
.error-title {
font-size: 28px;
margin-bottom: 16px;
color: #f7fafc;
font-weight: 600;
}
.error-message {
color: #cbd5e0;
line-height: 1.6;
margin-bottom: 32px;
font-size: 16px;
}
.error-meta {
background: #1a202c;
padding: 20px;
border-radius: 12px;
border: 1px solid #4a5568;
}
.meta-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
border-bottom: 1px solid #4a5568;
}
.meta-item:last-child {
border-bottom: none;
}
.meta-label {
font-weight: 600;
color: #a0aec0;
font-size: 13px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.meta-value {
font-family: 'Courier New', Courier, monospace;
color: #e2e8f0;
font-size: 13px;
word-break: break-all;
max-width: 60%;
text-align: right;
}
@media (max-width: 600px) {
.error-container {
margin: 40px 20px;
padding: 32px 24px;
}
.status-code {
font-size: 72px;
}
.error-title {
font-size: 22px;
}
.meta-item {
flex-direction: column;
align-items: flex-start;
gap: 4px;
}
.meta-value {
max-width: 100%;
text-align: left;
}
}
</style>

View File

@@ -0,0 +1,177 @@
<?php
declare(strict_types=1);
namespace App\Framework\ErrorHandling\ValueObjects;
use App\Framework\ErrorHandling\ErrorSeverity;
/**
* Value Object representing error classification metadata.
*
* Replaces primitive array pattern in ErrorHandler::createExceptionMetadata()
* with type-safe, immutable Value Object following framework principles.
*
* This is distinct from ExceptionMetadata (behavior control) - this handles
* error classification and HTTP response metadata.
*/
final readonly class ErrorMetadata
{
/**
* @param string $exceptionClass Fully qualified exception class name
* @param ErrorSeverity $errorLevel Error severity level
* @param int $httpStatus HTTP status code for response
* @param string|null $errorCode Optional error code from FrameworkException
* @param string|null $errorCategory Optional error category (e.g., 'AUTH', 'VAL')
* @param string|null $errorSeverity Optional severity from ErrorCode
* @param bool|null $isRecoverable Whether error is recoverable
* @param string|null $recoveryHint Optional hint for recovery (debug mode)
* @param array<string, string> $additionalHeaders HTTP headers to add to response
*/
public function __construct(
public string $exceptionClass,
public ErrorSeverity $errorLevel,
public int $httpStatus,
public ?string $errorCode = null,
public ?string $errorCategory = null,
public ?string $errorSeverity = null,
public ?bool $isRecoverable = null,
public ?string $recoveryHint = null,
public array $additionalHeaders = []
) {}
/**
* Create basic metadata for non-FrameworkException errors.
*/
public static function basic(
string $exceptionClass,
ErrorSeverity $errorLevel,
int $httpStatus
): self {
return new self(
exceptionClass: $exceptionClass,
errorLevel: $errorLevel,
httpStatus: $httpStatus
);
}
/**
* Create enhanced metadata for FrameworkException errors.
*/
public static function enhanced(
string $exceptionClass,
ErrorSeverity $errorLevel,
int $httpStatus,
string $errorCode,
string $errorCategory,
string $errorSeverity,
bool $isRecoverable,
?string $recoveryHint = null
): self {
return new self(
exceptionClass: $exceptionClass,
errorLevel: $errorLevel,
httpStatus: $httpStatus,
errorCode: $errorCode,
errorCategory: $errorCategory,
errorSeverity: $errorSeverity,
isRecoverable: $isRecoverable,
recoveryHint: $recoveryHint
);
}
/**
* Add additional HTTP headers (immutable).
*/
public function withAdditionalHeaders(array $headers): self
{
return new self(
exceptionClass: $this->exceptionClass,
errorLevel: $this->errorLevel,
httpStatus: $this->httpStatus,
errorCode: $this->errorCode,
errorCategory: $this->errorCategory,
errorSeverity: $this->errorSeverity,
isRecoverable: $this->isRecoverable,
recoveryHint: $this->recoveryHint,
additionalHeaders: array_merge($this->additionalHeaders, $headers)
);
}
/**
* Check if this is enhanced metadata (from FrameworkException).
*/
public function isEnhanced(): bool
{
return $this->errorCode !== null;
}
/**
* Check if metadata has additional headers.
*/
public function hasAdditionalHeaders(): bool
{
return !empty($this->additionalHeaders);
}
/**
* Convert to array for legacy compatibility.
*
* @return array<string, mixed>
*/
public function toArray(): array
{
$array = [
'exception_class' => $this->exceptionClass,
'error_level' => $this->errorLevel->name,
'http_status' => $this->httpStatus,
];
// Add enhanced fields if present
if ($this->errorCode !== null) {
$array['error_code'] = $this->errorCode;
}
if ($this->errorCategory !== null) {
$array['error_category'] = $this->errorCategory;
}
if ($this->errorSeverity !== null) {
$array['error_severity'] = $this->errorSeverity;
}
if ($this->isRecoverable !== null) {
$array['is_recoverable'] = $this->isRecoverable;
}
if ($this->recoveryHint !== null) {
$array['recovery_hint'] = $this->recoveryHint;
}
if (!empty($this->additionalHeaders)) {
$array['additional_headers'] = $this->additionalHeaders;
}
return $array;
}
/**
* Create from array (for testing/migration).
*
* @param array<string, mixed> $data
*/
public static function fromArray(array $data): self
{
return new self(
exceptionClass: $data['exception_class'] ?? '',
errorLevel: ErrorSeverity::from($data['error_level'] ?? 'ERROR'),
httpStatus: $data['http_status'] ?? 500,
errorCode: $data['error_code'] ?? null,
errorCategory: $data['error_category'] ?? null,
errorSeverity: $data['error_severity'] ?? null,
isRecoverable: $data['is_recoverable'] ?? null,
recoveryHint: $data['recovery_hint'] ?? null,
additionalHeaders: $data['additional_headers'] ?? []
);
}
}

View File

@@ -18,15 +18,17 @@ use App\Framework\View\TemplateRenderer;
use Throwable;
/**
* Renderer für Fehlerseiten unter Verwendung des View-Systems
* Renderer für Fehlerseiten mit intelligenter Fallback-Kaskade
*
* Fallback-Strategie:
* - Production: production.view.php (mit Layout) → production-minimal.view.php → fallback.html
* - Debug: debug.view.php → fallback.html
*/
final readonly class ErrorTemplateRenderer implements ErrorViewRendererInterface
{
public function __construct(
private TemplateRenderer $renderer,
private AppConfig $appConfig,
private string $debugTemplate = 'enhanced-debug',
private string $productionTemplate = 'production'
private AppConfig $appConfig
) {
}
@@ -35,247 +37,236 @@ final readonly class ErrorTemplateRenderer implements ErrorViewRendererInterface
*/
public function renderFromHandlerContext(ErrorHandlerContext $context, bool $isDebug = false): string
{
$template = $isDebug ? $this->debugTemplate : $this->productionTemplate;
if ($isDebug) {
return $this->renderDebugError($context);
}
return $this->renderProductionError($context);
}
/**
* Production Error mit 3-Stufen Fallback-Kaskade
*
* Level 1: production.view.php (mit Layout - beste UX)
* Level 2: production-minimal.view.php (standalone - robuster)
* Level 3: fallback.html (statisch - immer verfügbar)
*/
private function renderProductionError(ErrorHandlerContext $context): string
{
// Level 1: Mit Layout (bevorzugt)
try {
// Sichere Datenaufbereitung für ErrorHandlerContext
$errorFile = $this->extractErrorFile($context);
$errorLine = $this->extractErrorLine($context);
$safeData = [
'errorClass' => $this->extractExceptionClass($context),
'errorMessage' => $this->extractErrorMessage($context),
'errorFile' => $errorFile,
'errorLine' => $errorLine,
'errorCode' => $context->metadata['http_status'] ?? 500,
'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' => is_int($context->system->memoryUsage)
? Byte::fromBytes($context->system->memoryUsage)->toHumanReadable()
: ($context->system->memoryUsage ?? '0 B'),
'httpStatus' => $context->metadata['http_status'] ?? 500,
'clientIp' => $context->request->clientIp ?? 'Unknown',
'requestUri' => $context->request->requestUri ?? '/',
'requestMethod' => $context->request->requestMethod ?? 'GET',
'userAgent' => (string) ($context->request->userAgent ?? 'Unknown'),
'traceCount' => 0, // Will be updated below if trace is available
// Environment information
'environment' => $this->appConfig->type->value,
'debugMode' => $isDebug ? 'Enabled' : 'Disabled',
'phpVersion' => PHP_VERSION,
'frameworkVersion' => '1.0.0-dev',
// Performance information
'executionTime' => $context->system->executionTime !== null
? Duration::fromSeconds($context->system->executionTime)->toHumanReadable()
: 'N/A',
'memoryLimit' => ini_get('memory_limit') ?: 'Unknown',
// Server information
'serverSoftware' => $_SERVER['SERVER_SOFTWARE'] ?? 'Unknown',
'documentRoot' => $_SERVER['DOCUMENT_ROOT'] ?? '/var/www/html',
'serverName' => $_SERVER['SERVER_NAME'] ?? 'localhost',
// Immer Standard-Werte setzen
'dependencyInfo' => '',
'code' => RawHtml::from('<div style="padding: 20px; background: #f8f9fa; border-radius: 4px; color: #6c757d;">Code-Vorschau nicht verfügbar</div>'),
'trace' => [],
'traceCount' => 0,
];
// 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);
} else {
// Standard-Fallback wenn keine Dependency-Info verfügbar
$safeData['dependencyInfo'] = '';
}
// 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
// Stack Trace mit Index hinzufügen
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);
$traceItems = $stackTrace->getItems();
// Index zu jedem Item hinzufügen
$indexedTraceItems = [];
foreach ($traceItems as $index => $item) {
$indexedTraceItems[] = [
'file' => $item->getRelativeFile(),
'line' => $item->line,
'function' => $item->class ? $item->class . $item->type . $item->function : $item->function,
'class' => $item->class,
'index' => $index,
'isOrigin' => $item->isExceptionOrigin,
];
}
$safeData['trace'] = $indexedTraceItems;
$safeData['traceCount'] = count($traceItems);
} 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']);
$traceItems = $stackTrace->getItems();
// Index zu jedem Item hinzufügen
$indexedTraceItems = [];
foreach ($traceItems as $index => $item) {
$indexedTraceItems[] = [
'file' => $item->getRelativeFile(),
'line' => $item->line,
'function' => $item->class ? $item->class . $item->type . $item->function : $item->function,
'class' => $item->class,
'index' => $index,
'isOrigin' => $item->isExceptionOrigin,
];
}
$safeData['trace'] = $indexedTraceItems;
$safeData['traceCount'] = count($traceItems);
} else {
// Fallback: Erstelle minimalen Trace aus verfügbaren Daten
$file = $context->exception->data['exception_file'] ?? null;
$line = $context->exception->data['exception_line'] ?? null;
if ($file && $line) {
$safeData['trace'] = [[
'file' => str_replace(dirname(__DIR__, 4), '', $file),
'line' => (int)$line,
'function' => 'Unknown',
'class' => null,
'index' => 0,
'isOrigin' => true,
]];
$safeData['traceCount'] = 1;
} else {
$safeData['trace'] = [];
$safeData['traceCount'] = 0;
}
}
}
// Sicherstellen, dass alle Template-Variablen definiert sind
if (! isset($safeData['dependencyInfo'])) {
$safeData['dependencyInfo'] = '';
}
if (! isset($safeData['code'])) {
$safeData['code'] = RawHtml::from('<div style="padding: 20px; background: #f8f9fa; border-radius: 4px; color: #6c757d;">Code-Vorschau nicht verfügbar</div>');
}
if (! isset($safeData['trace'])) {
$safeData['trace'] = [];
}
if (! isset($safeData['traceCount'])) {
$safeData['traceCount'] = 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;
return $this->renderTemplate('production', $context, isDebug: false);
} catch (Throwable $e) {
// Fallback falls das Template-Rendering fehlschlägt
return $this->createFallbackErrorPageFromHandlerContext($context, $isDebug, $e);
// Level 2: Ohne Layout (Fallback)
try {
return $this->renderTemplate('production-minimal', $context, isDebug: false);
} catch (Throwable $e) {
// Level 3: Statischer Fallback
return $this->renderStaticFallback($context);
}
}
}
/**
* Extrahiert Fehlermeldung aus ErrorHandlerContext
* Debug Error mit 2-Stufen Fallback-Kaskade
*
* Level 1: debug.view.php (standalone - maximaler Fokus)
* Level 2: fallback.html (statisch - immer verfügbar)
*/
private function renderDebugError(ErrorHandlerContext $context): string
{
// Level 1: Debug Template (standalone)
try {
return $this->renderTemplate('debug', $context, isDebug: true);
} catch (Throwable $e) {
// Level 2: Statischer Fallback
return $this->renderStaticFallback($context);
}
}
/**
* Rendert ein Error-Template mit vorbereiteten Daten
*/
private function renderTemplate(string $templateName, ErrorHandlerContext $context, bool $isDebug): string
{
$data = $this->prepareTemplateData($context, $isDebug);
$renderContext = new RenderContext(
template: $templateName,
metaData: new MetaData('', '', new OpenGraphTypeWebsite()),
data: $data
);
$rendered = $this->renderer->render($renderContext);
if ($rendered === '') {
throw new \RuntimeException("Template '{$templateName}' returned empty string");
}
return $rendered;
}
/**
* Bereitet Template-Daten aus ErrorHandlerContext vor
*/
private function prepareTemplateData(ErrorHandlerContext $context, bool $isDebug): array
{
$data = [
// Error Information
'errorClass' => $this->extractExceptionClass($context),
'errorMessage' => $this->extractErrorMessage($context),
'errorFile' => $this->extractErrorFile($context),
'errorLine' => $this->extractErrorLine($context),
'errorCode' => $context->metadata['http_status'] ?? 500,
'error' => (object) [
'message' => $this->extractErrorMessage($context),
'operation' => $context->exception->operation ?? 'unknown',
'component' => $context->exception->component ?? 'Application',
'class' => $context->metadata['exception_class'] ?? 'Unknown',
],
// Request Information
'requestId' => $context->request->requestId?->toString() ?? 'unknown',
'timestamp' => date('c'),
'httpStatus' => $context->metadata['http_status'] ?? 500,
'clientIp' => $context->request->clientIp ?? 'Unknown',
'requestUri' => $context->request->requestUri ?? '/',
'requestMethod' => $context->request->requestMethod ?? 'GET',
'userAgent' => (string) ($context->request->userAgent ?? 'Unknown'),
// System Information
'environment' => $this->appConfig->type->value,
'debugMode' => $isDebug ? 'Enabled' : 'Disabled',
'phpVersion' => PHP_VERSION,
'frameworkVersion' => '1.0.0-dev',
'memory' => is_int($context->system->memoryUsage)
? Byte::fromBytes($context->system->memoryUsage)->toHumanReadable()
: ($context->system->memoryUsage ?? '0 B'),
'memoryLimit' => ini_get('memory_limit') ?: 'Unknown',
'executionTime' => $context->system->executionTime !== null
? Duration::fromSeconds($context->system->executionTime)->toHumanReadable()
: 'N/A',
// Debug-only data
'dependencyInfo' => '',
'code' => RawHtml::from(''),
'trace' => [],
'traceCount' => 0,
];
// Security Event Handling
if ($context->exception->metadata['security_event'] ?? false) {
if (! $isDebug) {
$data['errorMessage'] = 'Security violation detected. Access denied.';
$data['error']->message = 'Security violation detected. Access denied.';
}
}
// Add Debug-specific data
if ($isDebug) {
$this->addDebugData($data, $context);
}
return $data;
}
/**
* Fügt Debug-spezifische Daten hinzu (Code Preview, Stack Trace, etc.)
*/
private function addDebugData(array &$data, ErrorHandlerContext $context): void
{
// Dependency Information
if (isset($context->exception->data['dependencyChain'])) {
$chain = implode(' → ', $context->exception->data['dependencyChain']);
$target = $context->exception->data['class'] ?? 'Unknown';
$html = sprintf(
'<div class="dependency-info">' .
'<h3>🔄 Zyklische Abhängigkeit</h3>' .
'<p><strong>Kette:</strong><br>%s → %s</p>' .
'<p>Die Klasse kann nicht instanziiert werden.</p>' .
'</div>',
htmlspecialchars($chain, ENT_QUOTES, 'UTF-8'),
htmlspecialchars($target, ENT_QUOTES, 'UTF-8')
);
$data['dependencyInfo'] = RawHtml::from($html);
}
// Code Preview
$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);
$data['code'] = RawHtml::from($codeHtml);
}
// Stack Trace
$exception = $context->exception->data['original_exception'] ??
$context->exception->data['previous_exception'] ??
null;
if ($exception instanceof Throwable) {
$stackTrace = new StackTrace($exception);
$traceItems = $stackTrace->getItems();
$indexedTrace = [];
foreach ($traceItems as $index => $item) {
$indexedTrace[] = (object) [
'file' => $item->getRelativeFile(),
'line' => $item->line,
'function' => $item->class ? $item->class . $item->type . $item->function : $item->function,
'class' => $item->class,
'index' => $index,
'isOrigin' => $item->isExceptionOrigin,
];
}
$data['trace'] = $indexedTrace;
$data['traceCount'] = count($traceItems);
}
}
/**
* Statischer HTML-Fallback (Level 3 - kann NICHT fehlschlagen)
*/
private function renderStaticFallback(ErrorHandlerContext $context): string
{
$fallbackPath = __DIR__ . '/../fallback.html';
if (file_exists($fallbackPath)) {
$html = file_get_contents($fallbackPath);
return str_replace(
'{REQUEST_ID}',
htmlspecialchars($context->request->requestId?->toString() ?? 'unknown', ENT_QUOTES, 'UTF-8'),
$html
);
}
// Absolutes Notfall-Minimum
return '<!DOCTYPE html><html lang="de"><head><meta charset="UTF-8"><title>503 Service Unavailable</title></head><body><h1>503 Service Unavailable</h1></body></html>';
}
/**
* Extrahiert Fehlermeldung aus Context
*/
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'];
}
// Versuche die originale Exception zu verwenden
if (isset($context->exception->data['original_exception']) && $context->exception->data['original_exception'] instanceof \Throwable) {
if (isset($context->exception->data['original_exception']) && $context->exception->data['original_exception'] instanceof Throwable) {
return $context->exception->data['original_exception']->getMessage();
}
// Versuche previous_exception
if (isset($context->exception->data['previous_exception']) && $context->exception->data['previous_exception'] instanceof \Throwable) {
return $context->exception->data['previous_exception']->getMessage();
}
// Fallback: Operation und Component verwenden
$operation = $context->exception->operation ?? 'unknown_operation';
$component = $context->exception->component ?? 'Application';
@@ -283,190 +274,50 @@ final readonly class ErrorTemplateRenderer implements ErrorViewRendererInterface
}
/**
* Extrahiert die Exception-Klasse
* Extrahiert Exception-Klasse aus Context
*/
private function extractExceptionClass(ErrorHandlerContext $context): string
{
// Aus Metadata extrahieren
if (isset($context->metadata['exception_class'])) {
return $context->metadata['exception_class'];
}
// Versuche die originale Exception zu verwenden
if (isset($context->exception->data['original_exception']) && $context->exception->data['original_exception'] instanceof \Throwable) {
$originalException = $context->exception->data['original_exception'];
// Für FrameworkException: Zeige die FrameworkException-Klasse, nicht die PDO-Exception
// da DatabaseException die richtige Exception-Klasse ist
return get_class($originalException);
}
// Versuche previous_exception
if (isset($context->exception->data['previous_exception']) && $context->exception->data['previous_exception'] instanceof \Throwable) {
return get_class($context->exception->data['previous_exception']);
if (isset($context->exception->data['original_exception']) && $context->exception->data['original_exception'] instanceof Throwable) {
return get_class($context->exception->data['original_exception']);
}
return 'Unknown';
}
/**
* Extrahiert die Fehlerdatei
* Extrahiert Fehler-Datei aus Context
*/
private function extractErrorFile(ErrorHandlerContext $context): string
{
// 1. Priorität: Explizit gesetzte exception_file (für DatabaseException Fix)
if (isset($context->exception->data['exception_file']) && ! empty($context->exception->data['exception_file'])) {
return $context->exception->data['exception_file'];
}
// 2. Priorität: Versuche die originale Exception zu verwenden
if (isset($context->exception->data['original_exception']) && $context->exception->data['original_exception'] instanceof \Throwable) {
$originalException = $context->exception->data['original_exception'];
return $originalException->getFile();
}
// 3. Priorität: Versuche previous_exception
if (isset($context->exception->data['previous_exception']) && $context->exception->data['previous_exception'] instanceof \Throwable) {
return $context->exception->data['previous_exception']->getFile();
if (isset($context->exception->data['original_exception']) && $context->exception->data['original_exception'] instanceof Throwable) {
return $context->exception->data['original_exception']->getFile();
}
return 'Unknown';
}
/**
* Extrahiert die Fehlerzeile
* Extrahiert Fehler-Zeile aus Context
*/
private function extractErrorLine(ErrorHandlerContext $context): int
{
// 1. Priorität: Explizit gesetzte exception_line (für DatabaseException Fix)
if (isset($context->exception->data['exception_line']) && is_numeric($context->exception->data['exception_line'])) {
return (int) $context->exception->data['exception_line'];
}
// 2. Priorität: Versuche die originale Exception zu verwenden
if (isset($context->exception->data['original_exception']) && $context->exception->data['original_exception'] instanceof \Throwable) {
$originalException = $context->exception->data['original_exception'];
return $originalException->getLine();
}
// 3. Priorität: Versuche previous_exception
if (isset($context->exception->data['previous_exception']) && $context->exception->data['previous_exception'] instanceof \Throwable) {
return $context->exception->data['previous_exception']->getLine();
if (isset($context->exception->data['original_exception']) && $context->exception->data['original_exception'] instanceof Throwable) {
return $context->exception->data['original_exception']->getLine();
}
return 0;
}
/**
* 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')
);
}
return $content;
}
}

View 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>Service Unavailable</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background: #1a202c;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.container {
background: #2d3748;
border-radius: 16px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
max-width: 500px;
width: 100%;
padding: 48px 32px;
text-align: center;
}
.status {
font-size: 72px;
font-weight: 700;
color: #667eea;
margin-bottom: 16px;
line-height: 1;
}
h1 {
font-size: 24px;
margin-bottom: 16px;
color: #f7fafc;
font-weight: 600;
}
p {
color: #cbd5e0;
line-height: 1.6;
margin-bottom: 24px;
font-size: 16px;
}
.request-id {
font-family: 'Courier New', Courier, monospace;
background: #1a202c;
padding: 12px 16px;
border-radius: 8px;
font-size: 13px;
color: #e2e8f0;
word-break: break-all;
border: 1px solid #4a5568;
}
.request-id strong {
display: block;
margin-bottom: 4px;
color: #a0aec0;
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
@media (max-width: 600px) {
.container {
padding: 32px 24px;
}
.status {
font-size: 56px;
}
h1 {
font-size: 20px;
}
p {
font-size: 14px;
}
}
</style>
</head>
<body>
<div class="container">
<div class="status">503</div>
<h1>Service Temporarily Unavailable</h1>
<p>We're experiencing technical difficulties. Our team has been notified and is working on a solution.</p>
<div class="request-id">
<strong>Request ID</strong>
{REQUEST_ID}
</div>
</div>
</body>
</html>

View File

@@ -1,207 +0,0 @@
<!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: {{ memory }}</span>
</div>
{{ dependencyInfo }}
</div>
</div>
{{ 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>
</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>

View File

@@ -1,720 +0,0 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>🐛 Debug: {{ errorClass }}</title>
<style>
:root {
--primary: #1e40af;
--danger: #dc2626;
--warning: #d97706;
--success: #059669;
--info: #0284c7;
--gray-50: #f9fafb;
--gray-100: #f3f4f6;
--gray-200: #e5e7eb;
--gray-300: #d1d5db;
--gray-400: #9ca3af;
--gray-500: #6b7280;
--gray-600: #4b5563;
--gray-700: #374151;
--gray-800: #1f2937;
--gray-900: #111827;
}
@media (prefers-color-scheme: dark) {
:root {
--gray-50: #111827;
--gray-100: #1f2937;
--gray-200: #374151;
--gray-300: #4b5563;
--gray-400: #6b7280;
--gray-500: #9ca3af;
--gray-600: #d1d5db;
--gray-700: #e5e7eb;
--gray-800: #f3f4f6;
--gray-900: #f9fafb;
}
}
* {
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Inter", sans-serif;
font-size: 14px;
line-height: 1.5;
color: var(--gray-900);
background: var(--gray-50);
margin: 0;
padding: 0;
}
/* Layout */
.debug-layout {
display: grid;
grid-template-columns: 320px 1fr;
min-height: 100vh;
gap: 0;
}
@media (max-width: 1024px) {
.debug-layout {
grid-template-columns: 1fr;
}
.debug-sidebar {
order: 2;
border-top: 1px solid var(--gray-200);
border-right: none;
}
}
/* Sidebar */
.debug-sidebar {
background: var(--gray-100);
border-right: 1px solid var(--gray-200);
overflow-y: auto;
max-height: 100vh;
}
.debug-nav {
list-style: none;
margin: 0;
padding: 0;
}
.debug-nav-item {
border-bottom: 1px solid var(--gray-200);
}
.debug-nav-link {
display: block;
padding: 12px 16px;
color: var(--gray-700);
text-decoration: none;
font-weight: 500;
transition: all 0.15s;
position: relative;
}
.debug-nav-link:hover {
background: var(--gray-200);
color: var(--primary);
}
.debug-nav-link.active {
background: var(--primary);
color: white;
}
.debug-nav-link .count {
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
background: var(--gray-300);
color: var(--gray-700);
font-size: 11px;
padding: 2px 6px;
border-radius: 10px;
}
.debug-nav-link.active .count {
background: rgba(255,255,255,0.2);
color: white;
}
/* Main Content */
.debug-main {
background: white;
overflow-y: auto;
max-height: 100vh;
}
/* Header */
.debug-header {
background: linear-gradient(135deg, var(--danger) 0%, #dc2626 100%);
color: white;
padding: 24px;
position: sticky;
top: 0;
z-index: 10;
border-bottom: 1px solid var(--gray-200);
}
.debug-header h1 {
margin: 0 0 8px 0;
font-size: 24px;
font-weight: 700;
}
.debug-header p {
margin: 0;
opacity: 0.9;
font-size: 16px;
}
.debug-meta {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-top: 16px;
}
.debug-badge {
background: rgba(255,255,255,0.2);
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
/* Content Sections */
.debug-content {
padding: 24px;
}
.debug-section {
display: none;
margin-bottom: 32px;
}
.debug-section.active {
display: block;
}
.debug-section h2 {
margin: 0 0 16px 0;
font-size: 18px;
font-weight: 600;
color: var(--gray-800);
}
/* Cards */
.debug-card {
background: white;
border: 1px solid var(--gray-200);
border-radius: 8px;
margin-bottom: 16px;
overflow: hidden;
}
.debug-card-header {
background: var(--gray-50);
border-bottom: 1px solid var(--gray-200);
padding: 12px 16px;
font-weight: 600;
color: var(--gray-800);
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
}
.debug-card-header:hover {
background: var(--gray-100);
}
.debug-card-content {
padding: 16px;
}
.debug-card.collapsed .debug-card-content {
display: none;
}
.debug-card-header::after {
content: '';
font-weight: bold;
transform: rotate(0deg);
transition: transform 0.2s;
}
.debug-card.collapsed .debug-card-header::after {
transform: rotate(90deg);
content: '+';
}
/* Code */
.debug-code {
background: var(--gray-900);
color: var(--gray-100);
padding: 16px;
border-radius: 6px;
font-family: 'SF Mono', Monaco, 'Inconsolata', 'Roboto Mono', monospace;
font-size: 13px;
line-height: 1.4;
overflow-x: auto;
position: relative;
}
.debug-code-line {
position: relative;
padding-left: 60px;
}
.debug-code-line-number {
position: absolute;
left: 0;
width: 50px;
color: var(--gray-500);
text-align: right;
user-select: none;
}
.debug-code-line.highlight {
background: rgba(239, 68, 68, 0.1);
border-left: 3px solid var(--danger);
}
/* Stack Trace */
.debug-trace-item {
border-bottom: 1px solid var(--gray-200);
transition: all 0.15s;
}
.debug-trace-item:last-child {
border-bottom: none;
}
.debug-trace-item:hover {
background: var(--gray-50);
}
.debug-trace-item.origin {
background: rgba(239, 68, 68, 0.05);
border-left: 4px solid var(--danger);
}
.debug-trace-header {
padding: 12px 16px;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
}
.debug-trace-location {
font-family: monospace;
font-size: 13px;
color: var(--primary);
font-weight: 600;
}
.debug-trace-call {
font-family: monospace;
font-size: 12px;
color: var(--gray-600);
margin-top: 4px;
}
/* Tables */
.debug-table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
.debug-table th,
.debug-table td {
padding: 8px 12px;
text-align: left;
border-bottom: 1px solid var(--gray-200);
}
.debug-table th {
background: var(--gray-50);
font-weight: 600;
color: var(--gray-700);
}
.debug-table td {
font-family: monospace;
font-size: 12px;
}
/* Search */
.debug-search {
position: sticky;
top: 0;
background: white;
padding: 16px;
border-bottom: 1px solid var(--gray-200);
z-index: 5;
}
.debug-search input {
width: 100%;
padding: 8px 12px;
border: 1px solid var(--gray-300);
border-radius: 6px;
font-size: 14px;
}
.debug-search input:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
/* Utilities */
.text-sm { font-size: 12px; }
.text-xs { font-size: 11px; }
.font-mono { font-family: monospace; }
.font-semibold { font-weight: 600; }
.text-gray-500 { color: var(--gray-500); }
.text-gray-600 { color: var(--gray-600); }
.text-red-600 { color: var(--danger); }
.bg-red-50 { background: rgba(239, 68, 68, 0.05); }
.hidden { display: none; }
/* Mobile Responsive */
@media (max-width: 768px) {
.debug-layout {
grid-template-columns: 1fr;
}
.debug-header {
padding: 16px;
}
.debug-content {
padding: 16px;
}
.debug-meta {
flex-direction: column;
gap: 8px;
}
}
</style>
</head>
<body>
<div class="debug-layout">
<!-- Sidebar -->
<nav class="debug-sidebar">
<ul class="debug-nav">
<li class="debug-nav-item">
<a href="#overview" class="debug-nav-link active" data-section="overview">
Overview
</a>
</li>
<li class="debug-nav-item">
<a href="#stacktrace" class="debug-nav-link" data-section="stacktrace">
Stack Trace
<span class="count">{{ traceCount }}</span>
</a>
</li>
<li class="debug-nav-item">
<a href="#request" class="debug-nav-link" data-section="request">
Request
</a>
</li>
<li class="debug-nav-item">
<a href="#context" class="debug-nav-link" data-section="context">
Context
</a>
</li>
<li class="debug-nav-item">
<a href="#performance" class="debug-nav-link" data-section="performance">
Performance
</a>
</li>
<li class="debug-nav-item">
<a href="#environment" class="debug-nav-link" data-section="environment">
Environment
</a>
</li>
</ul>
</nav>
<!-- Main Content -->
<main class="debug-main">
<!-- Header -->
<header class="debug-header">
<h1>{{ errorClass }}</h1>
<p>{{ errorMessage }}</p>
<div class="debug-meta">
<span class="debug-badge">Request ID: {{ requestId }}</span>
<span class="debug-badge">{{ timestamp }}</span>
<span class="debug-badge">Memory: {{ memory }}</span>
<span class="debug-badge">{{ level }}</span>
</div>
</header>
<div class="debug-content">
<!-- Overview Section -->
<section id="overview" class="debug-section active">
<h2>Exception Overview</h2>
<div class="debug-card">
<div class="debug-card-header">
Error Details
</div>
<div class="debug-card-content">
<table class="debug-table">
<tr>
<th>Exception</th>
<td>{{ errorClass }}</td>
</tr>
<tr>
<th>Message</th>
<td>{{ errorMessage }}</td>
</tr>
<tr>
<th>File</th>
<td class="font-mono">{{ errorFile }}</td>
</tr>
<tr>
<th>Line</th>
<td>{{ errorLine }}</td>
</tr>
<tr>
<th>Code</th>
<td>{{ errorCode }}</td>
</tr>
</table>
</div>
</div>
{{ code }}
{{ dependencyInfo }}
</section>
<!-- Stack Trace Section -->
<section id="stacktrace" class="debug-section">
<div class="debug-search">
<input type="text" id="trace-search" placeholder="Search stack trace...">
</div>
<h2>Stack Trace</h2>
<div class="debug-card">
<div class="debug-card-content" style="padding: 0;">
<div style="padding: 16px;">
<p>Trace Count: {{ traceCount }}</p>
<for var="item" in="trace">
<div class="debug-trace-item {{ item.isOrigin }}" data-searchable="{{ item.file }} {{ item.class }}">
<div class="debug-trace-header">
<div>
<div class="debug-trace-location">
{{ item.file }}:{{ item.line }}
</div>
<div class="debug-trace-call">
{{ item.function }}
</div>
</div>
<span class="text-xs text-gray-500">#{{ item.index }}</span>
</div>
</div>
</for>
</div>
</div>
</div>
</section>
<!-- Request Section -->
<section id="request" class="debug-section">
<h2>Request Information</h2>
<div class="debug-card">
<div class="debug-card-header">
HTTP Request
</div>
<div class="debug-card-content">
<table class="debug-table">
<tr>
<th>Method</th>
<td>{{ requestMethod }}</td>
</tr>
<tr>
<th>URI</th>
<td class="font-mono">{{ requestUri }}</td>
</tr>
<tr>
<th>User Agent</th>
<td class="text-sm">{{ userAgent }}</td>
</tr>
<tr>
<th>IP Address</th>
<td>{{ clientIp }}</td>
</tr>
</table>
</div>
</div>
<div class="debug-card collapsed">
<div class="debug-card-header">
Request Headers
</div>
<div class="debug-card-content">
<table class="debug-table">
<tr><th>Accept</th><td>application/json, text/html</td></tr>
<tr><th>Accept-Language</th><td>de-DE,de;q=0.9,en;q=0.8</td></tr>
<tr><th>Cache-Control</th><td>no-cache</td></tr>
</table>
</div>
</div>
<div class="debug-card collapsed">
<div class="debug-card-header">
Request Parameters
</div>
<div class="debug-card-content">
<p class="text-sm text-gray-500">No parameters found</p>
</div>
</div>
</section>
<!-- Context Section -->
<section id="context" class="debug-section">
<h2>Application Context</h2>
<div class="debug-card">
<div class="debug-card-header">
Application State
</div>
<div class="debug-card-content">
<table class="debug-table">
<tr>
<th>Environment</th>
<td>{{ environment }}</td>
</tr>
<tr>
<th>Debug Mode</th>
<td>{{ debugMode }}</td>
</tr>
<tr>
<th>PHP Version</th>
<td>{{ phpVersion }}</td>
</tr>
<tr>
<th>Framework Version</th>
<td>{{ frameworkVersion }}</td>
</tr>
</table>
</div>
</div>
</section>
<!-- Performance Section -->
<section id="performance" class="debug-section">
<h2>Performance Metrics</h2>
<div class="debug-card">
<div class="debug-card-header">
Memory & Timing
</div>
<div class="debug-card-content">
<table class="debug-table">
<tr>
<th>Peak Memory</th>
<td>{{ memory }}</td>
</tr>
<tr>
<th>Execution Time</th>
<td>{{ executionTime }}</td>
</tr>
<tr>
<th>Memory Limit</th>
<td>{{ memoryLimit }}</td>
</tr>
</table>
</div>
</div>
</section>
<!-- Environment Section -->
<section id="environment" class="debug-section">
<h2>Environment</h2>
<div class="debug-card collapsed">
<div class="debug-card-header">
Server Information
</div>
<div class="debug-card-content">
<table class="debug-table">
<tr><th>Server Software</th><td>{{ serverSoftware }}</td></tr>
<tr><th>Document Root</th><td class="font-mono">{{ documentRoot }}</td></tr>
<tr><th>Server Name</th><td>{{ serverName }}</td></tr>
</table>
</div>
</div>
</section>
</div>
</main>
</div>
<script>
// Navigation
document.querySelectorAll('.debug-nav-link').forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
// Update active nav
document.querySelectorAll('.debug-nav-link').forEach(l => l.classList.remove('active'));
link.classList.add('active');
// Show section
const sectionId = link.dataset.section;
document.querySelectorAll('.debug-section').forEach(s => s.classList.remove('active'));
document.getElementById(sectionId).classList.add('active');
});
});
// Collapsible cards
document.querySelectorAll('.debug-card-header').forEach(header => {
header.addEventListener('click', () => {
header.parentElement.classList.toggle('collapsed');
});
});
// Search functionality
const searchInput = document.getElementById('trace-search');
if (searchInput) {
searchInput.addEventListener('input', (e) => {
const query = e.target.value.toLowerCase();
document.querySelectorAll('.debug-trace-item').forEach(item => {
const searchText = item.dataset.searchable?.toLowerCase() || '';
item.style.display = searchText.includes(query) ? 'block' : 'none';
});
});
}
// Keyboard shortcuts
document.addEventListener('keydown', (e) => {
if (e.ctrlKey || e.metaKey) {
switch(e.key) {
case '1':
e.preventDefault();
document.querySelector('[data-section="overview"]').click();
break;
case '2':
e.preventDefault();
document.querySelector('[data-section="stacktrace"]').click();
break;
case '3':
e.preventDefault();
document.querySelector('[data-section="request"]').click();
break;
case 'f':
e.preventDefault();
searchInput?.focus();
break;
}
}
});
// Auto-expand first trace item if it's the origin
document.addEventListener('DOMContentLoaded', () => {
const originTrace = document.querySelector('.debug-trace-item.origin');
if (originTrace) {
originTrace.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
});
</script>
</body>
</html>

View File

@@ -1,111 +0,0 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name='robots' content='noindex, nofollow, noarchive'/>
<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 {
}
.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-footer {
display: flex;
justify-content: center;
gap: 10px;
padding: 20px;
border-top: 1px solid var(--border-color);
}
@media (prefers-color-scheme: dark) {
.error-container {
background: #343a40;
}
}
</style>
</head>
<body>
<main class="container" role="main">
<section class="error-container" aria-live="assertive">
<header class="error-header">
<h1 class="error-title">Uups, da ist etwas schiefgelaufen</h1>
</header>
<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>
<footer class="error-footer">
<button onclick="location.reload();">Neu Laden</button>
<a href="/" title="Zur Startseite">Startseite</a>
<button onclick="history.back();">Zurück</button>
</footer>
</section>
</main>
</body>
</html>