Enable Discovery debug logging for production troubleshooting

- Add DISCOVERY_LOG_LEVEL=debug
- Add DISCOVERY_SHOW_PROGRESS=true
- Temporary changes for debugging InitializerProcessor fixes on production
This commit is contained in:
2025-08-11 20:13:26 +02:00
parent 59fd3dd3b1
commit 55a330b223
3683 changed files with 2956207 additions and 16948 deletions

View File

@@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
namespace App\Framework\ErrorBoundaries\Async;
use App\Framework\Exception\FrameworkException;
use Throwable;
/**
* Exception thrown when both async operation and fallback fail
*/
final class AsyncBoundaryFailedException extends FrameworkException
{
public function __construct(
string $message,
private readonly string $boundaryName,
private readonly Throwable $originalException,
private readonly Throwable $fallbackException,
int $code = 0,
?Throwable $previous = null
) {
parent::__construct($message, $code, $previous);
}
/**
* Get the boundary name where failure occurred
*/
public function getBoundaryName(): string
{
return $this->boundaryName;
}
/**
* Get the original operation exception
*/
public function getOriginalException(): Throwable
{
return $this->originalException;
}
/**
* Get the fallback exception
*/
public function getFallbackException(): Throwable
{
return $this->fallbackException;
}
/**
* Get detailed error information
*/
public function getErrorDetails(): array
{
return [
'boundary_name' => $this->boundaryName,
'original_error' => [
'class' => $this->originalException::class,
'message' => $this->originalException->getMessage(),
'file' => $this->originalException->getFile(),
'line' => $this->originalException->getLine(),
],
'fallback_error' => [
'class' => $this->fallbackException::class,
'message' => $this->fallbackException->getMessage(),
'file' => $this->fallbackException->getFile(),
'line' => $this->fallbackException->getLine(),
],
];
}
}

View File

