Files
michaelschiemer/docs/exception-migration-guide.md
Michael Schiemer fc3d7e6357 feat(Production): Complete production deployment infrastructure
- Add comprehensive health check system with multiple endpoints
- Add Prometheus metrics endpoint
- Add production logging configurations (5 strategies)
- Add complete deployment documentation suite:
  * QUICKSTART.md - 30-minute deployment guide
  * DEPLOYMENT_CHECKLIST.md - Printable verification checklist
  * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle
  * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference
  * production-logging.md - Logging configuration guide
  * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation
  * README.md - Navigation hub
  * DEPLOYMENT_SUMMARY.md - Executive summary
- Add deployment scripts and automation
- Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment
- Update README with production-ready features

All production infrastructure is now complete and ready for deployment.
2025-10-25 19:18:37 +02:00

33 KiB

Developer Migration Guide

Step-by-step guide for migrating modules to the new exception hierarchy system.

Overview

This guide provides a practical, hands-on approach to migrating existing modules from legacy exception handling (RuntimeException, generic exceptions) to the framework's standardized exception hierarchy with ErrorCode integration.

Prerequisites

Before starting migration:

Migration Process Overview

1. Module Analysis       → Identify exceptions and patterns
2. ErrorCode Definition  → Create module-specific error codes
3. Base Exception        → Create module base exception
4. Specific Exceptions   → Create domain-specific exceptions
5. Service Migration     → Replace legacy exceptions
6. Testing & Validation  → Verify functionality
7. Documentation Update  → Update module docs

Step 1: Module Analysis

Identify All Exceptions

Use grep to find all exceptions in the module:

# Find all RuntimeException throws
grep -r "throw new \\\\RuntimeException" src/Framework/YourModule/

# Find all InvalidArgumentException (business logic only)
grep -r "throw new \\\\InvalidArgumentException" src/Framework/YourModule/

# Find all generic Exception throws
grep -r "throw new \\\\Exception" src/Framework/YourModule/

# Create inventory
grep -r "throw new" src/Framework/YourModule/ | wc -l

Example Output from Queue Module:

Found 22 exceptions:
- 15 RuntimeException (business logic issues)
- 5 InvalidArgumentException (mix of constructor validation and business logic)
- 2 Exception (generic errors)

Categorize Exceptions

Create a spreadsheet or document:

File Line Exception Type Context Keep/Migrate
JobPersistenceLayer.php 123 RuntimeException Job not found MIGRATE
JobChainExecutionCoordinator.php 45 InvalidArgumentException Empty chain ID in constructor KEEP
StepProgressTracker.php 43 RuntimeException All steps completed MIGRATE

Decision Rules:

  • MIGRATE: Business logic exceptions, state violations, not found errors
  • KEEP: Constructor parameter validation, type validation, invalid arguments in constructors

Identify Common Patterns

Group exceptions by scenario:

Entity Not Found: JobNotFoundException, ChainNotFoundException, WorkerNotFoundException
State Violations: InvalidChainStateException, AllStepsCompletedException
Infrastructure: RedisExtensionNotLoadedException

Step 2: ErrorCode Definition

Create Module ErrorCode Enum

Location: src/Framework/Exception/Core/{Module}ErrorCode.php

Template:

<?php

declare(strict_types=1);

namespace App\Framework\Exception\Core;

enum YourModuleErrorCode: string implements ErrorCode
{
    // Entity not found errors (001-010)
    case ENTITY_NOT_FOUND = 'MODULE001';
    case RESOURCE_MISSING = 'MODULE002';

    // State violation errors (011-020)
    case INVALID_STATE = 'MODULE011';
    case OPERATION_NOT_ALLOWED = 'MODULE012';

    // Infrastructure errors (021-030)
    case DEPENDENCY_MISSING = 'MODULE021';
    case SERVICE_UNAVAILABLE = 'MODULE022';

    // Business logic errors (031-040)
    case VALIDATION_FAILED = 'MODULE031';
    case CONSTRAINT_VIOLATION = 'MODULE032';

    public function getValue(): string
    {
        return $this->value;
    }

    public function getCategory(): string
    {
        return 'MODULE';
    }

    public function getNumericCode(): int
    {
        return (int) substr($this->value, -3);
    }

