refactor(deployment): Remove WireGuard VPN dependency and restore public service access
Remove WireGuard integration from production deployment to simplify infrastructure: - Remove docker-compose-direct-access.yml (VPN-bound services) - Remove VPN-only middlewares from Grafana, Prometheus, Portainer - Remove WireGuard middleware definitions from Traefik - Remove WireGuard IPs (10.8.0.0/24) from Traefik forwarded headers All monitoring services now publicly accessible via subdomains: - grafana.michaelschiemer.de (with Grafana native auth) - prometheus.michaelschiemer.de (with Basic Auth) - portainer.michaelschiemer.de (with Portainer native auth) All services use Let's Encrypt SSL certificates via Traefik.
This commit is contained in:
798
docs/migration/ErrorHandling-to-ExceptionHandling-Strategy.md
Normal file
798
docs/migration/ErrorHandling-to-ExceptionHandling-Strategy.md
Normal file
@@ -0,0 +1,798 @@
|
||||
# ErrorHandling → ExceptionHandling Migration Strategy
|
||||
|
||||
**Status:** Task 13 Phase 5 - Migration Planning
|
||||
**Date:** 2025-11-05
|
||||
**Phase 4 Completion:** All legacy files examined, incompatibilities documented
|
||||
|
||||
## Executive Summary
|
||||
|
||||
The legacy `ErrorHandling` module cannot be removed until **5 critical incompatibilities** are resolved. This document provides implementation strategies for each blocker.
|
||||
|
||||
## Critical Blockers
|
||||
|
||||
| # | Blocker | Severity | Location | Impact |
|
||||
|---|---------|----------|----------|---------|
|
||||
| 1 | ErrorAggregator signature mismatch | 🔴 CRITICAL | ErrorHandler.php:128 | Prevents error aggregation |
|
||||
| 2 | ExceptionHandlingMiddleware unreachable code | 🔴 URGENT | ExceptionHandlingMiddleware.php:32-37 | Broken error recovery |
|
||||
| 3 | SecurityEventLogger old types | 🔴 HIGH | SecurityEventLogger.php:28-52 | Breaks DDoS logging |
|
||||
| 4 | Missing CLI error rendering | 🔴 HIGH | AppBootstrapper.php:155-163 | No CLI error handling |
|
||||
| 5 | Missing HTTP Response generation | 🔴 HIGH | Multiple locations | No middleware recovery |
|
||||
|
||||
---
|
||||
|
||||
## Strategy 1: Fix ErrorAggregator Signature Mismatch
|
||||
|
||||
### Current State (BROKEN)
|
||||
|
||||
**Location:** `src/Framework/ErrorHandling/ErrorHandler.php:127-128`
|
||||
|
||||
```php
|
||||
// BROKEN: OLD signature call
|
||||
$this->errorAggregator->processError($errorHandlerContext);
|
||||
```
|
||||
|
||||
**NEW signature requires:**
|
||||
```php
|
||||
public function processError(
|
||||
\Throwable $exception,
|
||||
ExceptionContextProvider $contextProvider,
|
||||
bool $isDebug = false
|
||||
): void
|
||||
```
|
||||
|
||||
### Migration Strategy
|
||||
|
||||
**Option A: Minimal Change (Recommended)**
|
||||
|
||||
Create adapter method in ErrorHandler that converts ErrorHandlerContext to ExceptionContextProvider:
|
||||
|
||||
```php
|
||||
// Add to ErrorHandler.php
|
||||
private function dispatchToErrorAggregator(
|
||||
\Throwable $exception,
|
||||
ErrorHandlerContext $errorHandlerContext
|
||||
): void {
|
||||
// Create ExceptionContextProvider instance
|
||||
$contextProvider = $this->container->get(ExceptionContextProvider::class);
|
||||
|
||||
// Convert ErrorHandlerContext to ExceptionContextData
|
||||
$contextData = ExceptionContextData::create(
|
||||
operation: $errorHandlerContext->exception->operation ?? null,
|
||||
component: $errorHandlerContext->exception->component ?? null,
|
||||
userId: $errorHandlerContext->request->userId,
|
||||
sessionId: $errorHandlerContext->request->sessionId,
|
||||
requestId: $errorHandlerContext->request->requestId,
|
||||
clientIp: $errorHandlerContext->request->clientIp,
|
||||
userAgent: $errorHandlerContext->request->userAgent,
|
||||
occurredAt: new \DateTimeImmutable(),
|
||||
tags: $errorHandlerContext->exception->tags ?? [],
|
||||
metadata: $errorHandlerContext->exception->metadata ?? [],
|
||||
data: $errorHandlerContext->metadata
|
||||
);
|
||||
|
||||
// Store in WeakMap
|
||||
$contextProvider->set($exception, $contextData);
|
||||
|
||||
// Call ErrorAggregator with NEW signature
|
||||
$this->errorAggregator->processError(
|
||||
$exception,
|
||||
$contextProvider,
|
||||
$this->isDebugMode()
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Change at line 127:**
|
||||
```php
|
||||
// BEFORE (BROKEN)
|
||||
$this->errorAggregator->processError($errorHandlerContext);
|
||||
|
||||
// AFTER (FIXED)
|
||||
$this->dispatchToErrorAggregator($exception, $errorHandlerContext);
|
||||
```
|
||||
|
||||
**Files to modify:**
|
||||
- ✏️ `src/Framework/ErrorHandling/ErrorHandler.php` (add adapter method)
|
||||
|
||||
**Testing:**
|
||||
- Trigger error that calls ErrorAggregator
|
||||
- Verify context data preserved in WeakMap
|
||||
- Check error aggregation dashboard shows correct context
|
||||
|
||||
---
|
||||
|
||||
## Strategy 2: Fix ExceptionHandlingMiddleware Unreachable Code
|
||||
|
||||
### Current State (BROKEN)
|
||||
|
||||
**Location:** `src/Framework/Http/Middlewares/ExceptionHandlingMiddleware.php:26-39`
|
||||
|
||||
```php
|
||||
public function __invoke(MiddlewareContext $context, Next $next, RequestStateManager $stateManager): MiddlewareContext
|
||||
{
|
||||
try {
|
||||
return $next($context);
|
||||
} catch (\Throwable $e) {
|
||||
$error = new ErrorKernel();
|
||||
$error->handle($e); // ← Calls exit() - terminates PHP
|
||||
|
||||
// UNREACHABLE CODE - execution never reaches here
|
||||
$response = $this->errorHandler->createHttpResponse($e, $context);
|
||||
return $context->withResponse($response);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Problem:** ErrorKernel.handle() calls exit(), making recovery impossible.
|
||||
|
||||
### Migration Strategy
|
||||
|
||||
**Solution: Add non-terminal mode to ErrorKernel**
|
||||
|
||||
**Step 1: Add createHttpResponse() to ErrorKernel**
|
||||
|
||||
```php
|
||||
// Add to src/Framework/ExceptionHandling/ErrorKernel.php
|
||||
|
||||
/**
|
||||
* Create HTTP Response without terminating execution
|
||||
* (for middleware recovery pattern)
|
||||
*/
|
||||
public function createHttpResponse(\Throwable $exception): Response
|
||||
{
|
||||
// Initialize context if not already done
|
||||
if ($this->contextProvider === null) {
|
||||
$this->initializeContext($exception);
|
||||
}
|
||||
|
||||
// Enrich context from request globals
|
||||
$this->enrichContextFromRequest($exception);
|
||||
|
||||
// Create Response using renderer chain
|
||||
$response = $this->createResponseFromException($exception);
|
||||
|
||||
// Log error (without terminating)
|
||||
$this->logError($exception);
|
||||
|
||||
// Dispatch to aggregator
|
||||
$this->dispatchToErrorAggregator($exception);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract response creation from handle()
|
||||
*/
|
||||
private function createResponseFromException(\Throwable $exception): Response
|
||||
{
|
||||
// Try framework exception handler
|
||||
if ($exception instanceof FrameworkException) {
|
||||
return $this->handleFrameworkException($exception);
|
||||
}
|
||||
|
||||
// Try specialized handlers
|
||||
if ($this->exceptionHandlerManager !== null) {
|
||||
$response = $this->exceptionHandlerManager->handle($exception);
|
||||
if ($response !== null) {
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to renderer chain
|
||||
return $this->rendererChain->render($exception, $this->contextProvider);
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: Update ExceptionHandlingMiddleware**
|
||||
|
||||
```php
|
||||
// Update src/Framework/Http/Middlewares/ExceptionHandlingMiddleware.php
|
||||
|
||||
use App\Framework\ExceptionHandling\ErrorKernel;
|
||||
|
||||
final readonly class ExceptionHandlingMiddleware
|
||||
{
|
||||
public function __construct(
|
||||
private ErrorKernel $errorKernel, // ← Inject ErrorKernel
|
||||
private Logger $logger
|
||||
) {}
|
||||
|
||||
public function __invoke(
|
||||
MiddlewareContext $context,
|
||||
Next $next,
|
||||
RequestStateManager $stateManager
|
||||
): MiddlewareContext {
|
||||
try {
|
||||
return $next($context);
|
||||
} catch (\Throwable $e) {
|
||||
// Log error
|
||||
$this->logger->error('[Middleware] Exception caught', [
|
||||
'exception' => get_class($e),
|
||||
'message' => $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine()
|
||||
]);
|
||||
|
||||
// Create recovery response (non-terminal)
|
||||
$response = $this->errorKernel->createHttpResponse($e);
|
||||
|
||||
// Return context with error response
|
||||
return $context->withResponse($response);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Files to modify:**
|
||||
- ✏️ `src/Framework/ExceptionHandling/ErrorKernel.php` (add createHttpResponse() method)
|
||||
- ✏️ `src/Framework/Http/Middlewares/ExceptionHandlingMiddleware.php` (fix catch block)
|
||||
|
||||
**Testing:**
|
||||
- Throw exception in middleware chain
|
||||
- Verify Response returned (no exit())
|
||||
- Check error logged and aggregated
|
||||
- Verify subsequent middleware not executed
|
||||
|
||||
---
|
||||
|
||||
## Strategy 3: Migrate SecurityEventLogger to ExceptionContextProvider
|
||||
|
||||
### Current State (OLD Architecture)
|
||||
|
||||
**Location:** `src/Framework/ErrorHandling/SecurityEventLogger.php:28-52`
|
||||
|
||||
```php
|
||||
public function logSecurityEvent(
|
||||
SecurityException $exception,
|
||||
ErrorHandlerContext $context // ← OLD architecture
|
||||
): void
|
||||
```
|
||||
|
||||
**Dependencies:** Used by DDoS system (AdaptiveResponseSystem.php:244-250, 371-379)
|
||||
|
||||
### Migration Strategy
|
||||
|
||||
**Solution: Create bridge adapter that converts ExceptionContextProvider to old format**
|
||||
|
||||
**Step 1: Add WeakMap support to SecurityEventLogger**
|
||||
|
||||
```php
|
||||
// Update src/Framework/ErrorHandling/SecurityEventLogger.php
|
||||
|
||||
use App\Framework\ExceptionHandling\Context\ExceptionContextProvider;
|
||||
|
||||
final readonly class SecurityEventLogger
|
||||
{
|
||||
public function __construct(
|
||||
private Logger $logger,
|
||||
private AppConfig $appConfig,
|
||||
private ?ExceptionContextProvider $contextProvider = null // ← NEW
|
||||
) {}
|
||||
|
||||
/**
|
||||
* NEW signature - preferred for new code
|
||||
*/
|
||||
public function logSecurityEventFromException(
|
||||
SecurityException $exception
|
||||
): void {
|
||||
if ($this->contextProvider === null) {
|
||||
throw new \RuntimeException('ExceptionContextProvider required for new logging');
|
||||
}
|
||||
|
||||
// Retrieve context from WeakMap
|
||||
$exceptionContext = $this->contextProvider->get($exception);
|
||||
|
||||
if ($exceptionContext === null) {
|
||||
// Fallback: Create minimal context
|
||||
$exceptionContext = ExceptionContextData::create();
|
||||
}
|
||||
|
||||
// Convert to OWASP format
|
||||
$owaspLog = $this->createOWASPLogFromWeakMap($exception, $exceptionContext);
|
||||
|
||||
// Log via framework logger
|
||||
$this->logToFramework($exception, $owaspLog);
|
||||
}
|
||||
|
||||
/**
|
||||
* LEGACY signature - kept for backward compatibility
|
||||
* @deprecated Use logSecurityEventFromException() instead
|
||||
*/
|
||||
public function logSecurityEvent(
|
||||
SecurityException $exception,
|
||||
ErrorHandlerContext $context
|
||||
): void {
|
||||
// Keep existing implementation for backward compatibility
|
||||
$owaspLog = $this->createOWASPLog($exception, $context);
|
||||
$this->logToFramework($exception, $owaspLog);
|
||||
}
|
||||
|
||||
private function createOWASPLogFromWeakMap(
|
||||
SecurityException $exception,
|
||||
ExceptionContextData $context
|
||||
): array {
|
||||
$securityEvent = $exception->getSecurityEvent();
|
||||
|
||||
return [
|
||||
'datetime' => date('c'),
|
||||
'appid' => $this->appConfig->name,
|
||||
'event' => $securityEvent->getEventIdentifier(),
|
||||
'level' => $securityEvent->getLogLevel()->value,
|
||||
'description' => $securityEvent->getDescription(),
|
||||
'useragent' => $context->userAgent,
|
||||
'source_ip' => $context->clientIp,
|
||||
'host_ip' => $_SERVER['SERVER_ADDR'] ?? 'unknown',
|
||||
'hostname' => $_SERVER['SERVER_NAME'] ?? 'unknown',
|
||||
'protocol' => $_SERVER['SERVER_PROTOCOL'] ?? 'unknown',
|
||||
'port' => $_SERVER['SERVER_PORT'] ?? 'unknown',
|
||||
'request_uri' => $_SERVER['REQUEST_URI'] ?? 'unknown',
|
||||
'request_method' => $_SERVER['REQUEST_METHOD'] ?? 'unknown',
|
||||
'category' => $securityEvent->getCategory(),
|
||||
'requires_alert' => $securityEvent->requiresAlert(),
|
||||
];
|
||||
}
|
||||
|
||||
private function logToFramework(
|
||||
SecurityException $exception,
|
||||
array $owaspLog
|
||||
): void {
|
||||
$securityEvent = $exception->getSecurityEvent();
|
||||
$frameworkLogLevel = $this->mapSecurityLevelToFrameworkLevel(
|
||||
$securityEvent->getLogLevel()
|
||||
);
|
||||
|
||||
$this->logger->log(
|
||||
$frameworkLogLevel,
|
||||
$securityEvent->getDescription(),
|
||||
[
|
||||
'security_event' => $securityEvent->getEventIdentifier(),
|
||||
'security_category' => $securityEvent->getCategory(),
|
||||
'requires_alert' => $securityEvent->requiresAlert(),
|
||||
'owasp_format' => $owaspLog,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: Update DDoS system to use array logging (no SecurityException needed)**
|
||||
|
||||
```php
|
||||
// Update src/Framework/DDoS/Response/AdaptiveResponseSystem.php
|
||||
|
||||
// CURRENT (line 244-250):
|
||||
$this->securityLogger->logSecurityEvent([
|
||||
'event_type' => 'ddos_enhanced_monitoring',
|
||||
'client_ip' => $assessment->clientIp->value,
|
||||
// ...
|
||||
]);
|
||||
|
||||
// AFTER: Keep as-is - this is array-based logging, not SecurityException
|
||||
// No changes needed here
|
||||
```
|
||||
|
||||
**Files to modify:**
|
||||
- ✏️ `src/Framework/ErrorHandling/SecurityEventLogger.php` (add WeakMap support)
|
||||
|
||||
**Files unchanged:**
|
||||
- ✅ `src/Framework/DDoS/Response/AdaptiveResponseSystem.php` (already uses array logging)
|
||||
|
||||
**Testing:**
|
||||
- Trigger DDoS detection
|
||||
- Verify OWASP logs generated
|
||||
- Check both old and new signatures work
|
||||
|
||||
---
|
||||
|
||||
## Strategy 4: Create CLI Error Rendering for ErrorKernel
|
||||
|
||||
### Current State
|
||||
|
||||
**Location:** `src/Framework/Core/AppBootstrapper.php:155-163`
|
||||
|
||||
```php
|
||||
private function registerCliErrorHandler(): void
|
||||
{
|
||||
$output = $this->container->has(ConsoleOutput::class)
|
||||
? $this->container->get(ConsoleOutput::class)
|
||||
: new ConsoleOutput();
|
||||
|
||||
$cliErrorHandler = new CliErrorHandler($output); // ← Legacy
|
||||
$cliErrorHandler->register();
|
||||
}
|
||||
```
|
||||
|
||||
**Legacy CliErrorHandler features:**
|
||||
- Colored console output (ConsoleColor enum)
|
||||
- Exit(1) on fatal errors
|
||||
- Stack trace formatting
|
||||
|
||||
### Migration Strategy
|
||||
|
||||
**Solution: Create CliErrorRenderer for ErrorKernel renderer chain**
|
||||
|
||||
**Step 1: Create CliErrorRenderer**
|
||||
|
||||
```php
|
||||
// Create src/Framework/ExceptionHandling/Renderers/CliErrorRenderer.php
|
||||
|
||||
namespace App\Framework\ExceptionHandling\Renderers;
|
||||
|
||||
use App\Framework\Console\ConsoleOutput;
|
||||
use App\Framework\Console\ConsoleColor;
|
||||
use App\Framework\ExceptionHandling\Context\ExceptionContextProvider;
|
||||
|
||||
final readonly class CliErrorRenderer implements ErrorRenderer
|
||||
{
|
||||
public function __construct(
|
||||
private ConsoleOutput $output
|
||||
) {}
|
||||
|
||||
public function canRender(\Throwable $exception): bool
|
||||
{
|
||||
// Render in CLI context only
|
||||
return PHP_SAPI === 'cli';
|
||||
}
|
||||
|
||||
public function render(
|
||||
\Throwable $exception,
|
||||
?ExceptionContextProvider $contextProvider = null
|
||||
): void {
|
||||
$this->output->writeLine(
|
||||
"❌ Uncaught " . get_class($exception) . ": " . $exception->getMessage(),
|
||||
ConsoleColor::BRIGHT_RED
|
||||
);
|
||||
|
||||
$this->output->writeLine(
|
||||
" File: " . $exception->getFile() . ":" . $exception->getLine(),
|
||||
ConsoleColor::RED
|
||||
);
|
||||
|
||||
if ($exception->getPrevious()) {
|
||||
$this->output->writeLine(
|
||||
" Caused by: " . $exception->getPrevious()->getMessage(),
|
||||
ConsoleColor::YELLOW
|
||||
);
|
||||
}
|
||||
|
||||
$this->output->writeLine(" Stack trace:", ConsoleColor::GRAY);
|
||||
foreach (explode("\n", $exception->getTraceAsString()) as $line) {
|
||||
$this->output->writeLine(" " . $line, ConsoleColor::GRAY);
|
||||
}
|
||||
|
||||
// Context information if available
|
||||
if ($contextProvider !== null) {
|
||||
$context = $contextProvider->get($exception);
|
||||
if ($context !== null && $context->operation !== null) {
|
||||
$this->output->writeLine(
|
||||
" Operation: " . $context->operation,
|
||||
ConsoleColor::CYAN
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: Register CLI renderer in ErrorKernel**
|
||||
|
||||
```php
|
||||
// Update src/Framework/ExceptionHandling/ErrorKernel.php initialization
|
||||
|
||||
private function initializeRendererChain(): void
|
||||
{
|
||||
$renderers = [];
|
||||
|
||||
// CLI renderer (highest priority in CLI context)
|
||||
if (PHP_SAPI === 'cli' && $this->container->has(ConsoleOutput::class)) {
|
||||
$renderers[] = new CliErrorRenderer(
|
||||
$this->container->get(ConsoleOutput::class)
|
||||
);
|
||||
}
|
||||
|
||||
// HTTP renderers
|
||||
$renderers[] = new HtmlErrorRenderer($this->container);
|
||||
$renderers[] = new JsonErrorRenderer();
|
||||
|
||||
$this->rendererChain = new ErrorRendererChain($renderers);
|
||||
}
|
||||
```
|
||||
|
||||
**Step 3: Update AppBootstrapper to use ErrorKernel in CLI**
|
||||
|
||||
```php
|
||||
// Update src/Framework/Core/AppBootstrapper.php
|
||||
|
||||
private function registerCliErrorHandler(): void
|
||||
{
|
||||
// NEW: Use ErrorKernel for CLI (unified architecture)
|
||||
new ExceptionHandlerManager();
|
||||
|
||||
// ErrorKernel will detect CLI context and use CliErrorRenderer
|
||||
// via its renderer chain
|
||||
}
|
||||
```
|
||||
|
||||
**Files to modify:**
|
||||
- ✏️ Create `src/Framework/ExceptionHandling/Renderers/CliErrorRenderer.php`
|
||||
- ✏️ `src/Framework/ExceptionHandling/ErrorKernel.php` (register CLI renderer)
|
||||
- ✏️ `src/Framework/Core/AppBootstrapper.php` (use ErrorKernel in CLI)
|
||||
|
||||
**Files to delete (after migration):**
|
||||
- 🗑️ `src/Framework/ErrorHandling/CliErrorHandler.php` (replaced by CliErrorRenderer)
|
||||
|
||||
**Testing:**
|
||||
- Run console command that throws exception
|
||||
- Verify colored output in terminal
|
||||
- Check stack trace formatting
|
||||
- Verify exit(1) called
|
||||
|
||||
---
|
||||
|
||||
## Strategy 5: Create HTTP Response Generation for ErrorKernel
|
||||
|
||||
### Current State
|
||||
|
||||
Legacy ErrorHandler.createHttpResponse() pattern (lines 71-86, 115-145) provides:
|
||||
- Response generation without terminating
|
||||
- ErrorResponseFactory for API/HTML rendering
|
||||
- Middleware recovery pattern support
|
||||
|
||||
### Migration Strategy
|
||||
|
||||
**Solution: Extract ErrorResponseFactory pattern into ErrorKernel**
|
||||
|
||||
**Step 1: Create ResponseErrorRenderer**
|
||||
|
||||
```php
|
||||
// Create src/Framework/ExceptionHandling/Renderers/ResponseErrorRenderer.php
|
||||
|
||||
namespace App\Framework\ExceptionHandling\Renderers;
|
||||
|
||||
use App\Framework\Http\Response;
|
||||
use App\Framework\Http\Status;
|
||||
use App\Framework\ExceptionHandling\Context\ExceptionContextProvider;
|
||||
use App\Framework\Template\TemplateRenderer;
|
||||
|
||||
final readonly class ResponseErrorRenderer
|
||||
{
|
||||
public function __construct(
|
||||
private ?TemplateRenderer $templateRenderer = null
|
||||
) {}
|
||||
|
||||
public function createResponse(
|
||||
\Throwable $exception,
|
||||
?ExceptionContextProvider $contextProvider = null
|
||||
): Response {
|
||||
// Determine if API or HTML response needed
|
||||
$isApiRequest = $this->isApiRequest();
|
||||
|
||||
if ($isApiRequest) {
|
||||
return $this->createApiResponse($exception, $contextProvider);
|
||||
}
|
||||
|
||||
return $this->createHtmlResponse($exception, $contextProvider);
|
||||
}
|
||||
|
||||
private function createApiResponse(
|
||||
\Throwable $exception,
|
||||
?ExceptionContextProvider $contextProvider
|
||||
): Response {
|
||||
$statusCode = $this->getHttpStatusCode($exception);
|
||||
|
||||
$body = json_encode([
|
||||
'error' => [
|
||||
'message' => $exception->getMessage(),
|
||||
'type' => get_class($exception),
|
||||
'code' => $exception->getCode(),
|
||||
]
|
||||
]);
|
||||
|
||||
return new Response(
|
||||
status: Status::from($statusCode),
|
||||
body: $body,
|
||||
headers: ['Content-Type' => 'application/json']
|
||||
);
|
||||
}
|
||||
|
||||
private function createHtmlResponse(
|
||||
\Throwable $exception,
|
||||
?ExceptionContextProvider $contextProvider
|
||||
): Response {
|
||||
$statusCode = $this->getHttpStatusCode($exception);
|
||||
|
||||
if ($this->templateRenderer !== null) {
|
||||
$body = $this->templateRenderer->render('errors/exception', [
|
||||
'exception' => $exception,
|
||||
'context' => $contextProvider?->get($exception),
|
||||
'statusCode' => $statusCode
|
||||
]);
|
||||
} else {
|
||||
$body = $this->createFallbackHtml($exception, $statusCode);
|
||||
}
|
||||
|
||||
return new Response(
|
||||
status: Status::from($statusCode),
|
||||
body: $body,
|
||||
headers: ['Content-Type' => 'text/html']
|
||||
);
|
||||
}
|
||||
|
||||
private function isApiRequest(): bool
|
||||
{
|
||||
// Check Accept header or URL prefix
|
||||
$accept = $_SERVER['HTTP_ACCEPT'] ?? '';
|
||||
$uri = $_SERVER['REQUEST_URI'] ?? '';
|
||||
|
||||
return str_contains($accept, 'application/json')
|
||||
|| str_starts_with($uri, '/api/');
|
||||
}
|
||||
|
||||
private function getHttpStatusCode(\Throwable $exception): int
|
||||
{
|
||||
// Map exception types to HTTP status codes
|
||||
return match (true) {
|
||||
$exception instanceof \InvalidArgumentException => 400,
|
||||
$exception instanceof \UnauthorizedException => 401,
|
||||
$exception instanceof \ForbiddenException => 403,
|
||||
$exception instanceof \NotFoundException => 404,
|
||||
default => 500
|
||||
};
|
||||
}
|
||||
|
||||
private function createFallbackHtml(\Throwable $exception, int $statusCode): string
|
||||
{
|
||||
return <<<HTML
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Error {$statusCode}</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Error {$statusCode}</h1>
|
||||
<p>{$exception->getMessage()}</p>
|
||||
</body>
|
||||
</html>
|
||||
HTML;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: Integrate into ErrorKernel.createHttpResponse()**
|
||||
|
||||
```php
|
||||
// Update src/Framework/ExceptionHandling/ErrorKernel.php
|
||||
|
||||
private ResponseErrorRenderer $responseRenderer;
|
||||
|
||||
private function initializeResponseRenderer(): void
|
||||
{
|
||||
$templateRenderer = $this->container->has(TemplateRenderer::class)
|
||||
? $this->container->get(TemplateRenderer::class)
|
||||
: null;
|
||||
|
||||
$this->responseRenderer = new ResponseErrorRenderer($templateRenderer);
|
||||
}
|
||||
|
||||
public function createHttpResponse(\Throwable $exception): Response
|
||||
{
|
||||
// Initialize context
|
||||
if ($this->contextProvider === null) {
|
||||
$this->initializeContext($exception);
|
||||
}
|
||||
|
||||
// Enrich from request
|
||||
$this->enrichContextFromRequest($exception);
|
||||
|
||||
// Create Response
|
||||
$response = $this->responseRenderer->createResponse(
|
||||
$exception,
|
||||
$this->contextProvider
|
||||
);
|
||||
|
||||
// Log error (without terminating)
|
||||
$this->logError($exception);
|
||||
|
||||
// Dispatch to aggregator
|
||||
$this->dispatchToErrorAggregator($exception);
|
||||
|
||||
return $response;
|
||||
}
|
||||
```
|
||||
|
||||
**Files to modify:**
|
||||
- ✏️ Create `src/Framework/ExceptionHandling/Renderers/ResponseErrorRenderer.php`
|
||||
- ✏️ `src/Framework/ExceptionHandling/ErrorKernel.php` (add createHttpResponse())
|
||||
|
||||
**Testing:**
|
||||
- Throw exception in middleware
|
||||
- Verify JSON response for /api/* routes
|
||||
- Verify HTML response for web routes
|
||||
- Check status codes correct
|
||||
|
||||
---
|
||||
|
||||
## Migration Execution Plan
|
||||
|
||||
### Phase 5a: Preparation (Current Phase)
|
||||
- ✅ Document all 5 strategies
|
||||
- ⏳ Review strategies with team
|
||||
- ⏳ Create feature branch: `feature/migrate-errorhandling-module`
|
||||
|
||||
### Phase 5b: Implementation Order
|
||||
|
||||
**Week 1: Foundation**
|
||||
1. Strategy 5: HTTP Response generation (enables middleware recovery)
|
||||
2. Strategy 2: Fix ExceptionHandlingMiddleware (depends on Strategy 5)
|
||||
|
||||
**Week 2: Compatibility**
|
||||
3. Strategy 1: ErrorAggregator signature fix (critical for logging)
|
||||
4. Strategy 3: SecurityEventLogger migration (preserves DDoS logging)
|
||||
|
||||
**Week 3: CLI Support**
|
||||
5. Strategy 4: CLI error rendering (replaces CliErrorHandler)
|
||||
|
||||
**Week 4: Cleanup**
|
||||
6. Remove legacy ErrorHandling module
|
||||
7. Update all import statements
|
||||
8. Run full test suite
|
||||
|
||||
### Testing Strategy
|
||||
|
||||
**Per-Strategy Testing:**
|
||||
- Unit tests for new components
|
||||
- Integration tests for error flows
|
||||
- Manual testing in development environment
|
||||
|
||||
**Final Integration Testing:**
|
||||
- Trigger errors in web context → verify HTTP Response
|
||||
- Trigger errors in CLI context → verify colored output
|
||||
- Trigger security events → verify OWASP logs
|
||||
- Trigger DDoS detection → verify adaptive response
|
||||
- Check ErrorAggregator dashboard → verify context preserved
|
||||
|
||||
### Rollback Plan
|
||||
|
||||
Each strategy is independent and can be rolled back:
|
||||
- Strategy 1: Remove adapter method
|
||||
- Strategy 2: Revert middleware catch block
|
||||
- Strategy 3: Remove WeakMap support from SecurityEventLogger
|
||||
- Strategy 4: Keep CliErrorHandler active
|
||||
- Strategy 5: Don't use createHttpResponse()
|
||||
|
||||
---
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- ✅ All 5 blockers resolved
|
||||
- ✅ Zero breaking changes to public APIs
|
||||
- ✅ DDoS system continues functioning
|
||||
- ✅ CLI error handling preserved
|
||||
- ✅ Middleware recovery pattern works
|
||||
- ✅ ErrorAggregator receives correct context
|
||||
- ✅ All tests passing
|
||||
- ✅ Legacy ErrorHandling module deleted
|
||||
|
||||
---
|
||||
|
||||
## Next Actions
|
||||
|
||||
**Immediate (Phase 5b start):**
|
||||
1. Create feature branch: `git checkout -b feature/migrate-errorhandling-module`
|
||||
2. Implement Strategy 5 (HTTP Response generation)
|
||||
3. Implement Strategy 2 (Fix middleware)
|
||||
4. Run tests and verify middleware recovery
|
||||
|
||||
**This Week:**
|
||||
- Complete Strategies 1-2
|
||||
- Manual testing in development
|
||||
|
||||
**Next Week:**
|
||||
- Complete Strategies 3-5
|
||||
- Integration testing
|
||||
- Code review
|
||||
|
||||
**Final Week:**
|
||||
- Remove legacy module
|
||||
- Documentation updates
|
||||
- Production deployment
|
||||
Reference in New Issue
Block a user