@@ -0,0 +1,171 @@
<?php
declare(strict_types=1);
namespace App\Framework\ErrorBoundaries\Async;
use App\Framework\Core\ValueObjects\Percentage;
use Throwable;
/**
* Result object for async bulk operations
*/
final readonly class AsyncBulkResult
{
public function __construct(
public array $results,
public array $errors,
public int $totalOperations,
public int $successfulOperations,
) {
}
/**
* Get success rate as percentage
*/
public function getSuccessRate(): Percentage
{
if ($this->totalOperations === 0) {
return Percentage::fromFloat(0.0);
}
return Percentage::fromFloat($this->successfulOperations / $this->totalOperations);
}
/**
* Get error rate as percentage
*/
public function getErrorRate(): Percentage
{
if ($this->totalOperations === 0) {
return Percentage::fromFloat(0.0);
}
return Percentage::fromFloat($this->getErrorCount() / $this->totalOperations);
}
/**
* Get number of errors
*/
public function getErrorCount(): int
{
return count($this->errors);
}
/**
* Check if any operations succeeded
*/
public function hasSuccesses(): bool
{
return $this->successfulOperations > 0;
}
/**
* Check if any operations failed
*/
public function hasErrors(): bool
{
return count($this->errors) > 0;
}
/**
* Check if all operations succeeded
*/
public function isCompleteSuccess(): bool
{
return $this->successfulOperations === $this->totalOperations;
}
/**
* Check if all operations failed
*/
public function isCompleteFailure(): bool
{
return count($this->errors) === $this->totalOperations;
}
/**
* Get all successful results
*/
public function getSuccessfulResults(): array
{
return $this->results;
}
/**
* Get all errors
*/
public function getErrors(): array
{
return $this->errors;
}
/**
* Get error by key
*/
public function getError(string $key): ?Throwable
{
return $this->errors[$key] ?? null;
}
/**
* Get result by key
*/
public function getResult(string $key): mixed
{
return $this->results[$key] ?? null;
}
/**
* Check if key has error
*/
public function hasError(string $key): bool
{
return isset($this->errors[$key]);
}
/**
* Check if key has result
*/
public function hasResult(string $key): bool
{
return isset($this->results[$key]);
}
/**
* Get summary statistics
*/
public function getSummary(): array
{
return [
'total_operations' => $this->totalOperations,
'successful_operations' => $this->successfulOperations,
'failed_operations' => $this->getErrorCount(),
'success_rate' => $this->getSuccessRate()->toFloat(),
'error_rate' => $this->getErrorRate()->toFloat(),
'is_complete_success' => $this->isCompleteSuccess(),
'is_complete_failure' => $this->isCompleteFailure(),
'has_partial_success' => $this->hasSuccesses() && $this->hasErrors(),
];
}
/**
* Convert to array
*/
public function toArray(): array
{
return [
'results' => $this->results,
'errors' => array_map(
fn (Throwable $e) => [
'class' => $e::class,
'message' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
],
$this->errors
),
'summary' => $this->getSummary(),
];
}
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace App\Framework\ErrorBoundaries\Async;
use App\Framework\Exception\FrameworkException;
/**
* Exception thrown when async circuit breaker is open
*/
final class AsyncCircuitBreakerOpenException extends FrameworkException
{
public function __construct(
string $message,
int $code = 0,
?\Throwable $previous = null
) {
parent::__construct($message, $code, $previous);
}
}

View File

@@ -0,0 +1,382 @@
<?php
declare(strict_types=1);
namespace App\Framework\ErrorBoundaries\Async;
use App\Framework\Async\AsyncPromise;
use App\Framework\Async\FiberManager;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\DateTime\Clock;
use App\Framework\DateTime\Timer;
use App\Framework\ErrorBoundaries\BoundaryConfig;
use App\Framework\ErrorBoundaries\CircuitBreaker\BoundaryCircuitBreakerManager;
use App\Framework\ErrorBoundaries\Events\BoundaryEventPublisher;
use App\Framework\ErrorBoundaries\Events\BoundaryExecutionFailed;
use App\Framework\ErrorBoundaries\Events\BoundaryExecutionSucceeded;
use App\Framework\ErrorBoundaries\Events\BoundaryFallbackExecuted;
use App\Framework\ErrorBoundaries\Events\BoundaryTimeoutOccurred;
use App\Framework\Logging\Logger;
use Throwable;
/**
* Async Error Boundary using framework's async components
*/
final readonly class AsyncErrorBoundary
{
public function __construct(
private string $boundaryName,
private BoundaryConfig $config,
private FiberManager $fiberManager,
private Clock $clock,
private Timer $timer,
private ?Logger $logger = null,
private ?BoundaryCircuitBreakerManager $circuitBreakerManager = null,
private ?BoundaryEventPublisher $eventPublisher = null,
) {
}
/**
* Execute async operation with error boundary
*
* @template T
* @param callable(): T $operation
* @param callable(): T|null $fallback
* @return AsyncPromise<T>
*/
public function executeAsync(callable $operation, ?callable $fallback = null): AsyncPromise
{
return AsyncPromise::create(function () use ($operation, $fallback) {
$startTime = $this->clock->time();
try {
$result = $this->executeWithRetryAsync($operation);
$executionTime = $startTime->age($this->clock);
// Publish success event
$this->publishEvent(new BoundaryExecutionSucceeded(
boundaryName: $this->boundaryName,
executionTime: $executionTime,
message: 'Async operation completed successfully',
));
return $result;
} catch (Throwable $e) {
$executionTime = $startTime->age($this->clock);
// Publish failure event
$this->publishEvent(new BoundaryExecutionFailed(
boundaryName: $this->boundaryName,
exception: $e,
executionTime: $executionTime,
willRetry: false,
message: 'Async operation failed',
));
return $this->handleFailureAsync($e, $fallback);
}
});
}
/**
* Execute multiple async operations concurrently with individual boundaries
*
* @param array<string, callable> $operations
* @param array<string, callable>|null $fallbacks
* @return AsyncPromise<array>
*/
public function executeConcurrent(array $operations, ?array $fallbacks = null): AsyncPromise
{
$promises = [];
foreach ($operations as $name => $operation) {
$fallback = $fallbacks[$name] ?? null;
$promises[$name] = $this->executeAsync($operation, $fallback);
}
return AsyncPromise::all($promises);
}
/**
* Execute with timeout using framework components
*
* @template T
* @param callable(): T $operation
* @param Duration $timeout
* @param callable(): T|null $fallback
* @return AsyncPromise<T>
*/
public function executeWithTimeout(
callable $operation,
Duration $timeout,
?callable $fallback = null
): AsyncPromise {
return AsyncPromise::create(function () use ($operation, $timeout, $fallback) {
try {
return $this->fiberManager->withTimeoutDuration($operation, $timeout);
} catch (Throwable $e) {
$this->publishEvent(new BoundaryTimeoutOccurred(
boundaryName: $this->boundaryName,
timeoutThreshold: $timeout,
actualExecutionTime: $timeout, // We don't know the exact time here
fallbackExecuted: $fallback !== null,
message: "Async operation timed out after {$timeout->toHumanReadable()}",
));
if ($fallback !== null) {
return $fallback();
}
throw $e;
}
});
}
/**
* Execute with circuit breaker support
*
* @template T
* @param callable(): T $operation
* @param callable(): T|null $fallback
* @return AsyncPromise<T>
*/
public function executeWithCircuitBreaker(callable $operation, ?callable $fallback = null): AsyncPromise
{
if ($this->config->circuitBreakerEnabled && $this->circuitBreakerManager) {
if ($this->circuitBreakerManager->isCircuitOpen($this->boundaryName, $this->config)) {
$this->log('info', 'Circuit breaker is open, using fallback');
if ($fallback !== null) {
return AsyncPromise::resolve($fallback());
}
return AsyncPromise::reject(new AsyncCircuitBreakerOpenException(
"Circuit breaker is open for boundary '{$this->boundaryName}'"
));
}
}
return AsyncPromise::create(function () use ($operation, $fallback) {
try {
$result = $operation();
if ($this->config->circuitBreakerEnabled && $this->circuitBreakerManager) {
$this->circuitBreakerManager->recordSuccess($this->boundaryName, $this->config);
}
return $result;
} catch (Throwable $e) {
if ($this->config->circuitBreakerEnabled && $this->circuitBreakerManager) {
$this->circuitBreakerManager->recordFailure($this->boundaryName, $this->config);
}
return $this->handleFailureAsync($e, $fallback);
}
});
}
/**
* Execute batch operations with individual error boundaries
*
* @template T
* @param array<T> $items
* @param callable(T): mixed $processor
* @param int $maxConcurrency
* @return AsyncPromise<AsyncBulkResult>
*/
public function executeBatch(
array $items,
callable $processor,
int $maxConcurrency = 10
): AsyncPromise {
return AsyncPromise::create(function () use ($items, $processor, $maxConcurrency) {
$operations = [];
foreach ($items as $key => $item) {
$operations[$key] = fn () => $processor($item);
}
$results = $this->fiberManager->throttled($operations, $maxConcurrency);
$successful = [];
$errors = [];
foreach ($results as $key => $result) {
if ($result instanceof Throwable) {
$errors[$key] = $result;
} else {
$successful[$key] = $result;
}
}
return new AsyncBulkResult(
results: $successful,
errors: $errors,
totalOperations: count($items),
successfulOperations: count($successful),
);
});
}
/**
* Execute operation with retry using framework components
*/
private function executeWithRetryAsync(callable $operation): mixed
{
$lastException = null;
for ($attempt = 1; $attempt <= $this->config->maxRetries; $attempt++) {
try {
return $operation();
} catch (Throwable $e) {
$lastException = $e;
// Don't retry certain types of errors
if (! $this->shouldRetry($e)) {
break;
}
if ($attempt < $this->config->maxRetries) {
$delay = $this->calculateRetryDelay($attempt);
$this->log('info', "Async attempt {$attempt} failed, retrying in {$delay}ms", [
'exception' => $e->getMessage(),
'attempt' => $attempt,
'delay' => $delay,
]);
// Use framework's timer for async sleep
$this->timer->sleep(Duration::fromMilliseconds($delay));
}
}
}
throw $lastException;
}
/**
* Handle failure with async fallback
*/
private function handleFailureAsync(Throwable $exception, ?callable $fallback): mixed
{
$this->logFailure($exception, 'Async operation failed');
if ($fallback === null) {
throw $exception;
}
try {
$result = $fallback();
// Publish fallback executed event
$this->publishEvent(new BoundaryFallbackExecuted(
boundaryName: $this->boundaryName,
originalException: $exception,
fallbackReason: 'Async operation failed: ' . $exception->getMessage(),
message: 'Async fallback executed successfully',
));
return $result;
} catch (Throwable $fallbackException) {
$this->log('error', 'Async fallback also failed', [
'original_exception' => $exception->getMessage(),
'fallback_exception' => $fallbackException->getMessage(),
]);
throw new AsyncBoundaryFailedException(
"Both async operation and fallback failed in boundary '{$this->boundaryName}'",
$this->boundaryName,
$exception,
$fallbackException
);
}
}
/**
* Check if exception should trigger retry
*/
private function shouldRetry(Throwable $e): bool
{
// Don't retry async-specific errors
if ($e instanceof AsyncCircuitBreakerOpenException) {
return false;
}
// Use same retry logic as synchronous boundary
return true; // Simplified for this example
}
/**
* Calculate retry delay
*/
private function calculateRetryDelay(int $attempt): int
{
$baseMs = (int) $this->config->baseDelay->toMilliseconds();
$maxMs = (int) $this->config->maxDelay->toMilliseconds();
return match ($this->config->retryStrategy) {
default => min($baseMs * $attempt, $maxMs),
};
}
/**
* Get async circuit breaker health
*/
public function getCircuitHealthAsync(): AsyncPromise
{
return AsyncPromise::create(function () {
if (! $this->config->circuitBreakerEnabled || ! $this->circuitBreakerManager) {
return null;
}
return $this->circuitBreakerManager->getCircuitHealth($this->boundaryName);
});
}
/**
* Reset circuit breaker asynchronously
*/
public function resetCircuitAsync(): AsyncPromise
{
return AsyncPromise::create(function () {
if ($this->config->circuitBreakerEnabled && $this->circuitBreakerManager) {
$this->circuitBreakerManager->resetCircuit($this->boundaryName);
}
});
}
private function logFailure(Throwable $e, string $message): void
{
$this->log('error', $message, [
'exception' => $e::class,
'message' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
]);
}
private function log(string $level, string $message, array $context = []): void
{
if ($this->logger) {
$context['boundary'] = $this->boundaryName;
$context['async'] = true;
match ($level) {
'debug' => $this->logger->debug("[AsyncBoundary] {$message}", $context),
'info' => $this->logger->info("[AsyncBoundary] {$message}", $context),
'warning' => $this->logger->warning("[AsyncBoundary] {$message}", $context),
'error' => $this->logger->error("[AsyncBoundary] {$message}", $context),
default => $this->logger->info("[AsyncBoundary] {$message}", $context),
};
}
}
private function publishEvent($event): void
{
if ($this->eventPublisher !== null) {
$this->eventPublisher->publish($event);
}
}
}

View File

@@ -0,0 +1,128 @@
<?php
declare(strict_types=1);
namespace App\Framework\ErrorBoundaries\Async;
use App\Framework\Async\FiberManager;
use App\Framework\DateTime\Clock;
use App\Framework\DateTime\Timer;
use App\Framework\ErrorBoundaries\BoundaryConfig;
use App\Framework\ErrorBoundaries\CircuitBreaker\BoundaryCircuitBreakerManager;
use App\Framework\ErrorBoundaries\Events\BoundaryEventPublisher;
use App\Framework\EventBus\EventBus;
use App\Framework\Logging\Logger;
use App\Framework\StateManagement\StateManagerFactory;
/**
* Factory for creating async error boundaries
*/
final readonly class AsyncErrorBoundaryFactory
{
public function __construct(
private FiberManager $fiberManager,
private Clock $clock,
private Timer $timer,
private ?Logger $logger = null,
private ?StateManagerFactory $stateManagerFactory = null,
private ?EventBus $eventBus = null,
) {
}
/**
* Create async error boundary
*/
public function create(string $name, BoundaryConfig $config): AsyncErrorBoundary
{
$circuitBreakerManager = null;
if ($config->circuitBreakerEnabled && $this->stateManagerFactory) {
$stateManager = $this->stateManagerFactory->createForErrorBoundary();
$circuitBreakerManager = new BoundaryCircuitBreakerManager($stateManager, $this->logger);
}
$eventPublisher = new BoundaryEventPublisher($this->eventBus, $this->logger);
return new AsyncErrorBoundary(
boundaryName: $name,
config: $config,
fiberManager: $this->fiberManager,
clock: $this->clock,
timer: $this->timer,
logger: $this->logger,
circuitBreakerManager: $circuitBreakerManager,
eventPublisher: $eventPublisher,
);
}
/**
* Create async boundary for external service calls
*/
public function createForExternalService(string $serviceName): AsyncErrorBoundary
{
return $this->create("async_external_{$serviceName}", BoundaryConfig::externalService());
}
/**
* Create async boundary for database operations
*/
public function createForDatabase(string $operation = 'database'): AsyncErrorBoundary
{
return $this->create("async_{$operation}", BoundaryConfig::database());
}
/**
* Create async boundary for background jobs
*/
public function createForBackgroundJob(string $jobName): AsyncErrorBoundary
{
return $this->create("async_job_{$jobName}", BoundaryConfig::backgroundJob());
}
/**
* Create async boundary for critical operations
*/
public function createForCriticalOperation(string $operationName): AsyncErrorBoundary
{
return $this->create("async_critical_{$operationName}", BoundaryConfig::critical());
}
/**
* Create async boundary for batch processing
*/
public function createForBatchProcessing(string $batchName): AsyncErrorBoundary
{
$config = new BoundaryConfig(
maxRetries: 2,
retryStrategy: \App\Framework\ErrorBoundaries\RetryStrategy::EXPONENTIAL_JITTER,
baseDelay: \App\Framework\Core\ValueObjects\Duration::fromMilliseconds(100),
maxDelay: \App\Framework\Core\ValueObjects\Duration::fromSeconds(5),
circuitBreakerEnabled: true,
circuitBreakerThreshold: 10, // Higher threshold for batch operations
circuitBreakerTimeout: \App\Framework\Core\ValueObjects\Duration::fromMinutes(2),
enableMetrics: true,
maxBulkErrorRate: 0.2, // Allow 20% error rate in batch
);
return $this->create("async_batch_{$batchName}", $config);
}
/**
* Create async boundary for high-throughput operations
*/
public function createForHighThroughput(string $operationName): AsyncErrorBoundary
{
$config = new BoundaryConfig(
maxRetries: 1, // Fewer retries for high throughput
retryStrategy: \App\Framework\ErrorBoundaries\RetryStrategy::FIXED,
baseDelay: \App\Framework\Core\ValueObjects\Duration::fromMilliseconds(50),
maxDelay: \App\Framework\Core\ValueObjects\Duration::fromMilliseconds(200),
circuitBreakerEnabled: true,
circuitBreakerThreshold: 20, // Higher threshold
circuitBreakerTimeout: \App\Framework\Core\ValueObjects\Duration::fromSeconds(30),
enableMetrics: true,
);
return $this->create("async_throughput_{$operationName}", $config);
}
}

View File

@@ -0,0 +1,266 @@
# Async ErrorBoundary System
Complete async error boundary implementation using the framework's async components (`FiberManager`, `AsyncPromise`, etc.).
## Components
### Core Classes
- **`AsyncErrorBoundary`** - Main async error boundary using framework's `FiberManager` and `AsyncPromise`
- **`AsyncErrorBoundaryFactory`** - Factory for creating async boundaries with different configurations
- **`AsyncBulkResult`** - Result object for batch async operations with success/error tracking
- **`AsyncBoundaryFailedException`** - Exception for when both operation and fallback fail
- **`AsyncCircuitBreakerOpenException`** - Exception for circuit breaker open state
## Framework Integration
### Uses Framework Async Components
- **`FiberManager`** - Fiber-based async execution with batching and throttling
- **`AsyncPromise`** - Promise-based async/await pattern
- **`Clock`/`Timer`** - Time management using framework value objects
- **`Duration`/`Timestamp`** - Proper time value objects
### ErrorBoundary Integration
- Circuit breaker state management via `BoundaryCircuitBreakerManager`
- Event publishing via `BoundaryEventPublisher`
- Metrics and observability support
- Same retry strategies and configuration as sync boundaries
## Usage Examples
### Basic Async Operation
```php
$factory = new AsyncErrorBoundaryFactory($fiberManager, $clock, $timer);
$boundary = $factory->createForExternalService('payment_api');
$promise = $boundary->executeAsync(
operation: fn() => $paymentApi->processPayment($request),
fallback: fn() => $this->createOfflinePaymentRecord($request)
);
$result = $promise->await();
```
### Concurrent Operations
```php
$operations = [
'user_data' => fn() => $userService->getUserData($userId),
'preferences' => fn() => $preferenceService->getPreferences($userId),
'notifications' => fn() => $notificationService->getUnread($userId),
];
$promise = $boundary->executeConcurrent($operations);
$results = $promise->await();
// Results: ['user_data' => ..., 'preferences' => ..., 'notifications' => ...]
```
### Batch Processing
```php
$batchBoundary = $factory->createForBatchProcessing('email_sending');
$promise = $batchBoundary->executeBatch(
items: $emailQueue,
processor: fn($email) => $mailService->send($email),
maxConcurrency: 5
);
$bulkResult = $promise->await();
echo "Success rate: " . $bulkResult->getSuccessRate()->toFloat() * 100 . "%\n";
echo "Processed: {$bulkResult->successfulOperations}/{$bulkResult->totalOperations}\n";
```
### Timeout Handling
```php
$promise = $boundary->executeWithTimeout(
operation: fn() => $heavyComputation->process($data),
timeout: Duration::fromSeconds(30),
fallback: fn() => $this->getCachedResult($data)
);
try {
$result = $promise->await();
} catch (AsyncTimeoutException $e) {
// Handle timeout
}
```
### Circuit Breaker with Async
```php
$promise = $boundary->executeWithCircuitBreaker(
operation: fn() => $externalApi->call($request),
fallback: fn() => $this->getDefaultResponse()
);
$result = $promise->await();
```
## Factory Configurations
### External Service Boundary
```php
$boundary = $factory->createForExternalService('payment_service');
```
- Retry strategy with exponential backoff
- Circuit breaker enabled
- Appropriate timeouts for external calls
### Database Boundary
```php
$boundary = $factory->createForDatabase('user_queries');
```
- Fast fail for database issues
- Connection pooling aware
- Optimized for database operation patterns
### Background Job Boundary
```php
$boundary = $factory->createForBackgroundJob('email_processor');
```
- Higher retry counts
- Longer timeouts
- Batch error tolerance
### High Throughput Boundary
```php
$boundary = $factory->createForHighThroughput('api_endpoints');
```
- Minimal retries for speed
- Higher circuit breaker thresholds
- Optimized for high-volume operations
## Async Result Types
### AsyncBulkResult
```php
$bulkResult = $promise->await();
// Statistics
$successRate = $bulkResult->getSuccessRate(); // Percentage object
$errorRate = $bulkResult->getErrorRate(); // Percentage object
// Individual results
$userResult = $bulkResult->getResult('user_1');
$userError = $bulkResult->getError('user_1');
// Status checks
$hasAnyErrors = $bulkResult->hasErrors();
$isCompleteSuccess = $bulkResult->isCompleteSuccess();
```
## Event Integration
Async boundaries publish the same events as sync boundaries:
- `BoundaryExecutionSucceeded` - Successful async operation
- `BoundaryExecutionFailed` - Failed async operation
- `BoundaryFallbackExecuted` - Fallback was used
- `BoundaryTimeoutOccurred` - Operation timed out
## Promise Chaining
```php
$promise = $boundary->executeAsync(fn() => $service->getData())
->then(fn($data) => $this->processData($data))
->then(fn($processed) => $this->saveData($processed))
->catch(fn($error) => $this->handleError($error))
->finally(fn() => $this->cleanup());
$result = $promise->await();
```
## Concurrent with Fallbacks
```php
$operations = [
'primary_data' => fn() => $primaryService->getData(),
'backup_data' => fn() => $backupService->getData(),
];
$fallbacks = [
'primary_data' => fn() => $cache->get('primary_data'),
'backup_data' => fn() => $cache->get('backup_data'),
];
$promise = $boundary->executeConcurrent($operations, $fallbacks);
$results = $promise->await();
```
## Health Monitoring
```php
// Async health check
$healthPromise = $boundary->getCircuitHealthAsync();
$health = $healthPromise->await();
if ($health && !$health['is_healthy']) {
$this->alerting->sendAlert("Boundary {$boundary->boundaryName} is unhealthy");
}
// Async circuit reset
$resetPromise = $boundary->resetCircuitAsync();
$resetPromise->await();
```
## Performance Benefits
### Framework Integration Benefits
- **Fiber-based**: Uses PHP 8.1+ Fibers for true async execution
- **Batching**: Framework's `FiberManager` handles optimal batching
- **Throttling**: Built-in concurrency control
- **Timeout Management**: Framework's timeout handling with proper cleanup
### Error Boundary Benefits
- **Graceful Degradation**: Async fallbacks prevent service interruption
- **Circuit Breaking**: Prevents cascade failures in async operations
- **Retry Logic**: Async retry with proper delays
- **Observability**: Events and metrics for async operations
## Best Practices
### Concurrency Management
```php
// Good: Use factory methods for appropriate concurrency
$batchBoundary = $factory->createForBatchProcessing('data_sync');
$bulkResult = $batchBoundary->executeBatch($items, $processor, 10);
// Good: Throttle concurrent operations
$results = $fiberManager->throttled($operations, 5);
```
### Error Handling
```php
// Good: Provide meaningful fallbacks
$promise = $boundary->executeAsync(
operation: fn() => $externalService->getData(),
fallback: fn() => $this->getCachedData() // Always have a fallback
);
// Good: Handle different error types
try {
$result = $promise->await();
} catch (AsyncCircuitBreakerOpenException $e) {
// Circuit breaker is open
} catch (AsyncBoundaryFailedException $e) {
// Both operation and fallback failed
$originalError = $e->getOriginalException();
$fallbackError = $e->getFallbackException();
}
```
### Performance Optimization
```php
// Good: Use appropriate configurations
$highThroughputBoundary = $factory->createForHighThroughput('api_calls');
$batchBoundary = $factory->createForBatchProcessing('data_processing');
// Good: Monitor performance
$bulkResult = $batchBoundary->executeBatch($items, $processor);
$this->metrics->record('batch_success_rate', $bulkResult->getSuccessRate());
```

View File

@@ -0,0 +1,158 @@
<?php
declare(strict_types=1);
namespace App\Framework\ErrorBoundaries;
use App\Framework\Core\ValueObjects\Duration;
/**
* Configuration for Error Boundaries
*/
final readonly class BoundaryConfig
{
public readonly Duration $baseDelay;
public readonly Duration $maxDelay;
public readonly Duration $circuitBreakerTimeout;
public function __construct(
public int $maxRetries = 3,
public RetryStrategy $retryStrategy = RetryStrategy::EXPONENTIAL_JITTER,
?Duration $baseDelay = null,
?Duration $maxDelay = null,
public bool $circuitBreakerEnabled = false,
public int $circuitBreakerThreshold = 5, // failures before opening circuit
?Duration $circuitBreakerTimeout = null,
public float $maxBulkErrorRate = 0.5, // Stop bulk processing if error rate exceeds this
public bool $enableMetrics = true,
public bool $enableTracing = false,
) {
$this->baseDelay = $baseDelay ?? Duration::fromMilliseconds(100);
$this->maxDelay = $maxDelay ?? Duration::fromSeconds(5);
$this->circuitBreakerTimeout = $circuitBreakerTimeout ?? Duration::fromMinutes(1);
}
/**
* Creates configuration for critical operations
*/
public static function critical(): self
{
return new self(
maxRetries: 5,
retryStrategy: RetryStrategy::EXPONENTIAL_JITTER,
baseDelay: Duration::fromMilliseconds(50),
maxDelay: Duration::fromSeconds(2),
circuitBreakerEnabled: true,
circuitBreakerThreshold: 3,
circuitBreakerTimeout: Duration::fromSeconds(30),
maxBulkErrorRate: 0.1,
enableMetrics: true,
enableTracing: true,
);
}
/**
* Creates configuration for external services
*/
public static function externalService(): self
{
return new self(
maxRetries: 3,
retryStrategy: RetryStrategy::EXPONENTIAL_JITTER,
baseDelay: Duration::fromMilliseconds(200),
maxDelay: Duration::fromSeconds(10),
circuitBreakerEnabled: true,
circuitBreakerThreshold: 5,
circuitBreakerTimeout: Duration::fromMinutes(2),
maxBulkErrorRate: 0.3,
enableMetrics: true,
);
}
/**
* Creates configuration for database operations
*/
public static function database(): self
{
return new self(
maxRetries: 2,
retryStrategy: RetryStrategy::EXPONENTIAL,
baseDelay: Duration::fromMilliseconds(100),
maxDelay: Duration::fromSeconds(1),
circuitBreakerEnabled: true,
circuitBreakerThreshold: 3,
circuitBreakerTimeout: Duration::fromMinutes(1),
maxBulkErrorRate: 0.2,
enableMetrics: true,
);
}
/**
* Creates configuration for UI components
*/
public static function ui(): self
{
return new self(
maxRetries: 1,
retryStrategy: RetryStrategy::FIXED,
baseDelay: Duration::fromMilliseconds(50),
maxDelay: Duration::fromMilliseconds(200),
circuitBreakerEnabled: false,
maxBulkErrorRate: 0.8, // UI can tolerate more partial failures
enableMetrics: false,
);
}
/**
* Creates configuration for background jobs
*/
public static function backgroundJob(): self
{
return new self(
maxRetries: 5,
retryStrategy: RetryStrategy::EXPONENTIAL_JITTER,
baseDelay: Duration::fromSeconds(1),
maxDelay: Duration::fromMinutes(1),
circuitBreakerEnabled: false, // Jobs will be retried by queue system
maxBulkErrorRate: 0.1,
enableMetrics: true,
enableTracing: true,
);
}
/**
* Creates configuration with no retries (fail fast)
*/
public static function failFast(): self
{
return new self(
maxRetries: 0,
retryStrategy: RetryStrategy::FIXED,
baseDelay: Duration::zero(),
maxDelay: Duration::zero(),
circuitBreakerEnabled: false,
circuitBreakerTimeout: Duration::zero(), // No timeout needed for fail fast
maxBulkErrorRate: 0.0,
enableMetrics: false,
);
}
/**
* Creates configuration for development/testing
*/
public static function development(): self
{
return new self(
maxRetries: 1,
retryStrategy: RetryStrategy::FIXED,
baseDelay: Duration::fromMilliseconds(10),
maxDelay: Duration::fromMilliseconds(100),
circuitBreakerEnabled: false,
maxBulkErrorRate: 1.0, // Allow all errors in development
enableMetrics: true,
enableTracing: true,
);
}
}

View File

@@ -0,0 +1,135 @@
<?php
declare(strict_types=1);
namespace App\Framework\ErrorBoundaries;
use App\Framework\Exception\ExceptionContext;
use App\Framework\Exception\FrameworkException;
use Throwable;
/**
* Exception thrown when error boundary fails
*/
final class BoundaryFailedException extends FrameworkException
{
public function __construct(
string $message,
private readonly string $boundaryName,
private readonly ?Throwable $originalException = null,
private readonly ?Throwable $fallbackException = null,
int $code = 0,
?Throwable $previous = null
) {
parent::__construct(
message: $message,
context: ExceptionContext::forOperation('error_boundary_failure', 'ErrorBoundary')
->withData([
'boundary_name' => $boundaryName,
'double_failure' => $originalException !== null && $fallbackException !== null,
'original_error' => $originalException?->getMessage(),
'fallback_error' => $fallbackException?->getMessage(),
]),
code: $code,
previous: $previous
);
}
/**
* Get the boundary name where failure occurred
*/
public function getBoundaryName(): string
{
return $this->boundaryName;
}
/**
* Get the original exception that caused the boundary to trigger
*/
public function getOriginalException(): ?Throwable
{
return $this->originalException;
}
/**
* Get the fallback exception (if fallback also failed)
*/
public function getFallbackException(): ?Throwable
{
return $this->fallbackException;
}
/**
* Check if this was a double failure (both operation and fallback failed)
*/
public function isDoubleFailure(): bool
{
return $this->originalException !== null && $this->fallbackException !== null;
}
/**
* Get detailed error information
*/
public function getDetailedMessage(): string
{
$message = "Error boundary '{$this->boundaryName}' failed: {$this->getMessage()}";
if ($this->originalException) {
$message .= "\nOriginal error: " . $this->originalException->getMessage();
}
if ($this->fallbackException) {
$message .= "\nFallback error: " . $this->fallbackException->getMessage();
}
return $message;
}
/**
* Get all exceptions in the failure chain
*
* @return Throwable[]
*/
public function getAllExceptions(): array
{
$exceptions = [$this];
if ($this->originalException) {
$exceptions[] = $this->originalException;
}
if ($this->fallbackException) {
$exceptions[] = $this->fallbackException;
}
return $exceptions;
}
/**
* Create exception for single failure
*/
public static function singleFailure(string $boundaryName, Throwable $originalException): self
{
return new self(
"Operation failed in boundary '{$boundaryName}'",
$boundaryName,
$originalException
);
}
/**
* Create exception for double failure (operation and fallback both failed)
*/
public static function doubleFailure(
string $boundaryName,
Throwable $originalException,
Throwable $fallbackException
): self {
return new self(
"Both operation and fallback failed in boundary '{$boundaryName}'",
$boundaryName,
$originalException,
$fallbackException
);
}
}

View File

@@ -0,0 +1,206 @@
<?php
declare(strict_types=1);
namespace App\Framework\ErrorBoundaries;
use Throwable;
/**
* Result wrapper for error boundary operations
*
* @template T
*/
final readonly class BoundaryResult
{
private function __construct(
private mixed $value,
private ?Throwable $error,
private bool $isSuccess,
private ?string $boundaryName = null
) {
}
/**
* Create successful result
*
* @template U
* @param U $value
* @return BoundaryResult<U>
*/
public static function success(mixed $value): self
{
return new self($value, null, true);
}
/**
* Create failed result
*
* @template U
* @param Throwable $error
* @param string|null $boundaryName
* @return BoundaryResult<U>
*/
public static function failure(Throwable $error, ?string $boundaryName = null): self
{
return new self(null, $error, false, $boundaryName);
}
/**
* Check if operation was successful
*/
public function isSuccess(): bool
{
return $this->isSuccess;
}
/**
* Check if operation failed
*/
public function isFailure(): bool
{
return ! $this->isSuccess;
}
/**
* Get the result value (only if successful)
*
* @return T
* @throws BoundaryFailedException
*/
public function getValue(): mixed
{
if (! $this->isSuccess) {
throw new BoundaryFailedException(
'Cannot get value from failed result',
$this->boundaryName ?? 'unknown',
$this->error
);
}
return $this->value;
}
/**
* Get the error (only if failed)
*/
public function getError(): ?Throwable
{
return $this->error;
}
/**
* Get boundary name
*/
public function getBoundaryName(): ?string
{
return $this->boundaryName;
}
/**
* Get value or return default if failed
*
* @param T $default
* @return T
*/
public function getValueOrDefault(mixed $default): mixed
{
return $this->isSuccess ? $this->value : $default;
}
/**
* Get value or execute fallback function if failed
*
* @param callable(): T $fallback
* @return T
*/
public function getValueOrElse(callable $fallback): mixed
{
return $this->isSuccess ? $this->value : $fallback();
}
/**
* Transform the result if successful
*
* @template U
* @param callable(T): U $mapper
* @return BoundaryResult<U>
*/
public function map(callable $mapper): self
{
if (! $this->isSuccess) {
return new self(null, $this->error, false, $this->boundaryName);
}
try {
$transformed = $mapper($this->value);
return new self($transformed, null, true, $this->boundaryName);
} catch (Throwable $e) {
return new self(null, $e, false, $this->boundaryName);
}
}
/**
* Chain another operation if current result is successful
*
* @template U
* @param callable(T): BoundaryResult<U> $mapper
* @return BoundaryResult<U>
*/
public function flatMap(callable $mapper): self
{
if (! $this->isSuccess) {
return new self(null, $this->error, false, $this->boundaryName);
}
try {
return $mapper($this->value);
} catch (Throwable $e) {
return new self(null, $e, false, $this->boundaryName);
}
}
/**
* Execute callback if result is successful
*
* @param callable(T): void $callback
* @return self
*/
public function onSuccess(callable $callback): self
{
if ($this->isSuccess) {
$callback($this->value);
}
return $this;
}
/**
* Execute callback if result failed
*
* @param callable(Throwable): void $callback
* @return self
*/
public function onFailure(callable $callback): self
{
if (! $this->isSuccess && $this->error) {
$callback($this->error);
}
return $this;
}
/**
* Convert to array representation
*/
public function toArray(): array
{
return [
'success' => $this->isSuccess,
'value' => $this->isSuccess ? $this->value : null,
'error' => $this->error?->getMessage(),
'boundary' => $this->boundaryName,
];
}
}

View File

@@ -0,0 +1,117 @@
<?php
declare(strict_types=1);
namespace App\Framework\ErrorBoundaries;
use App\Framework\Exception\ExceptionContext;
use App\Framework\Exception\FrameworkException;
/**
* Exception thrown when operation exceeds timeout
*/
final class BoundaryTimeoutException extends FrameworkException
{
public function __construct(
string $message,
private readonly string $boundaryName,
private readonly float $executionTime,
private readonly float $timeoutLimit = 0.0,
int $code = 0
) {
parent::__construct(
message: $message,
context: ExceptionContext::forOperation('error_boundary_timeout', 'ErrorBoundary')
->withData([
'boundary_name' => $boundaryName,
'execution_time' => $executionTime,
'timeout_limit' => $timeoutLimit,
'overage' => max(0.0, $executionTime - $timeoutLimit),
'hard_timeout' => $timeoutLimit > 0.0 && $executionTime > $timeoutLimit,
]),
code: $code
);
}
/**
* Get the boundary name where timeout occurred
*/
public function getBoundaryName(): string
{
return $this->boundaryName;
}
/**
* Get the actual execution time in seconds
*/
public function getExecutionTime(): float
{
return $this->executionTime;
}
/**
* Get the timeout limit in seconds
*/
public function getTimeoutLimit(): float
{
return $this->timeoutLimit;
}
/**
* Get how much the execution exceeded the timeout
*/
public function getOverage(): float
{
return max(0.0, $this->executionTime - $this->timeoutLimit);
}
/**
* Check if this was a hard timeout (exceeded limit)
*/
public function isHardTimeout(): bool
{
return $this->timeoutLimit > 0.0 && $this->executionTime > $this->timeoutLimit;
}
/**
* Get human-readable timeout information
*/
public function getTimeoutInfo(): string
{
$info = "Boundary '{$this->boundaryName}' timed out after {$this->executionTime}s";
if ($this->timeoutLimit > 0.0) {
$info .= " (limit: {$this->timeoutLimit}s, overage: " . $this->getOverage() . "s)";
}
return $info;
}
/**
* Create timeout exception with limit
*/
public static function withLimit(
string $boundaryName,
float $executionTime,
float $timeoutLimit
): self {
return new self(
"Operation in boundary '{$boundaryName}' exceeded timeout of {$timeoutLimit}s (took {$executionTime}s)",
$boundaryName,
$executionTime,
$timeoutLimit
);
}
/**
* Create timeout exception without specific limit
*/
public static function withoutLimit(string $boundaryName, float $executionTime): self
{
return new self(
"Operation in boundary '{$boundaryName}' timed out after {$executionTime}s",
$boundaryName,
$executionTime
);
}
}

View File

@@ -0,0 +1,269 @@
<?php
declare(strict_types=1);
namespace App\Framework\ErrorBoundaries;
use Throwable;
/**
* Result of bulk operations with partial failure support
*
* @template T
*/
final readonly class BulkResult
{
/**
* @param array<string|int, mixed> $results Successful results indexed by original key
* @param array<string|int, Throwable> $errors Failed operations indexed by original key
* @param int $processedCount Number of items actually processed
* @param int $totalCount Total number of items in the batch
*/
public function __construct(
private array $results,
private array $errors,
private int $processedCount,
private int $totalCount
) {
}
/**
* Get all successful results
*/
public function getResults(): array
{
return $this->results;
}
/**
* Get all errors
*
* @return array<string|int, Throwable>
*/
public function getErrors(): array
{
return $this->errors;
}
/**
* Get number of successful operations
*/
public function getSuccessCount(): int
{
return count($this->results);
}
/**
* Get number of failed operations
*/
public function getErrorCount(): int
{
return count($this->errors);
}
/**
* Get number of items actually processed (may be less than total if stopped early)
*/
public function getProcessedCount(): int
{
return $this->processedCount;
}
/**
* Get total number of items in the batch
*/
public function getTotalCount(): int
{
return $this->totalCount;
}
/**
* Get number of items skipped (not processed due to early termination)
*/
public function getSkippedCount(): int
{
return $this->totalCount - $this->processedCount;
}
/**
* Check if all operations were successful
*/
public function isCompleteSuccess(): bool
{
return count($this->errors) === 0 && $this->processedCount === $this->totalCount;
}
/**
* Check if any operations were successful
*/
public function hasSuccesses(): bool
{
return count($this->results) > 0;
}
/**
* Check if any operations failed
*/
public function hasErrors(): bool
{
return count($this->errors) > 0;
}
/**
* Check if processing was terminated early
*/
public function wasTerminatedEarly(): bool
{
return $this->processedCount < $this->totalCount;
}
/**
* Get success rate as percentage
*/
public function getSuccessRate(): float
{
if ($this->processedCount === 0) {
return 0.0;
}
return (count($this->results) / $this->processedCount) * 100.0;
}
/**
* Get error rate as percentage
*/
public function getErrorRate(): float
{
if ($this->processedCount === 0) {
return 0.0;
}
return (count($this->errors) / $this->processedCount) * 100.0;
}
/**
* Get result for specific key
*/
public function getResult(string|int $key): mixed
{
return $this->results[$key] ?? null;
}
/**
* Get error for specific key
*/
public function getError(string|int $key): ?Throwable
{
return $this->errors[$key] ?? null;
}
/**
* Check if specific key was successful
*/
public function wasSuccessful(string|int $key): bool
{
return array_key_exists($key, $this->results);
}
/**
* Check if specific key failed
*/
public function hasFailed(string|int $key): bool
{
return array_key_exists($key, $this->errors);
}
/**
* Get all keys that were successful
*/
public function getSuccessfulKeys(): array
{
return array_keys($this->results);
}
/**
* Get all keys that failed
*/
public function getFailedKeys(): array
{
return array_keys($this->errors);
}
/**
* Filter results by condition
*
* @param callable(mixed, string|int): bool $predicate
*/
public function filterResults(callable $predicate): array
{
return array_filter($this->results, $predicate, ARRAY_FILTER_USE_BOTH);
}
/**
* Transform successful results
*
* @template U
* @param callable(mixed, string|int): U $transformer
* @return array<string|int, U>
*/
public function mapResults(callable $transformer): array
{
$mapped = [];
foreach ($this->results as $key => $result) {
$mapped[$key] = $transformer($result, $key);
}
return $mapped;
}
/**
* Get summary statistics
*/
public function getSummary(): array
{
return [
'total' => $this->totalCount,
'processed' => $this->processedCount,
'successful' => $this->getSuccessCount(),
'failed' => $this->getErrorCount(),
'skipped' => $this->getSkippedCount(),
'success_rate' => round($this->getSuccessRate(), 2),
'error_rate' => round($this->getErrorRate(), 2),
'completed' => $this->isCompleteSuccess(),
'terminated_early' => $this->wasTerminatedEarly(),
];
}
/**
* Convert to array representation
*/
public function toArray(): array
{
return [
'results' => $this->results,
'errors' => array_map(fn (Throwable $e) => $e->getMessage(), $this->errors),
'summary' => $this->getSummary(),
];
}
/**
* Create empty result
*/
public static function empty(): self
{
return new self([], [], 0, 0);
}
/**
* Create from arrays
*/
public static function create(
array $results,
array $errors,
int $processedCount,
int $totalCount
): self {
return new self($results, $errors, $processedCount, $totalCount);
}
}

View File

@@ -0,0 +1,203 @@
<?php
declare(strict_types=1);
namespace App\Framework\ErrorBoundaries\CircuitBreaker;
use App\Framework\Core\ValueObjects\Timestamp;
use App\Framework\ErrorBoundaries\BoundaryConfig;
use App\Framework\Logging\Logger;
use App\Framework\StateManagement\StateManager;
/**
* Circuit breaker manager for error boundaries using generic state management
*/
final readonly class BoundaryCircuitBreakerManager
{
public function __construct(
/**
* @var StateManager<BoundaryCircuitBreakerState>
*/
private StateManager $stateManager,
private ?Logger $logger = null,
) {
}
/**
* Check if circuit is open for given boundary
*/
public function isCircuitOpen(string $boundaryName, BoundaryConfig $config): bool
{
$state = $this->getCircuitState($boundaryName);
if ($state->isOpen()) {
// Check if circuit should transition to half-open
if ($this->shouldTransitionToHalfOpen($state, $config)) {
$newState = $state->transitionToHalfOpen();
$this->stateManager->setState($boundaryName, $newState);
$this->log('info', "Circuit transitioned to HALF_OPEN for boundary: {$boundaryName}");
return false; // Allow operations in half-open state
}
return true;
}
return false;
}
/**
* Record a successful operation
*/
public function recordSuccess(string $boundaryName, BoundaryConfig $config): void
{
$newState = $this->stateManager->updateState(
$boundaryName,
function (?BoundaryCircuitBreakerState $currentState) use ($config, $boundaryName): BoundaryCircuitBreakerState {
$state = $currentState ?? new BoundaryCircuitBreakerState();
$newState = $state->recordSuccess();
// If in half-open state and enough successes, close the circuit
if ($state->isHalfOpen() && $newState->meetsSuccessThreshold($config->successThreshold ?? 3)) {
$newState = $newState->transitionToClosed();
$this->log('info', "Circuit transitioned to CLOSED for boundary: {$boundaryName}");
}
return $newState;
}
);
}
/**
* Record a failed operation
*/
public function recordFailure(string $boundaryName, BoundaryConfig $config): void
{
$newState = $this->stateManager->updateState(
$boundaryName,
function (?BoundaryCircuitBreakerState $currentState) use ($config, $boundaryName): BoundaryCircuitBreakerState {
$state = $currentState ?? new BoundaryCircuitBreakerState();
$newState = $state->recordFailure();
// Check if failure threshold is exceeded
if ($newState->exceedsFailureThreshold($config->circuitBreakerThreshold)) {
$newState = $newState->transitionToOpen();
$this->log('warning', "Circuit transitioned to OPEN for boundary: {$boundaryName} after {$config->circuitBreakerThreshold} failures");
}
// If in half-open state, any failure should open the circuit
if ($state->isHalfOpen()) {
$newState = $newState->transitionToOpen();
$this->log('warning', "Circuit transitioned to OPEN for boundary: {$boundaryName} due to failure in HALF_OPEN state");
}
return $newState;
}
);
}
/**
* Get current circuit state
*/
public function getCircuitState(string $boundaryName): BoundaryCircuitBreakerState
{
return $this->stateManager->getState($boundaryName) ?? new BoundaryCircuitBreakerState();
}
/**
* Reset circuit breaker state
*/
public function resetCircuit(string $boundaryName): void
{
$newState = new BoundaryCircuitBreakerState(); // Default closed state
$this->stateManager->setState($boundaryName, $newState);
$this->log('info', "Circuit reset for boundary: {$boundaryName}");
}
/**
* Get all circuit breaker states
*/
public function getAllCircuitStates(): array
{
$states = $this->stateManager->getAllStates();
$result = [];
foreach ($states as $boundaryName => $state) {
$result[$boundaryName] = $this->getCircuitHealth($boundaryName);
}
return $result;
}
/**
* Check circuit health for monitoring
*/
public function getCircuitHealth(string $boundaryName): array
{
$state = $this->getCircuitState($boundaryName);
return [
'boundary_name' => $boundaryName,
'state' => $state->state->value,
'state_description' => $state->state->getDescription(),
'failure_count' => $state->failureCount,
'success_count' => $state->successCount,
'last_failure_time' => $state->lastFailureTime?->toIsoString(),
'opened_at' => $state->openedAt?->toIsoString(),
'half_open_attempts' => $state->halfOpenAttempts,
'is_healthy' => $state->isClosed(),
'severity' => $state->state->getSeverity(),
];
}
/**
* Get state manager statistics
*/
public function getStatistics(): array
{
return $this->stateManager->getStatistics()->toArray();
}
/**
* Increment half-open attempts
*/
public function incrementHalfOpenAttempts(string $boundaryName): void
{
$this->stateManager->updateState(
$boundaryName,
function (?BoundaryCircuitBreakerState $currentState): BoundaryCircuitBreakerState {
$state = $currentState ?? new BoundaryCircuitBreakerState();
return $state->incrementHalfOpenAttempts();
}
);
}
private function shouldTransitionToHalfOpen(BoundaryCircuitBreakerState $state, BoundaryConfig $config): bool
{
if (! $state->isOpen() || $state->openedAt === null) {
return false;
}
$timeSinceOpened = Timestamp::now()->diff($state->openedAt);
return $timeSinceOpened->isGreaterThan($config->circuitBreakerTimeout);
}
private function log(string $level, string $message, array $context = []): void
{
if ($this->logger === null) {
return;
}
$context['component'] = 'BoundaryCircuitBreakerManager';
match ($level) {
'debug' => $this->logger->debug("[ErrorBoundary] {$message}", $context),
'info' => $this->logger->info("[ErrorBoundary] {$message}", $context),
'warning' => $this->logger->warning("[ErrorBoundary] {$message}", $context),
'error' => $this->logger->error("[ErrorBoundary] {$message}", $context),
default => $this->logger->info("[ErrorBoundary] {$message}", $context),
};
}
}

View File

@@ -0,0 +1,196 @@
<?php
declare(strict_types=1);
namespace App\Framework\ErrorBoundaries\CircuitBreaker;
use App\Framework\Core\ValueObjects\Timestamp;
use App\Framework\StateManagement\SerializableState;
/**
* Value object representing circuit breaker state for error boundaries
*/
final readonly class BoundaryCircuitBreakerState implements SerializableState
{
public function __construct(
public int $failureCount = 0,
public int $successCount = 0,
public ?Timestamp $lastFailureTime = null,
public ?Timestamp $openedAt = null,
public BoundaryCircuitState $state = BoundaryCircuitState::CLOSED,
public int $halfOpenAttempts = 0,
) {
}
/**
* Check if circuit is open
*/
public function isOpen(): bool
{
return $this->state === BoundaryCircuitState::OPEN;
}
/**
* Check if circuit is closed
*/
public function isClosed(): bool
{
return $this->state === BoundaryCircuitState::CLOSED;
}
/**
* Check if circuit is half-open
*/
public function isHalfOpen(): bool
{
return $this->state === BoundaryCircuitState::HALF_OPEN;
}
/**
* Record a failure and return new state
*/
public function recordFailure(): self
{
return new self(
failureCount: $this->failureCount + 1,
successCount: $this->successCount,
lastFailureTime: Timestamp::now(),
openedAt: $this->openedAt,
state: $this->state,
halfOpenAttempts: $this->halfOpenAttempts,
);
}
/**
* Record a success and return new state
*/
public function recordSuccess(): self
{
return new self(
failureCount: 0, // Reset failure count on success
successCount: $this->successCount + 1,
lastFailureTime: $this->lastFailureTime,
openedAt: null, // Reset opened time
state: $this->state,
halfOpenAttempts: 0, // Reset half-open attempts
);
}
/**
* Transition to OPEN state
*/
public function transitionToOpen(): self
{
return new self(
failureCount: $this->failureCount,
successCount: $this->successCount,
lastFailureTime: $this->lastFailureTime,
openedAt: Timestamp::now(),
state: BoundaryCircuitState::OPEN,
halfOpenAttempts: 0,
);
}
/**
* Transition to HALF_OPEN state
*/
public function transitionToHalfOpen(): self
{
return new self(
failureCount: $this->failureCount,
successCount: $this->successCount,
lastFailureTime: $this->lastFailureTime,
openedAt: $this->openedAt,
state: BoundaryCircuitState::HALF_OPEN,
halfOpenAttempts: 0,
);
}
/**
* Transition to CLOSED state
*/
public function transitionToClosed(): self
{
return new self(
failureCount: 0, // Reset failure count
successCount: $this->successCount,
lastFailureTime: null, // Reset last failure time
openedAt: null, // Reset opened time
state: BoundaryCircuitState::CLOSED,
halfOpenAttempts: 0,
);
}
/**
* Increment half-open attempts
*/
public function incrementHalfOpenAttempts(): self
{
return new self(
failureCount: $this->failureCount,
successCount: $this->successCount,
lastFailureTime: $this->lastFailureTime,
openedAt: $this->openedAt,
state: $this->state,
halfOpenAttempts: $this->halfOpenAttempts + 1,
);
}
/**
* Check if failure threshold is exceeded
*/
public function exceedsFailureThreshold(int $threshold): bool
{
return $this->failureCount >= $threshold;
}
/**
* Check if success threshold is met for half-open state
*/
public function meetsSuccessThreshold(int $threshold): bool
{
return $this->successCount >= $threshold;
}
/**
* Check if half-open attempts exceed maximum
*/
public function exceedsHalfOpenAttempts(int $maxAttempts): bool
{
return $this->halfOpenAttempts >= $maxAttempts;
}
/**
* Convert to array for serialization
*/
public function toArray(): array
{
return [
'failure_count' => $this->failureCount,
'success_count' => $this->successCount,
'last_failure_time' => $this->lastFailureTime?->toFloat(),
'opened_at' => $this->openedAt?->toFloat(),
'state' => $this->state->value,
'half_open_attempts' => $this->halfOpenAttempts,
];
}
/**
* Create from array (deserialization)
*/
public static function fromArray(array $data): static
{
return new self(
failureCount: $data['failure_count'] ?? 0,
successCount: $data['success_count'] ?? 0,
lastFailureTime: isset($data['last_failure_time'])
? Timestamp::fromFloat($data['last_failure_time'])
: null,
openedAt: isset($data['opened_at'])
? Timestamp::fromFloat($data['opened_at'])
: null,
state: BoundaryCircuitState::from($data['state'] ?? 'closed'),
halfOpenAttempts: $data['half_open_attempts'] ?? 0,
);
}
}

View File

@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace App\Framework\ErrorBoundaries\CircuitBreaker;
/**
* Circuit breaker states for error boundaries
*/
enum BoundaryCircuitState: string
{
case CLOSED = 'closed';
case OPEN = 'open';
case HALF_OPEN = 'half_open';
/**
* Get human-readable description
*/
public function getDescription(): string
{
return match ($this) {
self::CLOSED => 'Circuit is closed - operations are allowed',
self::OPEN => 'Circuit is open - operations are blocked',
self::HALF_OPEN => 'Circuit is half-open - limited operations are allowed for testing',
};
}
/**
* Check if operations are allowed in this state
*/
public function allowsOperations(): bool
{
return match ($this) {
self::CLOSED => true,
self::OPEN => false,
self::HALF_OPEN => true, // Limited operations allowed
};
}
/**
* Get the severity level of this state
*/
public function getSeverity(): string
{
return match ($this) {
self::CLOSED => 'info',
self::OPEN => 'error',
self::HALF_OPEN => 'warning',
};
}
}

View File

@@ -0,0 +1,279 @@
<?php
declare(strict_types=1);
namespace App\Framework\ErrorBoundaries\Commands;
use App\Framework\Console\ConsoleCommand;
use App\Framework\Console\ConsoleInput;
use App\Framework\Console\ConsoleOutput;
use App\Framework\Console\ExitCode;
use App\Framework\ErrorBoundaries\BoundaryConfig;
use App\Framework\ErrorBoundaries\ErrorBoundaryFactory;
/**
* Console commands for error boundary management
*/
final readonly class BoundaryCommand
{
public function __construct(
private ErrorBoundaryFactory $boundaryFactory
) {
}
#[ConsoleCommand(
name: 'boundary:test',
description: 'Test error boundary functionality'
)]
public function test(ConsoleInput $input, ConsoleOutput $output): int
{
$args = $input->getArguments();
$testType = $args[0] ?? 'basic';
$output->writeLine('<info>Testing Error Boundary functionality...</info>');
$output->writeLine('');
return match ($testType) {
'basic' => $this->testBasicFunctionality($output),
'retry' => $this->testRetryStrategies($output),
'circuit' => $this->testCircuitBreaker($output),
'bulk' => $this->testBulkOperations($output),
'timeout' => $this->testTimeout($output),
default => $this->showTestOptions($output),
};
}
#[ConsoleCommand(
name: 'boundary:stats',
description: 'Show error boundary statistics'
)]
public function stats(ConsoleInput $input, ConsoleOutput $output): int
{
$output->writeLine('<info>Error Boundary Statistics</info>');
$output->writeLine('');
// Get circuit breaker states
$states = $this->getCircuitBreakerStates();
if (empty($states)) {
$output->writeLine('<comment>No active circuit breakers found.</comment>');
} else {
$output->writeLine('<comment>Active Circuit Breakers:</comment>');
foreach ($states as $boundary => $state) {
$statusIcon = $state['failures'] >= 5 ? '🔴' : '🟢';
$output->writeLine(" {$statusIcon} <comment>{$boundary}</comment>: {$state['failures']} failures");
if ($state['failures'] > 0) {
$lastFailure = date('Y-m-d H:i:s', $state['opened_at']);
$output->writeLine(" Last failure: {$lastFailure}");
}
}
}
return ExitCode::SUCCESS;
}
#[ConsoleCommand(
name: 'boundary:reset',
description: 'Reset circuit breaker states'
)]
public function reset(ConsoleInput $input, ConsoleOutput $output): int
{
$args = $input->getArguments();
$boundaryName = $args[0] ?? null;
if ($boundaryName) {
$this->resetCircuitBreaker($boundaryName);
$output->writeLine("<info>Reset circuit breaker for boundary: {$boundaryName}</info>");
} else {
$this->resetAllCircuitBreakers();
$output->writeLine('<info>Reset all circuit breakers</info>');
}
return ExitCode::SUCCESS;
}
private function testBasicFunctionality(ConsoleOutput $output): int
{
$output->writeLine('<comment>Testing basic error boundary functionality...</comment>');
$boundary = $this->boundaryFactory->create('test_basic', BoundaryConfig::development());
// Test successful operation
$result = $boundary->execute(
operation: fn () => 'success',
fallback: fn () => 'fallback'
);
$successIcon = $result === 'success' ? '✅' : '❌';
$output->writeLine(" {$successIcon} Success case: {$result}");
// Test fallback
$result = $boundary->execute(
operation: fn () => throw new \Exception('Test failure'),
fallback: fn () => 'fallback_executed'
);
$fallbackIcon = $result === 'fallback_executed' ? '✅' : '❌';
$output->writeLine(" {$fallbackIcon} Fallback case: {$result}");
return ExitCode::SUCCESS;
}
private function testRetryStrategies(ConsoleOutput $output): int
{
$output->writeLine('<comment>Testing retry strategies...</comment>');
$strategies = [
'FIXED' => BoundaryConfig::development(),
'EXPONENTIAL' => new BoundaryConfig(maxRetries: 3, retryStrategy: \App\Framework\ErrorBoundaries\RetryStrategy::EXPONENTIAL),
'EXPONENTIAL_JITTER' => new BoundaryConfig(maxRetries: 3, retryStrategy: \App\Framework\ErrorBoundaries\RetryStrategy::EXPONENTIAL_JITTER),
];
foreach ($strategies as $name => $config) {
$boundary = $this->boundaryFactory->create("test_retry_{$name}", $config);
$attemptCount = 0;
$result = $boundary->executeOptional(function () use (&$attemptCount) {
$attemptCount++;
if ($attemptCount < 3) {
throw new \Exception("Attempt {$attemptCount}");
}
return "Success after {$attemptCount} attempts";
});
$icon = $result !== null ? '✅' : '❌';
$output->writeLine(" {$icon} {$name}: {$result}");
}
return ExitCode::SUCCESS;
}
private function testCircuitBreaker(ConsoleOutput $output): int
{
$output->writeLine('<comment>Testing circuit breaker...</comment>');
$config = new BoundaryConfig(
circuitBreakerEnabled: true,
circuitBreakerThreshold: 2,
maxRetries: 1
);
$boundary = $this->boundaryFactory->create('test_circuit', $config);
// Trigger failures to open circuit
for ($i = 1; $i <= 3; $i++) {
$result = $boundary->executeWithCircuitBreaker(
operation: fn () => throw new \Exception("Failure {$i}"),
fallback: fn () => "Fallback {$i}"
);
$output->writeLine(" Attempt {$i}: {$result}");
}
$output->writeLine(' <info>Circuit should now be open, subsequent calls should use fallback immediately</info>');
return ExitCode::SUCCESS;
}
private function testBulkOperations(ConsoleOutput $output): int
{
$output->writeLine('<comment>Testing bulk operations...</comment>');
$boundary = $this->boundaryFactory->create('test_bulk', BoundaryConfig::development());
$items = range(1, 10);
$result = $boundary->executeBulk($items, function ($item) {
if ($item % 3 === 0) {
throw new \Exception("Item {$item} failed");
}
return $item * 2;
});
$output->writeLine(" Processed: {$result->getProcessedCount()}/{$result->getTotalCount()}");
$output->writeLine(" Successful: {$result->getSuccessCount()}");
$output->writeLine(" Failed: {$result->getErrorCount()}");
$output->writeLine(" Success rate: " . round($result->getSuccessRate(), 1) . "%");
return ExitCode::SUCCESS;
}
private function testTimeout(ConsoleOutput $output): int
{
$output->writeLine('<comment>Testing timeout functionality...</comment>');
$boundary = $this->boundaryFactory->create('test_timeout', BoundaryConfig::development());
// Test normal operation
$result = $boundary->executeWithTimeout(
operation: fn () => 'quick_operation',
fallback: fn () => 'timeout_fallback',
timeoutSeconds: 1
);
$quickIcon = $result === 'quick_operation' ? '✅' : '❌';
$output->writeLine(" {$quickIcon} Quick operation: {$result}");
// Note: In real PHP, we can't actually test timeouts without async operations
$output->writeLine(' <comment>Note: True timeout testing requires async operations</comment>');
return ExitCode::SUCCESS;
}
private function showTestOptions(ConsoleOutput $output): int
{
$output->writeLine('<error>Invalid test type. Available options:</error>');
$output->writeLine(' <comment>basic</comment> - Test basic success/fallback functionality');
$output->writeLine(' <comment>retry</comment> - Test retry strategies');
$output->writeLine(' <comment>circuit</comment> - Test circuit breaker');
$output->writeLine(' <comment>bulk</comment> - Test bulk operations');
$output->writeLine(' <comment>timeout</comment> - Test timeout handling');
return ExitCode::FAILURE;
}
private function getCircuitBreakerStates(): array
{
$states = [];
$tempDir = sys_get_temp_dir();
$files = glob($tempDir . '/boundary_*_state');
foreach ($files as $file) {
$boundaryName = str_replace(
[$tempDir . '/boundary_', '_state'],
'',
$file
);
$content = file_get_contents($file);
$state = json_decode($content, true);
if ($state) {
$states[$boundaryName] = $state;
}
}
return $states;
}
private function resetCircuitBreaker(string $boundaryName): void
{
$stateFile = sys_get_temp_dir() . "/boundary_{$boundaryName}_state";
if (file_exists($stateFile)) {
unlink($stateFile);
}
}
private function resetAllCircuitBreakers(): void
{
$files = glob(sys_get_temp_dir() . '/boundary_*_state');
foreach ($files as $file) {
unlink($file);
}
}
}