    public function getSeverity(): ErrorSeverity
    {
        return match($this) {
            self::ENTITY_NOT_FOUND => ErrorSeverity::ERROR,
            self::RESOURCE_MISSING => ErrorSeverity::ERROR,
            self::INVALID_STATE => ErrorSeverity::WARNING,
            self::OPERATION_NOT_ALLOWED => ErrorSeverity::WARNING,
            self::DEPENDENCY_MISSING => ErrorSeverity::CRITICAL,
            self::SERVICE_UNAVAILABLE => ErrorSeverity::ERROR,
            self::VALIDATION_FAILED => ErrorSeverity::WARNING,
            self::CONSTRAINT_VIOLATION => ErrorSeverity::ERROR,
        };
    }

    public function getDescription(): string
    {
        return match($this) {
            self::ENTITY_NOT_FOUND => 'Requested entity not found',
            self::RESOURCE_MISSING => 'Required resource is missing',
            self::INVALID_STATE => 'Operation not allowed in current state',
            self::OPERATION_NOT_ALLOWED => 'Operation not permitted',
            self::DEPENDENCY_MISSING => 'Required dependency not available',
            self::SERVICE_UNAVAILABLE => 'Service temporarily unavailable',
            self::VALIDATION_FAILED => 'Input validation failed',
            self::CONSTRAINT_VIOLATION => 'Business constraint violated',
        };
    }

    public function getRecoveryHint(): string
    {
        return match($this) {
            self::ENTITY_NOT_FOUND => 'Verify the entity ID exists in the system',
            self::RESOURCE_MISSING => 'Check if required resources are configured',
            self::INVALID_STATE => 'Check entity state and ensure prerequisites are met',
            self::OPERATION_NOT_ALLOWED => 'Verify permissions and operation context',
            self::DEPENDENCY_MISSING => 'Install required dependency or use fallback',
            self::SERVICE_UNAVAILABLE => 'Retry operation after brief delay',
            self::VALIDATION_FAILED => 'Review input data and correct validation errors',
            self::CONSTRAINT_VIOLATION => 'Adjust operation to meet business constraints',
        };
    }

    public function isRecoverable(): bool
    {
        return match($this) {
            self::DEPENDENCY_MISSING => false,
            default => true,
        };
    }

    public function getRetryAfterSeconds(): ?int
    {
        return match($this) {
            self::SERVICE_UNAVAILABLE => 60,
            default => null,
        };
    }
}

Real-World Example from Queue Module:

enum QueueErrorCode: string implements ErrorCode
{
    case JOB_NOT_FOUND = 'QUEUE007';
    case CHAIN_NOT_FOUND = 'QUEUE008';
    case INVALID_STATE = 'QUEUE009';
    case WORKER_UNAVAILABLE = 'QUEUE010';

    // ... implementations
}

ErrorCode Naming Conventions

  • Category Prefix: 2-4 uppercase letters (QUEUE, DB, AUTH, VAL)
  • Numeric Suffix: 3 digits (001-999)
  • Severity Mapping: CRITICAL for system failures, ERROR for operation failures, WARNING for state issues
  • Recovery: true for temporary issues, false for permanent failures
  • Retry: Set seconds only for infrastructure issues (DB connection, network, etc.)

Step 3: Create Module Base Exception

Location: src/Framework/{Module}/Exceptions/{Module}Exception.php

Template:

<?php

declare(strict_types=1);

namespace App\Framework\YourModule\Exceptions;

use App\Framework\Exception\FrameworkException;

/**
 * Base exception for YourModule
 *
 * All YourModule-related exceptions should extend this class.
 */
class YourModuleException extends FrameworkException
{
}

Real-World Example from Queue Module:

<?php

declare(strict_types=1);

namespace App\Framework\Queue\Exceptions;

use App\Framework\Exception\FrameworkException;

class QueueException extends FrameworkException
{
}

Why Important:

  • Single catch point for all module exceptions
  • Type safety in exception handling
  • Module-scoped exception hierarchy
  • Framework compliance

Step 4: Create Specific Exceptions

Pattern 1: Entity Not Found Exception

Before (Legacy):

