Some checks failed
🚀 Build & Deploy Image / Determine Build Necessity (push) Failing after 10m14s
🚀 Build & Deploy Image / Build Runtime Base Image (push) Has been skipped
🚀 Build & Deploy Image / Build Docker Image (push) Has been skipped
🚀 Build & Deploy Image / Run Tests & Quality Checks (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Staging (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Production (push) Has been skipped
Security Vulnerability Scan / Check for Dependency Changes (push) Failing after 11m25s
Security Vulnerability Scan / Composer Security Audit (push) Has been cancelled
- Remove middleware reference from Gitea Traefik labels (caused routing issues) - Optimize Gitea connection pool settings (MAX_IDLE_CONNS=30, authentication_timeout=180s) - Add explicit service reference in Traefik labels - Fix intermittent 504 timeouts by improving PostgreSQL connection handling Fixes Gitea unreachability via git.michaelschiemer.de
645 lines
16 KiB
Markdown
645 lines
16 KiB
Markdown
# Error Handling & Debugging
|
|
|
|
This guide covers error handling patterns and debugging strategies in the framework.
|
|
|
|
## Exception Handling
|
|
|
|
All custom exceptions in the framework must extend `FrameworkException` to ensure consistent error handling, logging, and recovery mechanisms.
|
|
|
|
### The FrameworkException System
|
|
|
|
The framework provides a sophisticated exception system with:
|
|
- **ExceptionContext**: Rich context information for debugging
|
|
- **ErrorCode**: Categorized error codes with recovery hints
|
|
- **RetryAfter**: Support for recoverable operations
|
|
- **Fluent Interface**: Easy context building
|
|
|
|
### Creating Custom Exceptions
|
|
|
|
```php
|
|
namespace App\Domain\User\Exceptions;
|
|
|
|
use App\Framework\Exception\FrameworkException;
|
|
use App\Framework\Exception\Core\DatabaseErrorCode;
|
|
use App\Framework\Exception\ExceptionContext;
|
|
|
|
final class UserNotFoundException extends FrameworkException
|
|
{
|
|
public static function byId(UserId $id): self
|
|
{
|
|
return self::create(
|
|
DatabaseErrorCode::ENTITY_NOT_FOUND,
|
|
"User with ID '{$id->toString()}' not found"
|
|
)->withData([
|
|
'user_id' => $id->toString(),
|
|
'search_type' => 'by_id'
|
|
]);
|
|
}
|
|
|
|
public static function byEmail(Email $email): self
|
|
{
|
|
$context = ExceptionContext::forOperation('user.lookup', 'UserRepository')
|
|
->withData(['email' => $email->getMasked()]);
|
|
|
|
return self::fromContext(
|
|
"User with email not found",
|
|
$context,
|
|
DatabaseErrorCode::ENTITY_NOT_FOUND
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
### Using ErrorCode Enums
|
|
|
|
The framework provides category-specific error code enums for better organization and type safety:
|
|
|
|
```php
|
|
use App\Framework\Exception\Core\DatabaseErrorCode;
|
|
use App\Framework\Exception\Core\AuthErrorCode;
|
|
use App\Framework\Exception\Core\HttpErrorCode;
|
|
use App\Framework\Exception\Core\SecurityErrorCode;
|
|
use App\Framework\Exception\Core\ValidationErrorCode;
|
|
|
|
// Database errors
|
|
DatabaseErrorCode::CONNECTION_FAILED
|
|
DatabaseErrorCode::QUERY_FAILED
|
|
DatabaseErrorCode::TRANSACTION_FAILED
|
|
DatabaseErrorCode::CONSTRAINT_VIOLATION
|
|
|
|
// Authentication errors
|
|
AuthErrorCode::CREDENTIALS_INVALID
|
|
AuthErrorCode::TOKEN_EXPIRED
|
|
AuthErrorCode::SESSION_EXPIRED
|
|
AuthErrorCode::ACCOUNT_LOCKED
|
|
|
|
// HTTP errors
|
|
HttpErrorCode::BAD_REQUEST
|
|
HttpErrorCode::NOT_FOUND
|
|
HttpErrorCode::METHOD_NOT_ALLOWED
|
|
HttpErrorCode::RATE_LIMIT_EXCEEDED
|
|
|
|
// Security errors
|
|
SecurityErrorCode::CSRF_TOKEN_INVALID
|
|
SecurityErrorCode::SQL_INJECTION_DETECTED
|
|
SecurityErrorCode::XSS_DETECTED
|
|
SecurityErrorCode::PATH_TRAVERSAL_DETECTED
|
|
|
|
// Validation errors
|
|
ValidationErrorCode::INVALID_INPUT
|
|
ValidationErrorCode::REQUIRED_FIELD_MISSING
|
|
ValidationErrorCode::BUSINESS_RULE_VIOLATION
|
|
ValidationErrorCode::INVALID_FORMAT
|
|
|
|
// Using error codes in exceptions:
|
|
throw FrameworkException::create(
|
|
DatabaseErrorCode::QUERY_FAILED,
|
|
"Failed to execute user query"
|
|
)->withContext(
|
|
ExceptionContext::forOperation('user.find', 'UserRepository')
|
|
->withData(['query' => 'SELECT * FROM users WHERE id = ?'])
|
|
->withDebug(['bind_params' => [$userId]])
|
|
);
|
|
```
|
|
|
|
### Exception Context Building
|
|
|
|
```php
|
|
// Method 1: Using factory methods
|
|
$exception = FrameworkException::forOperation(
|
|
'payment.process',
|
|
'PaymentService',
|
|
'Payment processing failed',
|
|
HttpErrorCode::BAD_GATEWAY
|
|
)->withData([
|
|
'amount' => $amount->toArray(),
|
|
'gateway' => 'stripe',
|
|
'customer_id' => $customerId
|
|
])->withMetadata([
|
|
'attempt' => 1,
|
|
'idempotency_key' => $idempotencyKey
|
|
]);
|
|
|
|
// Method 2: Building context separately
|
|
$context = ExceptionContext::empty()
|
|
->withOperation('order.validate', 'OrderService')
|
|
->withData([
|
|
'order_id' => $orderId,
|
|
'total' => $total->toDecimal()
|
|
])
|
|
->withDebug([
|
|
'validation_rules' => ['min_amount', 'max_items'],
|
|
'failed_rule' => 'min_amount'
|
|
]);
|
|
|
|
throw FrameworkException::fromContext(
|
|
'Order validation failed',
|
|
$context,
|
|
ValidationErrorCode::BUSINESS_RULE_VIOLATION
|
|
);
|
|
```
|
|
|
|
### Recoverable Exceptions
|
|
|
|
```php
|
|
// Creating recoverable exceptions with retry hints
|
|
final class RateLimitException extends FrameworkException
|
|
{
|
|
public static function exceeded(int $retryAfter): self
|
|
{
|
|
return self::create(
|
|
HttpErrorCode::RATE_LIMIT_EXCEEDED,
|
|
'API rate limit exceeded'
|
|
)->withRetryAfter($retryAfter)
|
|
->withData(['retry_after_seconds' => $retryAfter]);
|
|
}
|
|
}
|
|
|
|
// Using in code
|
|
try {
|
|
$response = $apiClient->request($endpoint);
|
|
} catch (RateLimitException $e) {
|
|
if ($e->isRecoverable()) {
|
|
$waitTime = $e->getRetryAfter();
|
|
// Schedule retry after $waitTime seconds
|
|
}
|
|
throw $e;
|
|
}
|
|
```
|
|
|
|
### Exception Categories
|
|
|
|
```php
|
|
// Check exception category for handling strategies
|
|
try {
|
|
$result = $operation->execute();
|
|
} catch (FrameworkException $e) {
|
|
if ($e->isCategory('AUTH')) {
|
|
// Handle authentication errors
|
|
return $this->redirectToLogin();
|
|
}
|
|
|
|
if ($e->isCategory('VAL')) {
|
|
// Handle validation errors
|
|
return $this->validationErrorResponse($e);
|
|
}
|
|
|
|
if ($e->isErrorCode(DatabaseErrorCode::CONNECTION_FAILED)) {
|
|
// Handle specific database connection errors
|
|
$this->notifyOps($e);
|
|
}
|
|
|
|
throw $e;
|
|
}
|
|
```
|
|
|
|
### Simple Exceptions for Quick Use
|
|
|
|
```php
|
|
// When you don't need the full context system
|
|
throw FrameworkException::simple('Quick error message');
|
|
|
|
// With previous exception
|
|
} catch (\PDOException $e) {
|
|
throw FrameworkException::simple(
|
|
'Database operation failed',
|
|
$e,
|
|
500
|
|
);
|
|
}
|
|
```
|
|
|
|
### Exception Data Sanitization
|
|
|
|
The framework automatically sanitizes sensitive data in exceptions:
|
|
|
|
```php
|
|
// Sensitive keys are automatically redacted
|
|
$exception->withData([
|
|
'username' => 'john@example.com',
|
|
'password' => 'secret123', // Will be logged as '[REDACTED]'
|
|
'api_key' => 'sk_live_...' // Will be logged as '[REDACTED]'
|
|
]);
|
|
```
|
|
|
|
### Best Practices
|
|
|
|
1. **Always extend FrameworkException** for custom exceptions
|
|
2. **Use ErrorCode enum** for categorizable errors
|
|
3. **Provide rich context** with operation, component, and data
|
|
4. **Use factory methods** for consistent exception creation
|
|
5. **Sanitize sensitive data** (automatic for common keys)
|
|
6. **Make exceptions domain-specific** (UserNotFoundException vs generic NotFoundException)
|
|
7. **Include recovery hints** for recoverable errors
|
|
|
|
## Unified Error Kernel Architecture
|
|
|
|
The framework uses a **context-aware error handling system** centered around the `ErrorKernel` class that automatically detects execution context (CLI vs HTTP) and handles errors accordingly.
|
|
|
|
### ErrorKernel Overview
|
|
|
|
**Location**: `src/Framework/ExceptionHandling/ErrorKernel.php`
|
|
|
|
**Key Features**:
|
|
- Automatic context detection (CLI vs HTTP)
|
|
- Colored console output for CLI errors
|
|
- HTTP Response objects for web errors
|
|
- Integration with OWASP Security Event System
|
|
- Unified error logging via LogReporter
|
|
|
|
```php
|
|
use App\Framework\ExceptionHandling\ErrorKernel;
|
|
|
|
final readonly class ErrorKernel
|
|
{
|
|
public function __construct(
|
|
private ErrorRendererFactory $rendererFactory = new ErrorRendererFactory,
|
|
private ?ExecutionContext $executionContext = null,
|
|
private ?ConsoleOutput $consoleOutput = null
|
|
) {}
|
|
|
|
/**
|
|
* Context-aware exception handler
|
|
* - CLI: Colored console output
|
|
* - HTTP: Logs error (middleware creates response)
|
|
*/
|
|
public function handle(Throwable $e, array $context = []): mixed
|
|
{
|
|
// Automatic logging
|
|
$log = new LogReporter();
|
|
$log->report($e);
|
|
|
|
// Context-aware handling
|
|
$executionContext = $this->executionContext ?? ExecutionContext::detect();
|
|
|
|
if ($executionContext->isCli()) {
|
|
$this->handleCliException($e);
|
|
return null;
|
|
}
|
|
|
|
// HTTP context - middleware will create response
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Create HTTP Response from exception (for middleware recovery)
|
|
*/
|
|
public function createHttpResponse(
|
|
Throwable $exception,
|
|
?ExceptionContextProvider $contextProvider = null,
|
|
bool $isDebugMode = false
|
|
): Response {
|
|
$renderer = new ResponseErrorRenderer($isDebugMode);
|
|
return $renderer->createResponse($exception, $contextProvider);
|
|
}
|
|
}
|
|
```
|
|
|
|
### CLI Error Handling
|
|
|
|
**CliErrorHandler** registers global PHP error handlers for CLI context:
|
|
|
|
```php
|
|
use App\Framework\ExceptionHandling\CliErrorHandler;
|
|
use App\Framework\Console\ConsoleOutput;
|
|
|
|
// Registration in AppBootstrapper
|
|
$output = new ConsoleOutput();
|
|
$cliErrorHandler = new CliErrorHandler($output);
|
|
$cliErrorHandler->register();
|
|
|
|
// Automatic colored output for errors:
|
|
// - Red for uncaught exceptions
|
|
// - Yellow for warnings
|
|
// - Cyan for notices
|
|
// - Full stack traces in CLI
|
|
```
|
|
|
|
### HTTP Error Handling
|
|
|
|
**ExceptionHandlingMiddleware** catches exceptions in HTTP request pipeline:
|
|
|
|
```php
|
|
use App\Framework\Http\Middlewares\ExceptionHandlingMiddleware;
|
|
|
|
#[MiddlewarePriorityAttribute(MiddlewarePriority::ERROR_HANDLING)]
|
|
final readonly class ExceptionHandlingMiddleware implements HttpMiddleware
|
|
{
|
|
public function __invoke(
|
|
MiddlewareContext $context,
|
|
Next $next,
|
|
RequestStateManager $stateManager
|
|
): MiddlewareContext {
|
|
try {
|
|
return $next($context);
|
|
} catch (\Throwable $e) {
|
|
// Log exception
|
|
$this->logger->error('Unhandled exception in HTTP request', [
|
|
'exception' => get_class($e),
|
|
'message' => $e->getMessage(),
|
|
]);
|
|
|
|
// Create HTTP Response
|
|
$errorKernel = new ErrorKernel();
|
|
$response = $errorKernel->createHttpResponse(
|
|
$e,
|
|
null,
|
|
isDebugMode: false
|
|
);
|
|
|
|
return $context->withResponse($response);
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### OWASP Security Event Integration
|
|
|
|
Exceptions can trigger OWASP security events for audit logging:
|
|
|
|
```php
|
|
use App\Application\Security\OWASPSecurityEventLogger;
|
|
use App\Application\Security\OWASPEventIdentifier;
|
|
|
|
// Automatic security logging
|
|
try {
|
|
$this->authenticateUser($credentials);
|
|
} catch (AuthenticationException $e) {
|
|
// ErrorKernel logs exception
|
|
$this->errorKernel->handle($e);
|
|
|
|
// OWASP event for security audit trail
|
|
$this->eventDispatcher->dispatch(
|
|
new AuthenticationFailedEvent(
|
|
OWASPEventIdentifier::AUTHN_LOGIN_FAILURE,
|
|
$credentials->username,
|
|
$e->getMessage()
|
|
)
|
|
);
|
|
|
|
throw $e;
|
|
}
|
|
```
|
|
|
|
### Legacy ErrorHandling Module Removed
|
|
|
|
**IMPORTANT**: The legacy `ErrorHandling` module (`src/Framework/ErrorHandling/`) has been **completely removed** as of the unified exception architecture migration.
|
|
|
|
**Migration Path**:
|
|
- All error handling now uses `ErrorKernel` and `FrameworkException`
|
|
- CLI errors: `CliErrorHandler` → `ErrorKernel`
|
|
- HTTP errors: `ExceptionHandlingMiddleware` → `ErrorKernel`
|
|
- Security events: Direct event dispatch via `EventDispatcher`
|
|
|
|
**Old Pattern** (removed):
|
|
```php
|
|
// ❌ Legacy - NO LONGER EXISTS
|
|
use App\Framework\ErrorHandling\ErrorHandler;
|
|
use App\Framework\ErrorHandling\SecurityEventLogger;
|
|
|
|
$errorHandler = new ErrorHandler();
|
|
$errorHandler->register();
|
|
```
|
|
|
|
**New Pattern** (current):
|
|
```php
|
|
// ✅ Unified - ErrorKernel
|
|
use App\Framework\ExceptionHandling\ErrorKernel;
|
|
|
|
$errorKernel = new ErrorKernel();
|
|
$errorKernel->handle($exception);
|
|
```
|
|
|
|
## Logging Best Practices
|
|
|
|
### Automatic Exception Logging
|
|
|
|
All exceptions handled by `ErrorKernel` are automatically logged via `LogReporter`:
|
|
|
|
```php
|
|
// Automatic logging happens in ErrorKernel::handle()
|
|
$log = new LogReporter();
|
|
$log->report($exception);
|
|
|
|
// Logs include:
|
|
// - Exception class and message
|
|
// - Stack trace
|
|
// - File and line number
|
|
// - Context data
|
|
```
|
|
|
|
### Manual Logging
|
|
|
|
```php
|
|
use App\Framework\Logging\Logger;
|
|
|
|
// Log exceptions with context
|
|
try {
|
|
$user = $this->userRepository->find($userId);
|
|
} catch (UserNotFoundException $e) {
|
|
Logger::error('User lookup failed', [
|
|
'user_id' => $userId,
|
|
'exception' => $e->getMessage(),
|
|
'trace' => $e->getTraceAsString()
|
|
]);
|
|
|
|
throw $e;
|
|
}
|
|
|
|
// Log levels
|
|
Logger::debug('Debugging information');
|
|
Logger::info('Informational message');
|
|
Logger::warning('Warning condition');
|
|
Logger::error('Error condition');
|
|
Logger::critical('Critical failure');
|
|
```
|
|
|
|
## Debug Strategies
|
|
|
|
### Development vs Production
|
|
|
|
**Development** (`APP_DEBUG=true`):
|
|
- Full stack traces displayed
|
|
- Detailed error messages
|
|
- Debug data in responses
|
|
- SQL query logging
|
|
|
|
**Production** (`APP_DEBUG=false`):
|
|
- Generic error messages
|
|
- Stack traces hidden from users
|
|
- Errors logged server-side
|
|
- Security-safe responses
|
|
|
|
### Debugging Tools
|
|
|
|
```php
|
|
// Enable debug mode in ErrorKernel
|
|
$errorKernel = new ErrorKernel(
|
|
executionContext: ExecutionContext::cli(),
|
|
consoleOutput: new ConsoleOutput()
|
|
);
|
|
|
|
// HTTP Response with debug mode
|
|
$response = $errorKernel->createHttpResponse(
|
|
$exception,
|
|
$contextProvider,
|
|
isDebugMode: true // Shows stack trace in response
|
|
);
|
|
```
|
|
|
|
### Error Context Providers
|
|
|
|
```php
|
|
use App\Framework\ExceptionHandling\Context\ExceptionContextProvider;
|
|
|
|
// Attach custom context to exceptions
|
|
$contextProvider = new ExceptionContextProvider();
|
|
$contextProvider->attachContext($exception, [
|
|
'request_id' => $requestId,
|
|
'user_id' => $userId,
|
|
'operation' => 'payment.process'
|
|
]);
|
|
|
|
$response = $errorKernel->createHttpResponse(
|
|
$exception,
|
|
$contextProvider,
|
|
isDebugMode: false
|
|
);
|
|
```
|
|
|
|
## Error Recovery Patterns
|
|
|
|
### Graceful Degradation
|
|
|
|
```php
|
|
// Try primary service, fallback to secondary
|
|
try {
|
|
return $this->primaryCache->get($key);
|
|
} catch (CacheException $e) {
|
|
Logger::warning('Primary cache failed, using fallback', [
|
|
'error' => $e->getMessage()
|
|
]);
|
|
|
|
return $this->fallbackCache->get($key);
|
|
}
|
|
```
|
|
|
|
### Circuit Breaker Pattern
|
|
|
|
```php
|
|
use App\Framework\Resilience\CircuitBreaker;
|
|
|
|
$circuitBreaker = new CircuitBreaker(
|
|
failureThreshold: 5,
|
|
timeout: Duration::fromSeconds(60)
|
|
);
|
|
|
|
try {
|
|
return $circuitBreaker->call(function() {
|
|
return $this->externalApi->request($endpoint);
|
|
});
|
|
} catch (CircuitOpenException $e) {
|
|
// Circuit is open - use cached response
|
|
return $this->cachedResponse;
|
|
}
|
|
```
|
|
|
|
### Retry with Exponential Backoff
|
|
|
|
```php
|
|
use App\Framework\Queue\ValueObjects\RetryStrategy;
|
|
|
|
$retryStrategy = new ExponentialBackoffStrategy(
|
|
maxAttempts: 3,
|
|
baseDelaySeconds: 60
|
|
);
|
|
|
|
$attempt = 0;
|
|
while ($attempt < $retryStrategy->getMaxAttempts()) {
|
|
try {
|
|
return $this->performOperation();
|
|
} catch (TransientException $e) {
|
|
$attempt++;
|
|
if (!$retryStrategy->shouldRetry($attempt)) {
|
|
throw $e;
|
|
}
|
|
|
|
$delay = $retryStrategy->getDelay($attempt);
|
|
sleep($delay->toSeconds());
|
|
}
|
|
}
|
|
```
|
|
|
|
## Common Error Scenarios
|
|
|
|
### 1. Database Connection Failure
|
|
|
|
```php
|
|
try {
|
|
$connection = $this->connectionPool->getConnection();
|
|
} catch (ConnectionException $e) {
|
|
// Log error
|
|
$this->errorKernel->handle($e);
|
|
|
|
// Return cached data or error response
|
|
return $this->getCachedData() ?? $this->errorResponse();
|
|
}
|
|
```
|
|
|
|
### 2. Validation Errors
|
|
|
|
```php
|
|
try {
|
|
$user = User::create($email, $name);
|
|
} catch (ValidationException $e) {
|
|
// Return validation errors to user
|
|
return new JsonResult([
|
|
'errors' => $e->getErrors()
|
|
], status: Status::UNPROCESSABLE_ENTITY);
|
|
}
|
|
```
|
|
|
|
### 3. Authentication Failures
|
|
|
|
```php
|
|
try {
|
|
$user = $this->authenticator->authenticate($credentials);
|
|
} catch (AuthenticationException $e) {
|
|
// Log security event
|
|
$this->eventDispatcher->dispatch(
|
|
new AuthenticationFailedEvent($credentials->username)
|
|
);
|
|
|
|
// Return 401 Unauthorized
|
|
return new JsonResult([
|
|
'error' => 'Invalid credentials'
|
|
], status: Status::UNAUTHORIZED);
|
|
}
|
|
```
|
|
|
|
### 4. Resource Not Found
|
|
|
|
```php
|
|
try {
|
|
$order = $this->orderRepository->find($orderId);
|
|
} catch (OrderNotFoundException $e) {
|
|
// Return 404 Not Found
|
|
return new JsonResult([
|
|
'error' => 'Order not found'
|
|
], status: Status::NOT_FOUND);
|
|
}
|
|
```
|
|
|
|
### 5. Rate Limit Exceeded
|
|
|
|
```php
|
|
try {
|
|
$this->rateLimiter->checkLimit($userId);
|
|
} catch (RateLimitException $e) {
|
|
// Return 429 Too Many Requests with retry hint
|
|
return new JsonResult([
|
|
'error' => 'Rate limit exceeded',
|
|
'retry_after' => $e->getRetryAfter()
|
|
], status: Status::TOO_MANY_REQUESTS);
|
|
}
|
|
``` |