View File

@@ -0,0 +1,441 @@
<?php
declare(strict_types=1);
namespace App\Framework\ErrorBoundaries;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Core\ValueObjects\Timestamp;
use App\Framework\DateTime\Timer;
use App\Framework\ErrorBoundaries\CircuitBreaker\BoundaryCircuitBreakerManager;
use App\Framework\ErrorBoundaries\Events\BoundaryEventInterface;
use App\Framework\ErrorBoundaries\Events\BoundaryEventPublisher;
use App\Framework\ErrorBoundaries\Events\BoundaryExecutionFailed;
use App\Framework\ErrorBoundaries\Events\BoundaryExecutionSucceeded;
use App\Framework\ErrorBoundaries\Events\BoundaryFallbackExecuted;
use App\Framework\ErrorBoundaries\Events\BoundaryTimeoutOccurred;
use App\Framework\Exception\ErrorCode;
use App\Framework\Exception\FrameworkException;
use App\Framework\Logging\Logger;
use Throwable;
/**
* Error Boundary for graceful degradation
*
* Provides a safety net that catches errors and provides fallback functionality
* instead of letting the entire system fail.
*/
final readonly class ErrorBoundary
{
public function __construct(
private string $boundaryName,
private BoundaryConfig $config,
private Timer $timer,
private ?Logger $logger = null,
private ?BoundaryCircuitBreakerManager $circuitBreakerManager = null,
private ?BoundaryEventPublisher $eventPublisher = null,
) {
}
/**
* Executes operation within error boundary with fallback
*
* @template T
* @param callable(): T $operation
* @param callable(): T $fallback
* @return T
*/
public function execute(callable $operation, callable $fallback): mixed
{
$startTime = Timestamp::now();
try {
$result = $this->executeWithRetry($operation);
$executionTime = Timestamp::now()->diff($startTime);
// Publish success event
$this->publishEvent(new BoundaryExecutionSucceeded(
boundaryName: $this->boundaryName,
executionTime: $executionTime,
message: 'Operation completed successfully',
));
return $result;
} catch (Throwable $e) {
$executionTime = Timestamp::now()->diff($startTime);
// Publish failure event
$this->publishEvent(new BoundaryExecutionFailed(
boundaryName: $this->boundaryName,
exception: $e,
executionTime: $executionTime,
willRetry: false,
message: 'Operation failed, executing fallback',
));
return $this->handleFailure($e, $fallback);
}
}
/**
* Executes operation with optional result - returns null on failure if no fallback
*
* @template T
* @param callable(): T $operation
* @param callable(): T|null $fallback
* @return T|null
*/
public function executeOptional(callable $operation, ?callable $fallback = null): mixed
{
try {
return $this->executeWithRetry($operation);
} catch (Throwable $e) {
if ($fallback !== null) {
return $this->handleFailure($e, $fallback);
}
$this->logFailure($e, 'Optional operation failed, returning null');
return null;
}
}
/**
* Executes operation with default value on failure
*
* @template T
* @param callable(): T $operation
* @param T $defaultValue
* @return T
*/
public function executeWithDefault(callable $operation, mixed $defaultValue): mixed
{
try {
return $this->executeWithRetry($operation);
} catch (Throwable $e) {
$this->logFailure($e, 'Operation failed, returning default value');
return $defaultValue;
}
}
/**
* Executes operation and returns Result object
*
* @template T
* @param callable(): T $operation
* @return BoundaryResult<T>
*/
public function executeForResult(callable $operation): BoundaryResult
{
try {
$result = $this->executeWithRetry($operation);
return BoundaryResult::success($result);
} catch (Throwable $e) {
$this->logFailure($e, 'Operation failed, returning error result');
return BoundaryResult::failure($e, $this->boundaryName);
}
}
/**
* Executes multiple operations in parallel with individual boundaries
*
* @param array<string, callable> $operations
* @return array<string, BoundaryResult>
*/
public function executeParallel(array $operations): array
{
$results = [];
foreach ($operations as $name => $operation) {
$results[$name] = $this->executeForResult($operation);
}
return $results;
}
/**
* Circuit breaker pattern - prevents repeated calls if failure rate is high
*
* @template T
* @param callable(): T $operation
* @param callable(): T $fallback
* @return T
*/
public function executeWithCircuitBreaker(callable $operation, callable $fallback): mixed
{
if ($this->config->circuitBreakerEnabled && $this->circuitBreakerManager) {
if ($this->circuitBreakerManager->isCircuitOpen($this->boundaryName, $this->config)) {
$this->log('info', 'Circuit breaker is open, using fallback');
return $fallback();
}
}
try {
$result = $this->executeWithRetry($operation);
if ($this->config->circuitBreakerEnabled && $this->circuitBreakerManager) {
$this->circuitBreakerManager->recordSuccess($this->boundaryName, $this->config);
}
return $result;
} catch (Throwable $e) {
if ($this->config->circuitBreakerEnabled && $this->circuitBreakerManager) {
$this->circuitBreakerManager->recordFailure($this->boundaryName, $this->config);
}
return $this->handleFailure($e, $fallback);
}
}
/**
* Timeout boundary - prevents operations from running too long
*
* @template T
* @param callable(): T $operation
* @param callable(): T $fallback
* @param int $timeoutSeconds
* @return T
*/
public function executeWithTimeout(callable $operation, callable $fallback, int $timeoutSeconds): mixed
{
$startTime = Timestamp::now();
$timeoutThreshold = Duration::fromSeconds($timeoutSeconds);
try {
// PHP doesn't have native async/timeout, so we simulate with time checks
$result = $operation();
$executionTime = Timestamp::now()->diff($startTime);
if ($executionTime->greaterThan($timeoutThreshold)) {
// Publish timeout event
$this->publishEvent(new BoundaryTimeoutOccurred(
boundaryName: $this->boundaryName,
timeoutThreshold: $timeoutThreshold,
actualExecutionTime: $executionTime,
fallbackExecuted: true,
message: "Operation exceeded timeout of {$timeoutSeconds} seconds",
));
throw new BoundaryTimeoutException(
"Operation exceeded timeout of {$timeoutSeconds} seconds",
$this->boundaryName,
$executionTime->toSeconds()
);
}
return $result;
} catch (Throwable $e) {
$executionTime = Timestamp::now()->diff($startTime);
if ($executionTime->greaterThan($timeoutThreshold)) {
$this->log('warning', "Operation timed out after {$executionTime->toHumanReadable()}");
// Publish timeout event if not already published
if (! ($e instanceof BoundaryTimeoutException)) {
$this->publishEvent(new BoundaryTimeoutOccurred(
boundaryName: $this->boundaryName,
timeoutThreshold: $timeoutThreshold,
actualExecutionTime: $executionTime,
fallbackExecuted: true,
message: "Operation timed out during execution",
));
}
}
return $this->handleFailure($e, $fallback);
}
}
/**
* Bulk operation boundary - processes items individually with partial failure tolerance
*
* @template T
* @param array<T> $items
* @param callable(T): mixed $processor
* @return BulkResult<T>
*/
public function executeBulk(array $items, callable $processor): BulkResult
{
$results = [];
$errors = [];
$processed = 0;
foreach ($items as $key => $item) {
try {
$results[$key] = $processor($item);
$processed++;
} catch (Throwable $e) {
$errors[$key] = $e;
$this->logFailure($e, "Bulk processing failed for item {$key}");
// Stop processing if error rate is too high
if (count($errors) / (count($results) + count($errors)) > $this->config->maxBulkErrorRate) {
$this->log('error', 'Bulk processing stopped due to high error rate');
break;
}
}
}
return new BulkResult($results, $errors, $processed, count($items));
}
private function executeWithRetry(callable $operation): mixed
{
$lastException = null;
for ($attempt = 1; $attempt <= $this->config->maxRetries; $attempt++) {
try {
return $operation();
} catch (Throwable $e) {
$lastException = $e;
// Don't retry certain types of errors
if (! $this->shouldRetry($e)) {
break;
}
if ($attempt < $this->config->maxRetries) {
$delay = $this->calculateRetryDelay($attempt);
$this->log('info', "Attempt {$attempt} failed, retrying in {$delay}ms", [
'exception' => $e->getMessage(),
'attempt' => $attempt,
'delay' => $delay,
]);
$this->timer->sleep(Duration::fromMilliseconds($delay));
}
}
}
throw $lastException;
}
private function handleFailure(Throwable $exception, callable $fallback): mixed
{
$this->logFailure($exception, 'Operation failed, executing fallback');
try {
$result = $fallback();
// Publish fallback executed event
$this->publishEvent(new BoundaryFallbackExecuted(
boundaryName: $this->boundaryName,
originalException: $exception,
fallbackReason: 'Operation failed: ' . $exception->getMessage(),
message: 'Fallback executed successfully',
));
return $result;
} catch (Throwable $fallbackException) {
$this->log('error', 'Fallback also failed', [
'original_exception' => $exception->getMessage(),
'fallback_exception' => $fallbackException->getMessage(),
]);
// Create a boundary exception that includes both errors
throw new BoundaryFailedException(
"Both operation and fallback failed in boundary '{$this->boundaryName}'",
$this->boundaryName,
$exception,
$fallbackException
);
}
}
private function shouldRetry(Throwable $e): bool
{
// Don't retry validation errors, security errors, etc.
if ($e instanceof FrameworkException) {
$nonRetryableCodes = [
ErrorCode::VAL_REQUIRED_FIELD_MISSING,
ErrorCode::VAL_INVALID_FORMAT,
ErrorCode::SEC_XSS_ATTEMPT,
ErrorCode::SEC_SQL_INJECTION_ATTEMPT,
ErrorCode::AUTH_CREDENTIALS_INVALID,
ErrorCode::AUTH_INSUFFICIENT_PRIVILEGES,
];
if (in_array($e->getErrorCode(), $nonRetryableCodes)) {
return false;
}
}
return true;
}
private function calculateRetryDelay(int $attempt): int
{
$baseMs = (int) $this->config->baseDelay->toMilliseconds();
$maxMs = (int) $this->config->maxDelay->toMilliseconds();
return match ($this->config->retryStrategy) {
RetryStrategy::FIXED => $baseMs,
RetryStrategy::LINEAR => min($baseMs * $attempt, $maxMs),
RetryStrategy::EXPONENTIAL => min(
$baseMs * (2 ** ($attempt - 1)),
$maxMs
),
RetryStrategy::EXPONENTIAL_JITTER => min(
$baseMs * (2 ** ($attempt - 1)) + random_int(0, 100),
$maxMs
),
};
}
/**
* Get circuit breaker health information
*/
public function getCircuitHealth(): ?array
{
if (! $this->config->circuitBreakerEnabled || ! $this->circuitBreakerManager) {
return null;
}
return $this->circuitBreakerManager->getCircuitHealth($this->boundaryName);
}
/**
* Reset circuit breaker state
*/
public function resetCircuit(): void
{
if ($this->config->circuitBreakerEnabled && $this->circuitBreakerManager) {
$this->circuitBreakerManager->resetCircuit($this->boundaryName);
}
}
private function logFailure(Throwable $e, string $message): void
{
$this->log('error', $message, [
'exception' => $e::class,
'message' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
]);
}
private function log(string $level, string $message, array $context = []): void
{
if ($this->logger) {
$context['boundary'] = $this->boundaryName;
match ($level) {
'debug' => $this->logger->debug("[ErrorBoundary] {$message}", $context),
'info' => $this->logger->info("[ErrorBoundary] {$message}", $context),
'warning' => $this->logger->warning("[ErrorBoundary] {$message}", $context),
'error' => $this->logger->error("[ErrorBoundary] {$message}", $context),
default => $this->logger->info("[ErrorBoundary] {$message}", $context),
};
}
}
private function publishEvent(BoundaryEventInterface $event): void
{
$this->eventPublisher?->publish($event);
}
}