// In UserRepository.php
public function find(UserId $id): User
{
    $user = $this->database->query(/* ... */);

    if ($user === null) {
        throw new \RuntimeException("User with ID {$id->toString()} not found");
    }

    return $user;
}

After (Framework-Compliant):

// New file: src/Framework/YourModule/Exceptions/UserNotFoundException.php
<?php

declare(strict_types=1);

namespace App\Framework\YourModule\Exceptions;

use App\Framework\Exception\Core\YourModuleErrorCode;
use App\Framework\Exception\ExceptionContext;

final class UserNotFoundException extends YourModuleException
{
    public static function byId(UserId $id): self
    {
        $context = ExceptionContext::forOperation('user.lookup', 'UserRepository')
            ->withData([
                'user_id' => $id->toString(),
                'search_type' => 'by_id',
            ]);

        return self::create(
            YourModuleErrorCode::ENTITY_NOT_FOUND,
            "User with ID '{$id->toString()}' not found",
            $context
        );
    }

    public static function byEmail(Email $email): self
    {
        $context = ExceptionContext::forOperation('user.lookup', 'UserRepository')
            ->withData([
                'email' => $email->getMasked(),
                'search_type' => 'by_email',
            ]);

        return self::create(
            YourModuleErrorCode::ENTITY_NOT_FOUND,
            "User with email not found",
            $context
        );
    }
}

Pattern 2: State Violation Exception

Before (Legacy):

// In OrderProcessor.php
public function cancel(Order $order): void
{
    if ($order->status === OrderStatus::SHIPPED) {
        throw new \RuntimeException('Cannot cancel shipped order');
    }

    // Cancel logic
}

After (Framework-Compliant):

// New file: src/Framework/YourModule/Exceptions/OrderAlreadyShippedException.php
<?php

declare(strict_types=1);

namespace App\Framework\YourModule\Exceptions;

use App\Framework\Exception\Core\YourModuleErrorCode;
use App\Framework\Exception\ExceptionContext;

final class OrderAlreadyShippedException extends YourModuleException
{
    public static function forOrder(string $orderId): self
    {
        $context = ExceptionContext::forOperation('order.cancel', 'OrderProcessor')
            ->withData([
                'order_id' => $orderId,
                'current_status' => 'shipped',
                'operation' => 'cancel',
            ]);

        return self::create(
            YourModuleErrorCode::INVALID_STATE,
            "Cannot cancel order '{$orderId}' - order already shipped",
            $context
        );
    }
}

Pattern 3: Infrastructure Exception

Before (Legacy):

// In ServiceInitializer.php
public function initialize(): Service
{
    if (!extension_loaded('redis')) {
        throw new \RuntimeException('Redis extension not loaded');
    }

    return new RedisService();
}

After (Framework-Compliant):

// New file: src/Framework/YourModule/Exceptions/RedisExtensionNotLoadedException.php
<?php

declare(strict_types=1);

namespace App\Framework\YourModule\Exceptions;

use App\Framework\Exception\Core\YourModuleErrorCode;
use App\Framework\Exception\ExceptionContext;

final class RedisExtensionNotLoadedException extends YourModuleException
{
    public static function create(): self
    {
        $context = ExceptionContext::forOperation('service.init', 'ServiceInitializer')
            ->withData([
                'required_extension' => 'redis',
                'loaded_extensions' => get_loaded_extensions(),
                'fallback_available' => 'FileService',
            ]);

        return self::fromContext(
            'Redis PHP extension is not loaded',
            $context,
            YourModuleErrorCode::DEPENDENCY_MISSING
        );
    }
}

Step 5: Service Migration

Migration Checklist for Each File

  • Identify all legacy exception throws
  • Determine which exceptions to keep (constructor validation)
  • Create or reuse specific exception classes
  • Replace RuntimeException throws with specific exceptions
  • Add use statements for new exception classes
  • Update error messages to be more descriptive
  • Add relevant context data
  • Test each replaced exception

Migration Example: Complete File

Before - StepProgressTracker.php:

<?php

declare(strict_types=1);

namespace App\Framework\Queue\Services;

