- 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.
1133 lines
33 KiB
Markdown
1133 lines
33 KiB
Markdown
# 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](./exception-hierarchy-pattern.md)
|
|
- Read [ErrorHandler Enhancements Guide](./error-handler-enhancements.md)
|
|
- 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:
|
|
|
|
```bash
|
|
# 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
|
|
<?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**:
|
|
```php
|
|
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
|
|
<?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
|
|
<?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):
|
|
```php
|
|
// 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):
|
|
```php
|
|
// 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):
|
|
```php
|
|
// 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):
|
|
```php
|
|
// 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):
|
|
```php
|
|
// In ServiceInitializer.php
|
|
public function initialize(): Service
|
|
{
|
|
if (!extension_loaded('redis')) {
|
|
throw new \RuntimeException('Redis extension not loaded');
|
|
}
|
|
|
|
return new RedisService();
|
|
}
|
|
```
|
|
|
|
**After** (Framework-Compliant):
|
|
```php
|
|
// 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
|
|
<?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
|
|
<?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
|
|
<?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
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```php
|
|
// 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:
|
|
|
|
```markdown
|
|
## 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**:
|
|
```php
|
|
// ✅ 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**:
|
|
```php
|
|
// ❌ 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**:
|
|
```php
|
|
// ❌ 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**:
|
|
```php
|
|
// ❌ 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**:
|
|
```php
|
|
// ❌ 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
|
|
```php
|
|
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
|
|
|
|
- **Pattern Guide**: [exception-hierarchy-pattern.md](./exception-hierarchy-pattern.md)
|
|
- **Enhancement Guide**: [error-handler-enhancements.md](./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**:
|
|
|
|
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**:
|
|
```php
|
|
// 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**:
|
|
```php
|
|
// 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**:
|
|
```php
|
|
// 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**:
|
|
```php
|
|
// 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**
|
|
```php
|
|
// Before: Generic catch
|
|
catch (\RuntimeException $e) { }
|
|
|
|
// After: Specific exception handling
|
|
catch (DatabaseConnectionException $e) {
|
|
// Handle database connection specifically
|
|
$this->fallbackToCache();
|
|
}
|
|
```
|
|
|
|
**2. Rich Error Context**
|
|
```php
|
|
// 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**
|
|
```php
|
|
// 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**
|
|
```php
|
|
// 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**
|
|
```php
|
|
// 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
|
|
```bash
|
|
./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!
|