View File

@@ -0,0 +1,176 @@
<?php
declare(strict_types=1);
namespace App\Framework\ErrorBoundaries;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\DateTime\SystemTimer;
use App\Framework\DateTime\Timer;
use App\Framework\ErrorBoundaries\CircuitBreaker\BoundaryCircuitBreakerManager;
use App\Framework\ErrorBoundaries\Events\BoundaryEventPublisher;
use App\Framework\EventBus\EventBus;
use App\Framework\Logging\Logger;
use App\Framework\StateManagement\StateManagerFactory;
/**
* Factory for creating error boundaries with appropriate configurations
*/
final readonly class ErrorBoundaryFactory
{
private array $routeConfigs;
public function __construct(
private ?Timer $timer = null,
private ?Logger $logger = null,
private ?StateManagerFactory $stateManagerFactory = null,
private ?EventBus $eventBus = null,
array $routeConfigs = []
) {
$this->routeConfigs = array_merge($this->getDefaultRouteConfigs(), $routeConfigs);
}
/**
* Create error boundary for specific route
*/
public function createForRoute(string $routeName): ErrorBoundary
{
$config = $this->getConfigForRoute($routeName);
return $this->create("route_{$routeName}", $config);
}
/**
* Create error boundary for database operations
*/
public function createForDatabase(string $operation = 'database'): ErrorBoundary
{
return $this->create($operation, BoundaryConfig::database());
}
/**
* Create error boundary for external service calls
*/
public function createForExternalService(string $serviceName): ErrorBoundary
{
return $this->create("external_{$serviceName}", BoundaryConfig::externalService());
}
/**
* Create error boundary for UI components
*/
public function createForUI(string $componentName): ErrorBoundary
{
return $this->create("ui_{$componentName}", BoundaryConfig::ui());
}
/**
* Create error boundary for background jobs
*/
public function createForBackgroundJob(string $jobName): ErrorBoundary
{
return $this->create("job_{$jobName}", BoundaryConfig::backgroundJob());
}
/**
* Create error boundary for critical operations
*/
public function createForCriticalOperation(string $operationName): ErrorBoundary
{
return $this->create("critical_{$operationName}", BoundaryConfig::critical());
}
/**
* Create custom error boundary
*/
public function create(string $name, BoundaryConfig $config): ErrorBoundary
{
$circuitBreakerManager = null;
if ($config->circuitBreakerEnabled && $this->stateManagerFactory) {
$stateManager = $this->stateManagerFactory->createForErrorBoundary();
$circuitBreakerManager = new BoundaryCircuitBreakerManager($stateManager, $this->logger);
}
$eventPublisher = new BoundaryEventPublisher($this->eventBus, $this->logger);
return new ErrorBoundary(
boundaryName: $name,
config: $config,
timer: $this->timer ?? new SystemTimer(),
logger: $this->logger,
circuitBreakerManager: $circuitBreakerManager,
eventPublisher: $eventPublisher,
);
}
private function getConfigForRoute(string $routeName): BoundaryConfig
{
// Check for exact route match
if (isset($this->routeConfigs[$routeName])) {
return $this->routeConfigs[$routeName];
}
// Check for pattern matches
foreach ($this->routeConfigs as $pattern => $config) {
if (str_contains($pattern, '*') && $this->matchesPattern($routeName, $pattern)) {
return $config;
}
}
// Default configuration
return $this->routeConfigs['default'];
}
private function matchesPattern(string $routeName, string $pattern): bool
{
$regex = str_replace('*', '.*', preg_quote($pattern, '/'));
return (bool) preg_match("/^{$regex}$/", $routeName);
}
private function getDefaultRouteConfigs(): array
{
return [
// API routes - more retries and circuit breaker
'api/*' => BoundaryConfig::externalService(),
// Admin routes - critical operations
'admin/*' => BoundaryConfig::critical(),
// Auth routes - fail fast for security
'auth/*' => BoundaryConfig::failFast(),
// Public routes - user-friendly defaults
'public/*' => BoundaryConfig::ui(),
// Background job routes
'job/*' => BoundaryConfig::backgroundJob(),
// Database operations
'db/*' => BoundaryConfig::database(),
// Default fallback
'default' => new BoundaryConfig(
maxRetries: 2,
retryStrategy: RetryStrategy::EXPONENTIAL_JITTER,
baseDelay: Duration::fromMilliseconds(100),
maxDelay: Duration::fromSeconds(2),
circuitBreakerEnabled: true,
circuitBreakerThreshold: 5,
circuitBreakerTimeout: Duration::fromMinutes(1),
enableMetrics: true
),
// HTTP request fallback
'http_request' => new BoundaryConfig(
maxRetries: 1,
retryStrategy: RetryStrategy::FIXED,
baseDelay: Duration::fromMilliseconds(50),
maxDelay: Duration::fromMilliseconds(200),
circuitBreakerEnabled: false,
enableMetrics: true
),
];
}
}