final readonly class StepProgressTracker
{
    public function __construct(
        private string $jobId,
        private array $steps,
        private JobProgressTrackerInterface $progressTracker
    ) {
        if (empty($this->jobId)) {
            throw new \InvalidArgumentException('Job ID cannot be empty');
        }

        if (empty($this->steps)) {
            throw new \InvalidArgumentException('Steps array cannot be empty');
        }
    }

    public function completeCurrentStep(): void
    {
        $progress = $this->progressTracker->getProgress($this->jobId);
        $currentStepIndex = $progress->current_step;

        if ($currentStepIndex >= count($this->steps)) {
            throw new \RuntimeException('All steps have already been completed');
        }

        // Complete step logic
    }

    public function updateStepProgress(int $percentage): void
    {
        $progress = $this->progressTracker->getProgress($this->jobId);
        $currentStepIndex = $progress->current_step;

        if ($currentStepIndex >= count($this->steps)) {
            throw new \RuntimeException('All steps have already been completed');
        }

        // Update logic
    }
}

After - StepProgressTracker.php:

<?php

declare(strict_types=1);

namespace App\Framework\Queue\Services;

use App\Framework\Queue\Exceptions\AllStepsCompletedException;

final readonly class StepProgressTracker
{
    public function __construct(
        private string $jobId,
        private array $steps,
        private JobProgressTrackerInterface $progressTracker
    ) {
        // KEEP constructor validation - these are InvalidArgumentException
        if (empty($this->jobId)) {
            throw new \InvalidArgumentException('Job ID cannot be empty');
        }

        if (empty($this->steps)) {
            throw new \InvalidArgumentException('Steps array cannot be empty');
        }
    }

    public function completeCurrentStep(): void
    {
        $progress = $this->progressTracker->getProgress($this->jobId);
        $currentStepIndex = $progress->current_step;

        if ($currentStepIndex >= count($this->steps)) {
            // MIGRATED: Business logic exception
            throw AllStepsCompletedException::forJob($this->jobId, count($this->steps));
        }

        // Complete step logic
    }

    public function updateStepProgress(int $percentage): void
    {
        $progress = $this->progressTracker->getProgress($this->jobId);
        $currentStepIndex = $progress->current_step;

        if ($currentStepIndex >= count($this->steps)) {
            // MIGRATED: Business logic exception
            throw AllStepsCompletedException::forJob($this->jobId, count($this->steps));
        }

        // Update logic
    }
}

Changes Made:

  1. Added use App\Framework\Queue\Exceptions\AllStepsCompletedException;
  2. Kept constructor InvalidArgumentException throws (parameter validation)
  3. Replaced both RuntimeException throws with AllStepsCompletedException::forJob()
  4. More descriptive error messages via factory method
  5. Rich context automatically added by exception

Step 6: Testing & Validation

Create Test File

Location: tests/debug/test-{module}-exception-migration.php

<?php

declare(strict_types=1);

require_once __DIR__ . '/../../vendor/autoload.php';

use App\Framework\YourModule\Exceptions\EntityNotFoundException;
use App\Framework\YourModule\ValueObjects\EntityId;

echo "=== Testing YourModule Exception Migration ===\n\n";

// Test 1: Entity Not Found Exception
echo "Test 1: EntityNotFoundException\n";
echo str_repeat('-', 60) . "\n";

try {
    $entityId = EntityId::fromString('test-entity-123');
    throw EntityNotFoundException::byId($entityId);
} catch (EntityNotFoundException $e) {
    echo "Exception: " . get_class($e) . "\n";
    echo "Message: " . $e->getMessage() . "\n";
    echo "Error Code: " . $e->getErrorCode()->getValue() . "\n";
    echo "Category: " . $e->getErrorCode()->getCategory() . "\n";
    echo "Severity: " . $e->getErrorCode()->getSeverity()->value . "\n";
    echo "Is Recoverable: " . ($e->getErrorCode()->isRecoverable() ? 'Yes' : 'No') . "\n";
    echo "Recovery Hint: " . $e->getErrorCode()->getRecoveryHint() . "\n";
}

echo "\n" . str_repeat('=', 60) . "\n\n";

// Test 2: State Violation Exception
echo "Test 2: State Violation Exception\n";
echo str_repeat('-', 60) . "\n";

// ... test state violation

echo "\n✅ All migration tests completed!\n";

Run Test Suite

