- 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.
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:
- Read Exception Hierarchy Pattern Guide
- Read ErrorHandler Enhancements Guide
- Understand ErrorCode system and ExceptionContext
- Have module test suite ready for validation
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:
- Added
use App\Framework\Queue\Exceptions\AllStepsCompletedException; - Kept constructor InvalidArgumentException throws (parameter validation)
- Replaced both RuntimeException throws with
AllStepsCompletedException::forJob() - More descriptive error messages via factory method
- 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
JobNotFoundException- Job not found by ID or in queueChainNotFoundException- Chain not found by ID or nameInvalidChainStateException- Chain not in correct stateCircularDependencyException- Circular dependency detectedAllStepsCompletedException- All steps already completedRedisExtensionNotLoadedException- Redis extension missingWorkerNotFoundException- 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
- Day 1: Analysis + ErrorCode definition (1h)
- Day 1: Base exception + 3 entity not found exceptions (1h)
- Day 2: State violation exceptions (0.5h)
- Day 2: Service migrations (0.5h)
- 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.phpinException/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.phpbase 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:
- Monitor Production: Watch for any regression issues
- Collect Feedback: Get team feedback on error clarity
- Iterate: Refine error messages based on feedback
- Share Learnings: Update this guide with lessons learned
- Help Others: Assist with other module migrations
Support & Resources
- Pattern Guide: exception-hierarchy-pattern.md
- Enhancement Guide: error-handler-enhancements.md
- ErrorCode Reference: See
src/Framework/Exception/Core/directory - Example Module: Queue module (
src/Framework/Queue/)
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:
-
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
-
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
-
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
-
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
-
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_FAILEDQueryExecutionException→ DatabaseErrorCode::QUERY_FAILEDTransactionException→ DatabaseErrorCode::TRANSACTION_FAILEDEntityNotFoundException→ DatabaseErrorCode::ENTITY_NOT_FOUNDConstraintViolationException→ 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_INVALIDTokenExpiredException→ AuthErrorCode::TOKEN_EXPIREDSessionExpiredException→ AuthErrorCode::SESSION_EXPIREDAccountLockedException→ AuthErrorCode::ACCOUNT_LOCKEDInsufficientPermissionsException→ 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_REQUESTNotFoundException→ HttpErrorCode::NOT_FOUNDMethodNotAllowedException→ HttpErrorCode::METHOD_NOT_ALLOWEDInternalServerErrorException→ 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_INVALIDSqlInjectionAttemptException→ SecurityErrorCode::SQL_INJECTION_DETECTEDXssAttemptException→ SecurityErrorCode::XSS_DETECTEDPathTraversalAttemptException→ 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):
- Domain Module Exceptions: Migrate domain-specific exceptions (User, Order, etc.)
- Infrastructure Exceptions: Migrate external service exceptions (Email, Storage, etc.)
- 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!