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.
This commit is contained in:
2025-10-25 19:18:37 +02:00
parent caa85db796
commit fc3d7e6357
83016 changed files with 378904 additions and 20919 deletions

View File

@@ -0,0 +1,684 @@
# Exception Hierarchy Pattern Guide
Comprehensive guide for creating module-specific exception hierarchies in the Custom PHP Framework.
## Overview
The framework uses a **module-based exception hierarchy** pattern that provides:
- **Type-safe exception handling** with domain-specific exceptions
- **Rich error context** with ErrorCode integration
- **Clean API** using factory methods
- **Automatic HTTP status mapping** and retry strategies
- **Structured logging** with severity levels
## Architecture
```
FrameworkException (Base)
└── ModuleException (e.g., QueueException)
├── SpecificException1 (e.g., JobNotFoundException)
├── SpecificException2 (e.g., ChainNotFoundException)
└── SpecificException3 (e.g., InvalidChainStateException)
```
### Key Components
1. **FrameworkException**: Base exception with ErrorCode and ExceptionContext
2. **ModuleException**: Module-specific base (e.g., `QueueException`, `DatabaseException`)
3. **Specific Exceptions**: Domain-specific exceptions with factory methods
4. **ErrorCode Enum**: Category-specific error codes (e.g., `QueueErrorCode`, `DatabaseErrorCode`)
## Creating a Module Exception Hierarchy
### Step 1: Create Module Base Exception
```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
{
}
```
**Location**: `src/Framework/YourModule/Exceptions/YourModuleException.php`
**Naming Convention**: `{ModuleName}Exception`
### Step 2: Add Module-Specific Error Codes
Add error codes to the appropriate ErrorCode enum (or create a new one):
```php
<?php
namespace App\Framework\Exception\Core;
enum YourModuleErrorCode: string implements ErrorCode
{
case RESOURCE_NOT_FOUND = 'MODULE001';
case INVALID_STATE = 'MODULE002';
case OPERATION_FAILED = 'MODULE003';
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::RESOURCE_NOT_FOUND => ErrorSeverity::ERROR,
self::INVALID_STATE => ErrorSeverity::WARNING,
self::OPERATION_FAILED => ErrorSeverity::CRITICAL,
};
}
public function getDescription(): string
{
return match($this) {
self::RESOURCE_NOT_FOUND => 'Resource not found in module',
self::INVALID_STATE => 'Operation not allowed in current state',
self::OPERATION_FAILED => 'Module operation failed',
};
}
public function getRecoveryHint(): string
{
return match($this) {
self::RESOURCE_NOT_FOUND => 'Verify resource ID and check if resource exists',
self::INVALID_STATE => 'Check current state and verify operation prerequisites',
self::OPERATION_FAILED => 'Review logs and retry operation',
};
}
public function isRecoverable(): bool
{
return match($this) {
self::OPERATION_FAILED => false,
default => true,
};
}
public function getRetryAfterSeconds(): ?int
{
return match($this) {
self::OPERATION_FAILED => 60,
default => null,
};
}
}
```
**Location**: `src/Framework/Exception/Core/YourModuleErrorCode.php`
**Error Code Format**: `{CATEGORY}{NUMBER}` (e.g., `MODULE001`, `MODULE002`)
**Severity Levels**:
- `CRITICAL`: System failure, immediate action required
- `ERROR`: Operation failed, attention needed
- `WARNING`: Potential issue, monitor
- `INFO`: Informational, no action needed
- `DEBUG`: Debugging information
### Step 3: Create Specific Exceptions
Create domain-specific exceptions with factory methods:
```php
<?php
declare(strict_types=1);
namespace App\Framework\YourModule\Exceptions;
use App\Framework\Exception\Core\YourModuleErrorCode;
use App\Framework\Exception\ExceptionContext;
/**
* Exception thrown when a resource is not found
*/
final class ResourceNotFoundException extends YourModuleException
{
public static function byId(ResourceId $id): self
{
$context = ExceptionContext::forOperation('resource.lookup', 'ResourceService')
->withData([
'resource_id' => $id->toString(),
'search_type' => 'by_id',
]);
return self::create(
YourModuleErrorCode::RESOURCE_NOT_FOUND,
"Resource with ID '{$id->toString()}' not found",
$context
);
}
public static function byName(string $name): self
{
$context = ExceptionContext::forOperation('resource.lookup', 'ResourceService')
->withData([
'resource_name' => $name,
'search_type' => 'by_name',
]);
return self::create(
YourModuleErrorCode::RESOURCE_NOT_FOUND,
"Resource with name '{$name}' not found",
$context
);
}
}
```
**Location**: `src/Framework/YourModule/Exceptions/ResourceNotFoundException.php`
### Step 4: Use Exceptions in Code
Replace generic exceptions with domain-specific ones:
```php
// ❌ Before: Generic RuntimeException
public function findResource(ResourceId $id): Resource
{
$resource = $this->repository->find($id);
if ($resource === null) {
throw new \RuntimeException("Resource {$id->toString()} not found");
}
return $resource;
}
// ✅ After: Domain-specific exception with factory method
use App\Framework\YourModule\Exceptions\ResourceNotFoundException;
public function findResource(ResourceId $id): Resource
{
$resource = $this->repository->find($id);
if ($resource === null) {
throw ResourceNotFoundException::byId($id);
}
return $resource;
}
```
## Factory Method Patterns
### Single Factory Method
For simple cases with one creation scenario:
```php
final class ConfigurationMissingException extends YourModuleException
{
public static function forKey(string $key): self
{
$context = ExceptionContext::forOperation('config.load', 'ConfigService')
->withData(['config_key' => $key]);
return self::create(
YourModuleErrorCode::CONFIG_MISSING,
"Configuration key '{$key}' is missing",
$context
);
}
}
```
### Multiple Factory Methods
For different creation scenarios:
```php
final class InvalidStateException extends YourModuleException
{
public static function notReady(string $resourceId, string $currentState): self
{
$context = ExceptionContext::forOperation('resource.start', 'ResourceService')
->withData([
'resource_id' => $resourceId,
'current_state' => $currentState,
'required_state' => 'ready',
]);
return self::create(
YourModuleErrorCode::INVALID_STATE,
"Resource '{$resourceId}' is not ready (current: {$currentState})",
$context
);
}
public static function alreadyCompleted(string $resourceId): self
{
$context = ExceptionContext::forOperation('resource.modify', 'ResourceService')
->withData([
'resource_id' => $resourceId,
'current_state' => 'completed',
]);
return self::create(
YourModuleErrorCode::INVALID_STATE,
"Resource '{$resourceId}' is already completed",
$context
);
}
}
```
### Type-Safe Factory Methods
Accept Value Objects for type safety:
```php
final class UserNotFoundException extends AuthModuleException
{
public static function byId(UserId $userId): self
{
$context = ExceptionContext::forOperation('user.lookup', 'UserRepository')
->withData(['user_id' => $userId->toString()]);
return self::create(
AuthErrorCode::USER_NOT_FOUND,
"User with ID '{$userId->toString()}' not found",
$context
);
}
public static function byEmail(Email $email): self
{
$context = ExceptionContext::forOperation('user.lookup', 'UserRepository')
->withData(['email' => $email->getMasked()]);
return self::create(
AuthErrorCode::USER_NOT_FOUND,
"User with email not found",
$context
);
}
}
```
## ExceptionContext Best Practices
### Operation and Component
Always specify operation and component for traceability:
```php
$context = ExceptionContext::forOperation(
'order.process', // Operation: what was being attempted
'OrderService' // Component: which class/service
);
```
### Context Data
Add relevant data for debugging:
```php
$context->withData([
'order_id' => $orderId,
'customer_id' => $customerId,
'total_amount' => $totalAmount,
'payment_method' => $paymentMethod,
]);
```
### Debug Information
Add internal details for development (automatically filtered in production):
```php
$context->withDebug([
'query' => $sql,
'bind_params' => $params,
'execution_time_ms' => $executionTime,
]);
```
### Metadata
Add structured metadata:
```php
$context->withMetadata([
'attempt' => $attemptNumber,
'max_attempts' => $maxAttempts,
'retry_strategy' => 'exponential_backoff',
]);
```
## Exception Catching Patterns
### Catch Specific Exception
```php
try {
$resource = $this->resourceService->find($id);
} catch (ResourceNotFoundException $e) {
// Handle specific exception
return $this->notFoundResponse($e->getMessage());
}
```
### Catch Module-Level Exception
```php
try {
$result = $this->moduleService->process($data);
} catch (YourModuleException $e) {
// Handle any exception from this module
$this->logger->error('Module operation failed', [
'error_code' => $e->getErrorCode()->getValue(),
'message' => $e->getMessage(),
]);
throw $e;
}
```
### Catch by Category
```php
try {
$result = $this->service->execute();
} catch (FrameworkException $e) {
if ($e->isCategory('DB')) {
// Handle database errors
return $this->databaseErrorResponse();
}
if ($e->isCategory('QUEUE')) {
// Handle queue errors
return $this->queueErrorResponse();
}
throw $e;
}
```
### Catch by Specific ErrorCode
```php
try {
$resource = $this->service->find($id);
} catch (FrameworkException $e) {
if ($e->isErrorCode(YourModuleErrorCode::RESOURCE_NOT_FOUND)) {
// Handle specific error code
return $this->notFoundResponse();
}
throw $e;
}
```
## ErrorHandler Integration
The ErrorHandler automatically leverages the exception hierarchy:
### Automatic HTTP Status Mapping
```php
// ErrorCode category determines HTTP status:
// AUTH -> 401 Unauthorized
// AUTHZ -> 403 Forbidden
// VAL -> 400 Bad Request
// DB/QUEUE/CACHE -> 500 Internal Server Error
```
### Automatic Severity-Based Logging
```php
// ErrorCode.getSeverity() determines log level:
// CRITICAL -> critical log
// ERROR -> error log
// WARNING -> warning log
// INFO -> info log
// DEBUG -> debug log
```
### Automatic Retry-After Headers
```php
// ErrorCode.getRetryAfterSeconds() adds Retry-After header:
if ($errorCode->getRetryAfterSeconds() !== null) {
$response->headers['Retry-After'] = $retryAfter;
}
```
### Recovery Hints in Debug Mode
```php
// ErrorCode.getRecoveryHint() included in debug responses:
$metadata['recovery_hint'] = $errorCode->getRecoveryHint();
```
## Migration Checklist
When migrating a module to the new exception hierarchy:
- [ ] Create module base exception (`{Module}Exception extends FrameworkException`)
- [ ] Add module-specific error codes to `{Module}ErrorCode` enum
- [ ] Implement all required ErrorCode methods (getValue, getCategory, getSeverity, etc.)
- [ ] Create specific exceptions with factory methods
- [ ] Replace `RuntimeException` with domain-specific exceptions
- [ ] Replace `InvalidArgumentException` in business logic (keep for constructor validation)
- [ ] Add comprehensive ExceptionContext to all exceptions
- [ ] Test exception creation and ErrorHandler integration
- [ ] Update module documentation
## Common Patterns
### Infrastructure Exceptions
For missing dependencies or configuration:
```php
final class RedisExtensionNotLoadedException extends QueueException
{
public static function create(): self
{
$context = ExceptionContext::forOperation('queue.init', 'QueueInitializer')
->withData([
'required_extension' => 'redis',
'loaded_extensions' => get_loaded_extensions(),
'fallback_available' => 'FileQueue',
]);
return self::fromContext(
'Redis PHP extension is not loaded',
$context,
QueueErrorCode::WORKER_UNAVAILABLE
);
}
}
```
### State Transition Exceptions
For invalid state transitions:
```php
final class AllStepsCompletedException extends QueueException
{
public static function forJob(string $jobId, int $totalSteps): self
{
$context = ExceptionContext::forOperation('step.complete', 'StepProgressTracker')
->withData([
'job_id' => $jobId,
'total_steps' => $totalSteps,
'current_step_index' => $totalSteps,
]);
return self::create(
QueueErrorCode::INVALID_STATE,
"All {$totalSteps} steps have already been completed for job '{$jobId}'",
$context
);
}
}
```
### Validation Exceptions
For business rule violations:
```php
final class CircularDependencyException extends QueueException
{
public static function inChain(string $chainId, array $circularDependencies = []): self
{
$context = ExceptionContext::forOperation('chain.validate', 'JobChainExecutionCoordinator')
->withData([
'chain_id' => $chainId,
'circular_dependencies' => $circularDependencies,
]);
return self::create(
QueueErrorCode::CIRCULAR_DEPENDENCY,
"Chain '{$chainId}' has circular dependencies",
$context
);
}
}
```
## Anti-Patterns to Avoid
### ❌ Don't Use FrameworkException::create() Directly
```php
// ❌ BAD: Clutters business logic
throw FrameworkException::create(
ErrorCode::RESOURCE_NOT_FOUND,
"Resource not found"
)->withContext(
ExceptionContext::forOperation('resource.lookup', 'ResourceService')
->withData(['resource_id' => $id])
);
// ✅ GOOD: Clean factory method
throw ResourceNotFoundException::byId($id);
```
### ❌ Don't Create Anonymous Exceptions
```php
// ❌ BAD: Anonymous exception, no reusability
throw new class extends FrameworkException {
// ...
};
// ✅ GOOD: Named exception class
throw ResourceNotFoundException::byId($id);
```
### ❌ Don't Mix Concerns in Exception Names
```php
// ❌ BAD: Too specific, hard to reuse
final class UserNotFoundInDatabaseByEmailException extends AuthException { }
// ✅ GOOD: Generic, multiple factory methods
final class UserNotFoundException extends AuthException
{
public static function byId(UserId $id): self { }
public static function byEmail(Email $email): self { }
}
```
### ❌ Don't Skip ExceptionContext
```php
// ❌ BAD: No context
return self::create(
ErrorCode::RESOURCE_NOT_FOUND,
"Resource not found"
);
// ✅ GOOD: Rich context
$context = ExceptionContext::forOperation('resource.lookup', 'ResourceService')
->withData(['resource_id' => $id]);
return self::create(
ErrorCode::RESOURCE_NOT_FOUND,
"Resource with ID '{$id}' not found",
$context
);
```
## Benefits
**Type Safety**: Catch specific exceptions, not generic RuntimeException
**Clean API**: `throw ResourceNotFoundException::byId($id)` vs manual setup
**Rich Context**: Automatic operation, component, and data tracking
**Automatic HTTP Status**: ErrorCode category determines response status
**Structured Logging**: Severity-based logging with rich context
**Retry Strategies**: Automatic Retry-After headers for recoverable errors
**Recovery Hints**: Actionable hints for developers in debug mode
**Category-Based Handling**: Catch exceptions by error category
**Testability**: Easy to mock and test specific exception scenarios
**Documentation**: Self-documenting exception hierarchy
## Real-World Example: Queue Module
The Queue module demonstrates the complete pattern:
```php
// Base exception
class QueueException extends FrameworkException { }
// Specific exceptions
final class JobNotFoundException extends QueueException
{
public static function byId(JobId $jobId): self { }
public static function inQueue(JobId $jobId, string $queueName): self { }
}
final class ChainNotFoundException extends QueueException
{
public static function byId(string $chainId): self { }
public static function byName(string $name): self { }
}
final class InvalidChainStateException extends QueueException
{
public static function notPending(string $chainId, string $currentStatus): self { }
public static function alreadyCompleted(string $chainId): self { }
public static function alreadyFailed(string $chainId): self { }
}
// Usage
throw JobNotFoundException::byId($jobId);
throw ChainNotFoundException::byName($chainName);
throw InvalidChainStateException::notPending($chainId, $status);
```
## See Also
- [ErrorHandler Enhancements Guide](./error-handler-enhancements.md)
- [Developer Migration Guide](./exception-migration-guide.md)
- [ErrorCode Reference](./error-code-reference.md)