View File

@@ -0,0 +1,106 @@
<?php
declare(strict_types=1);
namespace App\Framework\ErrorBoundaries;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\DI\Container;
use App\Framework\DI\Initializer;
use App\Framework\Logging\Logger;
/**
* Initializer for Error Boundaries
*/
final readonly class ErrorBoundaryInitializer
{
#[Initializer]
public function initialize(Container $container): void
{
// Factory
$container->bind(ErrorBoundaryFactory::class, function (Container $container) {
$logger = $container->has(Logger::class) ? $container->get(Logger::class) : null;
// Load custom route configurations from environment
$routeConfigs = $this->loadRouteConfigs();
return new ErrorBoundaryFactory(
logger: $logger,
routeConfigs: $routeConfigs
);
});
// Default boundary for general use
$container->bind(ErrorBoundary::class, function (Container $container) {
$factory = $container->get(ErrorBoundaryFactory::class);
return $factory->create('default', BoundaryConfig::database());
});
}
private function loadRouteConfigs(): array
{
$configs = [];
// Load from environment variables
// Format: ERROR_BOUNDARY_ROUTE_[ROUTE_NAME]_[SETTING] = value
foreach ($_ENV as $key => $value) {
if (! str_starts_with($key, 'ERROR_BOUNDARY_ROUTE_')) {
continue;
}
$parts = explode('_', str_replace('ERROR_BOUNDARY_ROUTE_', '', $key));
if (count($parts) < 2) {
continue;
}
$setting = array_pop($parts);
$routeName = strtolower(implode('_', $parts));
if (! isset($configs[$routeName])) {
$configs[$routeName] = [];
}
$configs[$routeName][$setting] = $this->parseConfigValue($setting, $value);
}
// Convert to BoundaryConfig objects
$boundaryConfigs = [];
foreach ($configs as $routeName => $settings) {
$boundaryConfigs[$routeName] = $this->createConfigFromArray($settings);
}
return $boundaryConfigs;
}
private function parseConfigValue(string $setting, string $value): mixed
{
return match (strtolower($setting)) {
'max_retries', 'circuit_breaker_threshold' => (int) $value,
'circuit_breaker_enabled', 'enable_metrics', 'enable_tracing' => filter_var($value, FILTER_VALIDATE_BOOLEAN),
'max_bulk_error_rate' => (float) $value,
'retry_strategy' => RetryStrategy::tryFrom(strtolower($value)) ?? RetryStrategy::EXPONENTIAL_JITTER,
'base_delay_ms' => Duration::fromMilliseconds((int) $value),
'max_delay_ms' => Duration::fromMilliseconds((int) $value),
'circuit_breaker_timeout_s' => Duration::fromSeconds((int) $value),
default => $value,
};
}
private function createConfigFromArray(array $settings): BoundaryConfig
{
return new BoundaryConfig(
maxRetries: $settings['max_retries'] ?? 3,
retryStrategy: $settings['retry_strategy'] ?? RetryStrategy::EXPONENTIAL_JITTER,
baseDelay: $settings['base_delay_ms'] ?? Duration::fromMilliseconds(100),
maxDelay: $settings['max_delay_ms'] ?? Duration::fromSeconds(5),
circuitBreakerEnabled: $settings['circuit_breaker_enabled'] ?? false,
circuitBreakerThreshold: $settings['circuit_breaker_threshold'] ?? 5,
circuitBreakerTimeout: $settings['circuit_breaker_timeout_s'] ?? Duration::fromMinutes(1),
maxBulkErrorRate: $settings['max_bulk_error_rate'] ?? 0.5,
enableMetrics: $settings['enable_metrics'] ?? true,
enableTracing: $settings['enable_tracing'] ?? false,
);
}
}

View File

@@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
namespace App\Framework\ErrorBoundaries\Events;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Core\ValueObjects\Timestamp;
use App\Framework\ErrorBoundaries\CircuitBreaker\BoundaryCircuitState;
/**
* Event fired when circuit breaker recovers (closes)
*/
final readonly class BoundaryCircuitBreakerRecovered implements BoundaryEventInterface
{
public readonly string $eventType;
public readonly Timestamp $occurredAt;
public readonly string $severity;
public function __construct(
public string $boundaryName,
public BoundaryCircuitState $previousState,
public BoundaryCircuitState $newState,
public Duration $downTime,
public int $successCount,
public ?string $message = null,
public array $context = [],
) {
$this->eventType = 'boundary.circuit_breaker.recovered';
$this->occurredAt = Timestamp::now();
$this->severity = 'info';
}
public function getEventId(): string
{
return "boundary_circuit_recovery_{$this->boundaryName}_{$this->occurredAt->toFloat()}";
}
public function shouldAlert(): bool
{
return false; // Recovery is good news, typically no alert needed
}
public function toArray(): array
{
return [
'event_id' => $this->getEventId(),
'event_type' => $this->eventType,
'boundary_name' => $this->boundaryName,
'previous_state' => $this->previousState->value,
'new_state' => $this->newState->value,
'down_time_ms' => $this->downTime->toMilliseconds(),
'down_time_human' => $this->downTime->toHumanReadable(),
'success_count' => $this->successCount,
'state_description' => $this->newState->getDescription(),
'message' => $this->message,
'context' => $this->context,
'occurred_at' => $this->occurredAt->toIsoString(),
'severity' => $this->severity,
'should_alert' => $this->shouldAlert(),
];
}
}

View File

@@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
namespace App\Framework\ErrorBoundaries\Events;
use App\Framework\Core\ValueObjects\Timestamp;
use App\Framework\ErrorBoundaries\CircuitBreaker\BoundaryCircuitState;
/**
* Event fired when circuit breaker trips (opens)
*/
final readonly class BoundaryCircuitBreakerTripped implements BoundaryEventInterface
{
public readonly string $eventType;
public readonly Timestamp $occurredAt;
public readonly string $severity;
public function __construct(
public string $boundaryName,
public BoundaryCircuitState $previousState,
public BoundaryCircuitState $newState,
public int $failureCount,
public int $failureThreshold,
public ?string $message = null,
public array $context = [],
) {
$this->eventType = 'boundary.circuit_breaker.tripped';
$this->occurredAt = Timestamp::now();
$this->severity = 'error';
}
public function getEventId(): string
{
return "boundary_circuit_trip_{$this->boundaryName}_{$this->occurredAt->toFloat()}";
}
public function shouldAlert(): bool
{
return true; // Circuit breaker trips always require attention
}
public function toArray(): array
{
return [
'event_id' => $this->getEventId(),
'event_type' => $this->eventType,
'boundary_name' => $this->boundaryName,
'previous_state' => $this->previousState->value,
'new_state' => $this->newState->value,
'failure_count' => $this->failureCount,
'failure_threshold' => $this->failureThreshold,
'state_description' => $this->newState->getDescription(),
'message' => $this->message,
'context' => $this->context,
'occurred_at' => $this->occurredAt->toIsoString(),
'severity' => $this->severity,
'should_alert' => $this->shouldAlert(),
];
}
}

View File

@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace App\Framework\ErrorBoundaries\Events;
use App\Framework\Core\ValueObjects\Timestamp;
/**
* Base interface for error boundary events
*/
interface BoundaryEventInterface
{
public string $boundaryName {get;}
public string $eventType {get;}
public Timestamp $occurredAt {get;}
public string $severity {get;}
public ?string $message {get;}
public array $context {get;}
/**
* Get event identifier for logging/tracking
*/
public function getEventId(): string;
/**
* Check if event should trigger alerts
*/
public function shouldAlert(): bool;
/**
* Get event payload for serialization
*/
public function toArray(): array;
}

View File