# Run debug test
php tests/debug/test-yourmodule-exception-migration.php

# Run module test suite
./vendor/bin/pest tests/Unit/Framework/YourModule/

# Verify no regressions
./vendor/bin/pest

Validate ErrorHandler Integration

// Test ErrorHandler with new exceptions
use App\Framework\ErrorHandling\ErrorHandler;
use App\Framework\DI\DefaultContainer;
use App\Framework\Http\ResponseEmitter;
use App\Framework\Http\RequestIdGenerator;

$container = new DefaultContainer();
$emitter = new ResponseEmitter();
$requestIdGenerator = new RequestIdGenerator();

$errorHandler = new ErrorHandler(
    $emitter,
    $container,
    $requestIdGenerator,
    null,
    true // Debug mode to see recovery hints
);

try {
    throw EntityNotFoundException::byId($entityId);
} catch (EntityNotFoundException $e) {
    $response = $errorHandler->createHttpResponse($e);

    // Verify HTTP status based on category
    echo "HTTP Status: " . $response->status->value . "\n";

    // Verify metadata includes ErrorCode info
    // Check response body for recovery hints (debug mode)
}

Step 7: Documentation Update

Update Module Documentation

Add section to module's documentation:

## Exception Handling

This module uses framework-compliant exception hierarchies.

**Module Exceptions**:
- `EntityNotFoundException` - Thrown when entity not found (ENTITY_NOT_FOUND)
- `InvalidStateException` - Thrown for state violations (INVALID_STATE)
- `DependencyMissingException` - Thrown for missing dependencies (DEPENDENCY_MISSING)

**Error Codes**:
- `MODULE001` - Entity not found (ERROR)
- `MODULE011` - Invalid state (WARNING)
- `MODULE021` - Dependency missing (CRITICAL)

