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.
799 lines
23 KiB
Markdown
799 lines
23 KiB
Markdown
# 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
|