@@ -0,0 +1,125 @@
<?php
declare(strict_types=1);
namespace App\Framework\ErrorBoundaries\Events;
use App\Framework\EventBus\EventBus;
use App\Framework\Logging\Logger;
/**
* Publisher for error boundary events
*/
final readonly class BoundaryEventPublisher
{
public function __construct(
private ?EventBus $eventBus = null,
private ?Logger $logger = null,
) {
}
/**
* Publish boundary event
*/
public function publish(BoundaryEventInterface $event): void
{
// Log the event
$this->logEvent($event);
// Publish to event bus if available
if ($this->eventBus !== null) {
$this->eventBus->dispatch($event);
}
}
/**
* Publish execution success event
*/
public function publishExecutionSucceeded(BoundaryExecutionSucceeded $event): void
{
$this->publish($event);
}
/**
* Publish execution failure event
*/
public function publishExecutionFailed(BoundaryExecutionFailed $event): void
{
$this->publish($event);
}
/**
* Publish fallback execution event
*/
public function publishFallbackExecuted(BoundaryFallbackExecuted $event): void
{
$this->publish($event);
}
/**
* Publish circuit breaker tripped event
*/
public function publishCircuitBreakerTripped(BoundaryCircuitBreakerTripped $event): void
{
$this->publish($event);
}
/**
* Publish circuit breaker recovered event
*/
public function publishCircuitBreakerRecovered(BoundaryCircuitBreakerRecovered $event): void
{
$this->publish($event);
}
/**
* Publish timeout event
*/
public function publishTimeoutOccurred(BoundaryTimeoutOccurred $event): void
{
$this->publish($event);
}
/**
* Batch publish multiple events
*/
public function publishBatch(array $events): void
{
foreach ($events as $event) {
if ($event instanceof BoundaryEventInterface) {
$this->publish($event);
}
}
}
private function logEvent(BoundaryEventInterface $event): void
{
if ($this->logger === null) {
return;
}
$context = [
'event_id' => $event->getEventId(),
'boundary_name' => $event->boundaryName,
'event_type' => $event->eventType,
'occurred_at' => $event->occurredAt->toIsoString(),
'should_alert' => $event->shouldAlert(),
'event_data' => $event->toArray(),
];
$logMessage = "[ErrorBoundary Event] {$event->eventType} for boundary '{$event->boundaryName}'";
if ($event->message !== null) {
$logMessage .= ": {$event->message}";
}
match ($event->severity) {
'debug' => $this->logger->debug($logMessage, $context),
'info' => $this->logger->info($logMessage, $context),
'warning' => $this->logger->warning($logMessage, $context),
'error' => $this->logger->error($logMessage, $context),
'critical' => $this->logger->error($logMessage, $context), // Map critical to error
default => $this->logger->info($logMessage, $context),
};
}
}

View File

@@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
namespace App\Framework\ErrorBoundaries\Events;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Core\ValueObjects\Timestamp;
use Throwable;
/**
* Event fired when boundary execution fails
*/
final readonly class BoundaryExecutionFailed implements BoundaryEventInterface
{
public readonly string $eventType;
public readonly Timestamp $occurredAt;
public readonly string $severity;
public function __construct(
public string $boundaryName,
public Throwable $exception,
public Duration $executionTime,
public bool $willRetry = false,
public ?string $message = null,
public array $context = [],
) {
$this->eventType = 'boundary.execution.failed';
$this->occurredAt = Timestamp::now();
$this->severity = $willRetry ? 'warning' : 'error';
}
public function getEventId(): string
{
return "boundary_failure_{$this->boundaryName}_{$this->occurredAt->toFloat()}";
}
public function shouldAlert(): bool
{
return ! $this->willRetry; // Alert on final failures, not retries
}
public function toArray(): array
{
return [
'event_id' => $this->getEventId(),
'event_type' => $this->eventType,
'boundary_name' => $this->boundaryName,
'exception_class' => $this->exception::class,
'exception_message' => $this->exception->getMessage(),
'exception_file' => $this->exception->getFile(),
'exception_line' => $this->exception->getLine(),
'execution_time_ms' => $this->executionTime->toMilliseconds(),
'execution_time_human' => $this->executionTime->toHumanReadable(),
'will_retry' => $this->willRetry,
'message' => $this->message,
'context' => $this->context,
'occurred_at' => $this->occurredAt->toIsoString(),
'severity' => $this->severity,
'should_alert' => $this->shouldAlert(),
];
}
}

View File

@@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace App\Framework\ErrorBoundaries\Events;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Core\ValueObjects\Timestamp;
/**
* Event fired when boundary execution succeeds
*/
final readonly class BoundaryExecutionSucceeded implements BoundaryEventInterface
{
public readonly string $eventType;
public readonly Timestamp $occurredAt;
public readonly string $severity;
public function __construct(
public string $boundaryName,
public Duration $executionTime,
public ?string $message = null,
public array $context = [],
) {
$this->eventType = 'boundary.execution.succeeded';
$this->occurredAt = Timestamp::now();
$this->severity = 'info';
}
public function getEventId(): string
{
return "boundary_success_{$this->boundaryName}_{$this->occurredAt->toFloat()}";
}
public function shouldAlert(): bool
{
return false; // Success events typically don't trigger alerts
}
public function toArray(): array
{
return [
'event_id' => $this->getEventId(),
'event_type' => $this->eventType,
'boundary_name' => $this->boundaryName,
'execution_time_ms' => $this->executionTime->toMilliseconds(),
'execution_time_human' => $this->executionTime->toHumanReadable(),
'message' => $this->message,
'context' => $this->context,
'occurred_at' => $this->occurredAt->toIsoString(),
'severity' => $this->severity,
'should_alert' => $this->shouldAlert(),
];
}
}

View File

@@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
namespace App\Framework\ErrorBoundaries\Events;
use App\Framework\Core\ValueObjects\Timestamp;
use Throwable;
/**
* Event fired when boundary fallback is executed
*/
final readonly class BoundaryFallbackExecuted implements BoundaryEventInterface
{
public readonly string $eventType;
public readonly Timestamp $occurredAt;
public readonly string $severity;
public function __construct(
public string $boundaryName,
public Throwable $originalException,
public string $fallbackReason,
public ?string $message = null,
public array $context = [],
) {
$this->eventType = 'boundary.fallback.executed';
$this->occurredAt = Timestamp::now();
$this->severity = 'warning';
}
public function getEventId(): string
{
return "boundary_fallback_{$this->boundaryName}_{$this->occurredAt->toFloat()}";
}
public function shouldAlert(): bool
{
return true; // Fallback executions indicate issues
}
public function toArray(): array
{
return [
'event_id' => $this->getEventId(),
'event_type' => $this->eventType,
'boundary_name' => $this->boundaryName,
'original_exception_class' => $this->originalException::class,
'original_exception_message' => $this->originalException->getMessage(),
'fallback_reason' => $this->fallbackReason,
'message' => $this->message,
'context' => $this->context,
'occurred_at' => $this->occurredAt->toIsoString(),
'severity' => $this->severity,
'should_alert' => $this->shouldAlert(),
];
}
}

View File

@@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
namespace App\Framework\ErrorBoundaries\Events;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Core\ValueObjects\Timestamp;
/**
* Event fired when boundary operation times out
*/
final readonly class BoundaryTimeoutOccurred implements BoundaryEventInterface
{
public readonly string $eventType;
public readonly Timestamp $occurredAt;
public readonly string $severity;
public function __construct(
public string $boundaryName,
public Duration $timeoutThreshold,
public Duration $actualExecutionTime,
public bool $fallbackExecuted = false,
public ?string $message = null,
public array $context = [],
) {
$this->eventType = 'boundary.timeout.occurred';
$this->occurredAt = Timestamp::now();
$this->severity = 'warning';
}
public function getEventId(): string
{
return "boundary_timeout_{$this->boundaryName}_{$this->occurredAt->toFloat()}";
}
public function shouldAlert(): bool
{
return true; // Timeouts indicate performance issues
}
public function toArray(): array
{
return [
'event_id' => $this->getEventId(),
'event_type' => $this->eventType,
'boundary_name' => $this->boundaryName,
'timeout_threshold_ms' => $this->timeoutThreshold->toMilliseconds(),
'timeout_threshold_human' => $this->timeoutThreshold->toHumanReadable(),
'actual_execution_time_ms' => $this->actualExecutionTime->toMilliseconds(),
'actual_execution_time_human' => $this->actualExecutionTime->toHumanReadable(),
'exceeded_by_ms' => $this->actualExecutionTime->diff($this->timeoutThreshold)->toMilliseconds(),
'fallback_executed' => $this->fallbackExecuted,
'message' => $this->message,
'context' => $this->context,
'occurred_at' => $this->occurredAt->toIsoString(),
'severity' => $this->severity,
'should_alert' => $this->shouldAlert(),
];
}
}

View File

@@ -0,0 +1,84 @@
<?php
declare(strict_types=1);
namespace App\Framework\ErrorBoundaries\Metrics;
/**
* Health status enumeration for error boundaries
*/
enum BoundaryHealthStatus: string
{
case HEALTHY = 'healthy';
case WARNING = 'warning';
case CRITICAL = 'critical';
case UNKNOWN = 'unknown';
/**
* Get human-readable description
*/
public function getDescription(): string
{
return match ($this) {
self::HEALTHY => 'Boundary is operating normally',
self::WARNING => 'Boundary has elevated error rates but is functional',
self::CRITICAL => 'Boundary has high error rates and may be failing',
self::UNKNOWN => 'Boundary health status cannot be determined',
};
}
/**
* Get severity level for alerting
*/
public function getSeverity(): int
{
return match ($this) {
self::HEALTHY => 0,
self::WARNING => 1,
self::CRITICAL => 2,
self::UNKNOWN => 1,
};
}
/**
* Get color representation for UI
*/
public function getColor(): string
{
return match ($this) {
self::HEALTHY => 'green',
self::WARNING => 'yellow',
self::CRITICAL => 'red',
self::UNKNOWN => 'gray',
};
}
/**
* Get emoji representation
*/
public function getEmoji(): string
{
return match ($this) {
self::HEALTHY => '✅',
self::WARNING => '⚠️',
self::CRITICAL => '🚨',
self::UNKNOWN => '❓',
};
}
/**
* Check if status requires immediate attention
*/
public function requiresAttention(): bool
{
return $this === self::CRITICAL;
}
/**
* Check if status should trigger alerts
*/
public function shouldAlert(): bool
{
return $this === self::WARNING || $this === self::CRITICAL;
}
}

View File

@@ -0,0 +1,308 @@
<?php
declare(strict_types=1);
namespace App\Framework\ErrorBoundaries\Metrics;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Core\ValueObjects\Percentage;
use App\Framework\Core\ValueObjects\Timestamp;
/**
* Metrics for error boundary operations
*/
final readonly class BoundaryMetrics
{
public function __construct(
public string $boundaryName,
public int $totalExecutions = 0,
public int $successfulExecutions = 0,
public int $failedExecutions = 0,
public int $fallbackExecutions = 0,
public int $timeoutExecutions = 0,
public int $circuitBreakerTrips = 0,
public Duration $totalExecutionTime = new Duration(0.0),
public Duration $averageExecutionTime = new Duration(0.0),
public Duration $maxExecutionTime = new Duration(0.0),
public Duration $minExecutionTime = new Duration(0.0),
public ?Timestamp $lastSuccessTime = null,
public ?Timestamp $lastFailureTime = null,
public ?Timestamp $lastFallbackTime = null,
public ?Timestamp $createdAt = null,
public ?Timestamp $updatedAt = null,
) {
}
/**
* Calculate success rate
*/
public function getSuccessRate(): Percentage
{
if ($this->totalExecutions === 0) {
return Percentage::fromFloat(0.0);
}
return Percentage::fromFloat($this->successfulExecutions / $this->totalExecutions);
}
/**
* Calculate failure rate
*/
public function getFailureRate(): Percentage
{
if ($this->totalExecutions === 0) {
return Percentage::fromFloat(0.0);
}
return Percentage::fromFloat($this->failedExecutions / $this->totalExecutions);
}
/**
* Calculate fallback rate
*/
public function getFallbackRate(): Percentage
{
if ($this->totalExecutions === 0) {
return Percentage::fromFloat(0.0);
}
return Percentage::fromFloat($this->fallbackExecutions / $this->totalExecutions);
}
/**
* Calculate timeout rate
*/
public function getTimeoutRate(): Percentage
{
if ($this->totalExecutions === 0) {
return Percentage::fromFloat(0.0);
}
return Percentage::fromFloat($this->timeoutExecutions / $this->totalExecutions);
}
/**
* Check if boundary is healthy (success rate > 90%)
*/
public function isHealthy(): bool
{
return $this->getSuccessRate()->toFloat() > 0.9;
}
/**
* Check if circuit breaker is frequently tripping
*/
public function hasFrequentCircuitBreakerTrips(): bool
{
if ($this->totalExecutions === 0) {
return false;
}
$tripRate = $this->circuitBreakerTrips / $this->totalExecutions;
return $tripRate > 0.1; // More than 10% of executions result in circuit breaker trips
}
/**
* Get health status
*/
public function getHealthStatus(): BoundaryHealthStatus
{
if ($this->isHealthy() && ! $this->hasFrequentCircuitBreakerTrips()) {
return BoundaryHealthStatus::HEALTHY;
}
if ($this->getSuccessRate()->toFloat() > 0.7) {
return BoundaryHealthStatus::WARNING;
}
return BoundaryHealthStatus::CRITICAL;
}
/**
* Record successful execution
*/
public function recordSuccess(Duration $executionTime): self
{
$newTotalTime = $this->totalExecutionTime->add($executionTime);
$newTotalExecutions = $this->totalExecutions + 1;
$newAverageTime = Duration::fromMilliseconds(
$newTotalTime->toMilliseconds() / $newTotalExecutions
);
return new self(
boundaryName: $this->boundaryName,
totalExecutions: $newTotalExecutions,
successfulExecutions: $this->successfulExecutions + 1,
failedExecutions: $this->failedExecutions,
fallbackExecutions: $this->fallbackExecutions,
timeoutExecutions: $this->timeoutExecutions,
circuitBreakerTrips: $this->circuitBreakerTrips,
totalExecutionTime: $newTotalTime,
averageExecutionTime: $newAverageTime,
maxExecutionTime: $executionTime->isGreaterThan($this->maxExecutionTime)
? $executionTime
: $this->maxExecutionTime,
minExecutionTime: $this->minExecutionTime->isZero() || $executionTime->isLessThan($this->minExecutionTime)
? $executionTime
: $this->minExecutionTime,
lastSuccessTime: Timestamp::now(),
lastFailureTime: $this->lastFailureTime,
lastFallbackTime: $this->lastFallbackTime,
createdAt: $this->createdAt ?? Timestamp::now(),
updatedAt: Timestamp::now(),
);
}
/**
* Record failed execution
*/
public function recordFailure(Duration $executionTime): self
{
$newTotalTime = $this->totalExecutionTime->add($executionTime);
$newTotalExecutions = $this->totalExecutions + 1;
$newAverageTime = Duration::fromMilliseconds(
$newTotalTime->toMilliseconds() / $newTotalExecutions
);
return new self(
boundaryName: $this->boundaryName,
totalExecutions: $newTotalExecutions,
successfulExecutions: $this->successfulExecutions,
failedExecutions: $this->failedExecutions + 1,
fallbackExecutions: $this->fallbackExecutions,
timeoutExecutions: $this->timeoutExecutions,
circuitBreakerTrips: $this->circuitBreakerTrips,
totalExecutionTime: $newTotalTime,
averageExecutionTime: $newAverageTime,
maxExecutionTime: $executionTime->isGreaterThan($this->maxExecutionTime)
? $executionTime
: $this->maxExecutionTime,
minExecutionTime: $this->minExecutionTime->isZero() || $executionTime->isLessThan($this->minExecutionTime)
? $executionTime
: $this->minExecutionTime,
lastSuccessTime: $this->lastSuccessTime,
lastFailureTime: Timestamp::now(),
lastFallbackTime: $this->lastFallbackTime,
createdAt: $this->createdAt ?? Timestamp::now(),
updatedAt: Timestamp::now(),
);
}
/**
* Record fallback execution
*/
public function recordFallback(): self
{
return new self(
boundaryName: $this->boundaryName,
totalExecutions: $this->totalExecutions + 1,
successfulExecutions: $this->successfulExecutions,
failedExecutions: $this->failedExecutions,
fallbackExecutions: $this->fallbackExecutions + 1,
timeoutExecutions: $this->timeoutExecutions,
circuitBreakerTrips: $this->circuitBreakerTrips,
totalExecutionTime: $this->totalExecutionTime,
averageExecutionTime: $this->averageExecutionTime,
maxExecutionTime: $this->maxExecutionTime,
minExecutionTime: $this->minExecutionTime,
lastSuccessTime: $this->lastSuccessTime,
lastFailureTime: $this->lastFailureTime,
lastFallbackTime: Timestamp::now(),
createdAt: $this->createdAt ?? Timestamp::now(),
updatedAt: Timestamp::now(),
);
}
/**
* Record timeout execution
*/
public function recordTimeout(Duration $executionTime): self
{
$newTotalTime = $this->totalExecutionTime->add($executionTime);
$newTotalExecutions = $this->totalExecutions + 1;
$newAverageTime = Duration::fromMilliseconds(
$newTotalTime->toMilliseconds() / $newTotalExecutions
);
return new self(
boundaryName: $this->boundaryName,
totalExecutions: $newTotalExecutions,
successfulExecutions: $this->successfulExecutions,
failedExecutions: $this->failedExecutions,
fallbackExecutions: $this->fallbackExecutions,
timeoutExecutions: $this->timeoutExecutions + 1,
circuitBreakerTrips: $this->circuitBreakerTrips,
totalExecutionTime: $newTotalTime,
averageExecutionTime: $newAverageTime,
maxExecutionTime: $executionTime->isGreaterThan($this->maxExecutionTime)
? $executionTime
: $this->maxExecutionTime,
minExecutionTime: $this->minExecutionTime->isZero() || $executionTime->isLessThan($this->minExecutionTime)
? $executionTime
: $this->minExecutionTime,
lastSuccessTime: $this->lastSuccessTime,
lastFailureTime: $this->lastFailureTime,
lastFallbackTime: $this->lastFallbackTime,
createdAt: $this->createdAt ?? Timestamp::now(),
updatedAt: Timestamp::now(),
);
}
/**
* Record circuit breaker trip
*/
public function recordCircuitBreakerTrip(): self
{
return new self(
boundaryName: $this->boundaryName,
totalExecutions: $this->totalExecutions,
successfulExecutions: $this->successfulExecutions,
failedExecutions: $this->failedExecutions,
fallbackExecutions: $this->fallbackExecutions,
timeoutExecutions: $this->timeoutExecutions,
circuitBreakerTrips: $this->circuitBreakerTrips + 1,
totalExecutionTime: $this->totalExecutionTime,
averageExecutionTime: $this->averageExecutionTime,
maxExecutionTime: $this->maxExecutionTime,
minExecutionTime: $this->minExecutionTime,
lastSuccessTime: $this->lastSuccessTime,
lastFailureTime: $this->lastFailureTime,
lastFallbackTime: $this->lastFallbackTime,
createdAt: $this->createdAt ?? Timestamp::now(),
updatedAt: Timestamp::now(),
);
}
/**
* Convert to array for serialization
*/
public function toArray(): array
{
return [
'boundary_name' => $this->boundaryName,
'total_executions' => $this->totalExecutions,
'successful_executions' => $this->successfulExecutions,
'failed_executions' => $this->failedExecutions,
'fallback_executions' => $this->fallbackExecutions,
'timeout_executions' => $this->timeoutExecutions,
'circuit_breaker_trips' => $this->circuitBreakerTrips,
'success_rate' => $this->getSuccessRate()->toFloat(),
'failure_rate' => $this->getFailureRate()->toFloat(),
'fallback_rate' => $this->getFallbackRate()->toFloat(),
'timeout_rate' => $this->getTimeoutRate()->toFloat(),
'total_execution_time_ms' => $this->totalExecutionTime->toMilliseconds(),
'average_execution_time_ms' => $this->averageExecutionTime->toMilliseconds(),
'max_execution_time_ms' => $this->maxExecutionTime->toMilliseconds(),
'min_execution_time_ms' => $this->minExecutionTime->toMilliseconds(),
'last_success_time' => $this->lastSuccessTime?->toIsoString(),
'last_failure_time' => $this->lastFailureTime?->toIsoString(),
'last_fallback_time' => $this->lastFallbackTime?->toIsoString(),
'health_status' => $this->getHealthStatus()->value,
'is_healthy' => $this->isHealthy(),
'has_frequent_circuit_trips' => $this->hasFrequentCircuitBreakerTrips(),
'created_at' => $this->createdAt?->toIsoString(),
'updated_at' => $this->updatedAt?->toIsoString(),
];
}
}

