# 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 << Error {$statusCode}

Error {$statusCode}

{$exception->getMessage()}

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