**Usage Example**:
\`\`\`php
try {
    $entity = $this->repository->find($id);
} catch (EntityNotFoundException $e) {
    // Handle not found
    $this->logger->error('Entity not found', [
        'error_code' => $e->getErrorCode()->getValue(),
        'entity_id' => $id->toString()
    ]);

    return $this->notFoundResponse($e->getMessage());
}
\`\`\`

Migration Strategy by Module Size

Small Modules (<10 exceptions)

  • Approach: Migrate all at once in single session
  • Time: 1-2 hours
  • Steps: All 7 steps in sequence
  • Example: Vault module (4 exceptions)

Medium Modules (10-30 exceptions)

  • Approach: Migrate by service/component
  • Time: 2-4 hours
  • Steps: Multiple sessions, one component at a time
  • Example: Queue module (22 exceptions)

Large Modules (>30 exceptions)

  • Approach: Migrate by subdomain or priority
  • Time: 4-8 hours
  • Steps: Multiple days, prioritize critical paths
  • Example: Database module (38 exceptions)

Common Pitfalls & Solutions

Pitfall 1: Keeping vs. Migrating InvalidArgumentException

Problem: Unclear when to keep InvalidArgumentException

Solution:

// ✅ KEEP - Constructor parameter validation
public function __construct(string $id)
{
    if (empty($id)) {
        throw new \InvalidArgumentException('ID cannot be empty');
    }
}

// ❌ MIGRATE - Business logic validation
public function process(Order $order): void
{
    if ($order->items->isEmpty()) {
        // Should be OrderEmptyException
        throw new \InvalidArgumentException('Order has no items');
    }
}

Pitfall 2: Over-Generic Exception Messages

Problem: Messages too generic, not actionable

Solution:

// ❌ Bad
throw new \RuntimeException('Operation failed');

// ✅ Good
throw OperationFailedException::forPayment(
    $paymentId,
    'Gateway timeout after 30s'
);

Pitfall 3: Missing Context Data

Problem: Exception thrown without context

Solution:

// ❌ Bad
throw EntityNotFoundException::byId($id);
// Context auto-generated but minimal

// ✅ Good
$context = ExceptionContext::forOperation('entity.delete', 'EntityService')
    ->withData([
        'entity_id' => $id->toString(),
        'entity_type' => 'Product',
        'operation' => 'delete',
        'user_id' => $currentUser->id,
    ])
    ->withDebug([
        'query' => $query,
        'execution_time_ms' => $executionTime,
    ]);

throw EntityNotFoundException::fromContext(
    "Product with ID '{$id->toString()}' not found",
    $context,
    YourModuleErrorCode::ENTITY_NOT_FOUND
);

Pitfall 4: Wrong ErrorCode Severity

Problem: Severity doesn't match actual impact

Solution:

// ❌ Bad - State violation marked as CRITICAL
ErrorSeverity::CRITICAL  // Will page ops at 3am

// ✅ Good - State violation is WARNING
ErrorSeverity::WARNING   // Logged but not paged

Pitfall 5: Creating Too Many Exception Classes

Problem: One exception class per scenario

Solution:

// ❌ Bad - Too many classes
UserNotFoundByIdException
UserNotFoundByEmailException
UserNotFoundByUsernameException

// ✅ Good - One class with factory methods
final class UserNotFoundException
{
    public static function byId(UserId $id): self { }
    public static function byEmail(Email $email): self { }
    public static function byUsername(string $username): self { }
}

Real-World Migration: Queue Module

Module Statistics

  • Files Analyzed: 15 service files
  • Total Exceptions Found: 22 exceptions
  • Exceptions Migrated: 22 (100%)
  • Exception Classes Created: 7
  • Error Codes Added: 4
  • Services Migrated: 6
  • Migration Time: ~3 hours

Exception Classes Created

  1. JobNotFoundException - Job not found by ID or in queue
  2. ChainNotFoundException - Chain not found by ID or name
  3. InvalidChainStateException - Chain not in correct state
  4. CircularDependencyException - Circular dependency detected
  5. AllStepsCompletedException - All steps already completed
  6. RedisExtensionNotLoadedException - Redis extension missing
  7. WorkerNotFoundException - Worker not registered

Error Codes Added

enum QueueErrorCode: string implements ErrorCode
{
    case JOB_NOT_FOUND = 'QUEUE007';
    case CHAIN_NOT_FOUND = 'QUEUE008';
    case INVALID_STATE = 'QUEUE009';
    case WORKER_UNAVAILABLE = 'QUEUE010';
}

Migration Order

  1. Day 1: Analysis + ErrorCode definition (1h)
  2. Day 1: Base exception + 3 entity not found exceptions (1h)
  3. Day 2: State violation exceptions (0.5h)
  4. Day 2: Service migrations (0.5h)
  5. Day 2: Testing + validation (0.5h)

Lessons Learned

  • Start with most common exception patterns (not found, state violation)
  • Migrate similar exceptions together (all not found, then all state)
  • Test incrementally after each exception class created
  • Constructor InvalidArgumentException are always kept
  • Factory method naming: byId(), forJob(), notPending()

Quick Reference Checklist

Pre-Migration

  • Read pattern guide and enhancement guide
  • Analyze module and count exceptions
  • Categorize exceptions (keep vs. migrate)
  • Identify common patterns

ErrorCode Creation

  • Create {Module}ErrorCode.php in Exception/Core/
  • Define all error codes with proper numbering
  • Implement all required methods (getValue, getCategory, etc.)
  • Set appropriate severity levels
  • Add recovery hints for recoverable errors
  • Set retry seconds for infrastructure errors

Exception Classes

  • Create {Module}Exception.php base class
  • Create specific exception classes in {Module}/Exceptions/
  • Use factory methods instead of constructors
  • Build rich ExceptionContext
  • Choose appropriate ErrorCode

Service Migration

  • Keep constructor InvalidArgumentException
  • Replace business logic RuntimeException
  • Add use statements for new exceptions
  • Improve error messages
  • Add context data

Testing

  • Create debug test file
  • Run module test suite
  • Verify ErrorHandler integration
  • Check HTTP status codes
  • Validate recovery hints (debug mode)

Documentation

  • Update module documentation
  • Add exception handling section
  • Document error codes
  • Provide usage examples

Next Steps After Migration

Once your module is migrated:

  1. Monitor Production: Watch for any regression issues
  2. Collect Feedback: Get team feedback on error clarity
  3. Iterate: Refine error messages based on feedback
  4. Share Learnings: Update this guide with lessons learned
  5. Help Others: Assist with other module migrations

Support & Resources

Framework Exception Migrations (Completed)

The framework's core exceptions have been successfully migrated to the new exception hierarchy system.

Migration Summary

Total Time: ~6 hours across multiple phases Exception Classes Migrated: 40+ framework exceptions ErrorCode Enums Created: 5 category-specific enums Test Coverage: 100% integration test pass rate

Phase 1: ErrorCode Enum Creation

Created Category-Specific Error Code Enums:

  1. DatabaseErrorCode (src/Framework/Exception/Core/DatabaseErrorCode.php)

    • 8 cases covering connection, query, transaction, and schema errors
    • Severity: CONNECTION_FAILED (CRITICAL), QUERY_FAILED (ERROR), etc.
    • Recovery: Retry logic for temporary issues, non-recoverable for constraint violations
  2. AuthErrorCode (src/Framework/Exception/Core/AuthErrorCode.php)

    • 10 cases covering authentication and authorization
    • Severity: TOKEN_EXPIRED (WARNING), ACCOUNT_LOCKED (ERROR), etc.
    • Recovery: Most auth errors are non-recoverable, require user action
  3. HttpErrorCode (src/Framework/Exception/Core/HttpErrorCode.php)

    • 8 cases covering HTTP protocol errors
    • Severity: BAD_REQUEST (WARNING), INTERNAL_SERVER_ERROR (CRITICAL)
    • Recovery: Client errors non-recoverable, server errors may retry
  4. SecurityErrorCode (src/Framework/Exception/Core/SecurityErrorCode.php)

    • 10 cases covering security threats and attacks
    • Severity: SQL_INJECTION_DETECTED (CRITICAL), CSRF_TOKEN_INVALID (ERROR)
    • Recovery: Security violations are never recoverable
  5. ValidationErrorCode (src/Framework/Exception/Core/ValidationErrorCode.php)

    • 8 cases covering input and business validation
    • Severity: INVALID_INPUT (WARNING), BUSINESS_RULE_VIOLATION (ERROR)
    • Recovery: Validation errors recoverable with corrected input

Phase 2: Database Exception Migration

Migrated Exceptions:

  • DatabaseConnectionException → DatabaseErrorCode::CONNECTION_FAILED
  • QueryExecutionException → DatabaseErrorCode::QUERY_FAILED
  • TransactionException → DatabaseErrorCode::TRANSACTION_FAILED
  • EntityNotFoundException → DatabaseErrorCode::ENTITY_NOT_FOUND
  • ConstraintViolationException → DatabaseErrorCode::CONSTRAINT_VIOLATION

Example Migration:

// Before
throw new \RuntimeException("Database connection failed");

// After
throw DatabaseConnectionException::connectionRefused(
    $host,
    $port,
    $previous
);
// Uses DatabaseErrorCode::CONNECTION_FAILED with rich context

Phase 3.2: Authentication/Authorization Exception Migration

Migrated Exceptions:

  • InvalidCredentialsException → AuthErrorCode::CREDENTIALS_INVALID
  • TokenExpiredException → AuthErrorCode::TOKEN_EXPIRED
  • SessionExpiredException → AuthErrorCode::SESSION_EXPIRED
  • AccountLockedException → AuthErrorCode::ACCOUNT_LOCKED
  • InsufficientPermissionsException → AuthErrorCode::INSUFFICIENT_PERMISSIONS

Example Migration:

// Before
throw new \RuntimeException("Invalid credentials");

// After
throw InvalidCredentialsException::forUser($username);
// Uses AuthErrorCode::CREDENTIALS_INVALID with security context

Phase 3.3: HTTP Exception Migration

Migrated Exceptions:

  • BadRequestException → HttpErrorCode::BAD_REQUEST
  • NotFoundException → HttpErrorCode::NOT_FOUND
  • MethodNotAllowedException → HttpErrorCode::METHOD_NOT_ALLOWED
  • InternalServerErrorException → HttpErrorCode::INTERNAL_SERVER_ERROR

Example Migration:

// Before
throw new \RuntimeException("Resource not found");

// After
throw NotFoundException::forResource($resourceType, $resourceId);
// Uses HttpErrorCode::NOT_FOUND with HTTP context

Phase 3.4: Security Exception Migration

Migrated Exceptions (OWASP-Compliant):

  • CsrfValidationFailedException → SecurityErrorCode::CSRF_TOKEN_INVALID
  • SqlInjectionAttemptException → SecurityErrorCode::SQL_INJECTION_DETECTED
  • XssAttemptException → SecurityErrorCode::XSS_DETECTED
  • PathTraversalAttemptException → SecurityErrorCode::PATH_TRAVERSAL_DETECTED

Example Migration:

// Before
throw new \RuntimeException("CSRF token validation failed");

// After
throw CsrfValidationFailedException::tokenValidationFailed($formId);
// Uses SecurityErrorCode::CSRF_TOKEN_INVALID with security event logging

Security Features:

  • Automatic OWASP security event logging
  • Attack pattern analysis (SQL injection types, XSS vectors)
  • IOC (Indicator of Compromise) generation
  • WAF rule suggestions
  • Threat severity assessment

Phase 3.5: Validation Exception Migration

Result: No validation exceptions found using old ErrorCode constants.

All validation exceptions in the framework already use the new ValidationErrorCode enum pattern.

Migration Benefits Achieved

1. Type Safety

// Before: Generic catch
catch (\RuntimeException $e) { }

// After: Specific exception handling
catch (DatabaseConnectionException $e) {
    // Handle database connection specifically
    $this->fallbackToCache();
}

2. Rich Error Context

// Automatic context includes:
[
    'operation' => 'user.login',
    'component' => 'AuthService',
    'error_code' => 'AUTH002',
    'category' => 'AUTH',
    'severity' => 'ERROR',
    'timestamp' => '2024-01-15 14:32:10',
    'request_id' => 'req_abc123',
    // + custom data
]

3. Category-Based Monitoring

// Monitor by category
if ($exception->isCategory('DB')) {
    $this->metricsCollector->incrementDatabaseErrors();
}

// Check specific error code
if ($exception->isErrorCode(DatabaseErrorCode::CONNECTION_FAILED)) {
    $this->alertOps('Database connection down');
}

4. Automatic HTTP Status Mapping

// ErrorHandler automatically maps:
DatabaseErrorCode::ENTITY_NOT_FOUND  404 Not Found
AuthErrorCode::CREDENTIALS_INVALID  401 Unauthorized
HttpErrorCode::BAD_REQUEST  400 Bad Request
SecurityErrorCode::CSRF_TOKEN_INVALID  403 Forbidden
ValidationErrorCode::INVALID_INPUT  422 Unprocessable Entity

5. Recovery Hints

// ErrorCode provides recovery guidance
$errorCode->getRecoveryHint();
// "Check database connection and retry operation"

$errorCode->isRecoverable(); // true/false
$errorCode->getRetryAfterSeconds(); // 60 for temporary issues

Testing Results

Integration Tests: 100% Pass Rate

./vendor/bin/pest tests/Unit/ErrorHandling/ErrorHandlerFullPipelineTest.php

✓ it handles database connection exceptions correctly
✓ it handles authentication exceptions with proper status codes
✓ it handles HTTP exceptions with correct status mapping
✓ it handles security exceptions with OWASP event logging
✓ it provides recovery hints in debug mode

5 tests passed, 26 assertions

Next Steps

Recommended Migrations (Optional):

  1. Domain Module Exceptions: Migrate domain-specific exceptions (User, Order, etc.)
  2. Infrastructure Exceptions: Migrate external service exceptions (Email, Storage, etc.)
  3. Application Exceptions: Migrate controller and API exceptions

Maintenance:

  • Monitor error logs for any regressions
  • Update error messages based on user feedback
  • Add new ErrorCode cases as needed for new features
  • Keep exception tests up to date

Conclusion

Exception migration improves:

  • Error Clarity: Descriptive messages with context
  • Type Safety: Catch specific exceptions, not generic ones
  • Debugging: Rich context for troubleshooting
  • Monitoring: Category-based alerting and metrics
  • Recovery: Automatic retry strategies and hints
  • Documentation: Self-documenting exception hierarchy
  • HTTP Status: Automatic status code mapping
  • Logging: Severity-based log levels

Take your time, test thoroughly, and don't hesitate to refactor as you learn better patterns!