View File

@@ -0,0 +1,121 @@
<?php
declare(strict_types=1);
namespace App\Framework\ErrorBoundaries\Middleware;
use App\Framework\ErrorBoundaries\BoundaryConfig;
use App\Framework\ErrorBoundaries\ErrorBoundary;
use App\Framework\ErrorBoundaries\ErrorBoundaryFactory;
use App\Framework\Http\HttpMiddleware;
use App\Framework\Http\MiddlewareContext;
use App\Framework\Http\Next;
use App\Framework\Http\RequestStateManager;
use App\Framework\Http\Responses\JsonResponse;
use App\Framework\Http\Status;
use App\Framework\Logging\Logger;
/**
* Specialized middleware for API endpoints with detailed error boundaries
*/
final readonly class ApiErrorBoundaryMiddleware implements HttpMiddleware
{
public function __construct(
private ErrorBoundaryFactory $boundaryFactory,
private ?Logger $logger = null,
private bool $includeDebugInfo = false,
) {
}
public function __invoke(MiddlewareContext $context, Next $next, RequestStateManager $stateManager): MiddlewareContext
{
$request = $context->request;
$endpoint = $this->extractEndpoint($request);
$boundary = $this->createApiBoundary($endpoint);
return $boundary->execute(
operation: fn () => $next($context, $stateManager),
fallback: fn () => $context->withResponse($this->createApiErrorResponse($request, $boundary))
);
}
/**
* Create specialized boundary for API endpoints
*/
private function createApiBoundary(string $endpoint): ErrorBoundary
{
$config = BoundaryConfig::externalService(); // API-friendly config
return $this->boundaryFactory->create("api_{$endpoint}", $config);
}
/**
* Create comprehensive API error response
*/
private function createApiErrorResponse($request, ErrorBoundary $boundary): JsonResponse
{
$requestId = $request->headers->get('X-Request-ID') ?? uniqid('req_');
$timestamp = date(\DateTimeInterface::ISO8601);
$errorData = [
'error' => [
'code' => 'API_SERVICE_UNAVAILABLE',
'message' => 'The API service is temporarily unavailable due to high load or maintenance.',
'type' => 'service_unavailable',
'timestamp' => $timestamp,
'request_id' => $requestId,
'retry_after' => 60, // seconds
],
'meta' => [
'endpoint' => $request->uri->getPath(),
'method' => $request->getMethod()->value,
'boundary_used' => true,
'fallback_response' => true,
],
'links' => [
'status' => '/api/status',
'documentation' => '/api/docs',
'support' => '/api/support',
],
];
// Add debug information in development
if ($this->includeDebugInfo) {
$circuitHealth = $boundary->getCircuitHealth();
if ($circuitHealth !== null) {
$errorData['debug'] = [
'circuit_breaker' => $circuitHealth,
'boundary_name' => $circuitHealth['boundary_name'] ?? 'unknown',
];
}
}
$response = new JsonResponse($errorData, Status::SERVICE_UNAVAILABLE);
// Add retry-after header
$response = $response->withHeader('Retry-After', '60');
$response = $response->withHeader('X-Request-ID', $requestId);
return $response;
}
/**
* Extract API endpoint name for boundary identification
*/
private function extractEndpoint($request): string
{
$path = $request->uri->getPath();
$method = $request->getMethod()->value;
// Clean up path for boundary naming
$cleanPath = trim($path, '/');
$cleanPath = str_replace(['/', '-', '.'], '_', $cleanPath);
$cleanPath = preg_replace('/[^a-zA-Z0-9_]/', '', $cleanPath);
if (empty($cleanPath)) {
$cleanPath = 'root';
}
return strtolower("{$method}_{$cleanPath}");
}
}

View File

@@ -0,0 +1,215 @@
<?php
declare(strict_types=1);
namespace App\Framework\ErrorBoundaries\Middleware;
use App\Framework\ErrorBoundaries\ErrorBoundaryFactory;
use App\Framework\Http\HttpMiddleware;
use App\Framework\Http\MiddlewareContext;
use App\Framework\Http\Next;
use App\Framework\Http\RequestStateManager;
use App\Framework\Http\Responses\JsonResponse;
use App\Framework\Http\Status;
/**
* Middleware that provides circuit breaker health information
*/
final readonly class CircuitBreakerHealthMiddleware implements HttpMiddleware
{
public function __construct(
private ErrorBoundaryFactory $boundaryFactory,
private array $monitoredBoundaries = [],
) {
}
public function __invoke(MiddlewareContext $context, Next $next, RequestStateManager $stateManager): MiddlewareContext
{
$request = $context->request;
// Only handle health check requests
if (! $this->isHealthCheckRequest($request)) {
return $next($context, $stateManager);
}
return $context->withResponse($this->createHealthResponse());
}
/**
* Check if this is a health check request
*/
private function isHealthCheckRequest($request): bool
{
$path = $request->uri->getPath();
return in_array($path, [
'/health/circuit-breakers',
'/api/health/circuit-breakers',
'/admin/health/circuit-breakers',
], true);
}
/**
* Create comprehensive health response
*/
private function createHealthResponse(): JsonResponse
{
$boundaries = $this->getMonitoredBoundaries();
$healthData = [];
$overallHealth = 'healthy';
$unhealthyCount = 0;
foreach ($boundaries as $boundaryName) {
$boundary = $this->boundaryFactory->create($boundaryName, \App\Framework\ErrorBoundaries\BoundaryConfig::development());
$health = $boundary->getCircuitHealth();
if ($health !== null) {
$healthData[$boundaryName] = $health;
if (! $health['is_healthy']) {
$unhealthyCount++;
if ($health['severity'] === 'error') {
$overallHealth = 'critical';
} elseif ($overallHealth === 'healthy') {
$overallHealth = 'warning';
}
}
}
}
$response = [
'status' => $overallHealth,
'timestamp' => date(\DateTimeInterface::ISO8601),
'summary' => [
'total_boundaries' => count($boundaries),
'healthy_boundaries' => count($boundaries) - $unhealthyCount,
'unhealthy_boundaries' => $unhealthyCount,
'overall_health_score' => $this->calculateHealthScore($healthData),
],
'boundaries' => $healthData,
'recommendations' => $this->generateRecommendations($healthData),
];
$statusCode = match ($overallHealth) {
'healthy' => Status::OK,
'warning' => Status::OK, // Still OK but with warnings
'critical' => Status::SERVICE_UNAVAILABLE,
default => Status::OK,
};
return new JsonResponse($response, $statusCode);
}
/**
* Get list of boundaries to monitor
*/
private function getMonitoredBoundaries(): array
{
if (! empty($this->monitoredBoundaries)) {
return $this->monitoredBoundaries;
}
// Default boundaries to monitor
return [
'api_health',
'database_connection',
'external_payment_service',
'external_email_service',
'cache_service',
'file_storage',
];
}
/**
* Calculate overall health score (0-100)
*/
private function calculateHealthScore(array $healthData): int
{
if (empty($healthData)) {
return 100;
}
$totalScore = 0;
$count = 0;
foreach ($healthData as $health) {
$boundaryScore = match ($health['severity']) {
'info' => 100, // Healthy
'warning' => 70, // Warning
'error' => 20, // Critical
default => 50, // Unknown
};
$totalScore += $boundaryScore;
$count++;
}
return $count > 0 ? (int) round($totalScore / $count) : 100;
}
/**
* Generate actionable recommendations based on health data
*/
private function generateRecommendations(array $healthData): array
{
$recommendations = [];
foreach ($healthData as $boundaryName => $health) {
if (! $health['is_healthy']) {
$recommendation = $this->getRecommendationForBoundary($boundaryName, $health);
if ($recommendation !== null) {
$recommendations[] = $recommendation;
}
}
}
// Add general recommendations
if (count($recommendations) > 3) {
$recommendations[] = [
'type' => 'general',
'priority' => 'high',
'message' => 'Multiple circuit breakers are unhealthy. Consider system-wide health check.',
'action' => 'Review system load and dependencies',
];
}
return $recommendations;
}
/**
* Get specific recommendation for a boundary
*/
private function getRecommendationForBoundary(string $boundaryName, array $health): ?array
{
$recommendations = [
'database_connection' => [
'type' => 'database',
'priority' => 'critical',
'message' => 'Database connection circuit breaker is open',
'action' => 'Check database connectivity and performance',
],
'external_payment_service' => [
'type' => 'external_service',
'priority' => 'high',
'message' => 'Payment service circuit breaker is failing',
'action' => 'Verify payment service status and API connectivity',
],
'cache_service' => [
'type' => 'cache',
'priority' => 'medium',
'message' => 'Cache service circuit breaker is degraded',
'action' => 'Check Redis/cache service health and memory usage',
],
];
$recommendation = $recommendations[$boundaryName] ?? null;
if ($recommendation !== null) {
$recommendation['boundary'] = $boundaryName;
$recommendation['state'] = $health['state'];
$recommendation['failure_count'] = $health['failure_count'];
}
return $recommendation;
}
}

View File

@@ -0,0 +1,198 @@
<?php
declare(strict_types=1);
namespace App\Framework\ErrorBoundaries\Middleware;
use App\Framework\ErrorBoundaries\ErrorBoundaryFactory;
use App\Framework\Http\HttpMiddleware;
use App\Framework\Http\MiddlewareContext;
use App\Framework\Http\Next;
use App\Framework\Http\RequestStateManager;
use App\Framework\Http\Responses\JsonResponse;
use App\Framework\Http\Status;
use App\Framework\Logging\Logger;
use App\Framework\Router\Result\ViewResult;
/**
* HTTP Middleware that wraps requests in error boundaries
*/
final readonly class ErrorBoundaryMiddleware implements HttpMiddleware
{
public function __construct(
private ErrorBoundaryFactory $boundaryFactory,
private ?Logger $logger = null,
) {
}
public function __invoke(MiddlewareContext $context, Next $next, RequestStateManager $stateManager): MiddlewareContext
{
$request = $context->request;
$routeName = $this->extractRouteName($request, $context);
$boundary = $this->boundaryFactory->createForRoute($routeName);
return $boundary->execute(
operation: fn () => $next($context, $stateManager),
fallback: fn () => $context->withResponse($this->createFallbackResponse($request, $context))
);
}
/**
* Create a graceful fallback response when the main operation fails
*/
private function createFallbackResponse($request, MiddlewareContext $context)
{
$acceptsJson = $this->acceptsJson($request);
if ($acceptsJson) {
return $this->createJsonFallbackResponse($request);
}
return $this->createHtmlFallbackResponse($request, $context);
}
/**
* Create JSON fallback response for API requests
*/
private function createJsonFallbackResponse($request): JsonResponse
{
$errorData = [
'error' => [
'code' => 'SERVICE_TEMPORARILY_UNAVAILABLE',
'message' => 'The service is temporarily unavailable. Please try again later.',
'timestamp' => date(\DateTimeInterface::ISO8601),
'request_id' => $request->headers->get('X-Request-ID') ?? uniqid(),
],
'fallback' => true,
];
return new JsonResponse($errorData, Status::SERVICE_UNAVAILABLE);
}
/**
* Create HTML fallback response for web requests
*/
private function createHtmlFallbackResponse($request, MiddlewareContext $context)
{
$fallbackHtml = $this->getFallbackHtmlContent($request);
return new ViewResult($fallbackHtml, [
'request' => $request,
'timestamp' => date(\DateTimeInterface::ISO8601),
'request_id' => $request->headers->get('X-Request-ID') ?? uniqid(),
], Status::SERVICE_UNAVAILABLE);
}
/**
* Get fallback HTML content
*/
private function getFallbackHtmlContent($request): string
{
return <<<HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Service Temporarily Unavailable</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
margin: 0;
padding: 0;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.container {
text-align: center;
max-width: 600px;
padding: 2rem;
background: rgba(255, 255, 255, 0.1);
border-radius: 12px;
backdrop-filter: blur(10px);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
}
h1 {
font-size: 2.5rem;
margin-bottom: 1rem;
font-weight: 300;
}
p {
font-size: 1.1rem;
margin-bottom: 2rem;
opacity: 0.9;
}
.retry-btn {
background: rgba(255, 255, 255, 0.2);
border: 1px solid rgba(255, 255, 255, 0.3);
color: white;
padding: 12px 24px;
border-radius: 6px;
cursor: pointer;
font-size: 1rem;
transition: all 0.3s ease;
}
.retry-btn:hover {
background: rgba(255, 255, 255, 0.3);
}
.details {
margin-top: 2rem;
font-size: 0.9rem;
opacity: 0.7;
}
.icon {
font-size: 4rem;
margin-bottom: 1rem;
}
</style>
</head>
<body>
<div class="container">
<div class="icon">⚠️</div>
<h1>Service Temporarily Unavailable</h1>
<p>We're experiencing technical difficulties at the moment. Our team has been notified and is working to resolve the issue.</p>
<button class="retry-btn" onclick="window.location.reload()">Try Again</button>
<div class="details">
<p>If the problem persists, please contact our support team.</p>
</div>
</div>
</body>
</html>
HTML;
}
/**
* Extract route name from request/context for boundary naming
*/
private function extractRouteName($request, MiddlewareContext $context): string
{
// Try to get route name from context
$routeName = $context->get('route_name');
if ($routeName !== null) {
return (string) $routeName;
}
// Fallback to request path
$path = $request->uri->getPath();
$method = $request->getMethod()->value;
return "{$method}_{$path}";
}
/**
* Check if request accepts JSON response
*/
private function acceptsJson($request): bool
{
$acceptHeader = $request->headers->get('Accept', '');
return str_contains($acceptHeader, 'application/json') ||
str_contains($acceptHeader, 'application/*') ||
$request->uri->getPath() === '' ||
str_starts_with($request->uri->getPath(), '/api/');
}
}

View File

