feat: CI/CD pipeline setup complete - Ansible playbooks updated, secrets configured, workflow ready
This commit is contained in:
@@ -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()
|
||||
);
|
||||
}
|
||||
|
||||
37
src/Framework/ErrorHandling/ErrorHandlerInitializer.php
Normal file
37
src/Framework/ErrorHandling/ErrorHandlerInitializer.php
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
174
src/Framework/ErrorHandling/ErrorHandlerManager.php
Normal file
174
src/Framework/ErrorHandling/ErrorHandlerManager.php
Normal 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;
|
||||
}
|
||||
}
|
||||
86
src/Framework/ErrorHandling/ErrorHandlerRegistry.php
Normal file
86
src/Framework/ErrorHandling/ErrorHandlerRegistry.php
Normal 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([]);
|
||||
}
|
||||
}
|
||||
58
src/Framework/ErrorHandling/ErrorHandlingResult.php
Normal file
58
src/Framework/ErrorHandling/ErrorHandlingResult.php
Normal 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
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
52
src/Framework/ErrorHandling/Handlers/HandlerRegistration.php
Normal file
52
src/Framework/ErrorHandling/Handlers/HandlerRegistration.php
Normal 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;
|
||||
}
|
||||
}
|
||||
32
src/Framework/ErrorHandling/Handlers/HandlerResult.php
Normal file
32
src/Framework/ErrorHandling/Handlers/HandlerResult.php
Normal 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);
|
||||
}
|
||||
}
|
||||
45
src/Framework/ErrorHandling/Handlers/HttpErrorHandler.php
Normal file
45
src/Framework/ErrorHandling/Handlers/HttpErrorHandler.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
571
src/Framework/ErrorHandling/Templates/debug.view.php
Normal file
571
src/Framework/ErrorHandling/Templates/debug.view.php
Normal 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>
|
||||
@@ -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>
|
||||
118
src/Framework/ErrorHandling/Templates/production.view.php
Normal file
118
src/Framework/ErrorHandling/Templates/production.view.php
Normal 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>
|
||||
177
src/Framework/ErrorHandling/ValueObjects/ErrorMetadata.php
Normal file
177
src/Framework/ErrorHandling/ValueObjects/ErrorMetadata.php
Normal 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'] ?? []
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
106
src/Framework/ErrorHandling/fallback.html
Normal file
106
src/Framework/ErrorHandling/fallback.html
Normal file
@@ -0,0 +1,106 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user