@@ -0,0 +1,203 @@
<?php
declare(strict_types=1);
namespace App\Framework\ErrorBoundaries\Middleware;
use App\Framework\ErrorBoundaries\ErrorBoundaryFactory;
use App\Framework\Http\MiddlewareManager;
use App\Framework\Http\MiddlewarePriority;
use App\Framework\Logging\Logger;
/**
* Registry for error boundary middleware components
*/
final readonly class ErrorBoundaryMiddlewareRegistry
{
public function __construct(
private MiddlewareManager $middlewareManager,
private ErrorBoundaryFactory $boundaryFactory,
private MiddlewareConfiguration $configuration,
private ?Logger $logger = null,
) {
}
/**
* Register all error boundary middleware
*/
public function registerAll(): void
{
$this->registerMainMiddleware();
if ($this->configuration->enableApiMiddleware) {
$this->registerApiMiddleware();
}
if ($this->configuration->enableHealthMiddleware) {
$this->registerHealthMiddleware();
}
}
/**
* Register main error boundary middleware
*/
public function registerMainMiddleware(): void
{
$middleware = new ErrorBoundaryMiddleware(
boundaryFactory: $this->boundaryFactory,
logger: $this->logger,
);
$this->middlewareManager->register(
middleware: $middleware,
priority: $this->configuration->priority,
condition: fn ($request) => $this->shouldApplyMainMiddleware($request),
);
}
/**
* Register API-specific error boundary middleware
*/
public function registerApiMiddleware(): void
{
$middleware = new ApiErrorBoundaryMiddleware(
boundaryFactory: $this->boundaryFactory,
logger: $this->logger,
includeDebugInfo: $this->configuration->includeDebugInfo,
);
$this->middlewareManager->register(
middleware: $middleware,
priority: $this->configuration->priority + 10, // Higher priority for API
condition: fn ($request) => $this->shouldApplyApiMiddleware($request),
);
}
/**
* Register health check middleware
*/
public function registerHealthMiddleware(): void
{
$middleware = new CircuitBreakerHealthMiddleware(
boundaryFactory: $this->boundaryFactory,
monitoredBoundaries: $this->getMonitoredBoundaries(),
);
$this->middlewareManager->register(
middleware: $middleware,
priority: MiddlewarePriority::HIGH, // Health checks should run early
condition: fn ($request) => $this->shouldApplyHealthMiddleware($request),
);
}
/**
* Register middleware with custom configuration
*/
public function registerCustom(
string $middlewareClass,
array $constructorArgs,
int $priority,
?callable $condition = null
): void {
$middleware = new $middlewareClass(...$constructorArgs);
$this->middlewareManager->register(
middleware: $middleware,
priority: $priority,
condition: $condition,
);
}
/**
* Check if main middleware should be applied
*/
private function shouldApplyMainMiddleware($request): bool
{
$path = $request->uri->getPath();
// Don't apply to health endpoints (they have their own middleware)
if ($this->configuration->isHealthPath($path)) {
return false;
}
// Don't apply to API endpoints if API middleware is enabled
if ($this->configuration->enableApiMiddleware && $this->configuration->isApiPath($path)) {
return false;
}
return $this->configuration->shouldEnableForRoute($path);
}
/**
* Check if API middleware should be applied
*/
private function shouldApplyApiMiddleware($request): bool
{
$path = $request->uri->getPath();
return $this->configuration->isApiPath($path) &&
$this->configuration->shouldEnableForRoute($path);
}
/**
* Check if health middleware should be applied
*/
private function shouldApplyHealthMiddleware($request): bool
{
$path = $request->uri->getPath();
return $this->configuration->isHealthPath($path);
}
/**
* Get list of boundaries to monitor for health checks
*/
private function getMonitoredBoundaries(): array
{
return [
'api_health',
'database_connection',
'external_payment_service',
'external_email_service',
'cache_service',
'file_storage',
'mail_service',
'notification_service',
'search_service',
'analytics_service',
];
}
/**
* Unregister all error boundary middleware
*/
public function unregisterAll(): void
{
$this->middlewareManager->unregister(ErrorBoundaryMiddleware::class);
$this->middlewareManager->unregister(ApiErrorBoundaryMiddleware::class);
$this->middlewareManager->unregister(CircuitBreakerHealthMiddleware::class);
}
/**
* Get middleware statistics
*/
public function getStatistics(): array
{
return [
'configuration' => [
'enable_for_all_routes' => $this->configuration->enableForAllRoutes,
'enabled_routes_count' => count($this->configuration->enabledRoutes),
'excluded_routes_count' => count($this->configuration->excludedRoutes),
'api_middleware_enabled' => $this->configuration->enableApiMiddleware,
'health_middleware_enabled' => $this->configuration->enableHealthMiddleware,
'debug_info_enabled' => $this->configuration->includeDebugInfo,
],
'middleware_registered' => [
'main_middleware' => $this->middlewareManager->isRegistered(ErrorBoundaryMiddleware::class),
'api_middleware' => $this->middlewareManager->isRegistered(ApiErrorBoundaryMiddleware::class),
'health_middleware' => $this->middlewareManager->isRegistered(CircuitBreakerHealthMiddleware::class),
],
'monitored_boundaries_count' => count($this->getMonitoredBoundaries()),
];
}
}

View File

@@ -0,0 +1,229 @@
<?php
declare(strict_types=1);
namespace App\Framework\ErrorBoundaries\Middleware;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\ErrorBoundaries\BoundaryConfig;
use App\Framework\ErrorBoundaries\RetryStrategy;
/**
* Configuration for error boundary middleware
*/
final readonly class MiddlewareConfiguration
{
public function __construct(
public bool $enableForAllRoutes = false,
public array $enabledRoutes = [],
public array $excludedRoutes = [],
public bool $enableApiMiddleware = true,
public bool $enableHealthMiddleware = true,
public array $routeConfigurations = [],
public array $apiPaths = ['/api', '/v1', '/v2'],
public array $healthPaths = [
'/health/circuit-breakers',
'/api/health/circuit-breakers',
'/admin/health/circuit-breakers',
],
public bool $includeDebugInfo = false,
public int $priority = 100,
) {
}
/**
* Create development configuration
*/
public static function development(): self
{
return new self(
enableForAllRoutes: true,
enableApiMiddleware: true,
enableHealthMiddleware: true,
includeDebugInfo: true,
excludedRoutes: [
'/assets/*',
'/css/*',
'/js/*',
'/favicon.ico',
],
);
}
/**
* Create production configuration
*/
public static function production(): self
{
return new self(
enableForAllRoutes: false,
enabledRoutes: [
'/api/*',
'/admin/*',
'/critical/*',
],
enableApiMiddleware: true,
enableHealthMiddleware: true,
includeDebugInfo: false,
excludedRoutes: [
'/assets/*',
'/public/*',
'/static/*',
],
);
}
/**
* Create API-only configuration
*/
public static function apiOnly(): self
{
return new self(
enableForAllRoutes: false,
enabledRoutes: ['/api/*'],
enableApiMiddleware: true,
enableHealthMiddleware: true,
apiPaths: ['/api', '/v1', '/v2', '/graphql'],
);
}
/**
* Check if middleware should be enabled for a route
*/
public function shouldEnableForRoute(string $routePath): bool
{
// First check exclusions
if ($this->isExcludedRoute($routePath)) {
return false;
}
// If enabled for all routes, enable unless excluded
if ($this->enableForAllRoutes) {
return true;
}
// Check specific enabled routes
return $this->isEnabledRoute($routePath);
}
/**
* Check if route is specifically excluded
*/
public function isExcludedRoute(string $routePath): bool
{
foreach ($this->excludedRoutes as $pattern) {
if ($this->matchesPattern($routePath, $pattern)) {
return true;
}
}
return false;
}
/**
* Check if route is specifically enabled
*/
public function isEnabledRoute(string $routePath): bool
{
foreach ($this->enabledRoutes as $pattern) {
if ($this->matchesPattern($routePath, $pattern)) {
return true;
}
}
return false;
}
/**
* Check if route is an API path
*/
public function isApiPath(string $routePath): bool
{
foreach ($this->apiPaths as $apiPath) {
if (str_starts_with($routePath, $apiPath)) {
return true;
}
}
return false;
}
/**
* Check if route is a health check path
*/
public function isHealthPath(string $routePath): bool
{
return in_array($routePath, $this->healthPaths, true);
}
/**
* Get configuration for specific route
*/
public function getRouteConfiguration(string $routePath): ?BoundaryConfig
{
// Check for exact match first
if (isset($this->routeConfigurations[$routePath])) {
return $this->routeConfigurations[$routePath];
}
// Check for pattern matches
foreach ($this->routeConfigurations as $pattern => $config) {
if ($this->matchesPattern($routePath, $pattern)) {
return $config;
}
}
return null;
}
/**
* Add route-specific configuration
*/
public function withRouteConfiguration(string $routePattern, BoundaryConfig $config): self
{
$configurations = $this->routeConfigurations;
$configurations[$routePattern] = $config;
return new self(
enableForAllRoutes: $this->enableForAllRoutes,
enabledRoutes: $this->enabledRoutes,
excludedRoutes: $this->excludedRoutes,
enableApiMiddleware: $this->enableApiMiddleware,
enableHealthMiddleware: $this->enableHealthMiddleware,
routeConfigurations: $configurations,
apiPaths: $this->apiPaths,
healthPaths: $this->healthPaths,
includeDebugInfo: $this->includeDebugInfo,
priority: $this->priority,
);
}
/**
* Get default route configurations
*/
public function getDefaultRouteConfigurations(): array
{
return [
'/api/*' => BoundaryConfig::externalService(),
'/admin/*' => BoundaryConfig::critical(),
'/auth/*' => BoundaryConfig::failFast(),
'/public/*' => BoundaryConfig::ui(),
'/health/*' => new BoundaryConfig(
maxRetries: 0,
retryStrategy: RetryStrategy::FIXED,
baseDelay: Duration::fromMilliseconds(0),
maxDelay: Duration::fromMilliseconds(0),
circuitBreakerEnabled: false,
enableMetrics: true
),
];
}
private function matchesPattern(string $path, string $pattern): bool
{
// Convert shell-style wildcards to regex
$regex = str_replace('*', '.*', preg_quote($pattern, '/'));
return (bool) preg_match("/^{$regex}$/", $path);
}
}

View File

@@ -0,0 +1,163 @@
<?php
declare(strict_types=1);
namespace App\Framework\ErrorBoundaries\Middleware;
use App\Framework\DI\Container;
use App\Framework\DI\Initializer;
use App\Framework\ErrorBoundaries\ErrorBoundaryFactory;
use App\Framework\Http\MiddlewareManager;
use App\Framework\Logging\Logger;
/**
* Service provider for error boundary middleware
*/
final readonly class MiddlewareServiceProvider
{
public function __construct(
private MiddlewareConfiguration $configuration,
) {
}
#[Initializer]
public function initialize(Container $container): void
{
$this->registerMiddlewareConfiguration($container);
$this->registerMiddlewareComponents($container);
$this->registerMiddlewareRegistry($container);
$this->initializeMiddleware($container);
}
/**
* Register middleware configuration
*/
private function registerMiddlewareConfiguration(Container $container): void
{
$container->bindInstance(
MiddlewareConfiguration::class,
$this->configuration
);
}
/**
* Register middleware components
*/
private function registerMiddlewareComponents(Container $container): void
{
// Register main error boundary middleware
$container->bind(
ErrorBoundaryMiddleware::class,
function (Container $container) {
return new ErrorBoundaryMiddleware(
boundaryFactory: $container->get(ErrorBoundaryFactory::class),
logger: $container->getOptional(Logger::class),
);
}
);
// Register API error boundary middleware
$container->bind(
ApiErrorBoundaryMiddleware::class,
function (Container $container) {
$config = $container->get(MiddlewareConfiguration::class);
return new ApiErrorBoundaryMiddleware(
boundaryFactory: $container->get(ErrorBoundaryFactory::class),
logger: $container->getOptional(Logger::class),
includeDebugInfo: $config->includeDebugInfo,
);
}
);
// Register circuit breaker health middleware
$container->bind(
CircuitBreakerHealthMiddleware::class,
function (Container $container) {
return new CircuitBreakerHealthMiddleware(
boundaryFactory: $container->get(ErrorBoundaryFactory::class),
monitoredBoundaries: $this->getDefaultMonitoredBoundaries(),
);
}
);
}
/**
* Register middleware registry
*/
private function registerMiddlewareRegistry(Container $container): void
{
$container->bind(
ErrorBoundaryMiddlewareRegistry::class,
function (Container $container) {
return new ErrorBoundaryMiddlewareRegistry(
middlewareManager: $container->get(MiddlewareManager::class),
boundaryFactory: $container->get(ErrorBoundaryFactory::class),
configuration: $container->get(MiddlewareConfiguration::class),
logger: $container->getOptional(Logger::class),
);
}
);
}
/**
* Initialize and register middleware
*/
private function initializeMiddleware(Container $container): void
{
// Get the registry and register all middleware
$registry = $container->get(ErrorBoundaryMiddlewareRegistry::class);
$registry->registerAll();
}
/**
* Create service provider with development configuration
*/
public static function development(): self
{
return new self(MiddlewareConfiguration::development());
}
/**
* Create service provider with production configuration
*/
public static function production(): self
{
return new self(MiddlewareConfiguration::production());
}
/**
* Create service provider with API-only configuration
*/
public static function apiOnly(): self
{
return new self(MiddlewareConfiguration::apiOnly());
}
/**
* Create service provider with custom configuration
*/
public static function custom(MiddlewareConfiguration $configuration): self
{
return new self($configuration);
}
/**
* Get default monitored boundaries
*/
private function getDefaultMonitoredBoundaries(): array
{
return [
'api_health',
'database_connection',
'external_payment_service',
'external_email_service',
'cache_service',
'file_storage',
'mail_service',
'notification_service',
'search_service',
'analytics_service',
];
}
}

View File

@@ -0,0 +1,223 @@
# ErrorBoundary Middleware System
Complete HTTP middleware system for automatic error boundary integration in web applications.
## Components
### Core Middleware Classes
- **`ErrorBoundaryMiddleware`** - Main middleware for web requests with graceful fallback responses
- **`ApiErrorBoundaryMiddleware`** - Specialized API middleware with detailed JSON error responses
- **`CircuitBreakerHealthMiddleware`** - Health check endpoints for monitoring circuit breaker states
### Configuration & Management
- **`MiddlewareConfiguration`** - Centralized configuration for all middleware components
- **`ErrorBoundaryMiddlewareRegistry`** - Registration and management of middleware components
- **`MiddlewareServiceProvider`** - Dependency injection and service initialization
## Features
### Automatic Error Boundaries
- Wraps HTTP requests in error boundaries
- Provides graceful fallback responses for failed operations
- Supports both JSON (API) and HTML (web) responses
- Route-based configuration and conditional middleware application
### Circuit Breaker Integration
- Automatic circuit breaker management for routes
- Health check endpoints with comprehensive status information
- Configurable thresholds and recovery mechanisms
- Real-time monitoring and alerting capabilities
### Flexible Configuration
- Development, production, and API-only presets
- Route-pattern matching with wildcards
- Granular enable/disable controls per route type
- Debug information inclusion for development environments
## Usage Examples
### Basic Setup
```php
// In your application bootstrap
$serviceProvider = MiddlewareServiceProvider::development();
$serviceProvider->initialize($container);
```
### Custom Configuration
```php
$config = new MiddlewareConfiguration(
enableForAllRoutes: false,
enabledRoutes: ['/api/*', '/admin/*'],
excludedRoutes: ['/assets/*', '/health/*'],
enableApiMiddleware: true,
enableHealthMiddleware: true,
includeDebugInfo: false,
);
$serviceProvider = MiddlewareServiceProvider::custom($config);
$serviceProvider->initialize($container);
```
### Route-Specific Configuration
```php
$config = MiddlewareConfiguration::production()
->withRouteConfiguration('/api/payments/*', BoundaryConfig::critical())
->withRouteConfiguration('/api/notifications/*', BoundaryConfig::externalService());
$serviceProvider = MiddlewareServiceProvider::custom($config);
```
## Configuration Presets
### Development Mode
```php
MiddlewareConfiguration::development()
```
- Enabled for all routes
- Debug information included
- Health monitoring enabled
- Excludes only static assets
### Production Mode
```php
MiddlewareConfiguration::production()
```
- Selective route enabling
- No debug information
- Optimized for performance
- Comprehensive exclusions
### API-Only Mode
```php
MiddlewareConfiguration::apiOnly()
```
- Only API routes enabled
- JSON responses only
- GraphQL support included
- Health monitoring enabled
## Health Check Endpoints
### Circuit Breaker Status
```
GET /health/circuit-breakers
GET /api/health/circuit-breakers
GET /admin/health/circuit-breakers
```
**Response Format:**
```json
{
"status": "healthy|warning|critical",
"timestamp": "2024-01-22T14:30:00+00:00",
"summary": {
"total_boundaries": 6,
"healthy_boundaries": 5,
"unhealthy_boundaries": 1,
"overall_health_score": 85
},
"boundaries": {
"api_health": {
"is_healthy": true,
"state": "CLOSED",
"failure_count": 0,
"severity": "info"
}
},
"recommendations": [
{
"type": "database",
"priority": "critical",
"message": "Database connection circuit breaker is open",
"action": "Check database connectivity and performance"
}
]
}
```
## Error Response Formats
### Web Requests (HTML)
- Beautiful fallback pages with retry functionality
- User-friendly error messages
- Responsive design with modern styling
- Request tracking for debugging
### API Requests (JSON)
```json
{
"error": {
"code": "API_SERVICE_UNAVAILABLE",
"message": "The API service is temporarily unavailable due to high load or maintenance.",
"type": "service_unavailable",
"timestamp": "2024-01-22T14:30:00+00:00",
"request_id": "req_abc123",
"retry_after": 60
},
"meta": {
"endpoint": "/api/users",
"method": "GET",
"boundary_used": true,
"fallback_response": true
},
"links": {
"status": "/api/status",
"documentation": "/api/docs",
"support": "/api/support"
}
}
```
## Monitoring & Observability
### Middleware Statistics
```php
$registry = $container->get(ErrorBoundaryMiddlewareRegistry::class);
$stats = $registry->getStatistics();
```
### Event Integration
- Boundary execution events
- Circuit breaker state changes
- Fallback execution tracking
- Performance metrics collection
## Best Practices
### Route Configuration
- Use specific patterns over broad wildcards
- Configure critical routes with appropriate boundaries
- Exclude static assets and health checks from main middleware
- Test fallback responses in development
### Circuit Breaker Tuning
- Set appropriate failure thresholds for each service type
- Configure realistic timeout values
- Monitor and adjust based on actual traffic patterns
- Implement proper alerting for circuit breaker events
### Performance Optimization
- Use conditional middleware application
- Configure appropriate middleware priorities
- Monitor middleware execution time
- Cache boundary configurations where possible
## Integration Points
### Framework Integration
- Integrates with framework's HTTP middleware system
- Uses dependency injection for component management
- Leverages event system for observability
- Compatible with existing error handling
### External Dependencies
- **ErrorBoundaryFactory** - For boundary creation
- **StateManager** - For circuit breaker persistence
- **Logger** - For operational logging
- **EventBus** - For event publishing
- **MiddlewareManager** - For HTTP middleware registration

View File

@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace App\Framework\ErrorBoundaries;
/**
* Retry strategies for error boundaries
*/
enum RetryStrategy: string
{
case FIXED = 'fixed';
case LINEAR = 'linear';
case EXPONENTIAL = 'exponential';
case EXPONENTIAL_JITTER = 'exponential_jitter';
/**
* Get a human-readable description
*/
public function getDescription(): string
{
return match ($this) {
self::FIXED => 'Fixed delay between retries',
self::LINEAR => 'Linear increase in delay',
self::EXPONENTIAL => 'Exponential backoff',
self::EXPONENTIAL_JITTER => 'Exponential backoff with random jitter',
};
}
/**
* Check if the strategy includes randomization
*/
public function hasJitter(): bool
{
return $this === self::EXPONENTIAL_JITTER;
}
}