feat(database): 添加用户角色权限系统及相关监控功能

- 创建用户表(users)包含基本信息和认证字段
- 创建角色表(roles)用于权限控制
- 创建权限表(permissions)定义系统权限
- 创建用户角色关联表(user_roles)建立用户与角色关系
- 创建角色权限关联表(role_permissions)建立角色与权限关系
- 创建迁移记录表(migrations)追踪数据库变更
- 添加AdminController提供管理员面板功能
- 实现系统监控、配置管理、缓存清理等功能
- 添加AOP切面编程支持的各种通知类型
- 实现告警管理AlertManager支持多渠道告警
- 添加文档注解接口规范
This commit is contained in:
Lawson
2026-04-08 17:00:28 +08:00
commit 2782d765fb
270 changed files with 107192 additions and 0 deletions

View File

@@ -0,0 +1,577 @@
<?php
declare(strict_types=1);
namespace Fendx\Service\CircuitBreaker;
use Fendx\Service\CircuitBreaker\State\CircuitState;
use Fendx\Service\CircuitBreaker\State\ClosedState;
use Fendx\Service\CircuitBreaker\State\OpenState;
use Fendx\Service\CircuitBreaker\State\HalfOpenState;
use Fendx\Service\CircuitBreaker\Storage\CircuitStorage;
use Fendx\Service\CircuitBreaker\Monitor\CircuitMonitor;
class CircuitBreaker
{
protected string $name;
protected array $config;
protected CircuitState $state;
protected CircuitStorage $storage;
protected CircuitMonitor $monitor;
protected array $statistics = [];
protected float $lastStateChange = 0;
protected int $failureCount = 0;
protected int $successCount = 0;
protected float $lastFailureTime = 0;
public function __construct(string $name, array $config = [])
{
$this->name = $name;
$this->config = array_merge($this->getDefaultConfig(), $config);
$this->storage = new CircuitStorage($this->config);
$this->monitor = new CircuitMonitor($this->config);
$this->initialize();
}
/**
* Execute function with circuit breaker protection.
*/
public function call(callable $function, ...$args)
{
if (!$this->canExecute()) {
throw new CircuitBreakerOpenException("Circuit breaker '{$this->name}' is open");
}
$startTime = microtime(true);
try {
$result = $function(...$args);
$duration = microtime(true) - $startTime;
$this->onSuccess($duration);
return $result;
} catch (\Exception $e) {
$duration = microtime(true) - $startTime;
$this->onFailure($e, $duration);
throw $e;
}
}
/**
* Execute function with fallback.
*/
public function callWithFallback(callable $function, callable $fallback, ...$args)
{
try {
return $this->call($function, ...$args);
} catch (CircuitBreakerOpenException $e) {
return $fallback(...$args);
}
}
/**
* Check if circuit breaker allows execution.
*/
public function canExecute(): bool
{
return $this->state->canExecute();
}
/**
* Get current state.
*/
public function getState(): string
{
return $this->state->getName();
}
/**
* Get circuit breaker name.
*/
public function getName(): string
{
return $this->name;
}
/**
* Force open circuit breaker.
*/
public function forceOpen(): void
{
$this->transitionTo(new OpenState($this->config));
$this->logInfo("Circuit breaker '{$this->name}' forced open");
}
/**
* Force close circuit breaker.
*/
public function forceClose(): void
{
$this->transitionTo(new ClosedState($this->config));
$this->logInfo("Circuit breaker '{$this->name}' forced closed");
}
/**
* Reset circuit breaker to initial state.
*/
public function reset(): void
{
$this->failureCount = 0;
$this->successCount = 0;
$this->lastFailureTime = 0;
$this->statistics = [];
$this->transitionTo(new ClosedState($this->config));
$this->logInfo("Circuit breaker '{$this->name}' reset");
}
/**
* Get circuit breaker statistics.
*/
public function getStatistics(): array
{
return array_merge($this->statistics, [
'name' => $this->name,
'state' => $this->getState(),
'failure_count' => $this->failureCount,
'success_count' => $this->successCount,
'last_failure_time' => $this->lastFailureTime,
'last_state_change' => $this->lastStateChange,
'uptime_percentage' => $this->calculateUptimePercentage(),
'average_response_time' => $this->calculateAverageResponseTime(),
'error_rate' => $this->calculateErrorRate()
]);
}
/**
* Get detailed status.
*/
public function getStatus(): array
{
return [
'name' => $this->name,
'state' => $this->getState(),
'config' => $this->config,
'statistics' => $this->getStatistics(),
'state_details' => $this->state->getDetails(),
'monitoring' => $this->monitor->getStatus()
];
}
/**
* Update configuration.
*/
public function updateConfig(array $config): void
{
$this->config = array_merge($this->config, $config);
$this->state->updateConfig($this->config);
$this->logInfo("Configuration updated for circuit breaker '{$this->name}'");
}
/**
* Get configuration.
*/
public function getConfig(): array
{
return $this->config;
}
/**
* Enable/disable circuit breaker.
*/
public function setEnabled(bool $enabled): void
{
$this->config['enabled'] = $enabled;
if (!$enabled) {
$this->forceClose();
}
$this->logInfo("Circuit breaker '{$this->name}' " . ($enabled ? 'enabled' : 'disabled'));
}
/**
* Check if circuit breaker is enabled.
*/
public function isEnabled(): bool
{
return $this->config['enabled'] ?? true;
}
/**
* Handle successful execution.
*/
protected function onSuccess(float $duration): void
{
$this->successCount++;
$this->recordExecution(true, $duration);
$this->state->onSuccess();
$this->monitor->recordSuccess($duration);
// Persist state
$this->persistState();
}
/**
* Handle failed execution.
*/
protected function onFailure(\Exception $exception, float $duration): void
{
$this->failureCount++;
$this->lastFailureTime = microtime(true);
$this->recordExecution(false, $duration, $exception);
$this->state->onFailure();
$this->monitor->recordFailure($exception, $duration);
// Persist state
$this->persistState();
}
/**
* Transition to new state.
*/
protected function transitionTo(CircuitState $newState): void
{
$previousState = $this->getState();
$this->state = $newState;
$this->lastStateChange = microtime(true);
// Record state transition
$this->recordStateTransition($previousState, $newState->getName());
// Notify monitor
$this->monitor->recordStateTransition($previousState, $newState->getName());
$this->logInfo("State transition: {$previousState} -> {$newState->getName()}");
}
/**
* Record execution statistics.
*/
protected function recordExecution(bool $success, float $duration, \Exception $exception = null): void
{
$timestamp = microtime(true);
$this->statistics['executions'][] = [
'timestamp' => $timestamp,
'success' => $success,
'duration' => $duration,
'exception' => $exception ? [
'class' => get_class($exception),
'message' => $exception->getMessage()
] : null
];
// Limit execution history
$maxHistory = $this->config['max_execution_history'] ?? 1000;
if (count($this->statistics['executions']) > $maxHistory) {
$this->statistics['executions'] = array_slice(
$this->statistics['executions'],
-$maxHistory
);
}
// Update aggregates
$this->updateAggregates();
}
/**
* Record state transition.
*/
protected function recordStateTransition(string $from, string $to): void
{
if (!isset($this->statistics['state_transitions'])) {
$this->statistics['state_transitions'] = [];
}
$this->statistics['state_transitions'][] = [
'timestamp' => microtime(true),
'from' => $from,
'to' => $to
];
// Limit transition history
$maxHistory = $this->config['max_transition_history'] ?? 100;
if (count($this->statistics['state_transitions']) > $maxHistory) {
$this->statistics['state_transitions'] = array_slice(
$this->statistics['state_transitions'],
-$maxHistory
);
}
}
/**
* Update aggregate statistics.
*/
protected function updateAggregates(): void
{
$executions = $this->statistics['executions'] ?? [];
if (empty($executions)) {
return;
}
$recentExecutions = array_slice($executions, -$this->config['window_size']);
$totalDuration = 0;
$successCount = 0;
foreach ($recentExecutions as $execution) {
$totalDuration += $execution['duration'];
if ($execution['success']) {
$successCount++;
}
}
$this->statistics['recent_average_duration'] = $totalDuration / count($recentExecutions);
$this->statistics['recent_success_rate'] = ($successCount / count($recentExecutions)) * 100;
$this->statistics['recent_error_rate'] = 100 - $this->statistics['recent_success_rate'];
}
/**
* Calculate uptime percentage.
*/
protected function calculateUptimePercentage(): float
{
$executions = $this->statistics['executions'] ?? [];
if (empty($executions)) {
return 100.0;
}
$successCount = 0;
foreach ($executions as $execution) {
if ($execution['success']) {
$successCount++;
}
}
return ($successCount / count($executions)) * 100;
}
/**
* Calculate average response time.
*/
protected function calculateAverageResponseTime(): float
{
$executions = $this->statistics['executions'] ?? [];
if (empty($executions)) {
return 0.0;
}
$totalDuration = 0;
foreach ($executions as $execution) {
$totalDuration += $execution['duration'];
}
return $totalDuration / count($executions);
}
/**
* Calculate error rate.
*/
protected function calculateErrorRate(): float
{
return 100.0 - $this->calculateUptimePercentage();
}
/**
* Persist circuit breaker state.
*/
protected function persistState(): void
{
if (!$this->config['persist_state']) {
return;
}
$state = [
'name' => $this->name,
'state' => $this->getState(),
'failure_count' => $this->failureCount,
'success_count' => $this->successCount,
'last_failure_time' => $this->lastFailureTime,
'last_state_change' => $this->lastStateChange,
'statistics' => $this->statistics
];
$this->storage->save($this->name, $state);
}
/**
* Load circuit breaker state.
*/
protected function loadState(): void
{
if (!$this->config['persist_state']) {
return;
}
$state = $this->storage->load($this->name);
if (!$state) {
return;
}
$this->failureCount = $state['failure_count'] ?? 0;
$this->successCount = $state['success_count'] ?? 0;
$this->lastFailureTime = $state['last_failure_time'] ?? 0;
$this->lastStateChange = $state['last_state_change'] ?? 0;
$this->statistics = $state['statistics'] ?? [];
// Restore state
switch ($state['state']) {
case 'open':
$this->state = new OpenState($this->config);
break;
case 'half_open':
$this->state = new HalfOpenState($this->config);
break;
default:
$this->state = new ClosedState($this->config);
break;
}
$this->logInfo("State loaded for circuit breaker '{$this->name}': {$state['state']}");
}
/**
* Initialize circuit breaker.
*/
protected function initialize(): void
{
// Load persisted state
$this->loadState();
// Initialize state if not loaded
if (!isset($this->state)) {
$this->state = new ClosedState($this->config);
$this->lastStateChange = microtime(true);
}
// Start monitoring
$this->monitor->start($this->name);
$this->logInfo("Circuit breaker '{$this->name}' initialized in {$this->getState()} state");
}
/**
* Log info message.
*/
protected function logInfo(string $message): void
{
if ($this->config['logging_enabled']) {
error_log("[CircuitBreaker:{$this->name}] {$message}");
}
}
/**
* Log warning message.
*/
protected function logWarning(string $message): void
{
if ($this->config['logging_enabled']) {
error_log("[CircuitBreaker:{$this->name}] WARNING: {$message}");
}
}
/**
* Get default configuration.
*/
protected function getDefaultConfig(): array
{
return [
'failure_threshold' => 5,
'success_threshold' => 3,
'timeout' => 60,
'half_open_max_calls' => 3,
'window_size' => 100,
'enabled' => true,
'persist_state' => true,
'logging_enabled' => true,
'max_execution_history' => 1000,
'max_transition_history' => 100,
'monitoring' => [
'enabled' => true,
'metrics_interval' => 60
],
'storage' => [
'type' => 'file',
'path' => __DIR__ . '/../../../storage/circuit_breaker'
]
];
}
/**
* Create circuit breaker instance.
*/
public static function create(string $name, array $config = []): self
{
return new self($name, $config);
}
/**
* Create for high availability.
*/
public static function forHighAvailability(string $name): self
{
return new self($name, [
'failure_threshold' => 3,
'success_threshold' => 2,
'timeout' => 30,
'half_open_max_calls' => 5,
'window_size' => 50
]);
}
/**
* Create for development.
*/
public static function forDevelopment(string $name): self
{
return new self($name, [
'failure_threshold' => 10,
'success_threshold' => 5,
'timeout' => 120,
'persist_state' => false,
'logging_enabled' => true
]);
}
/**
* Create for testing.
*/
public static function forTesting(string $name): self
{
return new self($name, [
'failure_threshold' => 2,
'success_threshold' => 1,
'timeout' => 5,
'persist_state' => false,
'logging_enabled' => false
]);
}
}
/**
* Circuit breaker open exception.
*/
class CircuitBreakerOpenException extends \Exception
{
public function __construct(string $message = "Circuit breaker is open", int $code = 0, \Throwable $previous = null)
{
parent::__construct($message, $code, $previous);
}
}

View File

@@ -0,0 +1,662 @@
<?php
declare(strict_types=1);
namespace Fendx\Service\CircuitBreaker\Detector;
use Fendx\Service\CircuitBreaker\Detector\Strategy\ErrorRateStrategy;
use Fendx\Service\CircuitBreaker\Detector\Strategy\ResponseTimeStrategy;
use Fendx\Service\CircuitBreaker\Detector\Strategy\TimeoutStrategy;
use Fendx\Service\CircuitBreaker\Detector\Strategy\CustomStrategy;
class FailureDetector
{
protected array $strategies = [];
protected array $config = [];
protected array $metrics = [];
protected array $detectionHistory = [];
public function __construct(array $config = [])
{
$this->config = array_merge($this->getDefaultConfig(), $config);
$this->initializeStrategies();
}
/**
* Detect failure based on metrics.
*/
public function detectFailure(array $metrics): array
{
$results = [];
$overallFailure = false;
$failureReasons = [];
foreach ($this->strategies as $name => $strategy) {
$strategyResult = $strategy->detect($metrics);
$results[$name] = $strategyResult;
if ($strategyResult['failure']) {
$overallFailure = true;
$failureReasons[] = $strategyResult['reason'];
}
}
$detection = [
'timestamp' => microtime(true),
'failure' => $overallFailure,
'reasons' => $failureReasons,
'strategies' => $results,
'metrics' => $metrics
];
// Record detection
$this->recordDetection($detection);
return $detection;
}
/**
* Add custom detection strategy.
*/
public function addStrategy(string $name, callable $detector): void
{
$this->strategies[$name] = new CustomStrategy($detector);
$this->logInfo("Added custom detection strategy: {$name}");
}
/**
* Remove detection strategy.
*/
public function removeStrategy(string $name): bool
{
if (!isset($this->strategies[$name])) {
return false;
}
unset($this->strategies[$name]);
$this->logInfo("Removed detection strategy: {$name}");
return true;
}
/**
* Get all strategies.
*/
public function getStrategies(): array
{
return array_keys($this->strategies);
}
/**
* Enable/disable strategy.
*/
public function setStrategyEnabled(string $name, bool $enabled): bool
{
if (!isset($this->strategies[$name])) {
return false;
}
$this->strategies[$name]->setEnabled($enabled);
$this->logInfo("Strategy '{$name}' " . ($enabled ? 'enabled' : 'disabled'));
return true;
}
/**
* Update strategy configuration.
*/
public function updateStrategyConfig(string $name, array $config): bool
{
if (!isset($this->strategies[$name])) {
return false;
}
$this->strategies[$name]->updateConfig($config);
$this->logInfo("Configuration updated for strategy: {$name}");
return true;
}
/**
* Record metrics for analysis.
*/
public function recordMetrics(array $metrics): void
{
$timestamp = microtime(true);
$this->metrics[] = array_merge($metrics, [
'timestamp' => $timestamp
]);
// Limit metrics history
$maxHistory = $this->config['max_metrics_history'] ?? 10000;
if (count($this->metrics) > $maxHistory) {
$this->metrics = array_slice($this->metrics, -$maxHistory);
}
}
/**
* Get recent metrics.
*/
public function getRecentMetrics(int $windowSize = null): array
{
$windowSize = $windowSize ?? $this->config['window_size'] ?? 100;
return array_slice($this->metrics, -$windowSize);
}
/**
* Get metrics in time range.
*/
public function getMetricsInRange(float $startTime, float $endTime): array
{
return array_filter($this->metrics, function($metric) use ($startTime, $endTime) {
return $metric['timestamp'] >= $startTime && $metric['timestamp'] <= $endTime;
});
}
/**
* Calculate aggregated metrics.
*/
public function calculateAggregates(array $metrics = null): array
{
$metrics = $metrics ?? $this->getRecentMetrics();
if (empty($metrics)) {
return [
'count' => 0,
'success_rate' => 0,
'error_rate' => 0,
'average_response_time' => 0,
'min_response_time' => 0,
'max_response_time' => 0,
'timeout_rate' => 0
];
}
$totalCount = count($metrics);
$successCount = 0;
$errorCount = 0;
$timeoutCount = 0;
$totalResponseTime = 0;
$minResponseTime = PHP_FLOAT_MAX;
$maxResponseTime = 0;
foreach ($metrics as $metric) {
$success = $metric['success'] ?? false;
$responseTime = $metric['response_time'] ?? 0;
$timeout = $metric['timeout'] ?? false;
if ($success) {
$successCount++;
} else {
$errorCount++;
}
if ($timeout) {
$timeoutCount++;
}
$totalResponseTime += $responseTime;
$minResponseTime = min($minResponseTime, $responseTime);
$maxResponseTime = max($maxResponseTime, $responseTime);
}
return [
'count' => $totalCount,
'success_rate' => ($successCount / $totalCount) * 100,
'error_rate' => ($errorCount / $totalCount) * 100,
'timeout_rate' => ($timeoutCount / $totalCount) * 100,
'average_response_time' => $totalResponseTime / $totalCount,
'min_response_time' => $minResponseTime === PHP_FLOAT_MAX ? 0 : $minResponseTime,
'max_response_time' => $maxResponseTime,
'success_count' => $successCount,
'error_count' => $errorCount,
'timeout_count' => $timeoutCount
];
}
/**
* Get detection statistics.
*/
public function getStatistics(): array
{
$aggregates = $this->calculateAggregates();
$detectionStats = [
'total_detections' => count($this->detectionHistory),
'failure_detections' => 0,
'strategy_performance' => []
];
foreach ($this->detectionHistory as $detection) {
if ($detection['failure']) {
$detectionStats['failure_detections']++;
}
foreach ($detection['strategies'] as $strategyName => $result) {
if (!isset($detectionStats['strategy_performance'][$strategyName])) {
$detectionStats['strategy_performance'][$strategyName] = [
'detections' => 0,
'failures' => 0
];
}
$detectionStats['strategy_performance'][$strategyName]['detections']++;
if ($result['failure']) {
$detectionStats['strategy_performance'][$strategyName]['failures']++;
}
}
}
// Calculate strategy effectiveness
foreach ($detectionStats['strategy_performance'] as $strategyName => &$stats) {
$stats['failure_rate'] = $stats['detections'] > 0 ?
($stats['failures'] / $stats['detections']) * 100 : 0;
}
return array_merge($aggregates, $detectionStats);
}
/**
* Get detection history.
*/
public function getDetectionHistory(int $limit = 100): array
{
return array_slice($this->detectionHistory, -$limit);
}
/**
* Analyze failure patterns.
*/
public function analyzeFailurePatterns(): array
{
$patterns = [];
// Time-based patterns
$hourlyFailures = [];
$dailyFailures = [];
foreach ($this->detectionHistory as $detection) {
if (!$detection['failure']) {
continue;
}
$timestamp = $detection['timestamp'];
$hour = date('H', (int) $timestamp);
$day = date('Y-m-d', (int) $timestamp);
$hourlyFailures[$hour] = ($hourlyFailures[$hour] ?? 0) + 1;
$dailyFailures[$day] = ($dailyFailures[$day] ?? 0) + 1;
}
// Strategy correlation
$strategyCorrelations = [];
foreach ($this->detectionHistory as $detection) {
foreach ($detection['strategies'] as $strategyName => $result) {
if (!isset($strategyCorrelations[$strategyName])) {
$strategyCorrelations[$strategyName] = [
'total' => 0,
'failures' => 0
];
}
$strategyCorrelations[$strategyName]['total']++;
if ($result['failure']) {
$strategyCorrelations[$strategyName]['failures']++;
}
}
}
// Calculate correlation rates
foreach ($strategyCorrelations as $strategyName => &$correlation) {
$correlation['correlation_rate'] = $correlation['total'] > 0 ?
($correlation['failures'] / $correlation['total']) * 100 : 0;
}
return [
'hourly_pattern' => $hourlyFailures,
'daily_pattern' => $dailyFailures,
'strategy_correlations' => $strategyCorrelations,
'peak_failure_hour' => $this->findPeakHour($hourlyFailures),
'most_correlated_strategy' => $this->findMostCorrelatedStrategy($strategyCorrelations)
];
}
/**
* Predict failure probability.
*/
public function predictFailureProbability(): float
{
$recentMetrics = $this->getRecentMetrics($this->config['prediction_window'] ?? 50);
if (count($recentMetrics) < $this->config['min_prediction_samples'] ?? 10) {
return 0.0;
}
$aggregates = $this->calculateAggregates($recentMetrics);
// Simple probability calculation based on recent trends
$errorRate = $aggregates['error_rate'];
$timeoutRate = $aggregates['timeout_rate'];
// Weight factors
$errorWeight = $this->config['error_rate_weight'] ?? 0.6;
$timeoutWeight = $this->config['timeout_rate_weight'] ?? 0.4;
$probability = ($errorRate * $errorWeight) + ($timeoutRate * $timeoutWeight);
// Apply trend analysis
$trendFactor = $this->calculateTrendFactor($recentMetrics);
$probability *= $trendFactor;
return min(100.0, max(0.0, $probability));
}
/**
* Get health score.
*/
public function getHealthScore(): float
{
$failureProbability = $this->predictFailureProbability();
// Convert failure probability to health score (0-100)
$healthScore = 100.0 - $failureProbability;
// Apply stability factor
$stabilityFactor = $this->calculateStabilityFactor();
$healthScore *= $stabilityFactor;
return min(100.0, max(0.0, $healthScore));
}
/**
* Reset detector state.
*/
public function reset(): void
{
$this->metrics = [];
$this->detectionHistory = [];
$this->logInfo("Failure detector reset");
}
/**
* Export detector configuration and data.
*/
public function exportData(): array
{
return [
'config' => $this->config,
'strategies' => $this->getStrategyConfigs(),
'metrics' => $this->metrics,
'detection_history' => $this->detectionHistory,
'statistics' => $this->getStatistics(),
'exported_at' => date('Y-m-d H:i:s')
];
}
/**
* Record detection result.
*/
protected function recordDetection(array $detection): void
{
$this->detectionHistory[] = $detection;
// Limit history size
$maxHistory = $this->config['max_detection_history'] ?? 1000;
if (count($this->detectionHistory) > $maxHistory) {
$this->detectionHistory = array_slice($this->detectionHistory, -$maxHistory);
}
}
/**
* Initialize built-in strategies.
*/
protected function initializeStrategies(): void
{
if ($this->config['strategies']['error_rate']['enabled'] ?? true) {
$this->strategies['error_rate'] = new ErrorRateStrategy(
$this->config['strategies']['error_rate'] ?? []
);
}
if ($this->config['strategies']['response_time']['enabled'] ?? true) {
$this->strategies['response_time'] = new ResponseTimeStrategy(
$this->config['strategies']['response_time'] ?? []
);
}
if ($this->config['strategies']['timeout']['enabled'] ?? true) {
$this->strategies['timeout'] = new TimeoutStrategy(
$this->config['strategies']['timeout'] ?? []
);
}
}
/**
* Get strategy configurations.
*/
protected function getStrategyConfigs(): array
{
$configs = [];
foreach ($this->strategies as $name => $strategy) {
$configs[$name] = $strategy->getConfig();
}
return $configs;
}
/**
* Find peak failure hour.
*/
protected function findPeakHour(array $hourlyFailures): ?int
{
if (empty($hourlyFailures)) {
return null;
}
return array_keys($hourlyFailures, max($hourlyFailures))[0];
}
/**
* Find most correlated strategy.
*/
protected function findMostCorrelatedStrategy(array $correlations): ?string
{
if (empty($correlations)) {
return null;
}
$maxCorrelation = 0;
$mostCorrelated = null;
foreach ($correlations as $strategy => $data) {
if ($data['correlation_rate'] > $maxCorrelation) {
$maxCorrelation = $data['correlation_rate'];
$mostCorrelated = $strategy;
}
}
return $mostCorrelated;
}
/**
* Calculate trend factor.
*/
protected function calculateTrendFactor(array $recentMetrics): float
{
if (count($recentMetrics) < 10) {
return 1.0;
}
// Split into two halves
$mid = count($recentMetrics) / 2;
$firstHalf = array_slice($recentMetrics, 0, $mid);
$secondHalf = array_slice($recentMetrics, $mid);
$firstHalfAggregates = $this->calculateAggregates($firstHalf);
$secondHalfAggregates = $this->calculateAggregates($secondHalf);
// Compare error rates
$errorRateChange = $secondHalfAggregates['error_rate'] - $firstHalfAggregates['error_rate'];
// Convert to factor (1.0 = no change, >1.0 = worsening, <1.0 = improving)
$factor = 1.0 + ($errorRateChange / 100.0);
return max(0.5, min(2.0, $factor));
}
/**
* Calculate stability factor.
*/
protected function calculateStabilityFactor(): float
{
if (count($this->detectionHistory) < 10) {
return 1.0;
}
$recentDetections = array_slice($this->detectionHistory, -20);
$fluctuations = 0;
for ($i = 1; $i < count($recentDetections); $i++) {
$current = $recentDetections[$i]['failure'];
$previous = $recentDetections[$i - 1]['failure'];
if ($current !== $previous) {
$fluctuations++;
}
}
$fluctuationRate = $fluctuations / (count($recentDetections) - 1);
// Convert fluctuation rate to stability factor
return max(0.7, 1.0 - ($fluctuationRate * 0.5));
}
/**
* Log info message.
*/
protected function logInfo(string $message): void
{
if ($this->config['logging_enabled']) {
error_log("[FailureDetector] {$message}");
}
}
/**
* Get default configuration.
*/
protected function getDefaultConfig(): array
{
return [
'window_size' => 100,
'prediction_window' => 50,
'min_prediction_samples' => 10,
'max_metrics_history' => 10000,
'max_detection_history' => 1000,
'error_rate_weight' => 0.6,
'timeout_rate_weight' => 0.4,
'logging_enabled' => true,
'strategies' => [
'error_rate' => [
'enabled' => true,
'threshold' => 50.0,
'window_size' => 20
],
'response_time' => [
'enabled' => true,
'threshold' => 5000.0,
'window_size' => 10
],
'timeout' => [
'enabled' => true,
'threshold' => 10.0,
'window_size' => 30
]
]
];
}
/**
* Get configuration.
*/
public function getConfig(): array
{
return $this->config;
}
/**
* Set configuration.
*/
public function setConfig(array $config): void
{
$this->config = array_merge($this->config, $config);
// Reinitialize strategies
$this->initializeStrategies();
}
/**
* Create failure detector instance.
*/
public static function create(array $config = []): self
{
return new self($config);
}
/**
* Create for production.
*/
public static function forProduction(): self
{
return new self([
'window_size' => 200,
'prediction_window' => 100,
'min_prediction_samples' => 20,
'strategies' => [
'error_rate' => [
'threshold' => 30.0,
'window_size' => 50
],
'response_time' => [
'threshold' => 2000.0,
'window_size' => 25
],
'timeout' => [
'threshold' => 5.0,
'window_size' => 40
]
]
]);
}
/**
* Create for development.
*/
public static function forDevelopment(): self
{
return new self([
'window_size' => 50,
'prediction_window' => 25,
'min_prediction_samples' => 5,
'logging_enabled' => true,
'strategies' => [
'error_rate' => [
'threshold' => 70.0
],
'response_time' => [
'threshold' => 10000.0
]
]
]);
}
}

View File

@@ -0,0 +1,874 @@
<?php
declare(strict_types=1);
namespace Fendx\Service\CircuitBreaker\Monitor;
use Fendx\Service\CircuitBreaker\Monitor\Collector\MetricsCollector;
use Fendx\Service\CircuitBreaker\Monitor\Analyzer\StateAnalyzer;
use Fendx\Service\CircuitBreaker\Monitor\Alert\AlertManager;
class CircuitMonitor
{
protected array $config = [];
protected MetricsCollector $collector;
protected StateAnalyzer $analyzer;
protected AlertManager $alertManager;
protected array $circuitStates = [];
protected array $monitoringData = [];
protected array $alerts = [];
protected bool $isRunning = false;
public function __construct(array $config = [])
{
$this->config = array_merge($this->getDefaultConfig(), $config);
$this->collector = new MetricsCollector($this->config);
$this->analyzer = new StateAnalyzer($this->config);
$this->alertManager = new AlertManager($this->config);
}
/**
* Start monitoring for a circuit breaker.
*/
public function start(string $circuitBreakerName): void
{
$this->circuitStates[$circuitBreakerName] = [
'name' => $circuitBreakerName,
'started_at' => microtime(true),
'last_update' => microtime(true),
'state_transitions' => [],
'metrics' => [],
'alerts' => []
];
if (!$this->isRunning) {
$this->isRunning = true;
$this->startMonitoringLoop();
}
$this->logInfo("Started monitoring for circuit breaker: {$circuitBreakerName}");
}
/**
* Stop monitoring for a circuit breaker.
*/
public function stop(string $circuitBreakerName): void
{
if (!isset($this->circuitStates[$circuitBreakerName])) {
return;
}
unset($this->circuitStates[$circuitBreakerName]);
if (empty($this->circuitStates)) {
$this->isRunning = false;
}
$this->logInfo("Stopped monitoring for circuit breaker: {$circuitBreakerName}");
}
/**
* Record state transition.
*/
public function recordStateTransition(string $circuitBreakerName, string $fromState, string $toState): void
{
if (!isset($this->circuitStates[$circuitBreakerName])) {
$this->start($circuitBreakerName);
}
$transition = [
'timestamp' => microtime(true),
'from' => $fromState,
'to' => $toState,
'duration' => 0
];
$this->circuitStates[$circuitBreakerName]['state_transitions'][] = $transition;
$this->circuitStates[$circuitBreakerName]['last_update'] = microtime(true);
// Analyze transition
$this->analyzeTransition($circuitBreakerName, $transition);
// Check for alerts
$this->checkTransitionAlerts($circuitBreakerName, $transition);
$this->logInfo("State transition recorded for {$circuitBreakerName}: {$fromState} -> {$toState}");
}
/**
* Record successful execution.
*/
public function recordSuccess(string $circuitBreakerName, float $duration): void
{
if (!isset($this->circuitStates[$circuitBreakerName])) {
$this->start($circuitBreakerName);
}
$metric = [
'timestamp' => microtime(true),
'type' => 'success',
'duration' => $duration
];
$this->circuitStates[$circuitBreakerName]['metrics'][] = $metric;
$this->circuitStates[$circuitBreakerName]['last_update'] = microtime(true);
// Collect metrics
$this->collector->record($circuitBreakerName, $metric);
// Check performance alerts
$this->checkPerformanceAlerts($circuitBreakerName, $metric);
}
/**
* Record failed execution.
*/
public function recordFailure(string $circuitBreakerName, \Exception $exception, float $duration): void
{
if (!isset($this->circuitStates[$circuitBreakerName])) {
$this->start($circuitBreakerName);
}
$metric = [
'timestamp' => microtime(true),
'type' => 'failure',
'duration' => $duration,
'exception' => [
'class' => get_class($exception),
'message' => $exception->getMessage(),
'code' => $exception->getCode()
]
];
$this->circuitStates[$circuitBreakerName]['metrics'][] = $metric;
$this->circuitStates[$circuitBreakerName]['last_update'] = microtime(true);
// Collect metrics
$this->collector->record($circuitBreakerName, $metric);
// Check failure alerts
$this->checkFailureAlerts($circuitBreakerName, $metric);
}
/**
* Get monitoring status.
*/
public function getStatus(): array
{
return [
'is_running' => $this->isRunning,
'monitored_circuits' => count($this->circuitStates),
'circuit_names' => array_keys($this->circuitStates),
'total_metrics' => $this->getTotalMetricsCount(),
'total_transitions' => $this->getTotalTransitionsCount(),
'active_alerts' => count($this->alerts)
];
}
/**
* Get circuit breaker monitoring data.
*/
public function getCircuitData(string $circuitBreakerName): ?array
{
if (!isset($this->circuitStates[$circuitBreakerName])) {
return null;
}
$circuitData = $this->circuitStates[$circuitBreakerName];
// Add calculated metrics
$circuitData['calculated_metrics'] = $this->calculateCircuitMetrics($circuitBreakerName);
// Add analysis results
$circuitData['analysis'] = $this->analyzer->analyze($circuitBreakerName, $circuitData);
return $circuitData;
}
/**
* Get all circuit breaker data.
*/
public function getAllCircuitData(): array
{
$allData = [];
foreach (array_keys($this->circuitStates) as $circuitName) {
$allData[$circuitName] = $this->getCircuitData($circuitName);
}
return $allData;
}
/**
* Get monitoring dashboard data.
*/
public function getDashboardData(): array
{
$dashboard = [
'overview' => [
'total_circuits' => count($this->circuitStates),
'healthy_circuits' => 0,
'degraded_circuits' => 0,
'failed_circuits' => 0,
'total_requests' => 0,
'success_rate' => 0,
'average_response_time' => 0
],
'circuits' => [],
'alerts' => $this->getRecentAlerts(10),
'trends' => $this->getTrends()
];
foreach ($this->circuitStates as $circuitName => $circuitData) {
$metrics = $this->calculateCircuitMetrics($circuitName);
$analysis = $this->analyzer->analyze($circuitName, $circuitData);
$circuitInfo = [
'name' => $circuitName,
'status' => $analysis['health_status'],
'success_rate' => $metrics['success_rate'],
'average_response_time' => $metrics['average_response_time'],
'last_state_change' => $this->getLastStateChange($circuitName),
'uptime_percentage' => $metrics['uptime_percentage'],
'error_rate' => $metrics['error_rate']
];
$dashboard['circuits'][] = $circuitInfo;
// Update overview
switch ($analysis['health_status']) {
case 'healthy':
$dashboard['overview']['healthy_circuits']++;
break;
case 'degraded':
$dashboard['overview']['degraded_circuits']++;
break;
case 'failed':
$dashboard['overview']['failed_circuits']++;
break;
}
$dashboard['overview']['total_requests'] += $metrics['total_requests'];
}
// Calculate overview averages
if (count($this->circuitStates) > 0) {
$totalSuccessRate = 0;
$totalResponseTime = 0;
foreach ($dashboard['circuits'] as $circuit) {
$totalSuccessRate += $circuit['success_rate'];
$totalResponseTime += $circuit['average_response_time'];
}
$dashboard['overview']['success_rate'] = $totalSuccessRate / count($dashboard['circuits']);
$dashboard['overview']['average_response_time'] = $totalResponseTime / count($dashboard['circuits']);
}
return $dashboard;
}
/**
* Get recent alerts.
*/
public function getRecentAlerts(int $limit = 50): array
{
$alerts = $this->alertManager->getRecentAlerts($limit);
// Add circuit-specific alerts
foreach ($this->circuitStates as $circuitName => $circuitData) {
$circuitAlerts = $circuitData['alerts'] ?? [];
foreach ($circuitAlerts as $alert) {
$alerts[] = array_merge($alert, ['circuit' => $circuitName]);
}
}
// Sort by timestamp
usort($alerts, function($a, $b) {
return $b['timestamp'] <=> $a['timestamp'];
});
return array_slice($alerts, 0, $limit);
}
/**
* Get trends data.
*/
public function getTrends(): array
{
$trends = [
'success_rate_trend' => [],
'response_time_trend' => [],
'error_rate_trend' => [],
'state_change_frequency' => []
];
foreach ($this->circuitStates as $circuitName => $circuitData) {
$circuitTrends = $this->calculateCircuitTrends($circuitName);
foreach ($trends as $trendType => &$trendData) {
if (isset($circuitTrends[$trendType])) {
$trendData[$circuitName] = $circuitTrends[$trendType];
}
}
}
return $trends;
}
/**
* Get performance report.
*/
public function getPerformanceReport(string $circuitBreakerName = null): array
{
if ($circuitBreakerName) {
return $this->generateCircuitReport($circuitBreakerName);
}
$report = [
'generated_at' => date('Y-m-d H:i:s'),
'period' => 'last_24_hours',
'summary' => $this->generateSummaryReport(),
'circuits' => []
];
foreach (array_keys($this->circuitStates) as $circuitName) {
$report['circuits'][$circuitName] = $this->generateCircuitReport($circuitName);
}
return $report;
}
/**
* Set alert threshold.
*/
public function setAlertThreshold(string $metric, float $value): void
{
$this->alertManager->setThreshold($metric, $value);
$this->logInfo("Alert threshold set for {$metric}: {$value}");
}
/**
* Enable/disable alert.
*/
public function setAlertEnabled(string $alertType, bool $enabled): void
{
$this->alertManager->setEnabled($alertType, $enabled);
$this->logInfo("Alert '{$alertType}' " . ($enabled ? 'enabled' : 'disabled'));
}
/**
* Clear monitoring data.
*/
public function clearData(string $circuitBreakerName = null): void
{
if ($circuitBreakerName) {
if (isset($this->circuitStates[$circuitBreakerName])) {
$this->circuitStates[$circuitBreakerName]['metrics'] = [];
$this->circuitStates[$circuitBreakerName]['state_transitions'] = [];
$this->circuitStates[$circuitBreakerName]['alerts'] = [];
}
} else {
foreach ($this->circuitStates as $circuitName => &$circuitData) {
$circuitData['metrics'] = [];
$circuitData['state_transitions'] = [];
$circuitData['alerts'] = [];
}
}
$this->logInfo("Monitoring data cleared" . ($circuitBreakerName ? " for {$circuitBreakerName}" : ""));
}
/**
* Export monitoring data.
*/
public function exportData(string $format = 'json'): string
{
$data = [
'config' => $this->config,
'circuit_states' => $this->circuitStates,
'alerts' => $this->alerts,
'dashboard' => $this->getDashboardData(),
'exported_at' => date('Y-m-d H:i:s')
];
switch ($format) {
case 'json':
return json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
case 'php':
return '<?php return ' . var_export($data, true) . ';';
default:
throw new \InvalidArgumentException("Unsupported export format: {$format}");
}
}
/**
* Calculate circuit metrics.
*/
protected function calculateCircuitMetrics(string $circuitBreakerName): array
{
if (!isset($this->circuitStates[$circuitBreakerName])) {
return [];
}
$metrics = $this->circuitStates[$circuitBreakerName]['metrics'];
if (empty($metrics)) {
return [
'total_requests' => 0,
'success_count' => 0,
'failure_count' => 0,
'success_rate' => 100,
'error_rate' => 0,
'average_response_time' => 0,
'min_response_time' => 0,
'max_response_time' => 0,
'uptime_percentage' => 100
];
}
$totalRequests = count($metrics);
$successCount = 0;
$failureCount = 0;
$totalResponseTime = 0;
$minResponseTime = PHP_FLOAT_MAX;
$maxResponseTime = 0;
foreach ($metrics as $metric) {
$responseTime = $metric['duration'];
if ($metric['type'] === 'success') {
$successCount++;
} else {
$failureCount++;
}
$totalResponseTime += $responseTime;
$minResponseTime = min($minResponseTime, $responseTime);
$maxResponseTime = max($maxResponseTime, $responseTime);
}
return [
'total_requests' => $totalRequests,
'success_count' => $successCount,
'failure_count' => $failureCount,
'success_rate' => ($successCount / $totalRequests) * 100,
'error_rate' => ($failureCount / $totalRequests) * 100,
'average_response_time' => $totalResponseTime / $totalRequests,
'min_response_time' => $minResponseTime === PHP_FLOAT_MAX ? 0 : $minResponseTime,
'max_response_time' => $maxResponseTime,
'uptime_percentage' => ($successCount / $totalRequests) * 100
];
}
/**
* Calculate circuit trends.
*/
protected function calculateCircuitTrends(string $circuitBreakerName): array
{
if (!isset($this->circuitStates[$circuitBreakerName])) {
return [];
}
$metrics = $this->circuitStates[$circuitBreakerName]['metrics'];
// Group metrics by time windows (e.g., hourly)
$timeWindows = [];
$windowSize = 3600; // 1 hour
foreach ($metrics as $metric) {
$window = floor($metric['timestamp'] / $windowSize) * $windowSize;
if (!isset($timeWindows[$window])) {
$timeWindows[$window] = [];
}
$timeWindows[$window][] = $metric;
}
// Calculate trends for each window
$trends = [
'success_rate_trend' => [],
'response_time_trend' => [],
'error_rate_trend' => []
];
ksort($timeWindows);
foreach ($timeWindows as $window => $windowMetrics) {
$successCount = 0;
$totalResponseTime = 0;
foreach ($windowMetrics as $metric) {
if ($metric['type'] === 'success') {
$successCount++;
}
$totalResponseTime += $metric['duration'];
}
$totalCount = count($windowMetrics);
$successRate = ($successCount / $totalCount) * 100;
$averageResponseTime = $totalResponseTime / $totalCount;
$errorRate = 100 - $successRate;
$trends['success_rate_trend'][] = [
'timestamp' => $window,
'value' => $successRate
];
$trends['response_time_trend'][] = [
'timestamp' => $window,
'value' => $averageResponseTime
];
$trends['error_rate_trend'][] = [
'timestamp' => $window,
'value' => $errorRate
];
}
// Calculate state change frequency
$transitions = $this->circuitStates[$circuitBreakerName]['state_transitions'];
$trends['state_change_frequency'] = count($transitions);
return $trends;
}
/**
* Analyze state transition.
*/
protected function analyzeTransition(string $circuitBreakerName, array $transition): void
{
$analysis = [
'circuit' => $circuitBreakerName,
'transition' => $transition,
'analysis' => []
];
// Analyze transition patterns
$transitions = $this->circuitStates[$circuitBreakerName]['state_transitions'];
if (count($transitions) > 1) {
$previousTransition = $transitions[count($transitions) - 2];
$timeSincePrevious = $transition['timestamp'] - $previousTransition['timestamp'];
$analysis['analysis']['time_since_previous'] = $timeSincePrevious;
// Check for rapid transitions
if ($timeSincePrevious < 60) { // Less than 1 minute
$analysis['analysis']['rapid_transition'] = true;
}
}
// Store analysis
$this->monitoringData[] = $analysis;
}
/**
* Check transition alerts.
*/
protected function checkTransitionAlerts(string $circuitBreakerName, array $transition): void
{
// Check for frequent state changes
$transitions = $this->circuitStates[$circuitBreakerName]['state_transitions'];
if (count($transitions) >= 10) { // 10 transitions in monitoring period
$this->createAlert($circuitBreakerName, 'frequent_state_changes', [
'transition_count' => count($transitions),
'latest_transition' => $transition
]);
}
// Check for unwanted transitions (e.g., closed -> open)
if ($transition['from'] === 'closed' && $transition['to'] === 'open') {
$this->createAlert($circuitBreakerName, 'circuit_opened', [
'transition' => $transition
]);
}
}
/**
* Check performance alerts.
*/
protected function checkPerformanceAlerts(string $circuitBreakerName, array $metric): void
{
// Check response time threshold
$responseTimeThreshold = $this->config['alerts']['response_time_threshold'] ?? 5000; // 5 seconds
if ($metric['duration'] > $responseTimeThreshold) {
$this->createAlert($circuitBreakerName, 'slow_response', [
'duration' => $metric['duration'],
'threshold' => $responseTimeThreshold
]);
}
}
/**
* Check failure alerts.
*/
protected function checkFailureAlerts(string $circuitBreakerName, array $metric): void
{
// Check error rate threshold
$metrics = $this->circuitStates[$circuitBreakerName]['metrics'];
$recentMetrics = array_slice($metrics, -100); // Last 100 requests
if (count($recentMetrics) >= 10) {
$failureCount = 0;
foreach ($recentMetrics as $recentMetric) {
if ($recentMetric['type'] === 'failure') {
$failureCount++;
}
}
$errorRate = ($failureCount / count($recentMetrics)) * 100;
$errorRateThreshold = $this->config['alerts']['error_rate_threshold'] ?? 50; // 50%
if ($errorRate > $errorRateThreshold) {
$this->createAlert($circuitBreakerName, 'high_error_rate', [
'error_rate' => $errorRate,
'threshold' => $errorRateThreshold,
'sample_size' => count($recentMetrics)
]);
}
}
}
/**
* Create alert.
*/
protected function createAlert(string $circuitBreakerName, string $type, array $data): void
{
$alert = [
'id' => uniqid('alert_'),
'circuit' => $circuitBreakerName,
'type' => $type,
'timestamp' => microtime(true),
'data' => $data,
'acknowledged' => false
];
$this->alerts[] = $alert;
$this->circuitStates[$circuitBreakerName]['alerts'][] = $alert;
// Send to alert manager
$this->alertManager->process($alert);
$this->logInfo("Alert created for {$circuitBreakerName}: {$type}");
}
/**
* Get last state change.
*/
protected function getLastStateChange(string $circuitBreakerName): ?float
{
if (!isset($this->circuitStates[$circuitBreakerName])) {
return null;
}
$transitions = $this->circuitStates[$circuitBreakerName]['state_transitions'];
if (empty($transitions)) {
return null;
}
$lastTransition = end($transitions);
return $lastTransition['timestamp'];
}
/**
* Generate circuit report.
*/
protected function generateCircuitReport(string $circuitBreakerName): array
{
$circuitData = $this->getCircuitData($circuitBreakerName);
return [
'circuit' => $circuitBreakerName,
'period' => [
'start' => $circuitData['started_at'],
'end' => microtime(true)
],
'metrics' => $circuitData['calculated_metrics'],
'analysis' => $circuitData['analysis'],
'state_transitions' => $circuitData['state_transitions'],
'alerts' => $circuitData['alerts']
];
}
/**
* Generate summary report.
*/
protected function generateSummaryReport(): array
{
$summary = [
'total_circuits' => count($this->circuitStates),
'healthy_circuits' => 0,
'degraded_circuits' => 0,
'failed_circuits' => 0,
'total_requests' => 0,
'total_successes' => 0,
'total_failures' => 0,
'overall_success_rate' => 0,
'overall_average_response_time' => 0
];
foreach ($this->circuitStates as $circuitName => $circuitData) {
$metrics = $this->calculateCircuitMetrics($circuitName);
$analysis = $this->analyzer->analyze($circuitName, $circuitData);
switch ($analysis['health_status']) {
case 'healthy':
$summary['healthy_circuits']++;
break;
case 'degraded':
$summary['degraded_circuits']++;
break;
case 'failed':
$summary['failed_circuits']++;
break;
}
$summary['total_requests'] += $metrics['total_requests'];
$summary['total_successes'] += $metrics['success_count'];
$summary['total_failures'] += $metrics['failure_count'];
}
if ($summary['total_requests'] > 0) {
$summary['overall_success_rate'] = ($summary['total_successes'] / $summary['total_requests']) * 100;
}
return $summary;
}
/**
* Get total metrics count.
*/
protected function getTotalMetricsCount(): int
{
$total = 0;
foreach ($this->circuitStates as $circuitData) {
$total += count($circuitData['metrics']);
}
return $total;
}
/**
* Get total transitions count.
*/
protected function getTotalTransitionsCount(): int
{
$total = 0;
foreach ($this->circuitStates as $circuitData) {
$total += count($circuitData['state_transitions']);
}
return $total;
}
/**
* Start monitoring loop.
*/
protected function startMonitoringLoop(): void
{
// This would typically run as a background process
// For now, we'll just log that it would start
$this->logInfo("Monitoring loop started");
}
/**
* Log info message.
*/
protected function logInfo(string $message): void
{
if ($this->config['logging_enabled']) {
error_log("[CircuitMonitor] {$message}");
}
}
/**
* Get default configuration.
*/
protected function getDefaultConfig(): array
{
return [
'metrics_retention' => 86400, // 24 hours
'alerts_retention' => 604800, // 7 days
'monitoring_interval' => 60, // 1 minute
'logging_enabled' => true,
'alerts' => [
'response_time_threshold' => 5000, // 5 seconds
'error_rate_threshold' => 50, // 50%
'state_change_threshold' => 10, // 10 transitions
'enabled' => true
],
'dashboard' => [
'refresh_interval' => 30, // 30 seconds
'trend_window_size' => 24 // 24 hours
]
];
}
/**
* Get configuration.
*/
public function getConfig(): array
{
return $this->config;
}
/**
* Set configuration.
*/
public function setConfig(array $config): void
{
$this->config = array_merge($this->config, $config);
}
/**
* Create circuit monitor instance.
*/
public static function create(array $config = []): self
{
return new self($config);
}
/**
* Create for production.
*/
public static function forProduction(): self
{
return new self([
'metrics_retention' => 604800, // 7 days
'alerts_retention' => 2592000, // 30 days
'monitoring_interval' => 30, // 30 seconds
'logging_enabled' => false,
'alerts' => [
'response_time_threshold' => 2000, // 2 seconds
'error_rate_threshold' => 30, // 30%
'enabled' => true
]
]);
}
/**
* Create for development.
*/
public static function forDevelopment(): self
{
return new self([
'metrics_retention' => 3600, // 1 hour
'alerts_retention' => 86400, // 24 hours
'monitoring_interval' => 10, // 10 seconds
'logging_enabled' => true,
'alerts' => [
'response_time_threshold' => 10000, // 10 seconds
'error_rate_threshold' => 70, // 70%
'enabled' => true
]
]);
}
}

View File

@@ -0,0 +1,718 @@
<?php
declare(strict_types=1);
namespace Fendx\Service\CircuitBreaker\Recovery;
use Fendx\Service\CircuitBreaker\Recovery\Strategy\ExponentialBackoffStrategy;
use Fendx\Service\CircuitBreaker\Recovery\Strategy\LinearBackoffStrategy;
use Fendx\Service\CircuitBreaker\Recovery\Strategy\FixedIntervalStrategy;
use Fendx\Service\CircuitBreaker\Recovery\Strategy\AdaptiveStrategy;
class AutoRecovery
{
protected array $config = [];
protected array $strategies = [];
protected array $recoveryHistory = [];
protected array $activeRecoveries = [];
protected array $recoveryMetrics = [];
public function __construct(array $config = [])
{
$this->config = array_merge($this->getDefaultConfig(), $config);
$this->initializeStrategies();
}
/**
* Start auto recovery for a circuit breaker.
*/
public function startRecovery(string $circuitBreakerName, array $context = []): string
{
$recoveryId = $this->generateRecoveryId($circuitBreakerName);
$recovery = [
'id' => $recoveryId,
'circuit_breaker' => $circuitBreakerName,
'strategy' => $context['strategy'] ?? $this->config['default_strategy'],
'started_at' => microtime(true),
'attempts' => 0,
'max_attempts' => $context['max_attempts'] ?? $this->config['max_attempts'],
'context' => $context,
'status' => 'active',
'next_attempt' => null,
'last_attempt' => null,
'success_count' => 0,
'failure_count' => 0
];
$this->activeRecoveries[$recoveryId] = $recovery;
// Schedule first attempt
$this->scheduleNextAttempt($recoveryId);
$this->logInfo("Started auto recovery for {$circuitBreakerName} (ID: {$recoveryId})");
return $recoveryId;
}
/**
* Stop auto recovery.
*/
public function stopRecovery(string $recoveryId): bool
{
if (!isset($this->activeRecoveries[$recoveryId])) {
return false;
}
$recovery = $this->activeRecoveries[$recoveryId];
$recovery['status'] = 'stopped';
$recovery['stopped_at'] = microtime(true);
// Move to history
$this->recoveryHistory[] = $recovery;
unset($this->activeRecoveries[$recoveryId]);
$this->logInfo("Stopped recovery {$recoveryId} for {$recovery['circuit_breaker']}");
return true;
}
/**
* Record recovery attempt result.
*/
public function recordAttempt(string $recoveryId, bool $success, array $result = []): void
{
if (!isset($this->activeRecoveries[$recoveryId])) {
return;
}
$recovery = &$this->activeRecoveries[$recoveryId];
$recovery['attempts']++;
$recovery['last_attempt'] = microtime(true);
if ($success) {
$recovery['success_count']++;
// Check if recovery is complete
if ($this->isRecoveryComplete($recovery)) {
$this->completeRecovery($recoveryId, $result);
} else {
// Schedule next attempt
$this->scheduleNextAttempt($recoveryId);
}
} else {
$recovery['failure_count']++;
// Check if max attempts reached
if ($recovery['attempts'] >= $recovery['max_attempts']) {
$this->failRecovery($recoveryId, $result);
} else {
// Schedule next attempt with backoff
$this->scheduleNextAttempt($recoveryId);
}
}
// Record metrics
$this->recordRecoveryMetrics($recoveryId, $success, $result);
}
/**
* Get active recoveries.
*/
public function getActiveRecoveries(): array
{
return $this->activeRecoveries;
}
/**
* Get recovery history.
*/
public function getRecoveryHistory(int $limit = 100): array
{
return array_slice($this->recoveryHistory, -$limit);
}
/**
* Get recovery by ID.
*/
public function getRecovery(string $recoveryId): ?array
{
return $this->activeRecoveries[$recoveryId] ?? null;
}
/**
* Get recoveries for circuit breaker.
*/
public function getRecoveriesForCircuit(string $circuitBreakerName): array
{
$recoveries = [];
foreach ($this->activeRecoveries as $recoveryId => $recovery) {
if ($recovery['circuit_breaker'] === $circuitBreakerName) {
$recoveries[$recoveryId] = $recovery;
}
}
return $recoveries;
}
/**
* Get recovery statistics.
*/
public function getStatistics(): array
{
$stats = [
'active_recoveries' => count($this->activeRecoveries),
'total_recoveries' => count($this->recoveryHistory) + count($this->activeRecoveries),
'completed_recoveries' => 0,
'failed_recoveries' => 0,
'stopped_recoveries' => 0,
'success_rate' => 0,
'average_attempts' => 0,
'average_duration' => 0,
'strategy_performance' => []
];
$totalAttempts = 0;
$totalDuration = 0;
$completedCount = 0;
// Analyze history
foreach ($this->recoveryHistory as $recovery) {
switch ($recovery['status']) {
case 'completed':
$stats['completed_recoveries']++;
$completedCount++;
break;
case 'failed':
$stats['failed_recoveries']++;
break;
case 'stopped':
$stats['stopped_recoveries']++;
break;
}
$totalAttempts += $recovery['attempts'];
if (isset($recovery['completed_at']) || isset($recovery['failed_at'])) {
$endTime = $recovery['completed_at'] ?? $recovery['failed_at'];
$duration = $endTime - $recovery['started_at'];
$totalDuration += $duration;
}
// Strategy performance
$strategy = $recovery['strategy'];
if (!isset($stats['strategy_performance'][$strategy])) {
$stats['strategy_performance'][$strategy] = [
'total' => 0,
'completed' => 0,
'failed' => 0
];
}
$stats['strategy_performance'][$strategy]['total']++;
if ($recovery['status'] === 'completed') {
$stats['strategy_performance'][$strategy]['completed']++;
} elseif ($recovery['status'] === 'failed') {
$stats['strategy_performance'][$strategy]['failed']++;
}
}
// Calculate averages and rates
if ($stats['total_recoveries'] > 0) {
$stats['success_rate'] = ($stats['completed_recoveries'] / $stats['total_recoveries']) * 100;
}
if ($completedCount > 0) {
$stats['average_attempts'] = $totalAttempts / $completedCount;
$stats['average_duration'] = $totalDuration / $completedCount;
}
// Calculate strategy success rates
foreach ($stats['strategy_performance'] as $strategy => &$performance) {
if ($performance['total'] > 0) {
$performance['success_rate'] = ($performance['completed'] / $performance['total']) * 100;
} else {
$performance['success_rate'] = 0;
}
}
return $stats;
}
/**
* Get recovery metrics.
*/
public function getRecoveryMetrics(string $circuitBreakerName = null): array
{
if ($circuitBreakerName) {
return $this->recoveryMetrics[$circuitBreakerName] ?? [];
}
return $this->recoveryMetrics;
}
/**
* Check if recovery should be triggered.
*/
public function shouldTriggerRecovery(string $circuitBreakerName, array $context = []): bool
{
// Check if there's already an active recovery
$activeRecoveries = $this->getRecoveriesForCircuit($circuitBreakerName);
if (!empty($activeRecoveries)) {
return false;
}
// Check cooldown period
$lastRecovery = $this->getLastRecovery($circuitBreakerName);
if ($lastRecovery && isset($lastRecovery['completed_at'])) {
$timeSinceLastRecovery = microtime(true) - $lastRecovery['completed_at'];
$cooldownPeriod = $context['cooldown_period'] ?? $this->config['cooldown_period'];
if ($timeSinceLastRecovery < $cooldownPeriod) {
return false;
}
}
// Check failure conditions
return $this->checkFailureConditions($circuitBreakerName, $context);
}
/**
* Trigger automatic recovery if conditions are met.
*/
public function triggerAutoRecovery(string $circuitBreakerName, array $context = []): ?string
{
if (!$this->shouldTriggerRecovery($circuitBreakerName, $context)) {
return null;
}
// Select best strategy
$strategy = $this->selectBestStrategy($circuitBreakerName, $context);
$context['strategy'] = $strategy;
return $this->startRecovery($circuitBreakerName, $context);
}
/**
* Add custom recovery strategy.
*/
public function addStrategy(string $name, callable $strategy): void
{
$this->strategies[$name] = $strategy;
$this->logInfo("Added custom recovery strategy: {$name}");
}
/**
* Get next attempt time for recovery.
*/
public function getNextAttemptTime(string $recoveryId): ?float
{
if (!isset($this->activeRecoveries[$recoveryId])) {
return null;
}
return $this->activeRecoveries[$recoveryId]['next_attempt'];
}
/**
* Get time until next attempt.
*/
public function getTimeUntilNextAttempt(string $recoveryId): ?float
{
$nextAttempt = $this->getNextAttemptTime($recoveryId);
if ($nextAttempt === null) {
return null;
}
return max(0, $nextAttempt - microtime(true));
}
/**
* Cancel all recoveries for a circuit breaker.
*/
public function cancelAllRecoveries(string $circuitBreakerName): int
{
$cancelled = 0;
foreach ($this->activeRecoveries as $recoveryId => $recovery) {
if ($recovery['circuit_breaker'] === $circuitBreakerName) {
$this->stopRecovery($recoveryId);
$cancelled++;
}
}
$this->logInfo("Cancelled {$cancelled} recoveries for {$circuitBreakerName}");
return $cancelled;
}
/**
* Reset all recovery state.
*/
public function reset(): void
{
// Stop all active recoveries
foreach (array_keys($this->activeRecoveries) as $recoveryId) {
$this->stopRecovery($recoveryId);
}
$this->recoveryHistory = [];
$this->recoveryMetrics = [];
$this->logInfo("Auto recovery reset");
}
/**
* Export recovery data.
*/
public function exportData(): array
{
return [
'config' => $this->config,
'active_recoveries' => $this->activeRecoveries,
'recovery_history' => $this->recoveryHistory,
'recovery_metrics' => $this->recoveryMetrics,
'statistics' => $this->getStatistics(),
'exported_at' => date('Y-m-d H:i:s')
];
}
/**
* Schedule next recovery attempt.
*/
protected function scheduleNextAttempt(string $recoveryId): void
{
if (!isset($this->activeRecoveries[$recoveryId])) {
return;
}
$recovery = &$this->activeRecoveries[$recoveryId];
$strategy = $recovery['strategy'];
if (!isset($this->strategies[$strategy])) {
$this->logError("Unknown recovery strategy: {$strategy}");
return;
}
$delay = $this->strategies[$strategy]($recovery['attempts'], $recovery['context']);
$recovery['next_attempt'] = microtime(true) + $delay;
$this->logInfo("Scheduled next attempt for {$recoveryId} in {$delay} seconds");
}
/**
* Check if recovery is complete.
*/
protected function isRecoveryComplete(array $recovery): bool
{
$requiredSuccesses = $recovery['context']['required_successes'] ??
$this->config['required_successes'];
return $recovery['success_count'] >= $requiredSuccesses;
}
/**
* Complete recovery successfully.
*/
protected function completeRecovery(string $recoveryId, array $result): void
{
if (!isset($this->activeRecoveries[$recoveryId])) {
return;
}
$recovery = &$this->activeRecoveries[$recoveryId];
$recovery['status'] = 'completed';
$recovery['completed_at'] = microtime(true);
$recovery['result'] = $result;
// Move to history
$this->recoveryHistory[] = $recovery;
unset($this->activeRecoveries[$recoveryId]);
$this->logInfo("Recovery {$recoveryId} completed successfully for {$recovery['circuit_breaker']}");
}
/**
* Mark recovery as failed.
*/
protected function failRecovery(string $recoveryId, array $result): void
{
if (!isset($this->activeRecoveries[$recoveryId])) {
return;
}
$recovery = &$this->activeRecoveries[$recoveryId];
$recovery['status'] = 'failed';
$recovery['failed_at'] = microtime(true);
$recovery['result'] = $result;
// Move to history
$this->recoveryHistory[] = $recovery;
unset($this->activeRecoveries[$recoveryId]);
$this->logInfo("Recovery {$recoveryId} failed for {$recovery['circuit_breaker']}");
}
/**
* Record recovery metrics.
*/
protected function recordRecoveryMetrics(string $recoveryId, bool $success, array $result): void
{
if (!isset($this->activeRecoveries[$recoveryId])) {
return;
}
$recovery = $this->activeRecoveries[$recoveryId];
$circuitBreakerName = $recovery['circuit_breaker'];
if (!isset($this->recoveryMetrics[$circuitBreakerName])) {
$this->recoveryMetrics[$circuitBreakerName] = [
'total_attempts' => 0,
'successful_attempts' => 0,
'failed_attempts' => 0,
'last_attempt' => null,
'average_attempt_duration' => 0
];
}
$metrics = &$this->recoveryMetrics[$circuitBreakerName];
$metrics['total_attempts']++;
$metrics['last_attempt'] = microtime(true);
if ($success) {
$metrics['successful_attempts']++;
} else {
$metrics['failed_attempts']++;
}
// Update average duration if provided
if (isset($result['duration'])) {
$currentAvg = $metrics['average_attempt_duration'];
$totalAttempts = $metrics['total_attempts'];
$metrics['average_attempt_duration'] =
(($currentAvg * ($totalAttempts - 1)) + $result['duration']) / $totalAttempts;
}
}
/**
* Get last recovery for circuit breaker.
*/
protected function getLastRecovery(string $circuitBreakerName): ?array
{
// Check active recoveries first
foreach ($this->activeRecoveries as $recovery) {
if ($recovery['circuit_breaker'] === $circuitBreakerName) {
return $recovery;
}
}
// Check history
for ($i = count($this->recoveryHistory) - 1; $i >= 0; $i--) {
if ($this->recoveryHistory[$i]['circuit_breaker'] === $circuitBreakerName) {
return $this->recoveryHistory[$i];
}
}
return null;
}
/**
* Check failure conditions.
*/
protected function checkFailureConditions(string $circuitBreakerName, array $context): bool
{
$metrics = $this->recoveryMetrics[$circuitBreakerName] ?? [];
// Check failure rate
$failureRateThreshold = $context['failure_rate_threshold'] ?? 80.0;
if ($metrics['total_attempts'] > 0) {
$failureRate = ($metrics['failed_attempts'] / $metrics['total_attempts']) * 100;
if ($failureRate < $failureRateThreshold) {
return false;
}
}
// Check minimum attempts
$minAttempts = $context['min_attempts'] ?? $this->config['min_attempts'];
if ($metrics['total_attempts'] < $minAttempts) {
return false;
}
return true;
}
/**
* Select best recovery strategy.
*/
protected function selectBestStrategy(string $circuitBreakerName, array $context): string
{
$metrics = $this->recoveryMetrics[$circuitBreakerName] ?? [];
// Analyze past performance
$strategyPerformance = [];
foreach ($this->recoveryHistory as $recovery) {
if ($recovery['circuit_breaker'] === $circuitBreakerName) {
$strategy = $recovery['strategy'];
if (!isset($strategyPerformance[$strategy])) {
$strategyPerformance[$strategy] = [
'total' => 0,
'successful' => 0
];
}
$strategyPerformance[$strategy]['total']++;
if ($recovery['status'] === 'completed') {
$strategyPerformance[$strategy]['successful']++;
}
}
}
// Select strategy with best success rate
$bestStrategy = $this->config['default_strategy'];
$bestSuccessRate = 0;
foreach ($strategyPerformance as $strategy => $performance) {
if ($performance['total'] > 0) {
$successRate = ($performance['successful'] / $performance['total']) * 100;
if ($successRate > $bestSuccessRate) {
$bestSuccessRate = $successRate;
$bestStrategy = $strategy;
}
}
}
return $bestStrategy;
}
/**
* Generate unique recovery ID.
*/
protected function generateRecoveryId(string $circuitBreakerName): string
{
return $circuitBreakerName . '_' . uniqid() . '_' . time();
}
/**
* Initialize built-in strategies.
*/
protected function initializeStrategies(): void
{
$this->strategies['exponential_backoff'] = new ExponentialBackoffStrategy($this->config);
$this->strategies['linear_backoff'] = new LinearBackoffStrategy($this->config);
$this->strategies['fixed_interval'] = new FixedIntervalStrategy($this->config);
$this->strategies['adaptive'] = new AdaptiveStrategy($this->config);
}
/**
* Log error message.
*/
protected function logError(string $message): void
{
if ($this->config['logging_enabled']) {
error_log("[AutoRecovery] ERROR: {$message}");
}
}
/**
* Log info message.
*/
protected function logInfo(string $message): void
{
if ($this->config['logging_enabled']) {
error_log("[AutoRecovery] {$message}");
}
}
/**
* Get default configuration.
*/
protected function getDefaultConfig(): array
{
return [
'default_strategy' => 'exponential_backoff',
'max_attempts' => 5,
'required_successes' => 3,
'cooldown_period' => 300, // 5 minutes
'min_attempts' => 3,
'logging_enabled' => true,
'strategies' => [
'exponential_backoff' => [
'base_delay' => 1.0,
'max_delay' => 300.0,
'multiplier' => 2.0
],
'linear_backoff' => [
'base_delay' => 5.0,
'max_delay' => 300.0,
'increment' => 10.0
],
'fixed_interval' => [
'delay' => 30.0
],
'adaptive' => [
'min_delay' => 1.0,
'max_delay' => 300.0,
'success_factor' => 0.8,
'failure_factor' => 1.5
]
]
];
}
/**
* Get configuration.
*/
public function getConfig(): array
{
return $this->config;
}
/**
* Set configuration.
*/
public function setConfig(array $config): void
{
$this->config = array_merge($this->config, $config);
$this->initializeStrategies();
}
/**
* Create auto recovery instance.
*/
public static function create(array $config = []): self
{
return new self($config);
}
/**
* Create for production.
*/
public static function forProduction(): self
{
return new self([
'max_attempts' => 10,
'required_successes' => 5,
'cooldown_period' => 600, // 10 minutes
'min_attempts' => 5,
'logging_enabled' => false
]);
}
/**
* Create for development.
*/
public static function forDevelopment(): self
{
return new self([
'max_attempts' => 3,
'required_successes' => 2,
'cooldown_period' => 60, // 1 minute
'min_attempts' => 2,
'logging_enabled' => true
]);
}
}

View File

@@ -0,0 +1,771 @@
<?php
declare(strict_types=1);
namespace Fendx\Service\CloudNative;
/**
* Kubernetes Operator
* 自动化部署和运维 FendxPHP 应用
*/
class KubernetesOperator
{
private array $config;
private string $namespace;
private array $resources = [];
public function __construct(array $config = [])
{
$this->config = array_merge([
'app_name' => 'fendx-php',
'namespace' => 'fendx',
'replicas' => 3,
'image' => 'fendx/php:latest',
'port' => 9000,
'resources' => [
'requests' => [
'cpu' => '100m',
'memory' => '128Mi',
],
'limits' => [
'cpu' => '500m',
'memory' => '512Mi',
],
],
'auto_scaling' => [
'enabled' => true,
'min_replicas' => 2,
'max_replicas' => 10,
'cpu_threshold' => 70,
'memory_threshold' => 80,
],
], $config);
$this->namespace = $this->config['namespace'];
}
/**
* 部署应用
*/
public function deploy(): void
{
$this->createNamespace();
$this->createConfigMaps();
$this->createSecrets();
$this->createDeployment();
$this->createService();
$this->createIngress();
if ($this->config['auto_scaling']['enabled']) {
$this->configureHPA();
}
$this->configureRollingUpdate();
$this->configureHealthChecks();
$this->createServiceMonitor();
}
/**
* 创建命名空间
*/
private function createNamespace(): void
{
$namespace = [
'apiVersion' => 'v1',
'kind' => 'Namespace',
'metadata' => [
'name' => $this->namespace,
'labels' => [
'name' => $this->namespace,
'istio-injection' => 'enabled',
],
],
];
$this->applyResource('namespace.yaml', $namespace);
}
/**
* 创建配置映射
*/
private function createConfigMaps(): void
{
$configMap = [
'apiVersion' => 'v1',
'kind' => 'ConfigMap',
'metadata' => [
'name' => 'fendx-php-config',
'namespace' => $this->namespace,
],
'data' => [
'app.php' => $this->generateAppConfig(),
'database.php' => $this->generateDatabaseConfig(),
'cache.php' => $this->generateCacheConfig(),
'logging.php' => $this->generateLoggingConfig(),
],
];
$this->applyResource('configmap.yaml', $configMap);
}
/**
* 创建密钥
*/
private function createSecrets(): void
{
$secret = [
'apiVersion' => 'v1',
'kind' => 'Secret',
'metadata' => [
'name' => 'fendx-php-secrets',
'namespace' => $this->namespace,
],
'type' => 'Opaque',
'data' => [
'database-password' => base64_encode($this->config['database']['password'] ?? 'password'),
'jwt-secret' => base64_encode($this->config['jwt']['secret'] ?? 'your-secret-key'),
'redis-password' => base64_encode($this->config['redis']['password'] ?? ''),
],
];
$this->applyResource('secret.yaml', $secret);
}
/**
* 创建部署
*/
private function createDeployment(): void
{
$deployment = [
'apiVersion' => 'apps/v1',
'kind' => 'Deployment',
'metadata' => [
'name' => $this->config['app_name'],
'namespace' => $this->namespace,
'labels' => [
'app' => $this->config['app_name'],
'version' => 'v1',
],
],
'spec' => [
'replicas' => $this->config['replicas'],
'selector' => [
'matchLabels' => [
'app' => $this->config['app_name'],
],
],
'template' => [
'metadata' => [
'labels' => [
'app' => $this->config['app_name'],
'version' => 'v1',
],
'annotations' => [
'prometheus.io/scrape' => 'true',
'prometheus.io/port' => '9100',
'prometheus.io/path' => '/metrics',
],
],
'spec' => [
'containers' => [
[
'name' => $this->config['app_name'],
'image' => $this->config['image'],
'ports' => [
[
'containerPort' => $this->config['port'],
'protocol' => 'TCP',
],
[
'containerPort' => 9100,
'protocol' => 'TCP',
'name' => 'metrics',
],
],
'env' => $this->generateEnvironmentVariables(),
'resources' => $this->config['resources'],
'volumeMounts' => [
[
'name' => 'config-volume',
'mountPath' => '/app/config',
'readOnly' => true,
],
[
'name' => 'cache-volume',
'mountPath' => '/app/runtime/cache',
],
[
'name' => 'logs-volume',
'mountPath' => '/app/runtime/logs',
],
],
'livenessProbe' => [
'httpGet' => [
'path' => '/health',
'port' => $this->config['port'],
],
'initialDelaySeconds' => 30,
'periodSeconds' => 10,
'timeoutSeconds' => 5,
'failureThreshold' => 3,
],
'readinessProbe' => [
'httpGet' => [
'path' => '/ready',
'port' => $this->config['port'],
],
'initialDelaySeconds' => 5,
'periodSeconds' => 5,
'timeoutSeconds' => 3,
'failureThreshold' => 3,
],
'startupProbe' => [
'httpGet' => [
'path' => '/startup',
'port' => $this->config['port'],
],
'initialDelaySeconds' => 10,
'periodSeconds' => 10,
'timeoutSeconds' => 5,
'failureThreshold' => 30,
],
],
],
'volumes' => [
[
'name' => 'config-volume',
'configMap' => [
'name' => 'fendx-php-config',
],
],
[
'name' => 'cache-volume',
'emptyDir' => [
'sizeLimit' => '1Gi',
],
],
[
'name' => 'logs-volume',
'emptyDir' => [
'sizeLimit' => '500Mi',
],
],
],
'affinity' => [
'podAntiAffinity' => [
'preferredDuringSchedulingIgnoredDuringExecution' => [
[
'weight' => 100,
'podAffinityTerm' => [
'labelSelector' => [
'matchExpressions' => [
[
'key' => 'app',
'operator' => 'In',
'values' => [$this->config['app_name']],
],
],
],
'topologyKey' => 'kubernetes.io/hostname',
],
],
],
],
],
'tolerations' => [
[
'key' => 'node-role.kubernetes.io/master',
'operator' => 'Exists',
'effect' => 'NoSchedule',
],
],
],
],
'strategy' => [
'type' => 'RollingUpdate',
'rollingUpdate' => [
'maxUnavailable' => '25%',
'maxSurge' => '25%',
],
],
],
];
$this->applyResource('deployment.yaml', $deployment);
}
/**
* 创建服务
*/
private function createService(): void
{
$service = [
'apiVersion' => 'v1',
'kind' => 'Service',
'metadata' => [
'name' => $this->config['app_name'],
'namespace' => $this->namespace,
'labels' => [
'app' => $this->config['app_name'],
],
'annotations' => [
'prometheus.io/scrape' => 'true',
'prometheus.io/port' => '9100',
],
],
'spec' => [
'selector' => [
'app' => $this->config['app_name'],
],
'ports' => [
[
'name' => 'http',
'protocol' => 'TCP',
'port' => 80,
'targetPort' => $this->config['port'],
],
[
'name' => 'metrics',
'protocol' => 'TCP',
'port' => 9100,
'targetPort' => 9100,
],
],
'type' => 'ClusterIP',
],
];
$this->applyResource('service.yaml', $service);
}
/**
* 创建入口
*/
private function createIngress(): void
{
$ingress = [
'apiVersion' => 'networking.k8s.io/v1',
'kind' => 'Ingress',
'metadata' => [
'name' => $this->config['app_name'],
'namespace' => $this->namespace,
'annotations' => [
'kubernetes.io/ingress.class' => 'nginx',
'nginx.ingress.kubernetes.io/rewrite-target' => '/',
'nginx.ingress.kubernetes.io/ssl-redirect' => 'true',
'nginx.ingress.kubernetes.io/use-regex' => 'true',
'nginx.ingress.kubernetes.io/rate-limit' => '100',
'nginx.ingress.kubernetes.io/rate-limit-window' => '1m',
'cert-manager.io/cluster-issuer' => 'letsencrypt-prod',
],
],
'spec' => [
'tls' => [
[
'hosts' => ['fendx.example.com'],
'secretName' => 'fendx-tls',
],
],
'rules' => [
[
'host' => 'fendx.example.com',
'http' => [
'paths' => [
[
'path' => '/',
'pathType' => 'Prefix',
'backend' => [
'service' => [
'name' => $this->config['app_name'],
'port' => [
'number' => 80,
],
],
],
],
],
],
],
],
],
];
$this->applyResource('ingress.yaml', $ingress);
}
/**
* 配置水平自动扩缩容
*/
private function configureHPA(): void
{
$hpa = [
'apiVersion' => 'autoscaling/v2',
'kind' => 'HorizontalPodAutoscaler',
'metadata' => [
'name' => $this->config['app_name'] . '-hpa',
'namespace' => $this->namespace,
],
'spec' => [
'scaleTargetRef' => [
'apiVersion' => 'apps/v1',
'kind' => 'Deployment',
'name' => $this->config['app_name'],
],
'minReplicas' => $this->config['auto_scaling']['min_replicas'],
'maxReplicas' => $this->config['auto_scaling']['max_replicas'],
'metrics' => [
[
'type' => 'Resource',
'resource' => [
'name' => 'cpu',
'target' => [
'type' => 'Utilization',
'averageUtilization' => $this->config['auto_scaling']['cpu_threshold'],
],
],
],
[
'type' => 'Resource',
'resource' => [
'name' => 'memory',
'target' => [
'type' => 'Utilization',
'averageUtilization' => $this->config['auto_scaling']['memory_threshold'],
],
],
],
],
'behavior' => [
'scaleUp' => [
'stabilizationWindowSeconds' => 60,
'policies' => [
[
'type' => 'Percent',
'value' => 100,
'periodSeconds' => 15,
],
],
],
'scaleDown' => [
'stabilizationWindowSeconds' => 300,
'policies' => [
[
'type' => 'Percent',
'value' => 10,
'periodSeconds' => 60,
],
],
],
],
],
];
$this->applyResource('hpa.yaml', $hpa);
}
/**
* 配置滚动更新策略
*/
private function configureRollingUpdate(): void
{
// 滚动更新策略已在 Deployment 中配置
// 这里可以添加额外的配置,如暂停、恢复等
}
/**
* 配置健康检查
*/
private function configureHealthChecks(): void
{
// 健康检查已在 Deployment 中配置
// 这里可以添加额外的健康检查配置
}
/**
* 创建服务监控
*/
private function createServiceMonitor(): void
{
$serviceMonitor = [
'apiVersion' => 'monitoring.coreos.com/v1',
'kind' => 'ServiceMonitor',
'metadata' => [
'name' => $this->config['app_name'],
'namespace' => $this->namespace,
'labels' => [
'app' => $this->config['app_name'],
],
],
'spec' => [
'selector' => [
'matchLabels' => [
'app' => $this->config['app_name'],
],
],
'endpoints' => [
[
'port' => 'metrics',
'interval' => '30s',
'path' => '/metrics',
],
],
],
];
$this->applyResource('servicemonitor.yaml', $serviceMonitor);
}
/**
* 生成环境变量
*/
private function generateEnvironmentVariables(): array
{
return [
['name' => 'APP_ENV', 'value' => 'production'],
['name' => 'APP_DEBUG', 'value' => 'false'],
['name' => 'DB_HOST', 'value' => 'mysql-service'],
['name' => 'DB_PORT', 'value' => '3306'],
['name' => 'DB_DATABASE', 'value' => 'fendx_php'],
['name' => 'DB_USERNAME', 'value' => 'fendx'],
['name' => 'DB_PASSWORD', 'valueFrom' => ['secretKeyRef' => ['name' => 'fendx-php-secrets', 'key' => 'database-password']]],
['name' => 'REDIS_HOST', 'value' => 'redis-service'],
['name' => 'REDIS_PORT', 'value' => '6379'],
['name' => 'REDIS_PASSWORD', 'valueFrom' => ['secretKeyRef' => ['name' => 'fendx-php-secrets', 'key' => 'redis-password']]],
['name' => 'JWT_SECRET', 'valueFrom' => ['secretKeyRef' => ['name' => 'fendx-php-secrets', 'key' => 'jwt-secret']]],
['name' => 'LOG_LEVEL', 'value' => 'info'],
['name' => 'TRACE_ID_HEADER', 'value' => 'X-Trace-Id'],
];
}
/**
* 生成应用配置
*/
private function generateAppConfig(): string
{
return "<?php\nreturn [\n";
$config = [
'name' => 'FendxPHP',
'env' => 'production',
'debug' => false,
'url' => 'https://fendx.example.com',
'timezone' => 'UTC',
];
foreach ($config as $key => $value) {
if (is_string($value)) {
$configStr .= " '{$key}' => '{$value}',\n";
} else {
$configStr .= " '{$key}' => " . var_export($value, true) . ",\n";
}
}
return $configStr . "];\n";
}
/**
* 生成数据库配置
*/
private function generateDatabaseConfig(): string
{
return "<?php\nreturn [\n";
$config = [
'default' => 'mysql',
'connections' => [
'mysql' => [
'driver' => 'mysql',
'host' => '${DB_HOST}',
'port' => '${DB_PORT}',
'database' => '${DB_DATABASE}',
'username' => '${DB_USERNAME}',
'password' => '${DB_PASSWORD}',
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'prefix' => '',
'strict' => true,
'engine' => null,
],
],
];
return $this->arrayToPhp($config) . "];\n";
}
/**
* 生成缓存配置
*/
private function generateCacheConfig(): string
{
return "<?php\nreturn [\n";
$config = [
'default' => 'redis',
'stores' => [
'redis' => [
'driver' => 'redis',
'connection' => 'cache',
],
],
'redis' => [
'client' => 'phpredis',
'options' => [
'cluster' => 'redis',
'prefix' => 'fendx_cache:',
],
'default' => [
'url' => '${REDIS_HOST}:${REDIS_PORT}',
'password' => '${REDIS_PASSWORD}',
'database' => 0,
],
],
];
return $this->arrayToPhp($config) . "];\n";
}
/**
* 生成日志配置
*/
private function generateLoggingConfig(): string
{
return "<?php\nreturn [\n";
$config = [
'default' => 'stack',
'channels' => [
'stack' => [
'driver' => 'stack',
'channels' => ['single', 'daily'],
],
'single' => [
'driver' => 'single',
'path' => '/app/runtime/logs/app.log',
'level' => '${LOG_LEVEL}',
],
'daily' => [
'driver' => 'daily',
'path' => '/app/runtime/logs/app.log',
'level' => '${LOG_LEVEL}',
'days' => 14,
],
],
];
return $this->arrayToPhp($config) . "];\n";
}
/**
* 数组转 PHP 代码
*/
private function arrayToPhp(array $array, int $indent = 1): string
{
$result = '';
$spaces = str_repeat(' ', $indent);
foreach ($array as $key => $value) {
if (is_array($value)) {
$result .= "{$spaces}'{$key}' => [\n";
$result .= $this->arrayToPhp($value, $indent + 1);
$result .= "{$spaces}],\n";
} elseif (is_string($value)) {
$result .= "{$spaces}'{$key}' => '{$value}',\n";
} else {
$result .= "{$spaces}'{$key}' => " . var_export($value, true) . ",\n";
}
}
return $result;
}
/**
* 应用资源
*/
private function applyResource(string $filename, array $resource): void
{
$yaml = $this->arrayToYaml($resource);
$path = runtime_path("k8s/{$this->namespace}/{$filename}");
$this->ensureDirectory(dirname($path));
file_put_contents($path, $yaml);
$this->resources[] = $path;
}
/**
* 数组转 YAML
*/
private function arrayToYaml(array $array): string
{
// 简化的 YAML 转换,实际项目中建议使用 symfony/yaml
return json_encode($array, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
}
/**
* 确保目录存在
*/
private function ensureDirectory(string $dir): void
{
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
}
/**
* 部署到 Kubernetes
*/
public function deployToKubernetes(): bool
{
foreach ($this->resources as $resource) {
$output = shell_exec("kubectl apply -f {$resource} 2>&1");
if (strpos($output, 'error') !== false) {
return false;
}
}
return true;
}
/**
* 获取部署状态
*/
public function getDeploymentStatus(): array
{
$output = shell_exec("kubectl get deployment {$this->config['app_name']} -n {$this->namespace} -o json 2>&1");
$deployment = json_decode($output, true);
if (!$deployment) {
return ['status' => 'not_found'];
}
return [
'status' => 'found',
'replicas' => $deployment['spec']['replicas'] ?? 0,
'ready_replicas' => $deployment['status']['readyReplicas'] ?? 0,
'available_replicas' => $deployment['status']['availableReplicas'] ?? 0,
'updated_replicas' => $deployment['status']['updatedReplicas'] ?? 0,
'conditions' => $deployment['status']['conditions'] ?? [],
];
}
/**
* 扩缩容
*/
public function scale(int $replicas): bool
{
$output = shell_exec("kubectl scale deployment {$this->config['app_name']} --replicas={$replicas} -n {$this->namespace} 2>&1");
return strpos($output, 'error') === false;
}
/**
* 滚动更新
*/
public function rollingUpdate(string $image): bool
{
$output = shell_exec("kubectl set image deployment/{$this->config['app_name']} {$this->config['app_name']}={$image} -n {$this->namespace} 2>&1");
return strpos($output, 'error') === false;
}
/**
* 获取日志
*/
public function getLogs(int $lines = 100): string
{
return shell_exec("kubectl logs deployment/{$this->config['app_name']} -n {$this->namespace} --tail={$lines} 2>&1");
}
}

View File

@@ -0,0 +1,762 @@
<?php
declare(strict_types=1);
namespace Fendx\Service\Config;
use Fendx\Service\Config\Provider\ConfigProvider;
use Fendx\Service\Config\Provider\ConsulProvider;
use Fendx\Service\Config\Provider\EtcdProvider;
use Fendx\Service\Config\Provider\RedisProvider;
use Fendx\Service\Config\Storage\ConfigStorage;
use Fendx\Service\Config\Watcher\ConfigWatcher;
use Fendx\Service\Config\Encryption\ConfigEncryption;
class ConfigCenter
{
protected ConfigProvider $provider;
protected ConfigStorage $storage;
protected ConfigWatcher $watcher;
protected ConfigEncryption $encryption;
protected array $config = [];
protected array $cache = [];
protected array $watchers = [];
protected array $changeHistory = [];
protected bool $initialized = false;
public function __construct(array $config = [])
{
$this->config = array_merge($this->getDefaultConfig(), $config);
$this->encryption = new ConfigEncryption($this->config['encryption'] ?? []);
$this->storage = new ConfigStorage($this->config['storage'] ?? []);
$this->initialize();
}
/**
* Get configuration value.
*/
public function get(string $key, $default = null, string $namespace = 'default')
{
if (!$this->initialized) {
$this->initialize();
}
// Check cache first
$cacheKey = $this->getCacheKey($key, $namespace);
if (isset($this->cache[$cacheKey])) {
return $this->cache[$cacheKey]['value'];
}
// Get from storage
$value = $this->storage->get($key, $namespace);
if ($value === null) {
// Try to get from remote provider
$value = $this->provider->get($key, $namespace);
if ($value !== null) {
// Cache and store locally
$this->cache[$cacheKey] = [
'value' => $value,
'timestamp' => microtime(true),
'source' => 'remote'
];
$this->storage->set($key, $value, $namespace);
}
} else {
// Update cache
$this->cache[$cacheKey] = [
'value' => $value,
'timestamp' => microtime(true),
'source' => 'local'
];
}
return $value ?? $default;
}
/**
* Set configuration value.
*/
public function set(string $key, $value, string $namespace = 'default'): bool
{
if (!$this->initialized) {
$this->initialize();
}
// Encrypt if needed
if ($this->shouldEncrypt($key, $namespace)) {
$value = $this->encryption->encrypt($value);
}
// Set to remote provider
$success = $this->provider->set($key, $value, $namespace);
if ($success) {
// Update local storage and cache
$this->storage->set($key, $value, $namespace);
$cacheKey = $this->getCacheKey($key, $namespace);
$this->cache[$cacheKey] = [
'value' => $value,
'timestamp' => microtime(true),
'source' => 'local'
];
// Record change
$this->recordChange($key, $value, $namespace, 'set');
// Notify watchers
$this->notifyWatchers($key, $value, $namespace);
}
return $success;
}
/**
* Delete configuration value.
*/
public function delete(string $key, string $namespace = 'default'): bool
{
if (!$this->initialized) {
$this->initialize();
}
// Delete from remote provider
$success = $this->provider->delete($key, $namespace);
if ($success) {
// Remove from local storage and cache
$this->storage->delete($key, $namespace);
$cacheKey = $this->getCacheKey($key, $namespace);
unset($this->cache[$cacheKey]);
// Record change
$this->recordChange($key, null, $namespace, 'delete');
// Notify watchers
$this->notifyWatchers($key, null, $namespace);
}
return $success;
}
/**
* Get all configuration in namespace.
*/
public function getAll(string $namespace = 'default'): array
{
if (!$this->initialized) {
$this->initialize();
}
// Get from cache first
$namespaceCache = [];
$cachePrefix = $namespace . ':';
foreach ($this->cache as $cacheKey => $data) {
if (strpos($cacheKey, $cachePrefix) === 0) {
$key = substr($cacheKey, strlen($cachePrefix));
$namespaceCache[$key] = $data['value'];
}
}
// Get from storage
$storageData = $this->storage->getAll($namespace);
$namespaceCache = array_merge($namespaceCache, $storageData);
// Get from remote provider if needed
$remoteData = $this->provider->getAll($namespace);
$namespaceCache = array_merge($namespaceCache, $remoteData);
// Decrypt values if needed
foreach ($namespaceCache as $key => $value) {
if ($this->shouldEncrypt($key, $namespace)) {
try {
$namespaceCache[$key] = $this->encryption->decrypt($value);
} catch (\Exception $e) {
// Keep original value if decryption fails
}
}
}
return $namespaceCache;
}
/**
* Set multiple configuration values.
*/
public function setMultiple(array $configs, string $namespace = 'default'): array
{
$results = [];
foreach ($configs as $key => $value) {
$results[$key] = $this->set($key, $value, $namespace);
}
return $results;
}
/**
* Watch for configuration changes.
*/
public function watch(string $key, callable $callback, string $namespace = 'default'): string
{
$watchId = uniqid('watch_');
$this->watchers[$watchId] = [
'key' => $key,
'namespace' => $namespace,
'callback' => $callback,
'created_at' => microtime(true)
];
// Start watching if not already started
$this->watcher->watch($key, $namespace, function($newKey, $newValue, $changeNamespace) use ($watchId) {
$this->handleWatchChange($watchId, $newKey, $newValue, $changeNamespace);
});
return $watchId;
}
/**
* Stop watching.
*/
public function stopWatching(string $watchId): bool
{
if (!isset($this->watchers[$watchId])) {
return false;
}
$watcher = $this->watchers[$watchId];
$this->watcher->unwatch($watcher['key'], $watcher['namespace']);
unset($this->watchers[$watchId]);
return true;
}
/**
* Get all active watchers.
*/
public function getWatchers(): array
{
return $this->watchers;
}
/**
* Refresh configuration from remote.
*/
public function refresh(string $key = null, string $namespace = 'default'): void
{
if ($key) {
// Refresh specific key
$value = $this->provider->get($key, $namespace);
if ($value !== null) {
$this->storage->set($key, $value, $namespace);
$cacheKey = $this->getCacheKey($key, $namespace);
$this->cache[$cacheKey] = [
'value' => $value,
'timestamp' => microtime(true),
'source' => 'refresh'
];
$this->notifyWatchers($key, $value, $namespace);
}
} else {
// Refresh all
$remoteData = $this->provider->getAll($namespace);
foreach ($remoteData as $remoteKey => $value) {
$this->storage->set($remoteKey, $value, $namespace);
$cacheKey = $this->getCacheKey($remoteKey, $namespace);
$this->cache[$cacheKey] = [
'value' => $value,
'timestamp' => microtime(true),
'source' => 'refresh'
];
$this->notifyWatchers($remoteKey, $value, $namespace);
}
}
}
/**
* Get configuration statistics.
*/
public function getStatistics(): array
{
return [
'initialized' => $this->initialized,
'provider' => get_class($this->provider),
'cache_size' => count($this->cache),
'watchers_count' => count($this->watchers),
'change_history_count' => count($this->changeHistory),
'storage_stats' => $this->storage->getStatistics(),
'provider_stats' => $this->provider->getStatistics()
];
}
/**
* Get change history.
*/
public function getChangeHistory(int $limit = 100): array
{
return array_slice($this->changeHistory, -$limit);
}
/**
* Export configuration.
*/
public function export(string $namespace = 'default', bool $includeEncrypted = false): array
{
$configs = $this->getAll($namespace);
if (!$includeEncrypted) {
// Filter out encrypted values
$configs = array_filter($configs, function($value, $key) use ($namespace) {
return !$this->shouldEncrypt($key, $namespace);
}, ARRAY_FILTER_USE_BOTH);
}
return [
'namespace' => $namespace,
'configs' => $configs,
'exported_at' => date('Y-m-d H:i:s'),
'version' => $this->config['version'] ?? '1.0'
];
}
/**
* Import configuration.
*/
public function import(array $data, string $namespace = 'default'): array
{
if (!isset($data['configs'])) {
throw new \InvalidArgumentException('Invalid import data format');
}
$results = [];
foreach ($data['configs'] as $key => $value) {
$results[$key] = $this->set($key, $value, $namespace);
}
return $results;
}
/**
* Backup configuration.
*/
public function backup(string $path, string $namespace = 'default'): bool
{
$data = $this->export($namespace, true);
$json = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
$result = file_put_contents($path, $json);
if ($result !== false) {
$this->logInfo("Configuration backed up to: {$path}");
return true;
}
return false;
}
/**
* Restore configuration from backup.
*/
public function restore(string $path, string $namespace = 'default'): bool
{
if (!file_exists($path)) {
return false;
}
$json = file_get_contents($path);
if ($json === false) {
return false;
}
try {
$data = json_decode($json, true);
$this->import($data, $namespace);
$this->logInfo("Configuration restored from: {$path}");
return true;
} catch (\Exception $e) {
$this->logError("Failed to restore configuration: " . $e->getMessage());
return false;
}
}
/**
* Clear cache.
*/
public function clearCache(string $key = null, string $namespace = 'default'): void
{
if ($key) {
$cacheKey = $this->getCacheKey($key, $namespace);
unset($this->cache[$cacheKey]);
} else {
// Clear all cache for namespace
$cachePrefix = $namespace . ':';
foreach ($this->cache as $cacheKey => $data) {
if (strpos($cacheKey, $cachePrefix) === 0) {
unset($this->cache[$cacheKey]);
}
}
}
$this->logInfo("Cache cleared" . ($key ? " for key: {$key}" : " for namespace: {$namespace}"));
}
/**
* Check if key exists.
*/
public function has(string $key, string $namespace = 'default'): bool
{
return $this->get($key, null, $namespace) !== null;
}
/**
* Get configuration with environment variable fallback.
*/
public function getWithEnvFallback(string $key, $default = null, string $envVar = null, string $namespace = 'default')
{
$value = $this->get($key, null, $namespace);
if ($value !== null) {
return $value;
}
$envVar = $envVar ?: strtoupper(str_replace('.', '_', $key));
$envValue = getenv($envVar);
return $envValue !== false ? $envValue : $default;
}
/**
* Switch configuration provider.
*/
public function switchProvider(string $providerType, array $providerConfig = []): void
{
switch ($providerType) {
case 'consul':
$this->provider = new ConsulProvider($providerConfig);
break;
case 'etcd':
$this->provider = new EtcdProvider($providerConfig);
break;
case 'redis':
$this->provider = new RedisProvider($providerConfig);
break;
default:
throw new \InvalidArgumentException("Unsupported provider type: {$providerType}");
}
$this->logInfo("Switched to provider: {$providerType}");
// Reinitialize watcher
$this->watcher = new ConfigWatcher($this->provider, $this->config['watcher'] ?? []);
}
/**
* Get current provider.
*/
public function getProvider(): ConfigProvider
{
return $this->provider;
}
/**
* Initialize config center.
*/
protected function initialize(): void
{
if ($this->initialized) {
return;
}
// Initialize provider
$providerType = $this->config['provider']['type'] ?? 'consul';
$providerConfig = $this->config['provider']['config'] ?? [];
switch ($providerType) {
case 'consul':
$this->provider = new ConsulProvider($providerConfig);
break;
case 'etcd':
$this->provider = new EtcdProvider($providerConfig);
break;
case 'redis':
$this->provider = new RedisProvider($providerConfig);
break;
default:
throw new \InvalidArgumentException("Unsupported provider type: {$providerType}");
}
// Initialize watcher
$this->watcher = new ConfigWatcher($this->provider, $this->config['watcher'] ?? []);
// Load initial configuration
$this->loadInitialConfig();
$this->initialized = true;
$this->logInfo("Config center initialized with provider: {$providerType}");
}
/**
* Load initial configuration.
*/
protected function loadInitialConfig(): void
{
$namespaces = $this->config['namespaces'] ?? ['default'];
foreach ($namespaces as $namespace) {
$configs = $this->provider->getAll($namespace);
foreach ($configs as $key => $value) {
$this->storage->set($key, $value, $namespace);
$cacheKey = $this->getCacheKey($key, $namespace);
$this->cache[$cacheKey] = [
'value' => $value,
'timestamp' => microtime(true),
'source' => 'initial'
];
}
}
}
/**
* Handle watcher change.
*/
protected function handleWatchChange(string $watchId, string $key, $value, string $namespace): void
{
if (!isset($this->watchers[$watchId])) {
return;
}
$watcher = $this->watchers[$watchId];
// Update cache and storage
if ($value !== null) {
$this->storage->set($key, $value, $namespace);
$cacheKey = $this->getCacheKey($key, $namespace);
$this->cache[$cacheKey] = [
'value' => $value,
'timestamp' => microtime(true),
'source' => 'watcher'
];
} else {
$this->storage->delete($key, $namespace);
$cacheKey = $this->getCacheKey($key, $namespace);
unset($this->cache[$cacheKey]);
}
// Record change
$this->recordChange($key, $value, $namespace, 'watcher');
// Call callback
try {
call_user_func($watcher['callback'], $key, $value, $namespace);
} catch (\Exception $e) {
$this->logError("Watcher callback error: " . $e->getMessage());
}
}
/**
* Notify all watchers for a key.
*/
protected function notifyWatchers(string $key, $value, string $namespace): void
{
foreach ($this->watchers as $watchId => $watcher) {
if ($watcher['key'] === $key && $watcher['namespace'] === $namespace) {
try {
call_user_func($watcher['callback'], $key, $value, $namespace);
} catch (\Exception $e) {
$this->logError("Watcher callback error: " . $e->getMessage());
}
}
}
}
/**
* Record configuration change.
*/
protected function recordChange(string $key, $value, string $namespace, string $source): void
{
$this->changeHistory[] = [
'key' => $key,
'namespace' => $namespace,
'value' => $value,
'source' => $source,
'timestamp' => microtime(true)
];
// Limit history size
$maxHistory = $this->config['max_change_history'] ?? 1000;
if (count($this->changeHistory) > $maxHistory) {
$this->changeHistory = array_slice($this->changeHistory, -$maxHistory);
}
}
/**
* Check if key should be encrypted.
*/
protected function shouldEncrypt(string $key, string $namespace): bool
{
$encryptPatterns = $this->config['encryption']['patterns'] ?? [];
foreach ($encryptPatterns as $pattern) {
if (fnmatch($pattern, $key) || fnmatch($pattern, "{$namespace}.{$key}")) {
return true;
}
}
return false;
}
/**
* Generate cache key.
*/
protected function getCacheKey(string $key, string $namespace): string
{
return $namespace . ':' . $key;
}
/**
* Log info message.
*/
protected function logInfo(string $message): void
{
if ($this->config['logging_enabled']) {
error_log("[ConfigCenter] {$message}");
}
}
/**
* Log error message.
*/
protected function logError(string $message): void
{
if ($this->config['logging_enabled']) {
error_log("[ConfigCenter] ERROR: {$message}");
}
}
/**
* Get default configuration.
*/
protected function getDefaultConfig(): array
{
return [
'provider' => [
'type' => 'consul',
'config' => [
'host' => 'localhost',
'port' => 8500
]
],
'storage' => [
'type' => 'file',
'path' => __DIR__ . '/../../../storage/config'
],
'encryption' => [
'enabled' => false,
'key' => null,
'patterns' => [
'*.password',
'*.secret',
'*.key',
'*.token'
]
],
'watcher' => [
'enabled' => true,
'poll_interval' => 30
],
'namespaces' => ['default'],
'max_change_history' => 1000,
'cache_ttl' => 300,
'logging_enabled' => true,
'version' => '1.0'
];
}
/**
* Get configuration.
*/
public function getConfig(): array
{
return $this->config;
}
/**
* Set configuration.
*/
public function setConfig(array $config): void
{
$this->config = array_merge($this->config, $config);
}
/**
* Create config center instance.
*/
public static function create(array $config = []): self
{
return new self($config);
}
/**
* Create for development.
*/
public static function forDevelopment(): self
{
return new self([
'provider' => [
'type' => 'redis',
'config' => [
'host' => 'localhost',
'port' => 6379
]
],
'encryption' => [
'enabled' => false
],
'logging_enabled' => true
]);
}
/**
* Create for production.
*/
public static function forProduction(): self
{
return new self([
'provider' => [
'type' => 'consul',
'config' => [
'host' => 'consul.example.com',
'port' => 8500,
'token' => getenv('CONSUL_TOKEN')
]
],
'encryption' => [
'enabled' => true,
'key' => getenv('CONFIG_ENCRYPTION_KEY')
],
'logging_enabled' => false
]);
}
}

View File

@@ -0,0 +1,581 @@
<?php
declare(strict_types=1);
namespace Fendx\Service\Config\Encryption;
use Fendx\Service\Config\Encryption\Key\KeyManager;
use Fendx\Service\Config\Encryption\Cipher\CipherManager;
use Fendx\Service\Config\Encryption\Provider\EncryptionProvider;
class ConfigEncryption
{
protected KeyManager $keyManager;
protected CipherManager $cipherManager;
protected array $config = [];
protected array $encryptionCache = [];
protected array $decryptionCache = [];
public function __construct(array $config = [])
{
$this->config = array_merge($this->getDefaultConfig(), $config);
$this->keyManager = new KeyManager($this->config['key_management'] ?? []);
$this->cipherManager = new CipherManager($this->config['cipher'] ?? []);
$this->initialize();
}
/**
* Encrypt configuration value.
*/
public function encrypt($value, string $keyId = null): string
{
if ($value === null || $value === '') {
return $value;
}
// Check cache
$cacheKey = $this->getCacheKey($value, $keyId);
if (isset($this->encryptionCache[$cacheKey])) {
return $this->encryptionCache[$cacheKey];
}
try {
// Get encryption key
$keyId = $keyId ?? $this->config['default_key_id'] ?? 'default';
$encryptionKey = $this->keyManager->getKey($keyId);
if (!$encryptionKey) {
throw new \RuntimeException("Encryption key not found: {$keyId}");
}
// Serialize value if needed
$serialized = $this->serializeValue($value);
// Encrypt
$cipher = $this->config['cipher']['algorithm'] ?? 'aes-256-gcm';
$encrypted = $this->cipherManager->encrypt($serialized, $encryptionKey, $cipher);
// Add metadata
$result = [
'encrypted' => true,
'algorithm' => $cipher,
'key_id' => $keyId,
'data' => base64_encode($encrypted['data']),
'iv' => base64_encode($encrypted['iv']),
'tag' => isset($encrypted['tag']) ? base64_encode($encrypted['tag']) : null,
'timestamp' => microtime(true),
'version' => $this->config['version'] ?? '1.0'
];
$encryptedValue = json_encode($result);
// Cache result
$this->encryptionCache[$cacheKey] = $encryptedValue;
// Limit cache size
$this->limitCacheSize($this->encryptionCache);
$this->logDebug("Encrypted value with key: {$keyId}");
return $encryptedValue;
} catch (\Exception $e) {
$this->logError("Encryption failed: " . $e->getMessage());
throw new \RuntimeException("Encryption failed: " . $e->getMessage(), 0, $e);
}
}
/**
* Decrypt configuration value.
*/
public function decrypt(string $encryptedValue)
{
if ($encryptedValue === null || $encryptedValue === '') {
return $encryptedValue;
}
// Check if value is encrypted
if (!$this->isEncrypted($encryptedValue)) {
return $encryptedValue;
}
// Check cache
if (isset($this->decryptionCache[$encryptedValue])) {
return $this->decryptionCache[$encryptedValue];
}
try {
// Parse encrypted data
$data = json_decode($encryptedValue, true);
if (!$data || !isset($data['encrypted']) || !$data['encrypted']) {
throw new \InvalidArgumentException("Invalid encrypted data format");
}
// Get decryption key
$keyId = $data['key_id'] ?? 'default';
$encryptionKey = $this->keyManager->getKey($keyId);
if (!$encryptionKey) {
throw new \RuntimeException("Decryption key not found: {$keyId}");
}
// Prepare decryption data
$encrypted = [
'data' => base64_decode($data['data']),
'iv' => base64_decode($data['iv']),
'tag' => isset($data['tag']) ? base64_decode($data['tag']) : null
];
// Decrypt
$cipher = $data['algorithm'] ?? 'aes-256-gcm';
$decrypted = $this->cipherManager->decrypt($encrypted, $encryptionKey, $cipher);
// Deserialize value
$value = $this->deserializeValue($decrypted);
// Cache result
$this->decryptionCache[$encryptedValue] = $value;
// Limit cache size
$this->limitCacheSize($this->decryptionCache);
$this->logDebug("Decrypted value with key: {$keyId}");
return $value;
} catch (\Exception $e) {
$this->logError("Decryption failed: " . $e->getMessage());
throw new \RuntimeException("Decryption failed: " . $e->getMessage(), 0, $e);
}
}
/**
* Check if value is encrypted.
*/
public function isEncrypted($value): bool
{
if (!is_string($value)) {
return false;
}
try {
$data = json_decode($value, true);
return $data && isset($data['encrypted']) && $data['encrypted'] === true;
} catch (\Exception $e) {
return false;
}
}
/**
* Re-encrypt value with different key.
*/
public function reEncrypt($value, string $newKeyId, string $oldKeyId = null): string
{
// Decrypt first if already encrypted
if ($this->isEncrypted($value)) {
$decrypted = $this->decrypt($value);
} else {
$decrypted = $value;
}
// Encrypt with new key
return $this->encrypt($decrypted, $newKeyId);
}
/**
* Batch encrypt values.
*/
public function encryptBatch(array $values, string $keyId = null): array
{
$results = [];
foreach ($values as $key => $value) {
try {
$results[$key] = [
'success' => true,
'value' => $this->encrypt($value, $keyId)
];
} catch (\Exception $e) {
$results[$key] = [
'success' => false,
'error' => $e->getMessage()
];
}
}
return $results;
}
/**
* Batch decrypt values.
*/
public function decryptBatch(array $encryptedValues): array
{
$results = [];
foreach ($encryptedValues as $key => $value) {
try {
$results[$key] = [
'success' => true,
'value' => $this->decrypt($value)
];
} catch (\Exception $e) {
$results[$key] = [
'success' => false,
'error' => $e->getMessage()
];
}
}
return $results;
}
/**
* Rotate encryption key.
*/
public function rotateKey(string $oldKeyId, string $newKeyId): array
{
$results = [
'rotated' => 0,
'failed' => 0,
'errors' => []
];
// This would typically scan all configurations and re-encrypt
// For now, we'll just return a placeholder result
$this->logInfo("Key rotation initiated from {$oldKeyId} to {$newKeyId}");
return $results;
}
/**
* Get encryption metadata.
*/
public function getEncryptionInfo(string $encryptedValue): ?array
{
if (!$this->isEncrypted($encryptedValue)) {
return null;
}
$data = json_decode($encryptedValue, true);
return [
'algorithm' => $data['algorithm'] ?? 'unknown',
'key_id' => $data['key_id'] ?? 'unknown',
'timestamp' => $data['timestamp'] ?? 0,
'version' => $data['version'] ?? 'unknown'
];
}
/**
* Validate encrypted value.
*/
public function validateEncryptedValue(string $encryptedValue): array
{
$result = [
'valid' => false,
'errors' => []
];
try {
if (!$this->isEncrypted($encryptedValue)) {
$result['errors'][] = 'Value is not encrypted';
return $result;
}
// Try to decrypt
$this->decrypt($encryptedValue);
$result['valid'] = true;
} catch (\Exception $e) {
$result['errors'][] = $e->getMessage();
}
return $result;
}
/**
* Get encryption statistics.
*/
public function getStatistics(): array
{
return [
'encryption_cache_size' => count($this->encryptionCache),
'decryption_cache_size' => count($this->decryptionCache),
'key_stats' => $this->keyManager->getStatistics(),
'cipher_stats' => $this->cipherManager->getStatistics(),
'supported_algorithms' => $this->cipherManager->getSupportedAlgorithms()
];
}
/**
* Clear caches.
*/
public function clearCaches(): void
{
$this->encryptionCache = [];
$this->decryptionCache = [];
$this->logInfo("Encryption caches cleared");
}
/**
* Test encryption/decryption.
*/
public function test($value, string $keyId = null): array
{
$result = [
'original' => $value,
'encrypted' => null,
'decrypted' => null,
'success' => false,
'error' => null
];
try {
// Encrypt
$encrypted = $this->encrypt($value, $keyId);
$result['encrypted'] = $encrypted;
// Decrypt
$decrypted = $this->decrypt($encrypted);
$result['decrypted'] = $decrypted;
// Verify
$result['success'] = $this->equals($value, $decrypted);
} catch (\Exception $e) {
$result['error'] = $e->getMessage();
}
return $result;
}
/**
* Add custom encryption provider.
*/
public function addProvider(string $name, EncryptionProvider $provider): void
{
$this->cipherManager->addProvider($name, $provider);
$this->logInfo("Added encryption provider: {$name}");
}
/**
* Get available algorithms.
*/
public function getAvailableAlgorithms(): array
{
return $this->cipherManager->getSupportedAlgorithms();
}
/**
* Initialize encryption system.
*/
protected function initialize(): void
{
// Initialize key manager
$this->keyManager->initialize();
// Initialize cipher manager
$this->cipherManager->initialize();
// Validate configuration
$this->validateConfiguration();
$this->logInfo("Config encryption initialized");
}
/**
* Validate configuration.
*/
protected function validateConfiguration(): void
{
// Check if default key exists
$defaultKeyId = $this->config['default_key_id'] ?? 'default';
$defaultKey = $this->keyManager->getKey($defaultKeyId);
if (!$defaultKey) {
throw new \RuntimeException("Default encryption key not found: {$defaultKeyId}");
}
// Check algorithm support
$algorithm = $this->config['cipher']['algorithm'] ?? 'aes-256-gcm';
if (!$this->cipherManager->isSupported($algorithm)) {
throw new \RuntimeException("Unsupported encryption algorithm: {$algorithm}");
}
}
/**
* Serialize value for encryption.
*/
protected function serializeValue($value): string
{
if (is_string($value)) {
return $value;
}
return serialize($value);
}
/**
* Deserialize value after decryption.
*/
protected function deserializeValue(string $value)
{
// Try to unserialize first
$unserialized = @unserialize($value);
if ($unserialized !== false || $value === serialize(false)) {
return $unserialized;
}
// Return as string if unserialization fails
return $value;
}
/**
* Generate cache key.
*/
protected function getCacheKey($value, string $keyId = null): string
{
return md5(serialize($value) . ($keyId ?? 'default'));
}
/**
* Limit cache size.
*/
protected function limitCacheSize(array &$cache): void
{
$maxSize = $this->config['cache_size'] ?? 1000;
if (count($cache) > $maxSize) {
// Remove oldest entries
$cache = array_slice($cache, -$maxSize, null, true);
}
}
/**
* Compare values for equality.
*/
protected function equals($value1, $value2): bool
{
if (is_string($value1) && is_string($value2)) {
return $value1 === $value2;
}
return serialize($value1) === serialize($value2);
}
/**
* Log debug message.
*/
protected function logDebug(string $message): void
{
if ($this->config['debug_enabled']) {
error_log("[ConfigEncryption] DEBUG: {$message}");
}
}
/**
* Log error message.
*/
protected function logError(string $message): void
{
if ($this->config['logging_enabled']) {
error_log("[ConfigEncryption] ERROR: {$message}");
}
}
/**
* Log info message.
*/
protected function logInfo(string $message): void
{
if ($this->config['logging_enabled']) {
error_log("[ConfigEncryption] {$message}");
}
}
/**
* Get default configuration.
*/
protected function getDefaultConfig(): array
{
return [
'default_key_id' => 'default',
'cache_size' => 1000,
'logging_enabled' => true,
'debug_enabled' => false,
'key_management' => [
'storage_type' => 'file',
'storage_path' => __DIR__ . '/../../../storage/encryption_keys',
'key_rotation_days' => 90
],
'cipher' => [
'algorithm' => 'aes-256-gcm',
'providers' => ['openssl']
],
'version' => '1.0'
];
}
/**
* Get configuration.
*/
public function getConfig(): array
{
return $this->config;
}
/**
* Set configuration.
*/
public function setConfig(array $config): void
{
$this->config = array_merge($this->config, $config);
}
/**
* Create config encryption instance.
*/
public static function create(array $config = []): self
{
return new self($config);
}
/**
* Create for development.
*/
public static function forDevelopment(): self
{
return new self([
'debug_enabled' => true,
'cache_size' => 100,
'key_management' => [
'storage_type' => 'file',
'key_rotation_days' => 30
]
]);
}
/**
* Create for production.
*/
public static function forProduction(): self
{
return new self([
'debug_enabled' => false,
'cache_size' => 5000,
'key_management' => [
'storage_type' => 'vault',
'key_rotation_days' => 90
],
'cipher' => [
'algorithm' => 'aes-256-gcm'
]
]);
}
}

View File

@@ -0,0 +1,589 @@
<?php
declare(strict_types=1);
namespace Fendx\Service\Config\Updater;
use Fendx\Service\Config\Updater\Strategy\ImmediateStrategy;
use Fendx\Service\Config\Updater\Strategy\BatchStrategy;
use Fendx\Service\Config\Updater\Strategy\ScheduledStrategy;
use Fendx\Service\Config\Updater\Validator\ConfigValidator;
use Fendx\Service\Config\Updater\Notifier\ChangeNotifier;
class DynamicUpdater
{
protected array $config = [];
protected array $strategies = [];
protected ConfigValidator $validator;
protected ChangeNotifier $notifier;
protected array $pendingUpdates = [];
protected array $updateHistory = [];
protected array $subscribers = [];
protected bool $isRunning = false;
public function __construct(array $config = [])
{
$this->config = array_merge($this->getDefaultConfig(), $config);
$this->validator = new ConfigValidator($this->config['validation'] ?? []);
$this->notifier = new ChangeNotifier($this->config['notification'] ?? []);
$this->initializeStrategies();
}
/**
* Register configuration update.
*/
public function registerUpdate(string $key, $newValue, array $options = []): string
{
$updateId = uniqid('update_');
$update = [
'id' => $updateId,
'key' => $key,
'new_value' => $newValue,
'old_value' => $options['old_value'] ?? null,
'namespace' => $options['namespace'] ?? 'default',
'strategy' => $options['strategy'] ?? $this->config['default_strategy'],
'priority' => $options['priority'] ?? 0,
'metadata' => $options['metadata'] ?? [],
'created_at' => microtime(true),
'status' => 'pending',
'validation_result' => null,
'applied_at' => null,
'error' => null
];
// Validate update
$validation = $this->validator->validate($key, $newValue, $options['namespace'] ?? 'default');
$update['validation_result'] = $validation;
if (!$validation['valid']) {
$update['status'] = 'failed';
$update['error'] = 'Validation failed: ' . implode(', ', $validation['errors']);
}
$this->pendingUpdates[$updateId] = $update;
// Process immediately if strategy requires
if ($update['strategy'] === 'immediate') {
$this->processUpdate($updateId);
}
$this->logInfo("Registered update {$updateId} for key: {$key}");
return $updateId;
}
/**
* Process update.
*/
public function processUpdate(string $updateId): bool
{
if (!isset($this->pendingUpdates[$updateId])) {
return false;
}
$update = &$this->pendingUpdates[$updateId];
if ($update['status'] !== 'pending') {
return false;
}
try {
// Get strategy
$strategy = $this->strategies[$update['strategy']] ?? null;
if (!$strategy) {
throw new \InvalidArgumentException("Unknown strategy: {$update['strategy']}");
}
// Execute strategy
$result = $strategy->execute($update);
if ($result['success']) {
$update['status'] = 'applied';
$update['applied_at'] = microtime(true);
// Move to history
$this->updateHistory[] = $update;
unset($this->pendingUpdates[$updateId]);
// Notify subscribers
$this->notifySubscribers($update);
// Send notifications
$this->notifier->notifyChange($update);
$this->logInfo("Update {$updateId} applied successfully");
} else {
$update['status'] = 'failed';
$update['error'] = $result['error'] ?? 'Unknown error';
$this->logError("Update {$updateId} failed: {$update['error']}");
}
return $result['success'];
} catch (\Exception $e) {
$update['status'] = 'failed';
$update['error'] = $e->getMessage();
$this->logError("Update {$updateId} exception: " . $e->getMessage());
return false;
}
}
/**
* Subscribe to configuration changes.
*/
public function subscribe(string $pattern, callable $callback, array $options = []): string
{
$subscriptionId = uniqid('sub_');
$this->subscribers[$subscriptionId] = [
'id' => $subscriptionId,
'pattern' => $pattern,
'callback' => $callback,
'options' => array_merge([
'namespace' => 'default',
'priority' => 0,
'async' => false
], $options),
'created_at' => microtime(true)
];
$this->logInfo("Added subscription {$subscriptionId} for pattern: {$pattern}");
return $subscriptionId;
}
/**
* Unsubscribe from configuration changes.
*/
public function unsubscribe(string $subscriptionId): bool
{
if (!isset($this->subscribers[$subscriptionId])) {
return false;
}
unset($this->subscribers[$subscriptionId]);
$this->logInfo("Removed subscription {$subscriptionId}");
return true;
}
/**
* Get pending updates.
*/
public function getPendingUpdates(): array
{
return $this->pendingUpdates;
}
/**
* Get update history.
*/
public function getUpdateHistory(int $limit = 100): array
{
return array_slice($this->updateHistory, -$limit);
}
/**
* Get update by ID.
*/
public function getUpdate(string $updateId): ?array
{
return $this->pendingUpdates[$updateId] ?? null;
}
/**
* Cancel pending update.
*/
public function cancelUpdate(string $updateId): bool
{
if (!isset($this->pendingUpdates[$updateId])) {
return false;
}
$update = $this->pendingUpdates[$updateId];
if ($update['status'] !== 'pending') {
return false;
}
$update['status'] = 'cancelled';
$update['cancelled_at'] = microtime(true);
// Move to history
$this->updateHistory[] = $update;
unset($this->pendingUpdates[$updateId]);
$this->logInfo("Cancelled update {$updateId}");
return true;
}
/**
* Retry failed update.
*/
public function retryUpdate(string $updateId): bool
{
// Look in history for failed update
foreach ($this->updateHistory as $update) {
if ($update['id'] === $updateId && $update['status'] === 'failed') {
// Reset to pending
$update['status'] = 'pending';
$update['error'] = null;
$update['retry_count'] = ($update['retry_count'] ?? 0) + 1;
$this->pendingUpdates[$updateId] = $update;
// Remove from history
$this->updateHistory = array_filter($this->updateHistory, function($u) use ($updateId) {
return $u['id'] !== $updateId;
});
return $this->processUpdate($updateId);
}
}
return false;
}
/**
* Process all pending updates.
*/
public function processAllPending(): array
{
$results = [];
// Sort by priority and creation time
$sortedUpdates = $this->pendingUpdates;
uasort($sortedUpdates, function($a, $b) {
if ($a['priority'] !== $b['priority']) {
return $b['priority'] <=> $a['priority'];
}
return $a['created_at'] <=> $b['created_at'];
});
foreach ($sortedUpdates as $updateId => $update) {
$results[$updateId] = $this->processUpdate($updateId);
}
return $results;
}
/**
* Get update statistics.
*/
public function getStatistics(): array
{
$stats = [
'pending_updates' => count($this->pendingUpdates),
'total_updates' => count($this->updateHistory) + count($this->pendingUpdates),
'applied_updates' => 0,
'failed_updates' => 0,
'cancelled_updates' => 0,
'subscribers_count' => count($this->subscribers),
'strategy_performance' => []
];
// Analyze history
foreach ($this->updateHistory as $update) {
switch ($update['status']) {
case 'applied':
$stats['applied_updates']++;
break;
case 'failed':
$stats['failed_updates']++;
break;
case 'cancelled':
$stats['cancelled_updates']++;
break;
}
// Strategy performance
$strategy = $update['strategy'];
if (!isset($stats['strategy_performance'][$strategy])) {
$stats['strategy_performance'][$strategy] = [
'total' => 0,
'applied' => 0,
'failed' => 0
];
}
$stats['strategy_performance'][$strategy]['total']++;
if ($update['status'] === 'applied') {
$stats['strategy_performance'][$strategy]['applied']++;
} elseif ($update['status'] === 'failed') {
$stats['strategy_performance'][$strategy]['failed']++;
}
}
// Calculate success rates
foreach ($stats['strategy_performance'] as $strategy => &$performance) {
if ($performance['total'] > 0) {
$performance['success_rate'] = ($performance['applied'] / $performance['total']) * 100;
} else {
$performance['success_rate'] = 0;
}
}
return $stats;
}
/**
* Start dynamic updater.
*/
public function start(): void
{
if ($this->isRunning) {
return;
}
$this->isRunning = true;
// Start background processing if configured
if ($this->config['background_processing']) {
$this->startBackgroundProcessing();
}
$this->logInfo("Dynamic updater started");
}
/**
* Stop dynamic updater.
*/
public function stop(): void
{
if (!$this->isRunning) {
return;
}
$this->isRunning = false;
$this->logInfo("Dynamic updater stopped");
}
/**
* Add custom update strategy.
*/
public function addStrategy(string $name, callable $strategy): void
{
$this->strategies[$name] = $strategy;
$this->logInfo("Added custom strategy: {$name}");
}
/**
* Get all strategies.
*/
public function getStrategies(): array
{
return array_keys($this->strategies);
}
/**
* Get subscribers.
*/
public function getSubscribers(): array
{
return $this->subscribers;
}
/**
* Validate configuration before update.
*/
public function validateConfig(string $key, $value, string $namespace = 'default'): array
{
return $this->validator->validate($key, $value, $namespace);
}
/**
* Export update data.
*/
public function exportData(): array
{
return [
'config' => $this->config,
'pending_updates' => $this->pendingUpdates,
'update_history' => $this->updateHistory,
'subscribers' => $this->subscribers,
'statistics' => $this->getStatistics(),
'exported_at' => date('Y-m-d H:i:s')
];
}
/**
* Notify subscribers of change.
*/
protected function notifySubscribers(array $update): void
{
foreach ($this->subscribers as $subscriptionId => $subscription) {
if ($this->matchesPattern($update['key'], $subscription['pattern']) &&
$update['namespace'] === $subscription['options']['namespace']) {
try {
if ($subscription['options']['async']) {
// Async notification
$this->notifyAsync($subscription, $update);
} else {
// Sync notification
call_user_func($subscription['callback'], $update);
}
} catch (\Exception $e) {
$this->logError("Subscriber notification error: " . $e->getMessage());
}
}
}
}
/**
* Check if key matches pattern.
*/
protected function matchesPattern(string $key, string $pattern): bool
{
// Simple pattern matching (can be extended)
if ($pattern === '*') {
return true;
}
if (strpos($pattern, '*') !== false) {
return fnmatch($pattern, $key);
}
return $key === $pattern;
}
/**
* Notify subscriber asynchronously.
*/
protected function notifyAsync(array $subscription, array $update): void
{
// This would typically use a queue or background process
// For now, we'll just log it
$this->logInfo("Async notification scheduled for subscription {$subscription['id']}");
}
/**
* Initialize built-in strategies.
*/
protected function initializeStrategies(): void
{
$this->strategies['immediate'] = new ImmediateStrategy($this->config);
$this->strategies['batch'] = new BatchStrategy($this->config);
$this->strategies['scheduled'] = new ScheduledStrategy($this->config);
}
/**
* Start background processing.
*/
protected function startBackgroundProcessing(): void
{
// This would typically run as a background process
// For now, we'll just log that it would start
$this->logInfo("Background processing started");
}
/**
* Log info message.
*/
protected function logInfo(string $message): void
{
if ($this->config['logging_enabled']) {
error_log("[DynamicUpdater] {$message}");
}
}
/**
* Log error message.
*/
protected function logError(string $message): void
{
if ($this->config['logging_enabled']) {
error_log("[DynamicUpdater] ERROR: {$message}");
}
}
/**
* Get default configuration.
*/
protected function getDefaultConfig(): array
{
return [
'default_strategy' => 'immediate',
'background_processing' => true,
'processing_interval' => 5, // seconds
'batch_size' => 10,
'max_retry_attempts' => 3,
'retry_delay' => 30, // seconds
'logging_enabled' => true,
'validation' => [
'enabled' => true,
'strict_mode' => false
],
'notification' => [
'enabled' => true,
'channels' => ['log']
]
];
}
/**
* Get configuration.
*/
public function getConfig(): array
{
return $this->config;
}
/**
* Set configuration.
*/
public function setConfig(array $config): void
{
$this->config = array_merge($this->config, $config);
}
/**
* Create dynamic updater instance.
*/
public static function create(array $config = []): self
{
return new self($config);
}
/**
* Create for development.
*/
public static function forDevelopment(): self
{
return new self([
'default_strategy' => 'immediate',
'background_processing' => false,
'validation' => [
'strict_mode' => false
],
'logging_enabled' => true
]);
}
/**
* Create for production.
*/
public static function forProduction(): self
{
return new self([
'default_strategy' => 'batch',
'background_processing' => true,
'processing_interval' => 10,
'batch_size' => 50,
'validation' => [
'strict_mode' => true
],
'logging_enabled' => false
]);
}
}

View File

@@ -0,0 +1,735 @@
<?php
declare(strict_types=1);
namespace Fendx\Service\Config\Version;
use Fendx\Service\Config\Version\Storage\VersionStorage;
use Fendx\Service\Config\Version\Comparator\VersionComparator;
use Fendx\Service\Config\Version\Merger\ConfigMerger;
use Fendx\Service\Config\Version\Rollback\RollbackManager;
class VersionManager
{
protected VersionStorage $storage;
protected VersionComparator $comparator;
protected ConfigMerger $merger;
protected RollbackManager $rollbackManager;
protected array $config = [];
protected array $versions = [];
protected array $branches = [];
protected array $tags = [];
protected string $currentBranch = 'main';
public function __construct(array $config = [])
{
$this->config = array_merge($this->getDefaultConfig(), $config);
$this->storage = new VersionStorage($this->config['storage'] ?? []);
$this->comparator = new VersionComparator($this->config['comparison'] ?? []);
$this->merger = new ConfigMerger($this->config['merger'] ?? []);
$this->rollbackManager = new RollbackManager($this->config['rollback'] ?? []);
$this->initialize();
}
/**
* Create new configuration version.
*/
public function createVersion(array $configs, string $message = '', array $metadata = []): string
{
$versionId = $this->generateVersionId();
$version = [
'id' => $versionId,
'branch' => $this->currentBranch,
'configs' => $configs,
'message' => $message,
'metadata' => array_merge([
'author' => $this->config['default_author'] ?? 'system',
'timestamp' => microtime(true),
'parent' => $this->getCurrentVersionId()
], $metadata),
'created_at' => microtime(true),
'size' => strlen(serialize($configs))
];
// Calculate version hash
$version['hash'] = $this->calculateHash($version);
// Store version
$this->storage->save($versionId, $version);
$this->versions[$versionId] = $version;
// Update branch head
$this->branches[$this->currentBranch]['head'] = $versionId;
$this->branches[$this->currentBranch]['updated_at'] = microtime(true);
$this->logInfo("Created version {$versionId} on branch {$this->currentBranch}");
return $versionId;
}
/**
* Get version by ID.
*/
public function getVersion(string $versionId): ?array
{
if (!isset($this->versions[$versionId])) {
$version = $this->storage->load($versionId);
if ($version) {
$this->versions[$versionId] = $version;
}
}
return $this->versions[$versionId] ?? null;
}
/**
* Get current version.
*/
public function getCurrentVersion(): ?array
{
$currentVersionId = $this->getCurrentVersionId();
return $currentVersionId ? $this->getVersion($currentVersionId) : null;
}
/**
* Get current version ID.
*/
public function getCurrentVersionId(): ?string
{
return $this->branches[$this->currentBranch]['head'] ?? null;
}
/**
* Get version history.
*/
public function getVersionHistory(string $branch = null, int $limit = 50): array
{
$branch = $branch ?? $this->currentBranch;
$history = [];
// Start from branch head
$versionId = $this->branches[$branch]['head'] ?? null;
while ($versionId && count($history) < $limit) {
$version = $this->getVersion($versionId);
if (!$version) {
break;
}
$history[] = $version;
$versionId = $version['metadata']['parent'] ?? null;
}
return $history;
}
/**
* Compare two versions.
*/
public function compareVersions(string $versionId1, string $versionId2): array
{
$version1 = $this->getVersion($versionId1);
$version2 = $this->getVersion($versionId2);
if (!$version1 || !$version2) {
throw new \InvalidArgumentException("One or both versions not found");
}
return $this->comparator->compare($version1['configs'], $version2['configs']);
}
/**
* Create branch.
*/
public function createBranch(string $branchName, string $fromVersionId = null): bool
{
if (isset($this->branches[$branchName])) {
return false;
}
$fromVersionId = $fromVersionId ?? $this->getCurrentVersionId();
$this->branches[$branchName] = [
'name' => $branchName,
'head' => $fromVersionId,
'created_at' => microtime(true),
'updated_at' => microtime(true),
'created_from' => $fromVersionId
];
$this->logInfo("Created branch {$branchName} from version {$fromVersionId}");
return true;
}
/**
* Switch to branch.
*/
public function switchBranch(string $branchName): bool
{
if (!isset($this->branches[$branchName])) {
return false;
}
$this->currentBranch = $branchName;
$this->logInfo("Switched to branch {$branchName}");
return true;
}
/**
* Get current branch.
*/
public function getCurrentBranch(): string
{
return $this->currentBranch;
}
/**
* Get all branches.
*/
public function getBranches(): array
{
return $this->branches;
}
/**
* Delete branch.
*/
public function deleteBranch(string $branchName): bool
{
if ($branchName === $this->currentBranch) {
return false;
}
if (!isset($this->branches[$branchName])) {
return false;
}
unset($this->branches[$branchName]);
$this->logInfo("Deleted branch {$branchName}");
return true;
}
/**
* Merge branch.
*/
public function mergeBranch(string $sourceBranch, string $targetBranch = null, array $options = []): array
{
$targetBranch = $targetBranch ?? $this->currentBranch;
if (!isset($this->branches[$sourceBranch]) || !isset($this->branches[$targetBranch])) {
throw new \InvalidArgumentException("Branch not found");
}
$sourceVersionId = $this->branches[$sourceBranch]['head'];
$targetVersionId = $this->branches[$targetBranch]['head'];
$sourceVersion = $this->getVersion($sourceVersionId);
$targetVersion = $this->getVersion($targetVersionId);
if (!$sourceVersion || !$targetVersion) {
throw new \InvalidArgumentException("Version not found");
}
// Find common ancestor
$ancestorId = $this->findCommonAncestor($sourceVersionId, $targetVersionId);
$ancestorVersion = $ancestorId ? $this->getVersion($ancestorId) : null;
// Merge configurations
$mergeResult = $this->merger->merge(
$ancestorVersion['configs'] ?? [],
$targetVersion['configs'],
$sourceVersion['configs'],
$options
);
if (!$mergeResult['success']) {
return [
'success' => false,
'conflicts' => $mergeResult['conflicts'] ?? [],
'message' => 'Merge conflicts detected'
];
}
// Create merge commit
$mergeVersionId = $this->createVersion(
$mergeResult['configs'],
"Merge branch {$sourceBranch} into {$targetBranch}",
array_merge($options['metadata'] ?? [], [
'merge' => true,
'source_branch' => $sourceBranch,
'target_branch' => $targetBranch,
'merge_author' => $this->config['default_author'] ?? 'system'
])
);
// Update target branch
$this->branches[$targetBranch]['head'] = $mergeVersionId;
$this->branches[$targetBranch]['updated_at'] = microtime(true);
return [
'success' => true,
'version_id' => $mergeVersionId,
'message' => 'Branch merged successfully'
];
}
/**
* Create tag.
*/
public function createTag(string $tagName, string $versionId, array $metadata = []): bool
{
$version = $this->getVersion($versionId);
if (!$version) {
return false;
}
if (isset($this->tags[$tagName])) {
return false;
}
$this->tags[$tagName] = [
'name' => $tagName,
'version_id' => $versionId,
'metadata' => array_merge([
'created_at' => microtime(true),
'author' => $this->config['default_author'] ?? 'system'
], $metadata)
];
$this->logInfo("Created tag {$tagName} for version {$versionId}");
return true;
}
/**
* Get tag.
*/
public function getTag(string $tagName): ?array
{
return $this->tags[$tagName] ?? null;
}
/**
* Get all tags.
*/
public function getTags(): array
{
return $this->tags;
}
/**
* Delete tag.
*/
public function deleteTag(string $tagName): bool
{
if (!isset($this->tags[$tagName])) {
return false;
}
unset($this->tags[$tagName]);
$this->logInfo("Deleted tag {$tagName}");
return true;
}
/**
* Rollback to version.
*/
public function rollback(string $versionId, array $options = []): bool
{
return $this->rollbackManager->rollback($versionId, $this, $options);
}
/**
* Get rollback history.
*/
public function getRollbackHistory(int $limit = 50): array
{
return $this->rollbackManager->getHistory($limit);
}
/**
* Get version diff.
*/
public function getDiff(string $fromVersionId, string $toVersionId): array
{
$fromVersion = $this->getVersion($fromVersionId);
$toVersion = $this->getVersion($toVersionId);
if (!$fromVersion || !$toVersion) {
throw new \InvalidArgumentException("Version not found");
}
return $this->comparator->diff($fromVersion['configs'], $toVersion['configs']);
}
/**
* Search versions.
*/
public function searchVersions(array $criteria): array
{
$results = [];
foreach ($this->versions as $versionId => $version) {
if ($this->matchesCriteria($version, $criteria)) {
$results[$versionId] = $version;
}
}
return $results;
}
/**
* Get version statistics.
*/
public function getStatistics(): array
{
$stats = [
'total_versions' => count($this->versions),
'total_branches' => count($this->branches),
'total_tags' => count($this->tags),
'current_branch' => $this->currentBranch,
'current_version' => $this->getCurrentVersionId(),
'branch_stats' => [],
'storage_stats' => $this->storage->getStatistics()
];
// Branch statistics
foreach ($this->branches as $branchName => $branch) {
$branchVersions = $this->getVersionHistory($branchName, 1000);
$stats['branch_stats'][$branchName] = [
'name' => $branchName,
'versions_count' => count($branchVersions),
'head_version' => $branch['head'],
'created_at' => $branch['created_at'],
'updated_at' => $branch['updated_at']
];
}
return $stats;
}
/**
* Export version data.
*/
public function exportData(string $format = 'json'): string
{
$data = [
'versions' => $this->versions,
'branches' => $this->branches,
'tags' => $this->tags,
'current_branch' => $this->currentBranch,
'statistics' => $this->getStatistics(),
'exported_at' => date('Y-m-d H:i:s'),
'version' => $this->config['version'] ?? '1.0'
];
switch ($format) {
case 'json':
return json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
case 'php':
return '<?php return ' . var_export($data, true) . ';';
default:
throw new \InvalidArgumentException("Unsupported export format: {$format}");
}
}
/**
* Import version data.
*/
public function importData(string $data, string $format = 'json'): void
{
switch ($format) {
case 'json':
$imported = json_decode($data, true);
break;
case 'php':
$imported = include 'data://text/plain;base64,' . base64_encode($data);
break;
default:
throw new \InvalidArgumentException("Unsupported import format: {$format}");
}
if (!$imported) {
throw new \InvalidArgumentException("Invalid import data");
}
if (isset($imported['versions'])) {
foreach ($imported['versions'] as $versionId => $version) {
$this->storage->save($versionId, $version);
$this->versions[$versionId] = $version;
}
}
if (isset($imported['branches'])) {
$this->branches = $imported['branches'];
}
if (isset($imported['tags'])) {
$this->tags = $imported['tags'];
}
if (isset($imported['current_branch'])) {
$this->currentBranch = $imported['current_branch'];
}
$this->logInfo("Version data imported successfully");
}
/**
* Cleanup old versions.
*/
public function cleanup(int $maxVersions = 100): array
{
$cleaned = [];
foreach ($this->branches as $branchName => $branch) {
$history = $this->getVersionHistory($branchName, $maxVersions + 50);
if (count($history) > $maxVersions) {
$toRemove = array_slice($history, $maxVersions);
foreach ($toRemove as $version) {
$this->storage->delete($version['id']);
unset($this->versions[$version['id']);
$cleaned[] = $version['id'];
}
}
}
$this->logInfo("Cleaned up " . count($cleaned) . " old versions");
return $cleaned;
}
/**
* Initialize version manager.
*/
protected function initialize(): void
{
// Load existing data
$this->loadData();
// Create default branch if not exists
if (!isset($this->branches['main'])) {
$this->createBranch('main');
}
$this->logInfo("Version manager initialized");
}
/**
* Load existing data.
*/
protected function loadData(): void
{
// Load branches
$this->branches = $this->storage->loadBranches();
// Load tags
$this->tags = $this->storage->loadTags();
// Load recent versions
$recentVersions = $this->storage->loadRecentVersions(1000);
foreach ($recentVersions as $versionId => $version) {
$this->versions[$versionId] = $version;
}
}
/**
* Find common ancestor of two versions.
*/
protected function findCommonAncestor(string $versionId1, string $versionId2): ?string
{
$ancestors1 = $this->getAncestors($versionId1);
$ancestors2 = $this->getAncestors($versionId2);
foreach ($ancestors1 as $ancestorId) {
if (in_array($ancestorId, $ancestors2)) {
return $ancestorId;
}
}
return null;
}
/**
* Get ancestors of a version.
*/
protected function getAncestors(string $versionId): array
{
$ancestors = [];
$currentId = $versionId;
while ($currentId) {
$version = $this->getVersion($currentId);
if (!$version) {
break;
}
$parentId = $version['metadata']['parent'] ?? null;
if ($parentId) {
$ancestors[] = $parentId;
}
$currentId = $parentId;
}
return $ancestors;
}
/**
* Check if version matches search criteria.
*/
protected function matchesCriteria(array $version, array $criteria): bool
{
foreach ($criteria as $key => $value) {
switch ($key) {
case 'branch':
if ($version['branch'] !== $value) {
return false;
}
break;
case 'author':
if (($version['metadata']['author'] ?? '') !== $value) {
return false;
}
break;
case 'message':
if (stripos($version['message'], $value) === false) {
return false;
}
break;
case 'date_from':
if ($version['created_at'] < $value) {
return false;
}
break;
case 'date_to':
if ($version['created_at'] > $value) {
return false;
}
break;
}
}
return true;
}
/**
* Generate version ID.
*/
protected function generateVersionId(): string
{
return uniqid('v_') . '_' . time();
}
/**
* Calculate version hash.
*/
protected function calculateHash(array $version): string
{
$data = serialize($version['configs']);
return hash('sha256', $data);
}
/**
* Log info message.
*/
protected function logInfo(string $message): void
{
if ($this->config['logging_enabled']) {
error_log("[VersionManager] {$message}");
}
}
/**
* Get default configuration.
*/
protected function getDefaultConfig(): array
{
return [
'default_author' => 'system',
'max_versions_per_branch' => 1000,
'auto_cleanup' => false,
'logging_enabled' => true,
'storage' => [
'type' => 'file',
'path' => __DIR__ . '/../../../storage/config_versions'
],
'comparison' => [
'ignore_whitespace' => false,
'case_sensitive' => true
],
'merger' => [
'conflict_resolution' => 'manual',
'auto_merge' => true
],
'rollback' => [
'create_backup' => true,
'max_rollback_history' => 50
],
'version' => '1.0'
];
}
/**
* Get configuration.
*/
public function getConfig(): array
{
return $this->config;
}
/**
* Set configuration.
*/
public function setConfig(array $config): void
{
$this->config = array_merge($this->config, $config);
}
/**
* Create version manager instance.
*/
public static function create(array $config = []): self
{
return new self($config);
}
/**
* Create for development.
*/
public static function forDevelopment(): self
{
return new self([
'max_versions_per_branch' => 100,
'auto_cleanup' => true,
'logging_enabled' => true
]);
}
/**
* Create for production.
*/
public static function forProduction(): self
{
return new self([
'max_versions_per_branch' => 1000,
'auto_cleanup' => false,
'logging_enabled' => false,
'storage' => [
'type' => 'database',
'connection' => 'config_versions'
]
]);
}
}

View File

@@ -0,0 +1,685 @@
<?php
declare(strict_types=1);
namespace Fendx\Service\Discovery;
use Fendx\Service\Discovery\Resolver\ServiceResolver;
use Fendx\Service\Discovery\Cache\DiscoveryCache;
use Fendx\Service\Discovery\Watcher\ServiceWatcher;
use Fendx\Service\Discovery\LoadBalancer\LoadBalancer;
class ServiceDiscovery
{
protected ServiceResolver $resolver;
protected DiscoveryCache $cache;
protected ServiceWatcher $watcher;
protected LoadBalancer $loadBalancer;
protected array $config = [];
protected array $discoveredServices = [];
protected array $serviceEndpoints = [];
protected array $watchers = [];
public function __construct(array $config = [])
{
$this->config = array_merge($this->getDefaultConfig(), $config);
$this->resolver = new ServiceResolver($this->config);
$this->cache = new DiscoveryCache($this->config);
$this->watcher = new ServiceWatcher($this->config);
$this->loadBalancer = new LoadBalancer($this->config);
$this->initialize();
}
/**
* Discover service instances.
*/
public function discover(string $serviceName, array $options = []): array
{
$cacheKey = $this->generateCacheKey($serviceName, $options);
// Check cache first
if ($this->config['cache_enabled']) {
$cached = $this->cache->get($cacheKey);
if ($cached !== null) {
$this->logDebug("Service discovered from cache: {$serviceName}");
return $cached;
}
}
// Discover from resolver
$instances = $this->resolver->resolve($serviceName, $options);
// Filter and validate instances
$validInstances = $this->filterValidInstances($instances);
// Cache results
if ($this->config['cache_enabled'] && !empty($validInstances)) {
$ttl = $options['cache_ttl'] ?? $this->config['default_cache_ttl'];
$this->cache->set($cacheKey, $validInstances, $ttl);
}
$this->discoveredServices[$serviceName] = $validInstances;
$this->logInfo("Discovered " . count($validInstances) . " instances for service: {$serviceName}");
return $validInstances;
}
/**
* Get a single service instance.
*/
public function getInstance(string $serviceName, array $options = []): ?array
{
$instances = $this->discover($serviceName, $options);
if (empty($instances)) {
return null;
}
// Use load balancer to select instance
$strategy = $options['load_balancing'] ?? $this->config['default_load_balancing'];
return $this->loadBalancer->select($instances, $strategy);
}
/**
* Get service URL.
*/
public function getServiceUrl(string $serviceName, array $options = []): ?string
{
$instance = $this->getInstance($serviceName, $options);
if (!$instance) {
return null;
}
return $this->buildServiceUrl($instance, $options);
}
/**
* Discover multiple services.
*/
public function discoverMultiple(array $serviceNames, array $options = []): array
{
$results = [];
foreach ($serviceNames as $serviceName) {
$results[$serviceName] = $this->discover($serviceName, $options);
}
return $results;
}
/**
* Watch for service changes.
*/
public function watch(string $serviceName, callable $callback, array $options = []): string
{
$watchId = $this->generateWatchId($serviceName);
$this->watchers[$watchId] = [
'service_name' => $serviceName,
'callback' => $callback,
'options' => $options,
'last_instances' => $this->discover($serviceName, $options),
'created_at' => time()
];
$this->watcher->startWatching($serviceName, $callback, $options);
$this->logInfo("Started watching service: {$serviceName} ({$watchId})");
return $watchId;
}
/**
* Stop watching service.
*/
public function stopWatching(string $watchId): bool
{
if (!isset($this->watchers[$watchId])) {
return false;
}
$watch = $this->watchers[$watchId];
$this->watcher->stopWatching($watch['service_name'], $watch['callback']);
unset($this->watchers[$watchId]);
$this->logInfo("Stopped watching service: {$watch['service_name']} ({$watchId})");
return true;
}
/**
* Get all discovered services.
*/
public function getDiscoveredServices(): array
{
return $this->discoveredServices;
}
/**
* Refresh service discovery.
*/
public function refresh(string $serviceName = null): void
{
if ($serviceName) {
// Clear cache for specific service
$this->clearServiceCache($serviceName);
// Rediscover
$this->discover($serviceName);
$this->logInfo("Refreshed service: {$serviceName}");
} else {
// Clear all cache
$this->cache->clear();
// Rediscover all services
$this->discoveredServices = [];
$this->logInfo("Refreshed all services");
}
}
/**
* Add service endpoint.
*/
public function addEndpoint(string $serviceName, array $endpoint): void
{
if (!isset($this->serviceEndpoints[$serviceName])) {
$this->serviceEndpoints[$serviceName] = [];
}
$this->serviceEndpoints[$serviceName][] = $endpoint;
// Clear cache to force rediscovery
$this->clearServiceCache($serviceName);
$this->logInfo("Added endpoint for service: {$serviceName}");
}
/**
* Remove service endpoint.
*/
public function removeEndpoint(string $serviceName, string $endpointId): bool
{
if (!isset($this->serviceEndpoints[$serviceName])) {
return false;
}
foreach ($this->serviceEndpoints[$serviceName] as $key => $endpoint) {
if ($endpoint['id'] === $endpointId) {
unset($this->serviceEndpoints[$serviceName][$key]);
$this->serviceEndpoints[$serviceName] = array_values($this->serviceEndpoints[$serviceName]);
// Clear cache to force rediscovery
$this->clearServiceCache($serviceName);
$this->logInfo("Removed endpoint from service: {$serviceName}");
return true;
}
}
return false;
}
/**
* Get service endpoints.
*/
public function getEndpoints(string $serviceName): array
{
return $this->serviceEndpoints[$serviceName] ?? [];
}
/**
* Check if service is available.
*/
public function isAvailable(string $serviceName, array $options = []): bool
{
$instances = $this->discover($serviceName, $options);
if (empty($instances)) {
return false;
}
// Check if any instance is healthy
foreach ($instances as $instance) {
if ($this->isInstanceHealthy($instance)) {
return true;
}
}
return false;
}
/**
* Get service health status.
*/
public function getHealthStatus(string $serviceName): array
{
$instances = $this->discover($serviceName);
if (empty($instances)) {
return [
'service' => $serviceName,
'status' => 'unknown',
'instances' => 0,
'healthy_instances' => 0,
'unhealthy_instances' => 0
];
}
$healthyCount = 0;
$instanceStatuses = [];
foreach ($instances as $instance) {
$isHealthy = $this->isInstanceHealthy($instance);
if ($isHealthy) {
$healthyCount++;
}
$instanceStatuses[] = [
'id' => $instance['id'],
'host' => $instance['host'],
'port' => $instance['port'],
'healthy' => $isHealthy,
'last_check' => time()
];
}
$status = $healthyCount === count($instances) ? 'healthy' :
($healthyCount > 0 ? 'degraded' : 'unhealthy');
return [
'service' => $serviceName,
'status' => $status,
'instances' => count($instances),
'healthy_instances' => $healthyCount,
'unhealthy_instances' => count($instances) - $healthyCount,
'instance_details' => $instanceStatuses
];
}
/**
* Get discovery statistics.
*/
public function getStatistics(): array
{
$totalServices = count($this->discoveredServices);
$totalInstances = 0;
$healthyInstances = 0;
$cacheStats = $this->cache->getStatistics();
foreach ($this->discoveredServices as $serviceName => $instances) {
$totalInstances += count($instances);
foreach ($instances as $instance) {
if ($this->isInstanceHealthy($instance)) {
$healthyInstances++;
}
}
}
return [
'total_services' => $totalServices,
'total_instances' => $totalInstances,
'healthy_instances' => $healthyInstances,
'unhealthy_instances' => $totalInstances - $healthyInstances,
'health_percentage' => $totalInstances > 0 ? ($healthyInstances / $totalInstances) * 100 : 0,
'active_watchers' => count($this->watchers),
'cache_stats' => $cacheStats,
'endpoints' => array_sum(array_map('count', $this->serviceEndpoints))
];
}
/**
* Set service priority.
*/
public function setPriority(string $serviceName, array $priorities): void
{
$this->loadBalancer->setPriorities($serviceName, $priorities);
// Clear cache to apply new priorities
$this->clearServiceCache($serviceName);
}
/**
* Get service priority.
*/
public function getPriority(string $serviceName): array
{
return $this->loadBalancer->getPriorities($serviceName);
}
/**
* Enable/disable service discovery.
*/
public function setEnabled(bool $enabled): void
{
$this->config['enabled'] = $enabled;
if (!$enabled) {
// Stop all watchers
foreach ($this->watchers as $watchId => $watch) {
$this->stopWatching($watchId);
}
}
$this->logInfo("Service discovery " . ($enabled ? 'enabled' : 'disabled'));
}
/**
* Check if discovery is enabled.
*/
public function isEnabled(): bool
{
return $this->config['enabled'] ?? true;
}
/**
* Clear service cache.
*/
protected function clearServiceCache(string $serviceName): void
{
if ($this->config['cache_enabled']) {
// Clear all cache keys for this service
$pattern = $this->generateCacheKey($serviceName);
$this->cache->clearPattern($pattern);
}
}
/**
* Filter valid instances.
*/
protected function filterValidInstances(array $instances): array
{
return array_filter($instances, function ($instance) {
// Check required fields
if (!isset($instance['host']) || !isset($instance['port'])) {
return false;
}
// Check if enabled
if (isset($instance['enabled']) && !$instance['enabled']) {
return false;
}
// Check health if required
if ($this->config['check_health'] && !$this->isInstanceHealthy($instance)) {
return false;
}
return true;
});
}
/**
* Check if instance is healthy.
*/
protected function isInstanceHealthy(array $instance): bool
{
// If instance has health status, use it
if (isset($instance['healthy'])) {
return $instance['healthy'];
}
// Otherwise, perform health check
return $this->performHealthCheck($instance);
}
/**
* Perform health check on instance.
*/
protected function performHealthCheck(array $instance): bool
{
$timeout = $this->config['health_check_timeout'] ?? 5;
try {
$context = stream_context_create([
'http' => [
'timeout' => $timeout,
'method' => 'GET'
]
]);
$url = $this->buildServiceUrl($instance) . '/health';
$response = @file_get_contents($url, false, $context);
if ($response === false) {
return false;
}
// Try to parse JSON response
$data = json_decode($response, true);
if ($data && isset($data['status'])) {
return $data['status'] === 'healthy' || $data['status'] === 'ok';
}
// If no JSON, consider any response as healthy
return true;
} catch (\Exception $e) {
return false;
}
}
/**
* Build service URL.
*/
protected function buildServiceUrl(array $instance, array $options = []): string
{
$protocol = $options['protocol'] ?? $instance['protocol'] ?? 'http';
$host = $instance['host'];
$port = $instance['port'];
$path = $options['path'] ?? $instance['path'] ?? '/';
$url = "{$protocol}://{$host}";
// Add port if not default
if (($protocol === 'http' && $port != 80) || ($protocol === 'https' && $port != 443)) {
$url .= ":{$port}";
}
$url .= $path;
return $url;
}
/**
* Generate cache key.
*/
protected function generateCacheKey(string $serviceName, array $options = []): string
{
$key = "service:{$serviceName}";
if (!empty($options)) {
ksort($options);
$key .= ':' . md5(serialize($options));
}
return $key;
}
/**
* Generate watch ID.
*/
protected function generateWatchId(string $serviceName): string
{
return $serviceName . '_' . uniqid();
}
/**
* Initialize discovery.
*/
protected function initialize(): void
{
// Initialize resolver
$this->resolver->initialize();
// Initialize cache
if ($this->config['cache_enabled']) {
$this->cache->initialize();
}
// Initialize watcher
$this->watcher->initialize();
// Start background tasks
if ($this->config['background_refresh']) {
$this->startBackgroundRefresh();
}
$this->logInfo("Service discovery initialized");
}
/**
* Start background refresh.
*/
protected function startBackgroundRefresh(): void
{
// This would typically be run as a background process
// For now, we'll just log that it would start
$this->logInfo("Background refresh started");
}
/**
* Log info message.
*/
protected function logInfo(string $message): void
{
if ($this->config['logging_enabled']) {
error_log("[ServiceDiscovery] {$message}");
}
}
/**
* Log debug message.
*/
protected function logDebug(string $message): void
{
if ($this->config['debug_enabled']) {
error_log("[ServiceDiscovery] DEBUG: {$message}");
}
}
/**
* Get default configuration.
*/
protected function getDefaultConfig(): array
{
return [
'enabled' => true,
'cache_enabled' => true,
'default_cache_ttl' => 60,
'check_health' => true,
'health_check_timeout' => 5,
'default_load_balancing' => 'round_robin',
'background_refresh' => true,
'refresh_interval' => 30,
'logging_enabled' => true,
'debug_enabled' => false,
'resolver' => [
'type' => 'consul',
'host' => 'localhost',
'port' => 8500
],
'cache' => [
'type' => 'redis',
'host' => 'localhost',
'port' => 6379,
'prefix' => 'discovery'
]
];
}
/**
* Get configuration.
*/
public function getConfig(): array
{
return $this->config;
}
/**
* Set configuration.
*/
public function setConfig(array $config): void
{
$this->config = array_merge($this->config, $config);
}
/**
* Create discovery instance.
*/
public static function create(array $config = []): self
{
return new self($config);
}
/**
* Create for Consul.
*/
public static function forConsul(string $host = 'localhost', int $port = 8500): self
{
return new self([
'resolver' => [
'type' => 'consul',
'host' => $host,
'port' => $port
]
]);
}
/**
* Create for Eureka.
*/
public static function forEureka(string $host = 'localhost', int $port = 8761): self
{
return new self([
'resolver' => [
'type' => 'eureka',
'host' => $host,
'port' => $port
]
]);
}
/**
* Create for Kubernetes.
*/
public static function forKubernetes(): self
{
return new self([
'resolver' => [
'type' => 'kubernetes',
'in_cluster' => true
]
]);
}
/**
* Create for development.
*/
public static function forDevelopment(): self
{
return new self([
'cache_enabled' => false,
'check_health' => false,
'background_refresh' => false,
'debug_enabled' => true
]);
}
/**
* Create for production.
*/
public static function forProduction(): self
{
return new self([
'cache_enabled' => true,
'default_cache_ttl' => 300,
'check_health' => true,
'health_check_timeout' => 3,
'background_refresh' => true,
'refresh_interval' => 60,
'logging_enabled' => false
]);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,771 @@
<?php
declare(strict_types=1);
namespace Fendx\Service\Documentation;
use Fendx\Service\Documentation\Comment\PhpCommentParser;
use Fendx\Service\Documentation\Comment\CommentAnalyzer;
use Fendx\Service\Documentation\Comment\CoverageCalculator;
use Fendx\Service\Documentation\Comment\QualityAssessor;
use Fendx\Service\Documentation\Reporter\CommentReporter;
class CommentCoverageChecker
{
protected array $config = [];
protected PhpCommentParser $commentParser;
protected CommentAnalyzer $commentAnalyzer;
protected CoverageCalculator $coverageCalculator;
protected QualityAssessor $qualityAssessor;
protected CommentReporter $reporter;
protected array $coverageResults = [];
protected array $fileMetrics = [];
public function __construct(array $config = [])
{
$this->config = array_merge($this->getDefaultConfig(), $config);
$this->commentParser = new PhpCommentParser($this->config['comment_parser'] ?? []);
$this->commentAnalyzer = new CommentAnalyzer($this->config['comment_analyzer'] ?? []);
$this->coverageCalculator = new CoverageCalculator($this->config['coverage_calculator'] ?? []);
$this->qualityAssessor = new QualityAssessor($this->config['quality_assessor'] ?? []);
$this->reporter = new CommentReporter($this->config['reporter'] ?? []);
}
/**
* Check comment coverage for a project.
*/
public function checkCommentCoverage(string $projectPath, array $options = []): array
{
$checkConfig = array_merge($this->config['coverage_check'] ?? [], $options);
$result = [
'check_type' => 'comment_coverage',
'project_path' => $projectPath,
'files_analyzed' => 0,
'total_classes' => 0,
'total_methods' => 0,
'total_properties' => 0,
'commented_classes' => 0,
'commented_methods' => 0,
'commented_properties' => 0,
'class_coverage' => 0,
'method_coverage' => 0,
'property_coverage' => 0,
'overall_coverage' => 0,
'quality_score' => 0,
'uncommented_items' => [],
'quality_issues' => [],
'recommendations' => [],
'check_duration' => 0,
'timestamp' => microtime(true)
];
$startTime = microtime(true);
// Find PHP files
$files = $this->findPhpFiles($projectPath, $checkConfig);
$result['files_analyzed'] = count($files);
$totalMetrics = [
'classes' => ['total' => 0, 'commented' => 0],
'methods' => ['total' => 0, 'commented' => 0],
'properties' => ['total' => 0, 'commented' => 0]
];
$allUncommented = [];
$allQualityIssues = [];
$fileMetrics = [];
foreach ($files as $file) {
$fileResult = $this->analyzeFileComments($file, $checkConfig);
// Aggregate metrics
foreach (['classes', 'methods', 'properties'] as $type) {
$totalMetrics[$type]['total'] += $fileResult[$type]['total'];
$totalMetrics[$type]['commented'] += $fileResult[$type]['commented'];
}
if (!empty($fileResult['uncommented'])) {
$allUncommented = array_merge($allUncommented, $fileResult['uncommented']);
}
if (!empty($fileResult['quality_issues'])) {
$allQualityIssues = array_merge($allQualityIssues, $fileResult['quality_issues']);
}
$fileMetrics[$file] = $fileResult;
}
$result['total_classes'] = $totalMetrics['classes']['total'];
$result['total_methods'] = $totalMetrics['methods']['total'];
$result['total_properties'] = $totalMetrics['properties']['total'];
$result['commented_classes'] = $totalMetrics['classes']['commented'];
$result['commented_methods'] = $totalMetrics['methods']['commented'];
$result['commented_properties'] = $totalMetrics['properties']['commented'];
// Calculate coverage percentages
$result['class_coverage'] = $this->calculateCoverage($totalMetrics['classes']);
$result['method_coverage'] = $this->calculateCoverage($totalMetrics['methods']);
$result['property_coverage'] = $this->calculateCoverage($totalMetrics['properties']);
$result['overall_coverage'] = round(($result['class_coverage'] + $result['method_coverage'] + $result['property_coverage']) / 3, 2);
$result['uncommented_items'] = $allUncommented;
$result['quality_issues'] = $allQualityIssues;
$result['quality_score'] = $this->calculateQualityScore($result);
$result['recommendations'] = $this->generateCoverageRecommendations($result);
$result['check_duration'] = microtime(true) - $startTime;
$this->fileMetrics = $fileMetrics;
// Store result
$this->coverageResults[] = $result;
return $result;
}
/**
* Check comment quality for specific files.
*/
public function checkCommentQuality(string $projectPath, array $options = []): array
{
$checkConfig = array_merge($this->config['quality_check'] ?? [], $options);
$result = [
'check_type' => 'comment_quality',
'project_path' => $projectPath,
'files_analyzed' => 0,
'quality_score' => 0,
'quality_distribution' => [
'excellent' => 0,
'good' => 0,
'fair' => 0,
'poor' => 0
],
'quality_issues' => [],
'best_practices_violations' => [],
'recommendations' => [],
'timestamp' => microtime(true)
];
$files = $this->findPhpFiles($projectPath, $checkConfig);
$result['files_analyzed'] = count($files);
$allQualityIssues = [];
$allViolations = [];
$qualityScores = [];
foreach ($files as $file) {
$qualityResult = $this->assessCommentQuality($file, $checkConfig);
$qualityScores[] = $qualityResult['quality_score'];
if (!empty($qualityResult['issues'])) {
$allQualityIssues = array_merge($allQualityIssues, $qualityResult['issues']);
}
if (!empty($qualityResult['violations'])) {
$allViolations = array_merge($allViolations, $qualityResult['violations']);
}
}
$result['quality_score'] = count($qualityScores) > 0 ?
round(array_sum($qualityScores) / count($qualityScores), 2) : 0;
// Calculate quality distribution
foreach ($qualityScores as $score) {
if ($score >= 90) {
$result['quality_distribution']['excellent']++;
} elseif ($score >= 75) {
$result['quality_distribution']['good']++;
} elseif ($score >= 60) {
$result['quality_distribution']['fair']++;
} else {
$result['quality_distribution']['poor']++;
}
}
$result['quality_issues'] = $allQualityIssues;
$result['best_practices_violations'] = $allViolations;
$result['recommendations'] = $this->generateQualityRecommendations($result);
return $result;
}
/**
* Check documentation standards compliance.
*/
public function checkDocumentationStandards(string $projectPath, array $options = []): array
{
$checkConfig = array_merge($this->config['standards_check'] ?? [], $options);
$result = [
'check_type' => 'documentation_standards',
'project_path' => $projectPath,
'files_analyzed' => 0,
'standards_compliant' => 0,
'compliance_rate' => 0,
'standards_violations' => [],
'compliance_issues' => [
'missing_doc_blocks' => 0,
'incomplete_doc_blocks' => 0,
'invalid_format' => 0,
'missing_examples' => 0
],
'compliance_score' => 0,
'recommendations' => [],
'timestamp' => microtime(true)
];
$files = $this->findPhpFiles($projectPath, $checkConfig);
$result['files_analyzed'] = count($files);
$compliantFiles = 0;
$allViolations = [];
$complianceIssues = [
'missing_doc_blocks' => 0,
'incomplete_doc_blocks' => 0,
'invalid_format' => 0,
'missing_examples' => 0
];
foreach ($files as $file) {
$standardsResult = $this->checkFileStandards($file, $checkConfig);
if ($standardsResult['compliant']) {
$compliantFiles++;
}
if (!empty($standardsResult['violations'])) {
$allViolations = array_merge($allViolations, $standardsResult['violations']);
}
// Aggregate compliance issues
foreach ($complianceIssues as $issue => $count) {
$complianceIssues[$issue] += $standardsResult['compliance_issues'][$issue] ?? 0;
}
}
$result['standards_compliant'] = $compliantFiles;
$result['compliance_rate'] = $result['files_analyzed'] > 0 ?
round(($compliantFiles / $result['files_analyzed']) * 100, 2) : 0;
$result['standards_violations'] = $allViolations;
$result['compliance_issues'] = $complianceIssues;
$result['compliance_score'] = $this->calculateComplianceScore($result);
$result['recommendations'] = $this->generateStandardsRecommendations($result);
return $result;
}
/**
* Generate comment coverage report.
*/
public function generateCoverageReport(array $results, string $format = 'html'): string
{
return $this->reporter->generate($results, $format);
}
/**
* Get coverage statistics.
*/
public function getStatistics(): array
{
return [
'total_checks' => count($this->coverageResults),
'files_analyzed' => $this->countTotalFilesAnalyzed(),
'average_coverage' => $this->calculateAverageCoverage(),
'quality_scores' => $this->getQualityScores(),
'common_issues' => $this->getCommonIssues(),
'coverage_trends' => $this->getCoverageTrends()
];
}
/**
* Clear coverage results.
*/
public function clearResults(): void
{
$this->coverageResults = [];
$this->fileMetrics = [];
}
/**
* Find PHP files.
*/
protected function findPhpFiles(string $path, array $config): array
{
$files = [];
$exclude = $config['exclude'] ?? ['vendor', 'node_modules', '.git', 'tests'];
$include = $config['include'] ?? ['*.php'];
$iterator = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($path)
);
foreach ($iterator as $file) {
if ($file->isDot() || !$file->isFile()) {
continue;
}
$filePath = $file->getPathname();
$extension = strtolower(pathinfo($filePath, PATHINFO_EXTENSION));
if ($extension === 'php') {
$excluded = false;
foreach ($exclude as $pattern) {
if (strpos($filePath, $pattern) !== false) {
$excluded = true;
break;
}
}
if (!$excluded) {
$files[] = $filePath;
}
}
}
return $files;
}
/**
* Analyze file comments.
*/
protected function analyzeFileComments(string $file, array $config): array
{
$content = file_get_contents($file);
$parsedCode = $this->commentParser->parseFile($content);
$result = [
'file' => $file,
'classes' => ['total' => 0, 'commented' => 0],
'methods' => ['total' => 0, 'commented' => 0],
'properties' => ['total' => 0, 'commented' => 0],
'uncommented' => [],
'quality_issues' => []
];
// Analyze classes
foreach ($parsedCode['classes'] as $class) {
$result['classes']['total']++;
if ($class['has_comment']) {
$result['classes']['commented']++;
} else {
$result['uncommented'][] = [
'type' => 'class',
'name' => $class['name'],
'file' => $file,
'line' => $class['line']
];
}
}
// Analyze methods
foreach ($parsedCode['methods'] as $method) {
$result['methods']['total']++;
if ($method['has_comment']) {
$result['methods']['commented']++;
} else {
$result['uncommented'][] = [
'type' => 'method',
'name' => $method['name'],
'class' => $method['class'] ?? 'global',
'file' => $file,
'line' => $method['line']
];
}
}
// Analyze properties
foreach ($parsedCode['properties'] as $property) {
$result['properties']['total']++;
if ($property['has_comment']) {
$result['properties']['commented']++;
} else {
$result['uncommented'][] = [
'type' => 'property',
'name' => $property['name'],
'class' => $property['class'] ?? 'global',
'file' => $file,
'line' => $property['line']
];
}
}
// Check quality issues
$result['quality_issues'] = $this->commentAnalyzer->analyzeQuality($parsedCode, $config);
return $result;
}
/**
* Assess comment quality.
*/
protected function assessCommentQuality(string $file, array $config): array
{
$content = file_get_contents($file);
$parsedCode = $this->commentParser->parseFile($content);
$qualityResult = $this->qualityAssessor->assess($parsedCode, $config);
return [
'file' => $file,
'quality_score' => $qualityResult['score'],
'issues' => $qualityResult['issues'],
'violations' => $qualityResult['violations']
];
}
/**
* Check file standards.
*/
protected function checkFileStandards(string $file, array $config): array
{
$content = file_get_contents($file);
$parsedCode = $this->commentParser->parseFile($content);
$standardsResult = $this->commentAnalyzer->checkStandards($parsedCode, $config);
return [
'file' => $file,
'compliant' => $standardsResult['compliant'],
'violations' => $standardsResult['violations'],
'compliance_issues' => $standardsResult['issues']
];
}
/**
* Calculate coverage percentage.
*/
protected function calculateCoverage(array $metrics): float
{
if ($metrics['total'] === 0) {
return 100.0;
}
return round(($metrics['commented'] / $metrics['total']) * 100, 2);
}
/**
* Calculate quality score.
*/
protected function calculateQualityScore(array $result): int
{
$score = 0;
// Coverage score (60%)
$coverageScore = $result['overall_coverage'];
$score += $coverageScore * 0.6;
// Quality issues penalty (20%)
$issueCount = count($result['quality_issues']);
$qualityPenalty = min(20, $issueCount * 2);
$score += (20 - $qualityPenalty);
// Balance score (20%)
$balanceScore = $this->calculateBalanceScore($result);
$score += $balanceScore * 0.2;
return (int) round($score);
}
/**
* Calculate balance score.
*/
protected function calculateBalanceScore(array $result): int
{
$classCoverage = $result['class_coverage'];
$methodCoverage = $result['method_coverage'];
$propertyCoverage = $result['property_coverage'];
// Calculate standard deviation to measure balance
$mean = ($classCoverage + $methodCoverage + $propertyCoverage) / 3;
$variance = pow($classCoverage - $mean, 2) +
pow($methodCoverage - $mean, 2) +
pow($propertyCoverage - $mean, 2);
$stdDev = sqrt($variance / 3);
// Lower standard deviation = better balance
return max(0, 100 - ($stdDev * 2));
}
/**
* Calculate compliance score.
*/
protected function calculateComplianceScore(array $result): int
{
return (int) $result['compliance_rate'];
}
/**
* Generate coverage recommendations.
*/
protected function generateCoverageRecommendations(array $result): array
{
$recommendations = [];
if ($result['class_coverage'] < 80) {
$recommendations[] = 'Add class-level documentation to improve coverage';
}
if ($result['method_coverage'] < 70) {
$recommendations[] = 'Document public and protected methods for better API documentation';
}
if ($result['property_coverage'] < 60) {
$recommendations[] = 'Add property comments for better code understanding';
}
if (!empty($result['uncommented_items'])) {
$recommendations[] = 'Focus on documenting ' . count($result['uncommented_items']) .
' uncommented code elements';
}
$recommendations[] = 'Establish coding standards for documentation';
$recommendations[] = 'Use automated tools to enforce comment coverage';
$recommendations[] = 'Include documentation in code review process';
return array_unique($recommendations);
}
/**
* Generate quality recommendations.
*/
protected function generateQualityRecommendations(array $result): array
{
$recommendations = [];
if ($result['quality_score'] < 70) {
$recommendations[] = 'Improve comment quality and completeness';
}
if (!empty($result['best_practices_violations'])) {
$recommendations[] = 'Address best practices violations in comments';
}
$recommendations[] = 'Use standardized comment formats (PHPDoc)';
$recommendations[] = 'Include examples in complex method documentation';
$recommendations[] = 'Keep comments up-to-date with code changes';
return array_unique($recommendations);
}
/**
* Generate standards recommendations.
*/
protected function generateStandardsRecommendations(array $result): array
{
$recommendations = [];
if ($result['compliance_rate'] < 80) {
$recommendations[] = 'Improve documentation standards compliance';
}
foreach ($result['compliance_issues'] as $issue => $count) {
if ($count > 0) {
switch ($issue) {
case 'missing_doc_blocks':
$recommendations[] = 'Add missing PHPDoc blocks';
break;
case 'incomplete_doc_blocks':
$recommendations[] = 'Complete incomplete PHPDoc blocks';
break;
case 'invalid_format':
$recommendations[] = 'Fix invalid PHPDoc format';
break;
case 'missing_examples':
$recommendations[] = 'Add examples to complex methods';
break;
}
}
}
$recommendations[] = 'Use automated tools to check documentation standards';
$recommendations[] = 'Include documentation standards in coding guidelines';
return array_unique($recommendations);
}
/**
* Count total files analyzed.
*/
protected function countTotalFilesAnalyzed(): int
{
$total = 0;
foreach ($this->coverageResults as $result) {
$total += $result['files_analyzed'] ?? 0;
}
return $total;
}
/**
* Calculate average coverage.
*/
protected function calculateAverageCoverage(): float
{
if (empty($this->coverageResults)) {
return 0;
}
$totalCoverage = 0;
$count = 0;
foreach ($this->coverageResults as $result) {
if (isset($result['overall_coverage'])) {
$totalCoverage += $result['overall_coverage'];
$count++;
}
}
return $count > 0 ? $totalCoverage / $count : 0;
}
/**
* Get quality scores.
*/
protected function getQualityScores(): array
{
$scores = [];
foreach ($this->coverageResults as $result) {
if (isset($result['quality_score'])) {
$scores[] = $result['quality_score'];
}
}
return $scores;
}
/**
* Get common issues.
*/
protected function getCommonIssues(): array
{
$issues = [];
foreach ($this->coverageResults as $result) {
if (isset($result['quality_issues'])) {
foreach ($result['quality_issues'] as $issue) {
$type = $issue['type'] ?? 'unknown';
$issues[$type] = ($issues[$type] ?? 0) + 1;
}
}
}
return $issues;
}
/**
* Get coverage trends.
*/
protected function getCoverageTrends(): array
{
if (count($this->coverageResults) < 2) {
return ['trend' => 'insufficient_data'];
}
$recent = array_slice($this->coverageResults, -5);
$coverages = array_column($recent, 'overall_coverage');
if (count($coverages) < 2) {
return ['trend' => 'insufficient_data'];
}
$first = $coverages[0];
$last = end($coverages);
if ($last > $first + 5) {
return ['trend' => 'improving', 'change' => $last - $first];
} elseif ($last < $first - 5) {
return ['trend' => 'declining', 'change' => $first - $last];
} else {
return ['trend' => 'stable', 'change' => 0];
}
}
/**
* Get default configuration.
*/
protected function getDefaultConfig(): array
{
return [
'coverage_check' => [
'exclude' => ['vendor', 'node_modules', '.git', 'tests'],
'include' => ['*.php'],
'min_coverage_threshold' => 70
],
'quality_check' => [
'min_quality_score' => 75,
'check_examples' => true,
'check_format' => true
],
'standards_check' => [
'require_phpdoc' => true,
'check_parameter_types' => true,
'check_return_types' => true
],
'comment_parser' => [],
'comment_analyzer' => [],
'coverage_calculator' => [],
'quality_assessor' => [],
'reporter' => []
];
}
/**
* Get configuration.
*/
public function getConfig(): array
{
return $this->config;
}
/**
* Set configuration.
*/
public function setConfig(array $config): void
{
$this->config = array_merge($this->config, $config);
}
/**
* Create comment coverage checker instance.
*/
public static function create(array $config = []): self
{
return new self($config);
}
/**
* Create for development.
*/
public static function forDevelopment(): self
{
return new self([
'coverage_check' => [
'min_coverage_threshold' => 50,
'exclude' => ['vendor', 'node_modules', '.git']
],
'quality_check' => [
'min_quality_score' => 60
]
]);
}
/**
* Create for production.
*/
public static function forProduction(): self
{
return new self([
'coverage_check' => [
'min_coverage_threshold' => 80,
'exclude' => ['vendor', 'node_modules', '.git', 'tests']
],
'quality_check' => [
'min_quality_score' => 85,
'strict_mode' => true
],
'standards_check' => [
'strict_mode' => true
]
]);
}
}

View File

@@ -0,0 +1,756 @@
<?php
declare(strict_types=1);
namespace Fendx\Service\Documentation;
use Fendx\Service\Documentation\Manual\ManualParser;
use Fendx\Service\Documentation\Manual\ContentValidator;
use Fendx\Service\Documentation\Manual\StructureAnalyzer;
use Fendx\Service\Documentation\Manual\ExampleValidator;
use Fendx\Service\Documentation\Reporter\ManualReporter;
class UserManualChecker
{
protected array $config = [];
protected ManualParser $manualParser;
protected ContentValidator $contentValidator;
protected StructureAnalyzer $structureAnalyzer;
protected ExampleValidator $exampleValidator;
protected ManualReporter $reporter;
protected array $manualResults = [];
protected array $manualSections = [];
public function __construct(array $config = [])
{
$this->config = array_merge($this->getDefaultConfig(), $config);
$this->manualParser = new ManualParser($this->config['manual_parser'] ?? []);
$this->contentValidator = new ContentValidator($this->config['content_validator'] ?? []);
$this->structureAnalyzer = new StructureAnalyzer($this->config['structure_analyzer'] ?? []);
$this->exampleValidator = new ExampleValidator($this->config['example_validator'] ?? []);
$this->reporter = new ManualReporter($this->config['reporter'] ?? []);
}
/**
* Check user manual completeness.
*/
public function checkUserManual(string $manualPath, array $options = []): array
{
$checkConfig = array_merge($this->config['manual_check'] ?? [], $options);
$result = [
'check_type' => 'user_manual',
'manual_path' => $manualPath,
'manual_exists' => false,
'format' => '',
'sections_found' => 0,
'sections_complete' => 0,
'expected_sections' => $checkConfig['expected_sections'] ?? [],
'missing_sections' => [],
'incomplete_sections' => [],
'content_quality_score' => 0,
'structure_score' => 0,
'overall_score' => 0,
'issues' => [],
'recommendations' => [],
'timestamp' => microtime(true)
];
if (!file_exists($manualPath)) {
$result['issues'][] = [
'type' => 'manual_not_found',
'severity' => 'critical',
'message' => "User manual not found at: {$manualPath}",
'recommendation' => 'Create a comprehensive user manual'
];
return $result;
}
$result['manual_exists'] = true;
$result['format'] = $this->detectManualFormat($manualPath);
// Parse manual
$parsedManual = $this->manualParser->parse($manualPath, $checkConfig);
// Analyze structure
$structureAnalysis = $this->structureAnalyzer->analyze($parsedManual, $checkConfig);
// Validate content
$contentValidation = $this->contentValidator->validate($parsedManual, $checkConfig);
// Validate examples
$exampleValidation = $this->exampleValidator->validate($parsedManual, $checkConfig);
$result['sections_found'] = $structureAnalysis['sections_found'];
$result['sections_complete'] = $structureAnalysis['sections_complete'];
$result['missing_sections'] = $structureAnalysis['missing_sections'];
$result['incomplete_sections'] = $structureAnalysis['incomplete_sections'];
$result['content_quality_score'] = $contentValidation['quality_score'];
$result['structure_score'] = $structureAnalysis['structure_score'];
// Calculate overall score
$result['overall_score'] = $this->calculateOverallScore($result);
// Aggregate issues
$result['issues'] = array_merge(
$structureAnalysis['issues'],
$contentValidation['issues'],
$exampleValidation['issues']
);
$result['recommendations'] = $this->generateManualRecommendations($result);
// Store result
$this->manualResults[] = $result;
return $result;
}
/**
* Check multiple manual formats.
*/
public function checkManualFormats(array $manualPaths, array $options = []): array
{
$checkConfig = array_merge($this->config['formats_check'] ?? [], $options);
$result = [
'check_type' => 'manual_formats',
'manuals_checked' => count($manualPaths),
'formats_found' => [],
'format_compatibility' => [],
'cross_reference_issues' => [],
'recommendations' => [],
'timestamp' => microtime(true)
];
$formatResults = [];
$allSections = [];
foreach ($manualPaths as $path) {
if (file_exists($path)) {
$format = $this->detectManualFormat($path);
$result['formats_found'][] = $format;
$manualCheck = $this->checkUserManual($path, $checkConfig);
$formatResults[$format] = $manualCheck;
// Collect sections for cross-reference
if (isset($manualCheck['sections_found'])) {
$allSections[$format] = $manualCheck['sections_found'];
}
}
}
// Check cross-reference consistency
$crossReferenceIssues = $this->checkCrossReferences($allSections, $checkConfig);
$result['cross_reference_issues'] = $crossReferenceIssues;
// Check format compatibility
$compatibilityIssues = $this->checkFormatCompatibility($formatResults, $checkConfig);
$result['format_compatibility'] = $compatibilityIssues;
$result['recommendations'] = $this->generateFormatRecommendations($result);
return $result;
}
/**
* Check manual examples and tutorials.
*/
public function checkManualExamples(string $manualPath, array $options = []): array
{
$checkConfig = array_merge($this->config['examples_check'] ?? [], $options);
$result = [
'check_type' => 'manual_examples',
'manual_path' => $manualPath,
'examples_found' => 0,
'examples_valid' => 0,
'examples_working' => 0,
'tutorial_sections' => 0,
'code_examples' => 0,
'example_issues' => [],
'quality_score' => 0,
'recommendations' => [],
'timestamp' => microtime(true)
];
if (!file_exists($manualPath)) {
$result['example_issues'][] = [
'type' => 'manual_not_found',
'severity' => 'critical',
'message' => "Manual not found: {$manualPath}"
];
return $result;
}
$parsedManual = $this->manualParser->parse($manualPath, $checkConfig);
$exampleAnalysis = $this->exampleValidator->analyzeExamples($parsedManual, $checkConfig);
$result['examples_found'] = $exampleAnalysis['examples_found'];
$result['examples_valid'] = $exampleAnalysis['examples_valid'];
$result['examples_working'] = $exampleAnalysis['examples_working'];
$result['tutorial_sections'] = $exampleAnalysis['tutorial_sections'];
$result['code_examples'] = $exampleAnalysis['code_examples'];
$result['example_issues'] = $exampleAnalysis['issues'];
$result['quality_score'] = $exampleAnalysis['quality_score'];
$result['recommendations'] = $this->generateExampleRecommendations($result);
return $result;
}
/**
* Check manual accessibility and usability.
*/
public function checkManualUsability(string $manualPath, array $options = []): array
{
$checkConfig = array_merge($this->config['usability_check'] ?? [], $options);
$result = [
'check_type' => 'manual_usability',
'manual_path' => $manualPath,
'readability_score' => 0,
'navigation_score' => 0,
'searchability_score' => 0,
'accessibility_score' => 0,
'usability_issues' => [],
'accessibility_issues' => [],
'improvement_suggestions' => [],
'overall_usability_score' => 0,
'recommendations' => [],
'timestamp' => microtime(true)
];
if (!file_exists($manualPath)) {
$result['usability_issues'][] = [
'type' => 'manual_not_found',
'severity' => 'critical',
'message' => "Manual not found: {$manualPath}"
];
return $result;
}
$parsedManual = $this->manualParser->parse($manualPath, $checkConfig);
// Check readability
$readabilityResult = $this->contentValidator->checkReadability($parsedManual, $checkConfig);
$result['readability_score'] = $readabilityResult['score'];
// Check navigation
$navigationResult = $this->structureAnalyzer->checkNavigation($parsedManual, $checkConfig);
$result['navigation_score'] = $navigationResult['score'];
// Check searchability
$searchabilityResult = $this->contentValidator->checkSearchability($parsedManual, $checkConfig);
$result['searchability_score'] = $searchabilityResult['score'];
// Check accessibility
$accessibilityResult = $this->contentValidator->checkAccessibility($parsedManual, $checkConfig);
$result['accessibility_score'] = $accessibilityResult['score'];
$result['accessibility_issues'] = $accessibilityResult['issues'];
// Aggregate usability issues
$result['usability_issues'] = array_merge(
$readabilityResult['issues'],
$navigationResult['issues'],
$searchabilityResult['issues']
);
// Calculate overall usability score
$result['overall_usability_score'] = $this->calculateUsabilityScore($result);
$result['improvement_suggestions'] = $this->generateUsabilitySuggestions($result);
$result['recommendations'] = $this->generateUsabilityRecommendations($result);
return $result;
}
/**
* Generate comprehensive manual report.
*/
public function generateManualReport(array $results, string $format = 'html'): string
{
return $this->reporter->generate($results, $format);
}
/**
* Get manual statistics.
*/
public function getStatistics(): array
{
return [
'total_manuals_checked' => count($this->manualResults),
'average_score' => $this->calculateAverageScore(),
'common_issues' => $this->getCommonIssues(),
'section_coverage' => $this->getSectionCoverage(),
'quality_trends' => $this->getQualityTrends()
];
}
/**
* Clear manual results.
*/
public function clearResults(): void
{
$this->manualResults = [];
$this->manualSections = [];
}
/**
* Detect manual format.
*/
protected function detectManualFormat(string $manualPath): string
{
$extension = strtolower(pathinfo($manualPath, PATHINFO_EXTENSION));
switch ($extension) {
case 'md':
case 'markdown':
return 'markdown';
case 'txt':
return 'text';
case 'html':
case 'htm':
return 'html';
case 'pdf':
return 'pdf';
case 'doc':
case 'docx':
return 'word';
default:
// Try to detect by content
$content = file_get_contents($manualPath);
if (strpos($content, '#') === 0) {
return 'markdown';
} elseif (strpos($content, '<html') !== false) {
return 'html';
} else {
return 'text';
}
}
}
/**
* Check cross-references between formats.
*/
protected function checkCrossReferences(array $allSections, array $config): array
{
$issues = [];
if (count($allSections) < 2) {
return $issues;
}
$formats = array_keys($allSections);
$referenceFormats = $config['reference_formats'] ?? $formats;
foreach ($referenceFormats as $format) {
if (!isset($allSections[$format])) {
$issues[] = [
'type' => 'missing_format_reference',
'severity' => 'medium',
'message' => "Reference format '{$format}' not found",
'format' => $format
];
}
}
// Check section consistency
$sectionCounts = array_map('count', $allSections);
$maxSections = max($sectionCounts);
$minSections = min($sectionCounts);
if (($maxSections - $minSections) > 2) {
$issues[] = [
'type' => 'section_count_inconsistency',
'severity' => 'low',
'message' => 'Significant difference in section counts between formats',
'max_sections' => $maxSections,
'min_sections' => $minSections
];
}
return $issues;
}
/**
* Check format compatibility.
*/
protected function checkFormatCompatibility(array $formatResults, array $config): array
{
$compatibility = [];
foreach ($formatResults as $format => $result) {
$compatibility[$format] = [
'score' => $result['overall_score'] ?? 0,
'compatible' => ($result['overall_score'] ?? 0) >= ($config['min_compatibility_score'] ?? 70)
];
}
return $compatibility;
}
/**
* Calculate overall score.
*/
protected function calculateOverallScore(array $result): int
{
$structureWeight = 0.4;
$contentWeight = 0.6;
$structureScore = $result['structure_score'] ?? 0;
$contentScore = $result['content_quality_score'] ?? 0;
return (int) round(($structureScore * $structureWeight) + ($contentScore * $contentWeight));
}
/**
* Calculate usability score.
*/
protected function calculateUsabilityScore(array $result): int
{
$weights = [
'readability' => 0.3,
'navigation' => 0.3,
'searchability' => 0.2,
'accessibility' => 0.2
];
$score = 0;
$score += ($result['readability_score'] ?? 0) * $weights['readability'];
$score += ($result['navigation_score'] ?? 0) * $weights['navigation'];
$score += ($result['searchability_score'] ?? 0) * $weights['searchability'];
$score += ($result['accessibility_score'] ?? 0) * $weights['accessibility'];
return (int) round($score);
}
/**
* Generate manual recommendations.
*/
protected function generateManualRecommendations(array $result): array
{
$recommendations = [];
if (!$result['manual_exists']) {
$recommendations[] = 'Create a comprehensive user manual';
return $recommendations;
}
if (!empty($result['missing_sections'])) {
$recommendations[] = 'Add missing sections: ' . implode(', ', $result['missing_sections']);
}
if (!empty($result['incomplete_sections'])) {
$recommendations[] = 'Complete incomplete sections: ' . implode(', ', $result['incomplete_sections']);
}
if ($result['content_quality_score'] < 70) {
$recommendations[] = 'Improve content quality and completeness';
}
if ($result['structure_score'] < 70) {
$recommendations[] = 'Improve manual structure and organization';
}
$recommendations[] = 'Include practical examples and tutorials';
$recommendations[] = 'Add troubleshooting section';
$recommendations[] = 'Keep manual updated with feature changes';
return array_unique($recommendations);
}
/**
* Generate format recommendations.
*/
protected function generateFormatRecommendations(array $result): array
{
$recommendations = [];
if (!empty($result['cross_reference_issues'])) {
$recommendations[] = 'Fix cross-reference issues between manual formats';
}
if (!empty($result['format_compatibility'])) {
$incompatibleFormats = array_filter($result['format_compatibility'], fn($f) => !$f['compatible']);
if (!empty($incompatibleFormats)) {
$formats = array_keys($incompatibleFormats);
$recommendations[] = 'Improve compatibility for formats: ' . implode(', ', $formats);
}
}
$recommendations[] = 'Maintain consistency across all manual formats';
$recommendations[] = 'Consider providing multiple format options for users';
return array_unique($recommendations);
}
/**
* Generate example recommendations.
*/
protected function generateExampleRecommendations(array $result): array
{
$recommendations = [];
if ($result['examples_found'] === 0) {
$recommendations[] = 'Add code examples to the manual';
} else {
$workingRate = $result['examples_found'] > 0 ?
($result['examples_working'] / $result['examples_found']) * 100 : 0;
if ($workingRate < 80) {
$recommendations[] = 'Fix non-working code examples';
}
if ($result['tutorial_sections'] < 3) {
$recommendations[] = 'Add more tutorial sections';
}
}
if ($result['quality_score'] < 70) {
$recommendations[] = 'Improve example quality and explanations';
}
$recommendations[] = 'Test all examples regularly';
$recommendations[] = 'Include both simple and advanced examples';
return array_unique($recommendations);
}
/**
* Generate usability suggestions.
*/
protected function generateUsabilitySuggestions(array $result): array
{
$suggestions = [];
if ($result['readability_score'] < 70) {
$suggestions[] = 'Improve text readability with shorter sentences and simpler language';
}
if ($result['navigation_score'] < 70) {
$suggestions[] = 'Improve navigation with better table of contents and internal links';
}
if ($result['searchability_score'] < 70) {
$suggestions[] = 'Improve searchability with better keywords and index';
}
if ($result['accessibility_score'] < 70) {
$suggestions[] = 'Improve accessibility with proper headings and alt text';
}
return $suggestions;
}
/**
* Generate usability recommendations.
*/
protected function generateUsabilityRecommendations(array $result): array
{
$recommendations = [];
if ($result['overall_usability_score'] < 70) {
$recommendations[] = 'Overall usability needs improvement';
}
$recommendations = array_merge($recommendations, $result['improvement_suggestions']);
$recommendations[] = 'Test manual usability with actual users';
$recommendations[] = 'Include quick start guide for beginners';
$recommendations[] = 'Add FAQ section for common questions';
return array_unique($recommendations);
}
/**
* Calculate average score.
*/
protected function calculateAverageScore(): float
{
if (empty($this->manualResults)) {
return 0;
}
$totalScore = 0;
$count = 0;
foreach ($this->manualResults as $result) {
if (isset($result['overall_score'])) {
$totalScore += $result['overall_score'];
$count++;
}
}
return $count > 0 ? $totalScore / $count : 0;
}
/**
* Get common issues.
*/
protected function getCommonIssues(): array
{
$issues = [];
foreach ($this->manualResults as $result) {
if (isset($result['issues'])) {
foreach ($result['issues'] as $issue) {
$type = $issue['type'] ?? 'unknown';
$issues[$type] = ($issues[$type] ?? 0) + 1;
}
}
}
return $issues;
}
/**
* Get section coverage.
*/
protected function getSectionCoverage(): array
{
$coverage = [];
foreach ($this->manualResults as $result) {
if (isset($result['sections_found']) && isset($result['sections_complete'])) {
$coverage[] = [
'found' => $result['sections_found'],
'complete' => $result['sections_complete'],
'rate' => $result['sections_found'] > 0 ?
round(($result['sections_complete'] / $result['sections_found']) * 100, 2) : 0
];
}
}
return $coverage;
}
/**
* Get quality trends.
*/
protected function getQualityTrends(): array
{
if (count($this->manualResults) < 2) {
return ['trend' => 'insufficient_data'];
}
$recent = array_slice($this->manualResults, -5);
$scores = array_column($recent, 'overall_score');
if (count($scores) < 2) {
return ['trend' => 'insufficient_data'];
}
$first = $scores[0];
$last = end($scores);
if ($last > $first + 5) {
return ['trend' => 'improving', 'change' => $last - $first];
} elseif ($last < $first - 5) {
return ['trend' => 'declining', 'change' => $first - $last];
} else {
return ['trend' => 'stable', 'change' => 0];
}
}
/**
* Get default configuration.
*/
protected function getDefaultConfig(): array
{
return [
'manual_check' => [
'expected_sections' => [
'Introduction', 'Installation', 'Getting Started', 'API Reference',
'Examples', 'Tutorials', 'Troubleshooting', 'FAQ', 'Contributing'
],
'min_section_length' => 100
],
'formats_check' => [
'reference_formats' => ['markdown', 'html', 'pdf'],
'min_compatibility_score' => 70
],
'examples_check' => [
'require_examples' => true,
'test_syntax' => true,
'min_examples' => 5
],
'usability_check' => [
'check_readability' => true,
'check_navigation' => true,
'check_searchability' => true,
'check_accessibility' => true
],
'manual_parser' => [],
'content_validator' => [],
'structure_analyzer' => [],
'example_validator' => [],
'reporter' => []
];
}
/**
* Get configuration.
*/
public function getConfig(): array
{
return $this->config;
}
/**
* Set configuration.
*/
public function setConfig(array $config): void
{
$this->config = array_merge($this->config, $config);
}
/**
* Create user manual checker instance.
*/
public static function create(array $config = []): self
{
return new self($config);
}
/**
* Create for development.
*/
public static function forDevelopment(): self
{
return new self([
'manual_check' => [
'expected_sections' => [
'Introduction', 'Installation', 'Getting Started'
],
'min_section_length' => 50
],
'examples_check' => [
'min_examples' => 2
]
]);
}
/**
* Create for production.
*/
public static function forProduction(): self
{
return new self([
'manual_check' => [
'expected_sections' => [
'Introduction', 'Installation', 'Getting Started', 'API Reference',
'Examples', 'Tutorials', 'Troubleshooting', 'FAQ', 'Contributing',
'Changelog', 'License'
],
'min_section_length' => 200
],
'examples_check' => [
'min_examples' => 10,
'test_syntax' => true,
'validate_examples' => true
],
'usability_check' => [
'strict_accessibility' => true
]
]);
}
}

View File

@@ -0,0 +1,629 @@
<?php
declare(strict_types=1);
namespace Fendx\Service\Health;
use Fendx\Service\Health\Checker\HttpHealthChecker;
use Fendx\Service\Health\Checker\TcpHealthChecker;
use Fendx\Service\Health\Checker\CustomHealthChecker;
use Fendx\Service\Health\Storage\HealthStorage;
use Fendx\Service\Health\Notifier\HealthNotifier;
class HealthChecker
{
protected HttpHealthChecker $httpChecker;
protected TcpHealthChecker $tcpChecker;
protected CustomHealthChecker $customChecker;
protected HealthStorage $storage;
protected HealthNotifier $notifier;
protected array $config = [];
protected array $monitoredServices = [];
protected array $healthStatuses = [];
protected array $checkHistory = [];
public function __construct(array $config = [])
{
$this->config = array_merge($this->getDefaultConfig(), $config);
$this->httpChecker = new HttpHealthChecker($this->config);
$this->tcpChecker = new TcpHealthChecker($this->config);
$this->customChecker = new CustomHealthChecker($this->config);
$this->storage = new HealthStorage($this->config);
$this->notifier = new HealthNotifier($this->config);
$this->initialize();
}
/**
* Start monitoring a service.
*/
public function startMonitoring(string $serviceId, array $healthConfig): void
{
$this->monitoredServices[$serviceId] = [
'id' => $serviceId,
'config' => $healthConfig,
'status' => 'unknown',
'last_check' => null,
'last_success' => null,
'last_failure' => null,
'consecutive_failures' => 0,
'consecutive_successes' => 0,
'total_checks' => 0,
'total_failures' => 0,
'total_successes' => 0,
'created_at' => time(),
'updated_at' => time()
];
// Perform initial health check
$this->checkHealth($serviceId);
$this->logInfo("Started health monitoring for service: {$serviceId}");
}
/**
* Stop monitoring a service.
*/
public function stopMonitoring(string $serviceId): void
{
if (isset($this->monitoredServices[$serviceId])) {
unset($this->monitoredServices[$serviceId]);
unset($this->healthStatuses[$serviceId]);
$this->logInfo("Stopped health monitoring for service: {$serviceId}");
}
}
/**
* Check health of a specific service.
*/
public function checkHealth(string $serviceId): array
{
if (!isset($this->monitoredServices[$serviceId])) {
throw new \InvalidArgumentException("Service not being monitored: {$serviceId}");
}
$service = &$this->monitoredServices[$serviceId];
$config = $service['config'];
$startTime = microtime(true);
$result = $this->performHealthCheck($config);
$duration = microtime(true) - $startTime;
// Update service statistics
$service['last_check'] = time();
$service['total_checks']++;
$service['updated_at'] = time();
if ($result['healthy']) {
$service['status'] = 'healthy';
$service['last_success'] = time();
$service['consecutive_successes']++;
$service['consecutive_failures'] = 0;
$service['total_successes']++;
} else {
$service['status'] = 'unhealthy';
$service['last_failure'] = time();
$service['consecutive_failures']++;
$service['consecutive_successes'] = 0;
$service['total_failures']++;
}
// Store health status
$this->healthStatuses[$serviceId] = array_merge($result, [
'service_id' => $serviceId,
'check_duration' => $duration,
'timestamp' => time()
]);
// Store in persistent storage
$this->storage->storeHealthCheck($serviceId, $this->healthStatuses[$serviceId]);
// Add to history
$this->addToHistory($serviceId, $this->healthStatuses[$serviceId]);
// Send notification if needed
$this->handleHealthChange($serviceId, $service, $result);
return $this->healthStatuses[$serviceId];
}
/**
* Check health of all monitored services.
*/
public function checkAllHealth(): array
{
$results = [];
foreach (array_keys($this->monitoredServices) as $serviceId) {
try {
$results[$serviceId] = $this->checkHealth($serviceId);
} catch (\Exception $e) {
$results[$serviceId] = [
'service_id' => $serviceId,
'healthy' => false,
'error' => $e->getMessage(),
'timestamp' => time()
];
}
}
return $results;
}
/**
* Get health status of a service.
*/
public function getHealthStatus(string $serviceId): ?array
{
return $this->healthStatuses[$serviceId] ?? null;
}
/**
* Check if service is healthy.
*/
public function isHealthy(string $serviceId): bool
{
$status = $this->getHealthStatus($serviceId);
if (!$status) {
return false;
}
return $status['healthy'] ?? false;
}
/**
* Get all monitored services.
*/
public function getMonitoredServices(): array
{
return $this->monitoredServices;
}
/**
* Get service health statistics.
*/
public function getServiceStatistics(string $serviceId): array
{
if (!isset($this->monitoredServices[$serviceId])) {
return [];
}
$service = $this->monitoredServices[$serviceId];
$uptime = $this->calculateUptime($serviceId);
return [
'service_id' => $serviceId,
'status' => $service['status'],
'total_checks' => $service['total_checks'],
'total_successes' => $service['total_successes'],
'total_failures' => $service['total_failures'],
'success_rate' => $service['total_checks'] > 0 ?
($service['total_successes'] / $service['total_checks']) * 100 : 0,
'consecutive_successes' => $service['consecutive_successes'],
'consecutive_failures' => $service['consecutive_failures'],
'last_check' => $service['last_check'],
'last_success' => $service['last_success'],
'last_failure' => $service['last_failure'],
'uptime_percentage' => $uptime,
'monitored_since' => $service['created_at']
];
}
/**
* Get overall health statistics.
*/
public function getOverallStatistics(): array
{
$totalServices = count($this->monitoredServices);
$healthyServices = 0;
$unhealthyServices = 0;
$totalChecks = 0;
$totalSuccesses = 0;
$totalFailures = 0;
foreach ($this->monitoredServices as $service) {
if ($service['status'] === 'healthy') {
$healthyServices++;
} else {
$unhealthyServices++;
}
$totalChecks += $service['total_checks'];
$totalSuccesses += $service['total_successes'];
$totalFailures += $service['total_failures'];
}
return [
'total_services' => $totalServices,
'healthy_services' => $healthyServices,
'unhealthy_services' => $unhealthyServices,
'unknown_services' => $totalServices - $healthyServices - $unhealthyServices,
'health_percentage' => $totalServices > 0 ? ($healthyServices / $totalServices) * 100 : 0,
'total_checks' => $totalChecks,
'total_successes' => $totalSuccesses,
'total_failures' => $totalFailures,
'overall_success_rate' => $totalChecks > 0 ?
($totalSuccesses / $totalChecks) * 100 : 0
];
}
/**
* Get health history for a service.
*/
public function getHealthHistory(string $serviceId, int $limit = 100): array
{
if (!isset($this->checkHistory[$serviceId])) {
return [];
}
$history = $this->checkHistory[$serviceId];
usort($history, function ($a, $b) {
return $b['timestamp'] <=> $a['timestamp'];
});
return array_slice($history, 0, $limit);
}
/**
* Get services with consecutive failures.
*/
public function getServicesWithFailures(int $threshold = 3): array
{
$services = [];
foreach ($this->monitoredServices as $serviceId => $service) {
if ($service['consecutive_failures'] >= $threshold) {
$services[$serviceId] = $service;
}
}
return $services;
}
/**
* Get services that need attention.
*/
public function getServicesNeedingAttention(): array
{
$services = [];
$failureThreshold = $this->config['failure_threshold'] ?? 3;
foreach ($this->monitoredServices as $serviceId => $service) {
$needsAttention = false;
$reason = [];
if ($service['consecutive_failures'] >= $failureThreshold) {
$needsAttention = true;
$reason[] = "Consecutive failures: {$service['consecutive_failures']}";
}
if ($service['status'] === 'unhealthy') {
$needsAttention = true;
$reason[] = "Currently unhealthy";
}
// Check if service hasn't been checked recently
$timeSinceLastCheck = time() - $service['last_check'];
$staleThreshold = $this->config['stale_threshold'] ?? 300;
if ($timeSinceLastCheck > $staleThreshold) {
$needsAttention = true;
$reason[] = "Last check was {$timeSinceLastCheck} seconds ago";
}
if ($needsAttention) {
$services[$serviceId] = array_merge($service, [
'attention_reasons' => $reason
]);
}
}
return $services;
}
/**
* Reset service statistics.
*/
public function resetStatistics(string $serviceId): bool
{
if (!isset($this->monitoredServices[$serviceId])) {
return false;
}
$service = &$this->monitoredServices[$serviceId];
$service['consecutive_failures'] = 0;
$service['consecutive_successes'] = 0;
$service['total_checks'] = 0;
$service['total_failures'] = 0;
$service['total_successes'] = 0;
$service['updated_at'] = time();
// Clear history
unset($this->checkHistory[$serviceId]);
$this->logInfo("Reset statistics for service: {$serviceId}");
return true;
}
/**
* Enable/disable service monitoring.
*/
public function setMonitoringEnabled(string $serviceId, bool $enabled): bool
{
if (!isset($this->monitoredServices[$serviceId])) {
return false;
}
$this->monitoredServices[$serviceId]['enabled'] = $enabled;
$this->monitoredServices[$serviceId]['updated_at'] = time();
$this->logInfo("Monitoring " . ($enabled ? 'enabled' : 'disabled') . " for service: {$serviceId}");
return true;
}
/**
* Update health check configuration.
*/
public function updateHealthConfig(string $serviceId, array $config): bool
{
if (!isset($this->monitoredServices[$serviceId])) {
return false;
}
$this->monitoredServices[$serviceId]['config'] = array_merge(
$this->monitoredServices[$serviceId]['config'],
$config
);
$this->monitoredServices[$serviceId]['updated_at'] = time();
// Perform health check with new config
$this->checkHealth($serviceId);
$this->logInfo("Updated health config for service: {$serviceId}");
return true;
}
/**
* Perform health check based on configuration.
*/
protected function performHealthCheck(array $config): array
{
$type = $config['type'] ?? 'http';
switch ($type) {
case 'http':
return $this->httpChecker->check($config);
case 'tcp':
return $this->tcpChecker->check($config);
case 'custom':
return $this->customChecker->check($config);
default:
throw new \InvalidArgumentException("Unsupported health check type: {$type}");
}
}
/**
* Add check to history.
*/
protected function addToHistory(string $serviceId, array $result): void
{
if (!isset($this->checkHistory[$serviceId])) {
$this->checkHistory[$serviceId] = [];
}
$this->checkHistory[$serviceId][] = $result;
// Limit history size
$maxHistory = $this->config['max_history'] ?? 1000;
if (count($this->checkHistory[$serviceId]) > $maxHistory) {
$this->checkHistory[$serviceId] = array_slice(
$this->checkHistory[$serviceId],
-$maxHistory
);
}
}
/**
* Handle health status changes.
*/
protected function handleHealthChange(string $serviceId, array $service, array $result): void
{
$previousStatus = $service['status'];
$currentStatus = $result['healthy'] ? 'healthy' : 'unhealthy';
// Status changed
if ($previousStatus !== $currentStatus) {
$this->logInfo("Service {$serviceId} status changed: {$previousStatus} -> {$currentStatus}");
// Send notification
$this->notifier->notifyStatusChange($serviceId, $previousStatus, $currentStatus, $result);
}
// Check for consecutive failures threshold
$failureThreshold = $this->config['failure_threshold'] ?? 3;
if ($service['consecutive_failures'] === $failureThreshold) {
$this->notifier->notifyFailureThreshold($serviceId, $failureThreshold, $result);
}
// Check for recovery
$recoveryThreshold = $this->config['recovery_threshold'] ?? 3;
if ($service['consecutive_successes'] === $recoveryThreshold &&
$service['consecutive_failures'] > 0) {
$this->notifier->notifyRecovery($serviceId, $service['consecutive_failures'], $result);
}
}
/**
* Calculate service uptime.
*/
protected function calculateUptime(string $serviceId): float
{
if (!isset($this->checkHistory[$serviceId]) || empty($this->checkHistory[$serviceId])) {
return 0.0;
}
$history = $this->checkHistory[$serviceId];
$healthyCount = 0;
foreach ($history as $check) {
if ($check['healthy']) {
$healthyCount++;
}
}
return ($healthyCount / count($history)) * 100;
}
/**
* Initialize health checker.
*/
protected function initialize(): void
{
// Load existing services from storage
$this->loadMonitoredServices();
// Start periodic health checks
if ($this->config['periodic_checks']) {
$this->startPeriodicChecks();
}
$this->logInfo("Health checker initialized");
}
/**
* Load monitored services from storage.
*/
protected function loadMonitoredServices(): void
{
$services = $this->storage->loadMonitoredServices();
foreach ($services as $service) {
$this->monitoredServices[$service['id']] = $service;
}
$this->logInfo("Loaded " . count($services) . " monitored services from storage");
}
/**
* Start periodic health checks.
*/
protected function startPeriodicChecks(): void
{
// This would typically be run as a background process
// For now, we'll just log that it would start
$this->logInfo("Periodic health checks started");
}
/**
* Log info message.
*/
protected function logInfo(string $message): void
{
if ($this->config['logging_enabled']) {
error_log("[HealthChecker] {$message}");
}
}
/**
* Get default configuration.
*/
protected function getDefaultConfig(): array
{
return [
'periodic_checks' => true,
'check_interval' => 30,
'failure_threshold' => 3,
'recovery_threshold' => 3,
'stale_threshold' => 300,
'max_history' => 1000,
'logging_enabled' => true,
'notifications' => [
'enabled' => true,
'channels' => ['email', 'slack'],
'on_status_change' => true,
'on_failure_threshold' => true,
'on_recovery' => true
],
'storage' => [
'type' => 'file',
'path' => __DIR__ . '/../../../storage/health'
],
'http' => [
'timeout' => 5,
'follow_redirects' => true,
'verify_ssl' => true
],
'tcp' => [
'timeout' => 5
]
];
}
/**
* Get configuration.
*/
public function getConfig(): array
{
return $this->config;
}
/**
* Set configuration.
*/
public function setConfig(array $config): void
{
$this->config = array_merge($this->config, $config);
}
/**
* Create health checker instance.
*/
public static function create(array $config = []): self
{
return new self($config);
}
/**
* Create for development.
*/
public static function forDevelopment(): self
{
return new self([
'periodic_checks' => false,
'check_interval' => 10,
'failure_threshold' => 2,
'logging_enabled' => true,
'notifications' => [
'enabled' => false
]
]);
}
/**
* Create for production.
*/
public static function forProduction(): self
{
return new self([
'periodic_checks' => true,
'check_interval' => 30,
'failure_threshold' => 3,
'recovery_threshold' => 3,
'stale_threshold' => 180,
'logging_enabled' => false,
'notifications' => [
'enabled' => true,
'channels' => ['email', 'slack', 'webhook']
],
'storage' => [
'type' => 'redis',
'host' => 'localhost',
'port' => 6379
]
]);
}
}

View File

@@ -0,0 +1,853 @@
<?php
declare(strict_types=1);
namespace Fendx\Service\LoadBalancer\Failover;
use Fendx\Service\LoadBalancer\Failover\Detector\FailureDetector;
use Fendx\Service\LoadBalancer\Failover\Strategy\FailoverStrategy;
use Fendx\Service\LoadBalancer\Failover\Recovery\RecoveryManager;
use Fendx\Service\LoadBalancer\Failover\Storage\FailoverStorage;
class FailoverManager
{
protected FailureDetector $detector;
protected FailoverStrategy $strategy;
protected RecoveryManager $recovery;
protected FailoverStorage $storage;
protected array $config = [];
protected array $services = [];
protected array $failoverStatus = [];
protected array $circuitBreakers = [];
protected array $failureHistory = [];
public function __construct(array $config = [])
{
$this->config = array_merge($this->getDefaultConfig(), $config);
$this->detector = new FailureDetector($this->config);
$this->strategy = new FailoverStrategy($this->config);
$this->recovery = new RecoveryManager($this->config);
$this->storage = new FailoverStorage($this->config);
$this->initialize();
}
/**
* Handle service failure and perform failover.
*/
public function handleFailure(string $serviceId, array $context = []): ?array
{
if (!isset($this->services[$serviceId])) {
throw new \InvalidArgumentException("Service not registered: {$serviceId}");
}
$service = $this->services[$serviceId];
// Record failure
$this->recordFailure($serviceId, $context);
// Check if failover should be triggered
if ($this->shouldTriggerFailover($serviceId)) {
$this->triggerFailover($serviceId);
}
// Get failover target
$target = $this->getFailoverTarget($serviceId);
if ($target) {
$this->logInfo("Failover triggered for {$serviceId} -> {$target['id']}");
return $target;
}
$this->logWarning("No failover target available for {$serviceId}");
return null;
}
/**
* Register service for failover monitoring.
*/
public function registerService(string $serviceId, array $serviceConfig): void
{
$this->services[$serviceId] = array_merge([
'id' => $serviceId,
'failover_enabled' => true,
'failover_targets' => [],
'circuit_breaker_enabled' => true,
'failure_threshold' => $this->config['default_failure_threshold'],
'recovery_threshold' => $this->config['default_recovery_threshold'],
'timeout' => $this->config['default_timeout'],
'retry_attempts' => $this->config['default_retry_attempts']
], $serviceConfig);
// Initialize circuit breaker if enabled
if ($serviceConfig['circuit_breaker_enabled'] ?? true) {
$this->initializeCircuitBreaker($serviceId);
}
// Initialize failover status
$this->failoverStatus[$serviceId] = [
'status' => 'active',
'failures' => 0,
'consecutive_failures' => 0,
'last_failure' => null,
'last_success' => time(),
'current_target' => null,
'circuit_breaker_state' => 'closed'
];
$this->logInfo("Service registered for failover: {$serviceId}");
}
/**
* Unregister service from failover monitoring.
*/
public function unregisterService(string $serviceId): void
{
unset($this->services[$serviceId]);
unset($this->failoverStatus[$serviceId]);
unset($this->circuitBreakers[$serviceId]);
$this->logInfo("Service unregistered from failover: {$serviceId}");
}
/**
* Record successful service call.
*/
public function recordSuccess(string $serviceId, array $context = []): void
{
if (!isset($this->services[$serviceId])) {
return;
}
$status = &$this->failoverStatus[$serviceId];
$status['consecutive_failures'] = 0;
$status['last_success'] = time();
// Update circuit breaker
if (isset($this->circuitBreakers[$serviceId])) {
$this->circuitBreakers[$serviceId]->recordSuccess();
}
// Check if service can recover
if ($status['status'] === 'failed_over') {
$this->attemptRecovery($serviceId);
}
$this->logDebug("Success recorded for {$serviceId}");
}
/**
* Check if service is available.
*/
public function isServiceAvailable(string $serviceId): bool
{
if (!isset($this->services[$serviceId])) {
return false;
}
$status = $this->failoverStatus[$serviceId];
// Check circuit breaker
if (isset($this->circuitBreakers[$serviceId])) {
if (!$this->circuitBreakers[$serviceId]->isOpen()) {
return false;
}
}
// Check failover status
return $status['status'] !== 'failed';
}
/**
* Get failover target for service.
*/
public function getFailoverTarget(string $serviceId): ?array
{
if (!isset($this->services[$serviceId])) {
return null;
}
$service = $this->services[$serviceId];
$targets = $service['failover_targets'] ?? [];
if (empty($targets)) {
return null;
}
// Filter available targets
$availableTargets = array_filter($targets, function($target) {
return $this->isServiceAvailable($target['id']);
});
if (empty($availableTargets)) {
return null;
}
// Select target using strategy
return $this->strategy->selectTarget($availableTargets, $serviceId);
}
/**
* Get service failover status.
*/
public function getFailoverStatus(string $serviceId): ?array
{
if (!isset($this->failoverStatus[$serviceId])) {
return null;
}
$status = $this->failoverStatus[$serviceId];
// Add circuit breaker status
if (isset($this->circuitBreakers[$serviceId])) {
$status['circuit_breaker'] = $this->circuitBreakers[$serviceId]->getStatus();
}
return $status;
}
/**
* Get all failover statuses.
*/
public function getAllFailoverStatuses(): array
{
$statuses = [];
foreach ($this->failoverStatus as $serviceId => $status) {
$statuses[$serviceId] = $this->getFailoverStatus($serviceId);
}
return $statuses;
}
/**
* Get failure history for service.
*/
public function getFailureHistory(string $serviceId, int $limit = 100): array
{
$history = $this->failureHistory[$serviceId] ?? [];
// Sort by timestamp (newest first)
usort($history, function($a, $b) {
return $b['timestamp'] <=> $a['timestamp'];
});
return array_slice($history, 0, $limit);
}
/**
* Get failover statistics.
*/
public function getStatistics(): array
{
$totalServices = count($this->services);
$activeServices = 0;
$failedServices = 0;
$failedOverServices = 0;
$totalFailures = 0;
$circuitBreakerStats = ['closed' => 0, 'open' => 0, 'half_open' => 0];
foreach ($this->failoverStatus as $serviceId => $status) {
switch ($status['status']) {
case 'active':
$activeServices++;
break;
case 'failed':
$failedServices++;
break;
case 'failed_over':
$failedOverServices++;
break;
}
$totalFailures += $status['failures'];
// Circuit breaker stats
if (isset($this->circuitBreakers[$serviceId])) {
$cbState = $this->circuitBreakers[$serviceId]->getState();
$circuitBreakerStats[$cbState]++;
}
}
return [
'total_services' => $totalServices,
'active_services' => $activeServices,
'failed_services' => $failedServices,
'failed_over_services' => $failedOverServices,
'total_failures' => $totalFailures,
'circuit_breaker_stats' => $circuitBreakerStats,
'availability_percentage' => $totalServices > 0 ?
(($activeServices + $failedOverServices) / $totalServices) * 100 : 0
];
}
/**
* Manually trigger failover for service.
*/
public function triggerFailover(string $serviceId): bool
{
if (!isset($this->services[$serviceId])) {
return false;
}
$status = &$this->failoverStatus[$serviceId];
$status['status'] = 'failed_over';
$status['current_target'] = $this->getFailoverTarget($serviceId);
// Open circuit breaker
if (isset($this->circuitBreakers[$serviceId])) {
$this->circuitBreakers[$serviceId]->open();
}
$this->logInfo("Manual failover triggered for {$serviceId}");
return true;
}
/**
* Manually recover service from failover.
*/
public function recoverService(string $serviceId): bool
{
if (!isset($this->services[$serviceId])) {
return false;
}
$status = &$this->failoverStatus[$serviceId];
$status['status'] = 'active';
$status['consecutive_failures'] = 0;
$status['current_target'] = null;
// Close circuit breaker
if (isset($this->circuitBreakers[$serviceId])) {
$this->circuitBreakers[$serviceId]->close();
}
$this->logInfo("Service recovered from failover: {$serviceId}");
return true;
}
/**
* Add failover target to service.
*/
public function addFailoverTarget(string $serviceId, array $target): void
{
if (!isset($this->services[$serviceId])) {
throw new \InvalidArgumentException("Service not registered: {$serviceId}");
}
$this->services[$serviceId]['failover_targets'][] = $target;
$this->logInfo("Failover target added to {$serviceId}: {$target['id']}");
}
/**
* Remove failover target from service.
*/
public function removeFailoverTarget(string $serviceId, string $targetId): bool
{
if (!isset($this->services[$serviceId])) {
return false;
}
$targets = $this->services[$serviceId]['failover_targets'];
$filtered = array_filter($targets, function($target) use ($targetId) {
return $target['id'] !== $targetId;
});
if (count($filtered) === count($targets)) {
return false;
}
$this->services[$serviceId]['failover_targets'] = array_values($filtered);
$this->logInfo("Failover target removed from {$serviceId}: {$targetId}");
return true;
}
/**
* Test failover configuration.
*/
public function testFailover(string $serviceId): array
{
if (!isset($this->services[$serviceId])) {
throw new \InvalidArgumentException("Service not registered: {$serviceId}");
}
$results = [
'service_id' => $serviceId,
'timestamp' => time(),
'tests' => []
];
// Test failover targets
$targets = $this->services[$serviceId]['failover_targets'] ?? [];
$availableTargets = [];
foreach ($targets as $target) {
$isAvailable = $this->isServiceAvailable($target['id']);
$availableTargets[] = [
'target_id' => $target['id'],
'available' => $isAvailable,
'response_time' => $this->testResponseTime($target)
];
}
$results['tests']['failover_targets'] = $availableTargets;
// Test circuit breaker
if (isset($this->circuitBreakers[$serviceId])) {
$results['tests']['circuit_breaker'] = $this->circuitBreakers[$serviceId]->getStatus();
}
// Test failure detection
$results['tests']['failure_detection'] = [
'threshold' => $this->services[$serviceId]['failure_threshold'],
'current_failures' => $this->failoverStatus[$serviceId]['consecutive_failures']
];
return $results;
}
/**
* Export failover configuration.
*/
public function exportConfiguration(string $format = 'json'): string
{
$data = [
'services' => $this->services,
'failover_status' => $this->failoverStatus,
'failure_history' => $this->failureHistory,
'statistics' => $this->getStatistics(),
'exported_at' => date('Y-m-d H:i:s'),
'version' => $this->config['version'] ?? '1.0'
];
switch ($format) {
case 'json':
return json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
case 'php':
return '<?php return ' . var_export($data, true) . ';';
default:
throw new \InvalidArgumentException("Unsupported export format: {$format}");
}
}
/**
* Reset failover state for service.
*/
public function resetFailoverState(string $serviceId): bool
{
if (!isset($this->services[$serviceId])) {
return false;
}
// Reset status
$this->failoverStatus[$serviceId] = [
'status' => 'active',
'failures' => 0,
'consecutive_failures' => 0,
'last_failure' => null,
'last_success' => time(),
'current_target' => null,
'circuit_breaker_state' => 'closed'
];
// Reset circuit breaker
if (isset($this->circuitBreakers[$serviceId])) {
$this->circuitBreakers[$serviceId]->reset();
}
// Clear failure history
unset($this->failureHistory[$serviceId]);
$this->logInfo("Failover state reset for {$serviceId}");
return true;
}
/**
* Check if failover should be triggered.
*/
protected function shouldTriggerFailover(string $serviceId): bool
{
if (!isset($this->services[$serviceId])) {
return false;
}
$service = $this->services[$serviceId];
$status = $this->failoverStatus[$serviceId];
// Check if failover is enabled
if (!$service['failover_enabled']) {
return false;
}
// Check failure threshold
if ($status['consecutive_failures'] >= $service['failure_threshold']) {
return true;
}
// Check circuit breaker
if (isset($this->circuitBreakers[$serviceId])) {
return $this->circuitBreakers[$serviceId]->shouldTrip();
}
return false;
}
/**
* Record service failure.
*/
protected function recordFailure(string $serviceId, array $context): void
{
$status = &$this->failoverStatus[$serviceId];
$status['failures']++;
$status['consecutive_failures']++;
$status['last_failure'] = time();
// Update circuit breaker
if (isset($this->circuitBreakers[$serviceId])) {
$this->circuitBreakers[$serviceId]->recordFailure();
}
// Add to failure history
$this->failureHistory[$serviceId][] = [
'timestamp' => time(),
'context' => $context,
'consecutive_failures' => $status['consecutive_failures']
];
// Limit history size
$maxHistory = $this->config['max_failure_history'] ?? 1000;
if (count($this->failureHistory[$serviceId]) > $maxHistory) {
$this->failureHistory[$serviceId] = array_slice(
$this->failureHistory[$serviceId],
-$maxHistory
);
}
$this->logDebug("Failure recorded for {$serviceId}: {$status['consecutive_failures']} consecutive");
}
/**
* Initialize circuit breaker for service.
*/
protected function initializeCircuitBreaker(string $serviceId): void
{
$service = $this->services[$serviceId];
$this->circuitBreakers[$serviceId] = new CircuitBreaker([
'failure_threshold' => $service['failure_threshold'],
'recovery_threshold' => $service['recovery_threshold'],
'timeout' => $service['timeout']
]);
}
/**
* Attempt to recover service.
*/
protected function attemptRecovery(string $serviceId): void
{
$status = $this->failoverStatus[$serviceId];
$service = $this->services[$serviceId];
// Check recovery threshold
if ($status['consecutive_failures'] === 0 &&
(time() - $status['last_failure']) > $service['recovery_threshold']) {
$this->recoverService($serviceId);
$this->logInfo("Automatic recovery successful for {$serviceId}");
}
}
/**
* Test response time for target.
*/
protected function testResponseTime(array $target): float
{
$startTime = microtime(true);
try {
// Simple ping test - in real implementation, this would be more sophisticated
$context = stream_context_create([
'http' => [
'timeout' => 5,
'method' => 'GET'
]
]);
$url = ($target['protocol'] ?? 'http') . '://' . $target['host'] . ':' . $target['port'] . '/health';
@file_get_contents($url, false, $context);
return microtime(true) - $startTime;
} catch (\Exception $e) {
return PHP_FLOAT_MAX;
}
}
/**
* Initialize failover manager.
*/
protected function initialize(): void
{
// Load existing configuration from storage
$this->loadConfiguration();
// Start background tasks
if ($this->config['auto_recovery']) {
$this->startAutoRecovery();
}
$this->logInfo("Failover manager initialized");
}
/**
* Load configuration from storage.
*/
protected function loadConfiguration(): void
{
$config = $this->storage->loadConfiguration();
if (isset($config['services'])) {
$this->services = $config['services'];
}
if (isset($config['failover_status'])) {
$this->failoverStatus = $config['failover_status'];
}
$this->logInfo("Configuration loaded from storage");
}
/**
* Start auto-recovery task.
*/
protected function startAutoRecovery(): void
{
// This would typically be run as a background process
// For now, we'll just log that it would start
$this->logInfo("Auto-recovery task started");
}
/**
* Log info message.
*/
protected function logInfo(string $message): void
{
if ($this->config['logging_enabled']) {
error_log("[FailoverManager] {$message}");
}
}
/**
* Log warning message.
*/
protected function logWarning(string $message): void
{
if ($this->config['logging_enabled']) {
error_log("[FailoverManager] WARNING: {$message}");
}
}
/**
* Log debug message.
*/
protected function logDebug(string $message): void
{
if ($this->config['debug_enabled']) {
error_log("[FailoverManager] DEBUG: {$message}");
}
}
/**
* Get default configuration.
*/
protected function getDefaultConfig(): array
{
return [
'default_failure_threshold' => 3,
'default_recovery_threshold' => 60,
'default_timeout' => 30,
'default_retry_attempts' => 3,
'auto_recovery' => true,
'recovery_check_interval' => 60,
'max_failure_history' => 1000,
'logging_enabled' => true,
'debug_enabled' => false,
'storage' => [
'type' => 'file',
'path' => __DIR__ . '/../../../storage/failover'
],
'circuit_breaker' => [
'enabled' => true,
'open_timeout' => 60,
'half_open_max_calls' => 3
],
'version' => '1.0'
];
}
/**
* Get configuration.
*/
public function getConfig(): array
{
return $this->config;
}
/**
* Set configuration.
*/
public function setConfig(array $config): void
{
$this->config = array_merge($this->config, $config);
}
/**
* Create failover manager instance.
*/
public static function create(array $config = []): self
{
return new self($config);
}
/**
* Create for high availability.
*/
public static function forHighAvailability(): self
{
return new self([
'default_failure_threshold' => 2,
'default_recovery_threshold' => 30,
'auto_recovery' => true,
'recovery_check_interval' => 30,
'circuit_breaker' => [
'enabled' => true,
'open_timeout' => 30,
'half_open_max_calls' => 5
]
]);
}
/**
* Create for development.
*/
public static function forDevelopment(): self
{
return new self([
'default_failure_threshold' => 5,
'default_recovery_threshold' => 120,
'auto_recovery' => false,
'logging_enabled' => true,
'debug_enabled' => true
]);
}
}
/**
* Simple Circuit Breaker implementation.
*/
class CircuitBreaker
{
protected array $config;
protected string $state = 'closed'; // closed, open, half_open
protected int $failures = 0;
protected int $successes = 0;
protected float $lastFailureTime = 0;
protected float $lastStateChange = 0;
public function __construct(array $config = [])
{
$this->config = array_merge([
'failure_threshold' => 5,
'recovery_threshold' => 60,
'timeout' => 30,
'half_open_max_calls' => 3
], $config);
}
public function recordFailure(): void
{
$this->failures++;
$this->lastFailureTime = microtime(true);
if ($this->state === 'closed' && $this->failures >= $this->config['failure_threshold']) {
$this->open();
} elseif ($this->state === 'half_open') {
$this->open();
}
}
public function recordSuccess(): void
{
$this->successes++;
if ($this->state === 'half_open' && $this->successes >= $this->config['half_open_max_calls']) {
$this->close();
}
}
public function isOpen(): bool
{
if ($this->state === 'open') {
// Check if timeout has passed
if (microtime(true) - $this->lastStateChange > $this->config['timeout']) {
$this->halfOpen();
return true; // Still not fully open
}
return false;
}
return $this->state !== 'open';
}
public function shouldTrip(): bool
{
return $this->failures >= $this->config['failure_threshold'];
}
public function open(): void
{
$this->state = 'open';
$this->lastStateChange = microtime(true);
}
public function close(): void
{
$this->state = 'closed';
$this->failures = 0;
$this->successes = 0;
$this->lastStateChange = microtime(true);
}
public function halfOpen(): void
{
$this->state = 'half_open';
$this->successes = 0;
$this->lastStateChange = microtime(true);
}
public function reset(): void
{
$this->close();
}
public function getState(): string
{
return $this->state;
}
public function getStatus(): array
{
return [
'state' => $this->state,
'failures' => $this->failures,
'successes' => $this->successes,
'last_failure_time' => $this->lastFailureTime,
'last_state_change' => $this->lastStateChange
];
}
}

View File

@@ -0,0 +1,678 @@
<?php
declare(strict_types=1);
namespace Fendx\Service\LoadBalancer;
use Fendx\Service\LoadBalancer\Algorithm\RoundRobinAlgorithm;
use Fendx\Service\LoadBalancer\Algorithm\WeightedRoundRobinAlgorithm;
use Fendx\Service\LoadBalancer\Algorithm\LeastConnectionsAlgorithm;
use Fendx\Service\LoadBalancer\Algorithm\RandomAlgorithm;
use Fendx\Service\LoadBalancer\Algorithm\HashAlgorithm;
use Fendx\Service\LoadBalancer\Strategy\LoadBalancingStrategy;
use Fendx\Service\LoadBalancer\Health\HealthAwareBalancer;
class LoadBalancer
{
protected RoundRobinAlgorithm $roundRobin;
protected WeightedRoundRobinAlgorithm $weightedRoundRobin;
protected LeastConnectionsAlgorithm $leastConnections;
protected RandomAlgorithm $random;
protected HashAlgorithm $hash;
protected LoadBalancingStrategy $strategy;
protected HealthAwareBalancer $healthAware;
protected array $config = [];
protected array $services = [];
protected array $weights = [];
protected array $connections = [];
protected array $statistics = [];
public function __construct(array $config = [])
{
$this->config = array_merge($this->getDefaultConfig(), $config);
$this->roundRobin = new RoundRobinAlgorithm();
$this->weightedRoundRobin = new WeightedRoundRobinAlgorithm();
$this->leastConnections = new LeastConnectionsAlgorithm();
$this->random = new RandomAlgorithm();
$this->hash = new HashAlgorithm();
$this->strategy = new LoadBalancingStrategy($this->config);
$this->healthAware = new HealthAwareBalancer($this->config);
$this->initialize();
}
/**
* Select service instance using specified algorithm.
*/
public function select(array $instances, string $algorithm = 'round_robin', array $options = []): ?array
{
if (empty($instances)) {
return null;
}
// Filter healthy instances if health checking is enabled
if ($this->config['health_aware']) {
$instances = $this->healthAware->filterHealthy($instances);
if (empty($instances)) {
return null;
}
}
// Apply additional filters
$instances = $this->applyFilters($instances, $options);
if (empty($instances)) {
return null;
}
// Select instance using algorithm
$selected = $this->selectByAlgorithm($instances, $algorithm, $options);
if ($selected) {
// Update statistics
$this->updateStatistics($selected['id'], $algorithm);
// Update connection count if tracking is enabled
if ($this->config['track_connections']) {
$this->incrementConnections($selected['id']);
}
}
return $selected;
}
/**
* Select service instance with session affinity.
*/
public function selectWithAffinity(array $instances, string $sessionId, string $algorithm = 'round_robin'): ?array
{
if (empty($instances)) {
return null;
}
// Try to find existing session mapping
$mappedInstanceId = $this->getSessionMapping($sessionId);
if ($mappedInstanceId) {
foreach ($instances as $instance) {
if ($instance['id'] === $mappedInstanceId && $this->isInstanceAvailable($instance)) {
return $instance;
}
}
}
// Select new instance and map session
$selected = $this->select($instances, $algorithm);
if ($selected) {
$this->setSessionMapping($sessionId, $selected['id']);
}
return $selected;
}
/**
* Select service instance using consistent hashing.
*/
public function selectByHash(array $instances, string $key): ?array
{
if (empty($instances)) {
return null;
}
return $this->hash->select($instances, $key);
}
/**
* Select multiple service instances.
*/
public function selectMultiple(array $instances, int $count, string $algorithm = 'round_robin'): array
{
if (empty($instances) || $count <= 0) {
return [];
}
$selected = [];
$availableInstances = $instances;
for ($i = 0; $i < $count && !empty($availableInstances); $i++) {
$instance = $this->select($availableInstances, $algorithm);
if ($instance) {
$selected[] = $instance;
// Remove selected instance from available pool
$availableInstances = array_filter($availableInstances, function($inst) use ($instance) {
return $inst['id'] !== $instance['id'];
});
}
}
return $selected;
}
/**
* Set service weight.
*/
public function setWeight(string $serviceId, int $weight): void
{
if ($weight < 1) {
throw new \InvalidArgumentException("Weight must be at least 1");
}
$this->weights[$serviceId] = $weight;
// Update weighted round robin algorithm
$this->weightedRoundRobin->setWeight($serviceId, $weight);
}
/**
* Get service weight.
*/
public function getWeight(string $serviceId): int
{
return $this->weights[$serviceId] ?? 1;
}
/**
* Set multiple service weights.
*/
public function setWeights(array $weights): void
{
foreach ($weights as $serviceId => $weight) {
$this->setWeight($serviceId, $weight);
}
}
/**
* Get all weights.
*/
public function getWeights(): array
{
return $this->weights;
}
/**
* Set service priority.
*/
public function setPriority(string $serviceId, int $priority): void
{
if (!isset($this->services[$serviceId])) {
$this->services[$serviceId] = [];
}
$this->services[$serviceId]['priority'] = $priority;
}
/**
* Get service priority.
*/
public function getPriority(string $serviceId): int
{
return $this->services[$serviceId]['priority'] ?? 0;
}
/**
* Get connection count for service.
*/
public function getConnections(string $serviceId): int
{
return $this->connections[$serviceId] ?? 0;
}
/**
* Increment connection count.
*/
public function incrementConnections(string $serviceId): void
{
$this->connections[$serviceId] = ($this->connections[$serviceId] ?? 0) + 1;
}
/**
* Decrement connection count.
*/
public function decrementConnections(string $serviceId): void
{
if (isset($this->connections[$serviceId])) {
$this->connections[$serviceId] = max(0, $this->connections[$serviceId] - 1);
}
}
/**
* Reset connection count.
*/
public function resetConnections(string $serviceId): void
{
$this->connections[$serviceId] = 0;
}
/**
* Get load balancing statistics.
*/
public function getStatistics(): array
{
$totalSelections = 0;
$algorithmStats = [];
foreach ($this->statistics as $serviceId => $stats) {
$totalSelections += $stats['total_selections'];
foreach ($stats['algorithms'] as $algorithm => $count) {
$algorithmStats[$algorithm] = ($algorithmStats[$algorithm] ?? 0) + $count;
}
}
return [
'total_selections' => $totalSelections,
'algorithm_distribution' => $algorithmStats,
'service_statistics' => $this->statistics,
'current_connections' => $this->connections,
'weights' => $this->weights,
'health_aware' => $this->config['health_aware'],
'track_connections' => $this->config['track_connections']
];
}
/**
* Get service-specific statistics.
*/
public function getServiceStatistics(string $serviceId): array
{
return $this->statistics[$serviceId] ?? [
'total_selections' => 0,
'algorithms' => [],
'first_selected' => null,
'last_selected' => null
];
}
/**
* Reset statistics.
*/
public function resetStatistics(): void
{
$this->statistics = [];
}
/**
* Reset service statistics.
*/
public function resetServiceStatistics(string $serviceId): void
{
unset($this->statistics[$serviceId]);
}
/**
* Enable/disable health awareness.
*/
public function setHealthAware(bool $enabled): void
{
$this->config['health_aware'] = $enabled;
}
/**
* Check if health awareness is enabled.
*/
public function isHealthAware(): bool
{
return $this->config['health_aware'];
}
/**
* Enable/disable connection tracking.
*/
public function setTrackConnections(bool $enabled): void
{
$this->config['track_connections'] = $enabled;
}
/**
* Check if connection tracking is enabled.
*/
public function isTrackingConnections(): bool
{
return $this->config['track_connections'];
}
/**
* Mark instance as healthy/unhealthy.
*/
public function setInstanceHealth(string $serviceId, bool $healthy): void
{
$this->healthAware->setHealth($serviceId, $healthy);
}
/**
* Check if instance is healthy.
*/
public function isInstanceHealthy(string $serviceId): bool
{
return $this->healthAware->isHealthy($serviceId);
}
/**
* Get all healthy instances.
*/
public function getHealthyInstances(array $instances): array
{
return $this->healthAware->filterHealthy($instances);
}
/**
* Set session mapping for affinity.
*/
protected function setSessionMapping(string $sessionId, string $serviceId): void
{
if (!$this->config['session_affinity']) {
return;
}
$_SESSION['load_balancer_affinity'][$sessionId] = $serviceId;
}
/**
* Get session mapping for affinity.
*/
protected function getSessionMapping(string $sessionId): ?string
{
if (!$this->config['session_affinity']) {
return null;
}
return $_SESSION['load_balancer_affinity'][$sessionId] ?? null;
}
/**
* Check if instance is available.
*/
protected function isInstanceAvailable(array $instance): bool
{
// Check if enabled
if (isset($instance['enabled']) && !$instance['enabled']) {
return false;
}
// Check health if health awareness is enabled
if ($this->config['health_aware'] && !$this->healthAware->isHealthy($instance['id'])) {
return false;
}
// Check connection limit if configured
if (isset($instance['max_connections']) &&
$this->getConnections($instance['id']) >= $instance['max_connections']) {
return false;
}
return true;
}
/**
* Apply filters to instances.
*/
protected function applyFilters(array $instances, array $options): array
{
$filtered = $instances;
// Filter by tags
if (isset($options['tags']) && !empty($options['tags'])) {
$filtered = array_filter($filtered, function($instance) use ($options) {
$instanceTags = $instance['tags'] ?? [];
return count(array_intersect($options['tags'], $instanceTags)) > 0;
});
}
// Filter by region
if (isset($options['region'])) {
$filtered = array_filter($filtered, function($instance) use ($options) {
return ($instance['region'] ?? null) === $options['region'];
});
}
// Filter by zone
if (isset($options['zone'])) {
$filtered = array_filter($filtered, function($instance) use ($options) {
return ($instance['zone'] ?? null) === $options['zone'];
});
}
// Filter by protocol
if (isset($options['protocol'])) {
$filtered = array_filter($filtered, function($instance) use ($options) {
return ($instance['protocol'] ?? 'http') === $options['protocol'];
});
}
// Filter by minimum weight
if (isset($options['min_weight'])) {
$filtered = array_filter($filtered, function($instance) use ($options) {
$weight = $this->getWeight($instance['id']);
return $weight >= $options['min_weight'];
});
}
// Filter by priority
if (isset($options['priority'])) {
$filtered = array_filter($filtered, function($instance) use ($options) {
$priority = $this->getPriority($instance['id']);
return $priority >= $options['priority'];
});
}
// Sort by priority if specified
if (isset($options['sort_by_priority']) && $options['sort_by_priority']) {
usort($filtered, function($a, $b) {
$priorityA = $this->getPriority($a['id']);
$priorityB = $this->getPriority($b['id']);
return $priorityB <=> $priorityA;
});
}
return array_values($filtered);
}
/**
* Select instance by algorithm.
*/
protected function selectByAlgorithm(array $instances, string $algorithm, array $options = []): ?array
{
switch ($algorithm) {
case 'round_robin':
return $this->roundRobin->select($instances);
case 'weighted_round_robin':
return $this->weightedRoundRobin->select($instances, $this->weights);
case 'least_connections':
return $this->leastConnections->select($instances, $this->connections);
case 'random':
return $this->random->select($instances);
case 'hash':
$key = $options['hash_key'] ?? 'default';
return $this->hash->select($instances, $key);
case 'weighted_random':
return $this->selectWeightedRandom($instances);
case 'ip_hash':
$clientIp = $options['client_ip'] ?? $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1';
return $this->hash->select($instances, $clientIp);
case 'uri_hash':
$uri = $options['uri'] ?? $_SERVER['REQUEST_URI'] ?? '/';
return $this->hash->select($instances, $uri);
default:
throw new \InvalidArgumentException("Unknown load balancing algorithm: {$algorithm}");
}
}
/**
* Select instance using weighted random algorithm.
*/
protected function selectWeightedRandom(array $instances): ?array
{
if (empty($instances)) {
return null;
}
$totalWeight = 0;
$weightedInstances = [];
foreach ($instances as $instance) {
$weight = $this->getWeight($instance['id']);
$totalWeight += $weight;
for ($i = 0; $i < $weight; $i++) {
$weightedInstances[] = $instance;
}
}
if (empty($weightedInstances)) {
return $instances[0];
}
return $weightedInstances[array_rand($weightedInstances)];
}
/**
* Update selection statistics.
*/
protected function updateStatistics(string $serviceId, string $algorithm): void
{
if (!isset($this->statistics[$serviceId])) {
$this->statistics[$serviceId] = [
'total_selections' => 0,
'algorithms' => [],
'first_selected' => time(),
'last_selected' => null
];
}
$this->statistics[$serviceId]['total_selections']++;
$this->statistics[$serviceId]['algorithms'][$algorithm] =
($this->statistics[$serviceId]['algorithms'][$algorithm] ?? 0) + 1;
$this->statistics[$serviceId]['last_selected'] = time();
}
/**
* Initialize load balancer.
*/
protected function initialize(): void
{
// Initialize session for affinity
if ($this->config['session_affinity'] && session_status() === PHP_SESSION_NONE) {
session_start();
}
// Initialize health aware balancer
$this->healthAware->initialize();
$this->logInfo("Load balancer initialized");
}
/**
* Log info message.
*/
protected function logInfo(string $message): void
{
if ($this->config['logging_enabled']) {
error_log("[LoadBalancer] {$message}");
}
}
/**
* Get default configuration.
*/
protected function getDefaultConfig(): array
{
return [
'default_algorithm' => 'round_robin',
'health_aware' => true,
'track_connections' => true,
'session_affinity' => false,
'session_timeout' => 1800, // 30 minutes
'logging_enabled' => true,
'health_check' => [
'enabled' => true,
'interval' => 30,
'timeout' => 5,
'unhealthy_threshold' => 3,
'healthy_threshold' => 2
],
'algorithms' => [
'round_robin' => true,
'weighted_round_robin' => true,
'least_connections' => true,
'random' => true,
'hash' => true,
'weighted_random' => true,
'ip_hash' => true,
'uri_hash' => true
]
];
}
/**
* Get configuration.
*/
public function getConfig(): array
{
return $this->config;
}
/**
* Set configuration.
*/
public function setConfig(array $config): void
{
$this->config = array_merge($this->config, $config);
}
/**
* Create load balancer instance.
*/
public static function create(array $config = []): self
{
return new self($config);
}
/**
* Create for high availability.
*/
public static function forHighAvailability(): self
{
return new self([
'default_algorithm' => 'least_connections',
'health_aware' => true,
'track_connections' => true,
'session_affinity' => true,
'health_check' => [
'enabled' => true,
'interval' => 10,
'timeout' => 3,
'unhealthy_threshold' => 2,
'healthy_threshold' => 2
]
]);
}
/**
* Create for performance.
*/
public static function forPerformance(): self
{
return new self([
'default_algorithm' => 'round_robin',
'health_aware' => false,
'track_connections' => false,
'session_affinity' => false,
'logging_enabled' => false
]);
}
/**
* Create for development.
*/
public static function forDevelopment(): self
{
return new self([
'default_algorithm' => 'random',
'health_aware' => false,
'track_connections' => true,
'session_affinity' => false,
'logging_enabled' => true
]);
}
}

View File

@@ -0,0 +1,545 @@
<?php
declare(strict_types=1);
namespace Fendx\Service\LoadBalancer;
/**
* 智能负载均衡器
* 支持多种负载均衡策略和自适应优化
*/
class SmartLoadBalancer
{
private array $strategies = [
'round_robin' => RoundRobinStrategy::class,
'weighted_round_robin' => WeightedRoundRobinStrategy::class,
'least_connections' => LeastConnectionsStrategy::class,
'least_response_time' => LeastResponseTimeStrategy::class,
'consistent_hash' => ConsistentHashStrategy::class,
'adaptive' => AdaptiveStrategy::class,
'maglev' => MaglevStrategy::class,
'ring_hash' => RingHashStrategy::class,
];
private LoadBalanceStrategy $currentStrategy;
private HealthChecker $healthChecker;
private MetricsCollector $metrics;
private array $instances = [];
private array $instanceStats = [];
public function __construct(
private string $strategy = 'adaptive',
array $config = []
) {
$this->currentStrategy = new $this->strategies[$strategy]();
$this->healthChecker = new HealthChecker($config['health'] ?? []);
$this->metrics = new MetricsCollector($config['metrics'] ?? []);
}
/**
* 选择实例
*/
public function select(array $instances, ?string $key = null): ?Instance
{
$this->instances = $this->filterHealthyInstances($instances);
if (empty($this->instances)) {
return null;
}
$selected = $this->currentStrategy->select($this->instances, $key);
if ($selected) {
$this->recordSelection($selected);
}
return $selected;
}
/**
* 添加实例
*/
public function addInstance(Instance $instance): void
{
$this->instances[] = $instance;
$this->instanceStats[$instance->getId()] = [
'requests' => 0,
'failures' => 0,
'total_response_time' => 0.0,
'last_used' => 0,
'created_at' => time(),
];
}
/**
* 移除实例
*/
public function removeInstance(string $instanceId): void
{
$this->instances = array_filter(
$this->instances,
fn($instance) => $instance->getId() !== $instanceId
);
unset($this->instanceStats[$instanceId]);
}
/**
* 更新实例统计
*/
public function updateInstanceStats(string $instanceId, float $responseTime, bool $success): void
{
if (!isset($this->instanceStats[$instanceId])) {
return;
}
$stats = &$this->instanceStats[$instanceId];
$stats['requests']++;
$stats['last_used'] = time();
if ($success) {
$stats['total_response_time'] += $responseTime;
} else {
$stats['failures']++;
}
// 自适应策略优化
if ($this->strategy === 'adaptive') {
$this->optimizeStrategy();
}
}
/**
* 过滤健康实例
*/
private function filterHealthyInstances(array $instances): array
{
return array_filter($instances, function($instance) {
return $this->healthChecker->isHealthy($instance);
});
}
/**
* 记录选择
*/
private function recordSelection(Instance $instance): void
{
$this->metrics->increment('load_balancer.selections', [
'instance' => $instance->getId(),
'strategy' => $this->strategy,
]);
}
/**
* 自适应策略优化
*/
private function optimizeStrategy(): void
{
$performance = $this->calculateStrategyPerformance();
// 根据性能指标动态调整策略
if ($performance['avg_response_time'] > 1000) {
// 响应时间过长,切换到最快响应策略
$this->switchStrategy('least_response_time');
} elseif ($performance['failure_rate'] > 0.05) {
// 失败率高,切换到最少连接策略
$this->switchStrategy('least_connections');
} elseif ($performance['imbalance_score'] > 0.3) {
// 负载不均衡,切换到加权轮询
$this->switchStrategy('weighted_round_robin');
}
}
/**
* 计算策略性能
*/
private function calculateStrategyPerformance(): array
{
$totalRequests = array_sum(array_column($this->instanceStats, 'requests'));
$totalFailures = array_sum(array_column($this->instanceStats, 'failures'));
$totalResponseTime = array_sum(array_column($this->instanceStats, 'total_response_time'));
$successRequests = $totalRequests - $totalFailures;
$avgResponseTime = $successRequests > 0 ? $totalResponseTime / $successRequests : 0;
$failureRate = $totalRequests > 0 ? $totalFailures / $totalRequests : 0;
// 计算负载不均衡分数
$requestsPerInstance = array_column($this->instanceStats, 'requests');
$avgRequests = array_sum($requestsPerInstance) / count($requestsPerInstance);
$variance = array_sum(array_map(
fn($r) => pow($r - $avgRequests, 2),
$requestsPerInstance
)) / count($requestsPerInstance);
$imbalanceScore = sqrt($variance) / $avgRequests;
return [
'avg_response_time' => $avgResponseTime,
'failure_rate' => $failureRate,
'imbalance_score' => $imbalanceScore,
];
}
/**
* 切换策略
*/
private function switchStrategy(string $newStrategy): void
{
if ($newStrategy !== $this->strategy && isset($this->strategies[$newStrategy])) {
$this->strategy = $newStrategy;
$this->currentStrategy = new $this->strategies[$newStrategy]();
$this->metrics->increment('load_balancer.strategy_switch', [
'from' => $this->strategy,
'to' => $newStrategy,
]);
}
}
/**
* 获取实例统计
*/
public function getInstanceStats(): array
{
return $this->instanceStats;
}
/**
* 获取负载均衡器状态
*/
public function getStatus(): array
{
return [
'strategy' => $this->strategy,
'total_instances' => count($this->instances),
'healthy_instances' => count($this->filterHealthyInstances($this->instances)),
'performance' => $this->calculateStrategyPerformance(),
'metrics' => $this->metrics->getSummary(),
];
}
}
/**
* 负载均衡策略接口
*/
interface LoadBalanceStrategy
{
public function select(array $instances, ?string $key = null): ?Instance;
}
/**
* 自适应负载均衡策略
*/
class AdaptiveStrategy implements LoadBalanceStrategy
{
private LeastResponseTimeStrategy $responseTimeStrategy;
private LeastConnectionsStrategy $connectionsStrategy;
private WeightedRoundRobinStrategy $weightedStrategy;
public function __construct()
{
$this->responseTimeStrategy = new LeastResponseTimeStrategy();
$this->connectionsStrategy = new LeastConnectionsStrategy();
$this->weightedStrategy = new WeightedRoundRobinStrategy();
}
public function select(array $instances, ?string $key = null): ?Instance
{
// 根据当前系统状态选择最优策略
$systemLoad = $this->getSystemLoad();
if ($systemLoad['cpu'] > 80) {
// CPU 高负载,使用最少连接策略
return $this->connectionsStrategy->select($instances, $key);
} elseif ($systemLoad['memory'] > 80) {
// 内存高负载,使用最快响应策略
return $this->responseTimeStrategy->select($instances, $key);
} else {
// 正常负载,使用加权轮询
return $this->weightedStrategy->select($instances, $key);
}
}
private function getSystemLoad(): array
{
return [
'cpu' => sys_getloadavg()[0] ?? 0,
'memory' => memory_get_usage() / memory_get_peak_usage() * 100,
];
}
}
/**
* Maglev 哈希策略
* Google 开发的一致性哈希算法,适合大规模分布式系统
*/
class MaglevStrategy implements LoadBalanceStrategy
{
private array $lookupTable = [];
private int $tableSize = 65537;
public function select(array $instances, ?string $key = null): ?Instance
{
if (empty($instances)) {
return null;
}
if (empty($this->lookupTable)) {
$this->buildLookupTable($instances);
}
if ($key === null) {
$key = (string) random_int(0, PHP_INT_MAX);
}
$hash = crc32($key);
$index = $hash % $this->tableSize;
return $this->lookupTable[$index] ?? null;
}
private function buildLookupTable(array $instances): void
{
$this->lookupTable = [];
$permutation = [];
foreach ($instances as $i => $instance) {
$offset = crc32($instance->getId() . 'offset') % $this->tableSize;
$skip = crc32($instance->getId() . 'skip') % (count($instances) - 1) + 1;
$permutation[$i] = [];
for ($j = 0; $j < $this->tableSize; $j++) {
$permutation[$i][$j] = ($offset + $j * $skip) % $this->tableSize;
}
}
// 填充查找表
for ($j = 0; $j < $this->tableSize; $j++) {
foreach ($instances as $i => $instance) {
$candidate = $permutation[$i][$j];
if (!isset($this->lookupTable[$candidate])) {
$this->lookupTable[$candidate] = $instance;
}
}
}
}
}
/**
* 环形哈希策略
*/
class RingHashStrategy implements LoadBalanceStrategy
{
private int $virtualNodes = 150;
public function select(array $instances, ?string $key = null): ?Instance
{
if (empty($instances)) {
return null;
}
$ring = $this->buildRing($instances);
if ($key === null) {
$key = (string) random_int(0, PHP_INT_MAX);
}
$hash = crc32($key);
// 顺时针查找第一个节点
foreach ($ring as $nodeHash => $instance) {
if ($nodeHash >= $hash) {
return $instance;
}
}
// 环形查找,返回第一个节点
return reset($ring);
}
private function buildRing(array $instances): array
{
$ring = [];
foreach ($instances as $instance) {
for ($i = 0; $i < $this->virtualNodes; $i++) {
$virtualKey = $instance->getId() . ':' . $i;
$hash = crc32($virtualKey);
$ring[$hash] = $instance;
}
}
ksort($ring);
return $ring;
}
}
/**
* 健康检查器
*/
class HealthChecker
{
private array $healthStatus = [];
private int $checkInterval = 30;
private int $timeout = 5;
public function __construct(array $config = [])
{
$this->checkInterval = $config['interval'] ?? 30;
$this->timeout = $config['timeout'] ?? 5;
}
public function isHealthy(Instance $instance): bool
{
$instanceId = $instance->getId();
if (!isset($this->healthStatus[$instanceId])) {
$this->healthStatus[$instanceId] = $this->performHealthCheck($instance);
}
return $this->healthStatus[$instanceId]['healthy'];
}
private function performHealthCheck(Instance $instance): array
{
$startTime = microtime(true);
$healthy = false;
$error = null;
try {
$context = stream_context_create([
'http' => [
'timeout' => $this->timeout,
'method' => 'GET',
],
]);
$url = $instance->getHealthUrl();
$response = @file_get_contents($url, false, $context);
if ($response !== false && strpos($response, 'OK') !== false) {
$healthy = true;
}
} catch (\Exception $e) {
$error = $e->getMessage();
}
return [
'healthy' => $healthy,
'response_time' => microtime(true) - $startTime,
'last_check' => time(),
'error' => $error,
];
}
}
/**
* 指标收集器
*/
class MetricsCollector
{
private array $counters = [];
private array $gauges = [];
private array $histograms = [];
public function increment(string $name, array $tags = []): void
{
$key = $this->buildKey($name, $tags);
$this->counters[$key] = ($this->counters[$key] ?? 0) + 1;
}
public function gauge(string $name, float $value, array $tags = []): void
{
$key = $this->buildKey($name, $tags);
$this->gauges[$key] = $value;
}
public function histogram(string $name, float $value, array $tags = []): void
{
$key = $this->buildKey($name, $tags);
if (!isset($this->histograms[$key])) {
$this->histograms[$key] = [];
}
$this->histograms[$key][] = $value;
}
public function getSummary(): array
{
return [
'counters' => $this->counters,
'gauges' => $this->gauges,
'histograms' => array_map(
fn($values) => [
'count' => count($values),
'sum' => array_sum($values),
'avg' => array_sum($values) / count($values),
'min' => min($values),
'max' => max($values),
],
$this->histograms
),
];
}
private function buildKey(string $name, array $tags): string
{
if (empty($tags)) {
return $name;
}
ksort($tags);
$tagString = implode(',', array_map(
fn($k, $v) => "$k=$v",
array_keys($tags),
$tags
));
return $name . '{' . $tagString . '}';
}
}
/**
* 实例类
*/
class Instance
{
public function __construct(
private string $id,
private string $host,
private int $port,
private int $weight = 1,
private array $metadata = []
) {}
public function getId(): string
{
return $this->id;
}
public function getHost(): string
{
return $this->host;
}
public function getPort(): int
{
return $this->port;
}
public function getWeight(): int
{
return $this->weight;
}
public function getMetadata(): array
{
return $this->metadata;
}
public function getHealthUrl(): string
{
return "http://{$this->host}:{$this->port}/health";
}
public function __toString(): string
{
return "{$this->host}:{$this->port}";
}
}

View File

@@ -0,0 +1,857 @@
<?php
declare(strict_types=1);
namespace Fendx\Service\LoadBalancer\Strategy;
use Fendx\Service\LoadBalancer\Strategy\Router\TrafficRouter;
use Fendx\Service\LoadBalancer\Strategy\Analyzer\TrafficAnalyzer;
use Fendx\Service\LoadBalancer\Strategy\Balancer\TrafficBalancer;
use Fendx\Service\LoadBalancer\Strategy\Monitor\TrafficMonitor;
class TrafficDistributionStrategy
{
protected TrafficRouter $router;
protected TrafficAnalyzer $analyzer;
protected TrafficBalancer $balancer;
protected TrafficMonitor $monitor;
protected array $config = [];
protected array $rules = [];
protected array $trafficStats = [];
protected array $distributionHistory = [];
public function __construct(array $config = [])
{
$this->config = array_merge($this->getDefaultConfig(), $config);
$this->router = new TrafficRouter($this->config);
$this->analyzer = new TrafficAnalyzer($this->config);
$this->balancer = new TrafficBalancer($this->config);
$this->monitor = new TrafficMonitor($this->config);
$this->initialize();
}
/**
* Distribute traffic to services based on strategy.
*/
public function distributeTraffic(array $instances, array $request, string $strategy = 'weighted'): ?array
{
if (empty($instances)) {
return null;
}
// Analyze request
$requestAnalysis = $this->analyzer->analyzeRequest($request);
// Apply routing rules
$filteredInstances = $this->applyRoutingRules($instances, $requestAnalysis);
if (empty($filteredInstances)) {
return null;
}
// Select instance based on strategy
$selectedInstance = $this->selectInstance($filteredInstances, $requestAnalysis, $strategy);
if ($selectedInstance) {
// Record traffic distribution
$this->recordTrafficDistribution($selectedInstance['id'], $requestAnalysis, $strategy);
// Update monitoring
$this->monitor->recordRequest($selectedInstance['id'], $requestAnalysis);
}
return $selectedInstance;
}
/**
* Add traffic distribution rule.
*/
public function addRule(string $name, array $rule): void
{
$this->validateRule($rule);
$this->rules[$name] = array_merge([
'name' => $name,
'enabled' => true,
'priority' => 100,
'conditions' => [],
'actions' => [],
'weight' => 1,
'created_at' => time(),
'updated_at' => time()
], $rule);
// Sort rules by priority
uasort($this->rules, function($a, $b) {
return $a['priority'] <=> $b['priority'];
});
$this->logInfo("Traffic distribution rule added: {$name}");
}
/**
* Remove traffic distribution rule.
*/
public function removeRule(string $name): bool
{
if (!isset($this->rules[$name])) {
return false;
}
unset($this->rules[$name]);
$this->logInfo("Traffic distribution rule removed: {$name}");
return true;
}
/**
* Update traffic distribution rule.
*/
public function updateRule(string $name, array $updates): bool
{
if (!isset($this->rules[$name])) {
return false;
}
$this->rules[$name] = array_merge($this->rules[$name], $updates);
$this->rules[$name]['updated_at'] = time();
$this->logInfo("Traffic distribution rule updated: {$name}");
return true;
}
/**
* Get all rules.
*/
public function getRules(): array
{
return $this->rules;
}
/**
* Get rule by name.
*/
public function getRule(string $name): ?array
{
return $this->rules[$name] ?? null;
}
/**
* Enable/disable rule.
*/
public function setRuleEnabled(string $name, bool $enabled): bool
{
if (!isset($this->rules[$name])) {
return false;
}
$this->rules[$name]['enabled'] = $enabled;
$this->rules[$name]['updated_at'] = time();
$this->logInfo("Rule " . ($enabled ? 'enabled' : 'disabled') . ": {$name}");
return true;
}
/**
* Set traffic weights for services.
*/
public function setTrafficWeights(array $weights): void
{
$this->balancer->setWeights($weights);
$this->logInfo("Traffic weights updated for " . count($weights) . " services");
}
/**
* Get traffic weights.
*/
public function getTrafficWeights(): array
{
return $this->balancer->getWeights();
}
/**
* Distribute traffic by percentage.
*/
public function distributeByPercentage(array $instances, array $percentages): ?array
{
if (empty($instances) || empty($percentages)) {
return null;
}
// Validate percentages sum to 100
$totalPercentage = array_sum($percentages);
if (abs($totalPercentage - 100) > 0.01) {
throw new \InvalidArgumentException("Percentages must sum to 100, got: {$totalPercentage}");
}
// Select instance based on percentage distribution
return $this->balancer->selectByPercentage($instances, $percentages);
}
/**
* Distribute traffic by geographic location.
*/
public function distributeByGeography(array $instances, string $clientLocation): ?array
{
if (empty($instances)) {
return null;
}
// Find closest instances
$closestInstances = $this->findClosestInstances($instances, $clientLocation);
if (empty($closestInstances)) {
// Fallback to any instance
return $instances[0];
}
// Select from closest instances using weighted random
return $this->balancer->selectWeightedRandom($closestInstances);
}
/**
* Distribute traffic by user segment.
*/
public function distributeBySegment(array $instances, string $userSegment): ?array
{
if (empty($instances)) {
return null;
}
// Filter instances by segment
$segmentInstances = array_filter($instances, function($instance) use ($userSegment) {
$segments = $instance['segments'] ?? [];
return in_array($userSegment, $segments) || in_array('all', $segments);
});
if (empty($segmentInstances)) {
// Fallback to instances without segment restriction
$segmentInstances = array_filter($instances, function($instance) {
$segments = $instance['segments'] ?? [];
return empty($segments) || in_array('all', $segments);
});
}
if (empty($segmentInstances)) {
return $instances[0]; // Ultimate fallback
}
return $this->balancer->selectRoundRobin(array_values($segmentInstances));
}
/**
* Distribute traffic by time of day.
*/
public function distributeByTime(array $instances, \DateTime $dateTime = null): ?array
{
$dateTime = $dateTime ?? new \DateTime();
$hour = (int) $dateTime->format('H');
if (empty($instances)) {
return null;
}
// Filter instances by time availability
$availableInstances = array_filter($instances, function($instance) use ($hour) {
$availability = $instance['availability'] ?? [];
if (empty($availability)) {
return true; // Always available
}
foreach ($availability as $period) {
if ($hour >= $period['start'] && $hour < $period['end']) {
return true;
}
}
return false;
});
if (empty($availableInstances)) {
return $instances[0]; // Fallback
}
return $this->balancer->selectWeightedRandom(array_values($availableInstances));
}
/**
* Distribute traffic by load capacity.
*/
public function distributeByCapacity(array $instances): ?array
{
if (empty($instances)) {
return null;
}
// Calculate available capacity for each instance
$capacityScores = [];
foreach ($instances as $instance) {
$maxCapacity = $instance['max_capacity'] ?? 100;
$currentLoad = $instance['current_load'] ?? 0;
$availableCapacity = max(0, $maxCapacity - $currentLoad);
$capacityScores[$instance['id']] = $availableCapacity;
}
// Select instance with most available capacity
$maxCapacity = max($capacityScores);
$bestInstanceIds = array_keys($capacityScores, $maxCapacity);
if (count($bestInstanceIds) === 1) {
$bestInstanceId = $bestInstanceIds[0];
} else {
// Multiple instances with same capacity, use round-robin
$bestInstanceId = $bestInstanceIds[array_rand($bestInstanceIds)];
}
foreach ($instances as $instance) {
if ($instance['id'] === $bestInstanceId) {
return $instance;
}
}
return null;
}
/**
* Get traffic distribution statistics.
*/
public function getStatistics(): array
{
$totalRequests = 0;
$serviceStats = [];
$ruleStats = [];
// Calculate service statistics
foreach ($this->trafficStats as $serviceId => $stats) {
$requests = $stats['total_requests'] ?? 0;
$totalRequests += $requests;
$serviceStats[$serviceId] = [
'total_requests' => $requests,
'percentage' => 0, // Will be calculated below
'average_response_time' => $this->calculateAverageResponseTime($serviceId),
'error_rate' => $this->calculateErrorRate($serviceId),
'last_request' => $stats['last_request'] ?? null
];
}
// Calculate percentages
if ($totalRequests > 0) {
foreach ($serviceStats as $serviceId => &$stats) {
$stats['percentage'] = ($stats['total_requests'] / $totalRequests) * 100;
}
}
// Calculate rule statistics
foreach ($this->rules as $ruleName => $rule) {
$ruleStats[$ruleName] = [
'enabled' => $rule['enabled'],
'priority' => $rule['priority'],
'match_count' => $rule['match_count'] ?? 0,
'last_matched' => $rule['last_matched'] ?? null
];
}
return [
'total_requests' => $totalRequests,
'service_statistics' => $serviceStats,
'rule_statistics' => $ruleStats,
'active_rules' => count(array_filter($this->rules, fn($r) => $r['enabled'])),
'distribution_history' => array_slice($this->distributionHistory, -10)
];
}
/**
* Get traffic distribution history.
*/
public function getDistributionHistory(int $limit = 100): array
{
return array_slice($this->distributionHistory, -$limit);
}
/**
* Get real-time traffic metrics.
*/
public function getRealTimeMetrics(): array
{
return [
'current_rps' => $this->monitor->getCurrentRPS(),
'active_connections' => $this->monitor->getActiveConnections(),
'average_response_time' => $this->monitor->getAverageResponseTime(),
'error_rate' => $this->monitor->getErrorRate(),
'top_services' => $this->monitor->getTopServices(10),
'geographic_distribution' => $this->monitor->getGeographicDistribution(),
'user_segment_distribution' => $this->monitor->getSegmentDistribution()
];
}
/**
* Optimize traffic distribution.
*/
public function optimizeDistribution(): array
{
$recommendations = [];
$currentStats = $this->getStatistics();
// Analyze service performance
foreach ($currentStats['service_statistics'] as $serviceId => $stats) {
if ($stats['error_rate'] > 5) {
$recommendations[] = [
'type' => 'high_error_rate',
'service_id' => $serviceId,
'message' => "Service {$serviceId} has high error rate: {$stats['error_rate']}%",
'action' => 'consider_reducing_weight_or_failing_over'
];
}
if ($stats['average_response_time'] > 1000) { // 1 second
$recommendations[] = [
'type' => 'slow_response',
'service_id' => $serviceId,
'message' => "Service {$serviceId} has slow response time: {$stats['average_response_time']}ms",
'action' => 'consider_reducing_weight'
];
}
}
// Analyze rule effectiveness
foreach ($currentStats['rule_statistics'] as $ruleName => $stats) {
if ($stats['enabled'] && $stats['match_count'] === 0) {
$recommendations[] = [
'type' => 'unused_rule',
'rule_name' => $ruleName,
'message' => "Rule {$ruleName} is enabled but never matches",
'action' => 'review_or_disable_rule'
];
}
}
// Suggest weight adjustments
$weightSuggestions = $this->suggestWeightAdjustments();
$recommendations = array_merge($recommendations, $weightSuggestions);
return $recommendations;
}
/**
* Reset traffic statistics.
*/
public function resetStatistics(): void
{
$this->trafficStats = [];
$this->distributionHistory = [];
foreach ($this->rules as &$rule) {
$rule['match_count'] = 0;
$rule['last_matched'] = null;
}
$this->logInfo("Traffic statistics reset");
}
/**
* Export traffic configuration.
*/
public function exportConfiguration(string $format = 'json'): string
{
$data = [
'rules' => $this->rules,
'traffic_weights' => $this->getTrafficWeights(),
'statistics' => $this->getStatistics(),
'distribution_history' => $this->distributionHistory,
'exported_at' => date('Y-m-d H:i:s'),
'version' => $this->config['version'] ?? '1.0'
];
switch ($format) {
case 'json':
return json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
case 'php':
return '<?php return ' . var_export($data, true) . ';';
default:
throw new \InvalidArgumentException("Unsupported export format: {$format}");
}
}
/**
* Apply routing rules to instances.
*/
protected function applyRoutingRules(array $instances, array $requestAnalysis): array
{
$filteredInstances = $instances;
foreach ($this->rules as $rule) {
if (!$rule['enabled']) {
continue;
}
if ($this->matchesRuleConditions($rule['conditions'], $requestAnalysis)) {
$rule['match_count'] = ($rule['match_count'] ?? 0) + 1;
$rule['last_matched'] = time();
// Apply rule actions
$filteredInstances = $this->applyRuleActions($filteredInstances, $rule['actions']);
// If rule has stop_processing flag, break
if ($rule['stop_processing'] ?? false) {
break;
}
}
}
return $filteredInstances;
}
/**
* Check if request matches rule conditions.
*/
protected function matchesRuleConditions(array $conditions, array $requestAnalysis): bool
{
foreach ($conditions as $condition) {
if (!$this->matchesCondition($condition, $requestAnalysis)) {
return false;
}
}
return true;
}
/**
* Check if single condition matches.
*/
protected function matchesCondition(array $condition, array $requestAnalysis): bool
{
$field = $condition['field'];
$operator = $condition['operator'];
$value = $condition['value'];
$requestValue = $this->getRequestValue($requestAnalysis, $field);
switch ($operator) {
case 'equals':
return $requestValue === $value;
case 'not_equals':
return $requestValue !== $value;
case 'in':
return in_array($requestValue, (array) $value);
case 'not_in':
return !in_array($requestValue, (array) $value);
case 'contains':
return is_string($requestValue) && strpos($requestValue, $value) !== false;
case 'starts_with':
return is_string($requestValue) && strpos($requestValue, $value) === 0;
case 'ends_with':
return is_string($requestValue) && substr($requestValue, -strlen($value)) === $value;
case 'greater_than':
return $requestValue > $value;
case 'less_than':
return $requestValue < $value;
case 'between':
return $requestValue >= $value[0] && $requestValue <= $value[1];
case 'regex':
return preg_match($value, (string) $requestValue) === 1;
default:
return false;
}
}
/**
* Get value from request analysis.
*/
protected function getRequestValue(array $requestAnalysis, string $field)
{
$fields = explode('.', $field);
$value = $requestAnalysis;
foreach ($fields as $f) {
if (!is_array($value) || !array_key_exists($f, $value)) {
return null;
}
$value = $value[$f];
}
return $value;
}
/**
* Apply rule actions to instances.
*/
protected function applyRuleActions(array $instances, array $actions): array
{
$filteredInstances = $instances;
foreach ($actions as $action) {
switch ($action['type']) {
case 'filter':
$filteredInstances = $this->filterInstances($filteredInstances, $action['conditions']);
break;
case 'set_weight':
$filteredInstances = $this->setInstanceWeights($filteredInstances, $action['weights']);
break;
case 'prioritize':
$filteredInstances = $this->prioritizeInstances($filteredInstances, $action['criteria']);
break;
case 'exclude':
$filteredInstances = $this->excludeInstances($filteredInstances, $action['instances']);
break;
}
}
return $filteredInstances;
}
/**
* Select instance based on strategy.
*/
protected function selectInstance(array $instances, array $requestAnalysis, string $strategy): ?array
{
switch ($strategy) {
case 'weighted':
return $this->balancer->selectWeightedRandom($instances);
case 'round_robin':
return $this->balancer->selectRoundRobin($instances);
case 'least_connections':
return $this->balancer->selectLeastConnections($instances);
case 'response_time':
return $this->balancer->selectByResponseTime($instances);
case 'geographic':
$location = $requestAnalysis['client']['location'] ?? 'unknown';
return $this->distributeByGeography($instances, $location);
case 'segment':
$segment = $requestAnalysis['user']['segment'] ?? 'default';
return $this->distributeBySegment($instances, $segment);
case 'capacity':
return $this->distributeByCapacity($instances);
default:
return $instances[0];
}
}
/**
* Find closest instances by geography.
*/
protected function findClosestInstances(array $instances, string $clientLocation): array
{
// This would use a proper geolocation service in production
// For now, return instances with location matching
return array_filter($instances, function($instance) use ($clientLocation) {
$instanceLocation = $instance['location'] ?? 'unknown';
return $instanceLocation === $clientLocation || $instanceLocation === 'global';
});
}
/**
* Record traffic distribution.
*/
protected function recordTrafficDistribution(string $serviceId, array $requestAnalysis, string $strategy): void
{
// Update service stats
if (!isset($this->trafficStats[$serviceId])) {
$this->trafficStats[$serviceId] = [
'total_requests' => 0,
'requests_by_strategy' => [],
'response_times' => [],
'errors' => 0,
'last_request' => null
];
}
$this->trafficStats[$serviceId]['total_requests']++;
$this->trafficStats[$serviceId]['requests_by_strategy'][$strategy] =
($this->trafficStats[$serviceId]['requests_by_strategy'][$strategy] ?? 0) + 1;
$this->trafficStats[$serviceId]['last_request'] = time();
// Add to distribution history
$this->distributionHistory[] = [
'timestamp' => time(),
'service_id' => $serviceId,
'strategy' => $strategy,
'request_analysis' => $requestAnalysis
];
// Limit history size
if (count($this->distributionHistory) > 1000) {
$this->distributionHistory = array_slice($this->distributionHistory, -1000);
}
}
/**
* Calculate average response time for service.
*/
protected function calculateAverageResponseTime(string $serviceId): float
{
$stats = $this->trafficStats[$serviceId] ?? [];
$responseTimes = $stats['response_times'] ?? [];
if (empty($responseTimes)) {
return 0.0;
}
return array_sum($responseTimes) / count($responseTimes);
}
/**
* Calculate error rate for service.
*/
protected function calculateErrorRate(string $serviceId): float
{
$stats = $this->trafficStats[$serviceId] ?? [];
$totalRequests = $stats['total_requests'] ?? 0;
$errors = $stats['errors'] ?? 0;
if ($totalRequests === 0) {
return 0.0;
}
return ($errors / $totalRequests) * 100;
}
/**
* Suggest weight adjustments.
*/
protected function suggestWeightAdjustments(): array
{
$suggestions = [];
$stats = $this->getStatistics();
foreach ($stats['service_statistics'] as $serviceId => $serviceStats) {
if ($serviceStats['error_rate'] > 2) {
$suggestions[] = [
'type' => 'weight_adjustment',
'service_id' => $serviceId,
'current_weight' => $this->balancer->getWeight($serviceId),
'suggested_weight' => max(1, (int) ($this->balancer->getWeight($serviceId) * 0.5)),
'reason' => 'High error rate detected'
];
}
}
return $suggestions;
}
/**
* Validate rule configuration.
*/
protected function validateRule(array $rule): void
{
if (empty($rule['conditions'])) {
throw new \InvalidArgumentException("Rule must have at least one condition");
}
if (empty($rule['actions'])) {
throw new \InvalidArgumentException("Rule must have at least one action");
}
foreach ($rule['conditions'] as $condition) {
if (!isset($condition['field']) || !isset($condition['operator']) || !isset($condition['value'])) {
throw new \InvalidArgumentException("Condition must have field, operator, and value");
}
}
}
/**
* Initialize traffic distribution strategy.
*/
protected function initialize(): void
{
// Load configuration from storage
$this->loadConfiguration();
// Start monitoring
$this->monitor->start();
$this->logInfo("Traffic distribution strategy initialized");
}
/**
* Load configuration from storage.
*/
protected function loadConfiguration(): void
{
// This would load from persistent storage in production
$this->logInfo("Configuration loaded");
}
/**
* Log info message.
*/
protected function logInfo(string $message): void
{
if ($this->config['logging_enabled']) {
error_log("[TrafficDistributionStrategy] {$message}");
}
}
/**
* Get default configuration.
*/
protected function getDefaultConfig(): array
{
return [
'default_strategy' => 'weighted',
'enable_monitoring' => true,
'monitoring_interval' => 60,
'history_retention' => 1000,
'logging_enabled' => true,
'optimization_enabled' => true,
'optimization_interval' => 300,
'version' => '1.0'
];
}
/**
* Get configuration.
*/
public function getConfig(): array
{
return $this->config;
}
/**
* Set configuration.
*/
public function setConfig(array $config): void
{
$this->config = array_merge($this->config, $config);
}
/**
* Create traffic distribution strategy instance.
*/
public static function create(array $config = []): self
{
return new self($config);
}
/**
* Create for high traffic.
*/
public static function forHighTraffic(): self
{
return new self([
'default_strategy' => 'capacity',
'enable_monitoring' => true,
'monitoring_interval' => 30,
'optimization_enabled' => true,
'optimization_interval' => 120
]);
}
/**
* Create for global distribution.
*/
public static function forGlobalDistribution(): self
{
return new self([
'default_strategy' => 'geographic',
'enable_monitoring' => true,
'logging_enabled' => true
]);
}
}

View File

@@ -0,0 +1,666 @@
<?php
declare(strict_types=1);
namespace Fendx\Service\LoadBalancer\Weight;
use Fendx\Service\LoadBalancer\Weight\Calculator\WeightCalculator;
use Fendx\Service\LoadBalancer\Weight\Storage\WeightStorage;
use Fendx\Service\LoadBalancer\Weight\Adjuster\WeightAdjuster;
class WeightManager
{
protected WeightCalculator $calculator;
protected WeightStorage $storage;
protected WeightAdjuster $adjuster;
protected array $config = [];
protected array $weights = [];
protected array $metadata = [];
protected array $adjustmentHistory = [];
public function __construct(array $config = [])
{
$this->config = array_merge($this->getDefaultConfig(), $config);
$this->calculator = new WeightCalculator($this->config);
$this->storage = new WeightStorage($this->config);
$this->adjuster = new WeightAdjuster($this->config);
$this->initialize();
}
/**
* Set service weight.
*/
public function setWeight(string $serviceId, int $weight, array $metadata = []): void
{
if ($weight < $this->config['min_weight'] || $weight > $this->config['max_weight']) {
throw new \InvalidArgumentException("Weight must be between {$this->config['min_weight']} and {$this->config['max_weight']}");
}
$previousWeight = $this->weights[$serviceId] ?? 1;
$this->weights[$serviceId] = $weight;
$this->metadata[$serviceId] = array_merge($this->metadata[$serviceId] ?? [], $metadata);
$this->metadata[$serviceId]['updated_at'] = time();
// Persist to storage
$this->storage->store($serviceId, $weight, $this->metadata[$serviceId]);
// Record adjustment
$this->recordAdjustment($serviceId, $previousWeight, $weight, 'manual', $metadata);
$this->logInfo("Weight set for service {$serviceId}: {$weight} (was {$previousWeight})");
}
/**
* Get service weight.
*/
public function getWeight(string $serviceId): int
{
return $this->weights[$serviceId] ?? $this->config['default_weight'];
}
/**
* Get all weights.
*/
public function getWeights(): array
{
return $this->weights;
}
/**
* Get service metadata.
*/
public function getMetadata(string $serviceId): array
{
return $this->metadata[$serviceId] ?? [];
}
/**
* Set multiple weights.
*/
public function setWeights(array $weights): void
{
foreach ($weights as $serviceId => $data) {
if (is_array($data)) {
$this->setWeight($serviceId, $data['weight'], $data['metadata'] ?? []);
} else {
$this->setWeight($serviceId, $data);
}
}
}
/**
* Remove service weight.
*/
public function removeWeight(string $serviceId): bool
{
if (!isset($this->weights[$serviceId])) {
return false;
}
$previousWeight = $this->weights[$serviceId];
unset($this->weights[$serviceId]);
unset($this->metadata[$serviceId]);
// Remove from storage
$this->storage->remove($serviceId);
// Record adjustment
$this->recordAdjustment($serviceId, $previousWeight, 0, 'removed');
$this->logInfo("Weight removed for service: {$serviceId}");
return true;
}
/**
* Adjust weight by percentage.
*/
public function adjustWeightByPercentage(string $serviceId, float $percentage, string $reason = ''): void
{
$currentWeight = $this->getWeight($serviceId);
$adjustment = (int) round($currentWeight * ($percentage / 100));
$newWeight = max($this->config['min_weight'],
min($this->config['max_weight'], $currentWeight + $adjustment));
$this->setWeight($serviceId, $newWeight, [
'adjustment_type' => 'percentage',
'percentage' => $percentage,
'reason' => $reason
]);
}
/**
* Adjust weight by absolute value.
*/
public function adjustWeightByValue(string $serviceId, int $adjustment, string $reason = ''): void
{
$currentWeight = $this->getWeight($serviceId);
$newWeight = max($this->config['min_weight'],
min($this->config['max_weight'], $currentWeight + $adjustment));
$this->setWeight($serviceId, $newWeight, [
'adjustment_type' => 'absolute',
'adjustment' => $adjustment,
'reason' => $reason
]);
}
/**
* Auto-adjust weights based on performance metrics.
*/
public function autoAdjustWeights(array $metrics): array
{
$adjustments = [];
foreach ($metrics as $serviceId => $serviceMetrics) {
$currentWeight = $this->getWeight($serviceId);
$recommendedWeight = $this->calculator->calculateOptimalWeight($serviceMetrics, $currentWeight);
if ($recommendedWeight !== $currentWeight) {
$this->setWeight($serviceId, $recommendedWeight, [
'adjustment_type' => 'auto',
'metrics' => $serviceMetrics,
'previous_weight' => $currentWeight
]);
$adjustments[$serviceId] = [
'previous' => $currentWeight,
'new' => $recommendedWeight,
'change' => $recommendedWeight - $currentWeight
];
}
}
$this->logInfo("Auto-adjusted weights for " . count($adjustments) . " services");
return $adjustments;
}
/**
* Balance weights across services.
*/
public function balanceWeights(array $serviceIds, int $totalWeight = null): array
{
$totalWeight = $totalWeight ?? $this->config['default_total_weight'];
$serviceCount = count($serviceIds);
if ($serviceCount === 0) {
return [];
}
$baseWeight = (int) floor($totalWeight / $serviceCount);
$remainder = $totalWeight % $serviceCount;
$balancedWeights = [];
$currentWeights = [];
// Get current weights for reference
foreach ($serviceIds as $i => $serviceId) {
$currentWeights[$serviceId] = $this->getWeight($serviceId);
$balancedWeights[$serviceId] = $baseWeight + ($i < $remainder ? 1 : 0);
}
// Apply balanced weights
foreach ($balancedWeights as $serviceId => $weight) {
$this->setWeight($serviceId, $weight, [
'adjustment_type' => 'balanced',
'previous_weight' => $currentWeights[$serviceId],
'total_weight' => $totalWeight
]);
}
$this->logInfo("Balanced weights for {$serviceCount} services (total: {$totalWeight})");
return $balancedWeights;
}
/**
* Normalize weights to a specific total.
*/
public function normalizeWeights(array $serviceIds, int $targetTotal = 100): array
{
$currentTotal = 0;
$currentWeights = [];
foreach ($serviceIds as $serviceId) {
$weight = $this->getWeight($serviceId);
$currentWeights[$serviceId] = $weight;
$currentTotal += $weight;
}
if ($currentTotal === 0) {
return [];
}
$normalizedWeights = [];
$distributed = 0;
foreach ($serviceIds as $i => $serviceId) {
$ratio = $currentWeights[$serviceId] / $currentTotal;
$normalizedWeight = (int) round($ratio * $targetTotal);
// Ensure minimum weight
$normalizedWeight = max($this->config['min_weight'], $normalizedWeight);
$normalizedWeights[$serviceId] = $normalizedWeight;
$distributed += $normalizedWeight;
}
// Distribute remainder
$remainder = $targetTotal - $distributed;
if ($remainder > 0) {
$serviceIds = array_values($serviceIds);
for ($i = 0; $i < $remainder && $i < count($serviceIds); $i++) {
$serviceId = $serviceIds[$i];
$normalizedWeights[$serviceId]++;
}
}
// Apply normalized weights
foreach ($normalizedWeights as $serviceId => $weight) {
$this->setWeight($serviceId, $weight, [
'adjustment_type' => 'normalized',
'previous_weight' => $currentWeights[$serviceId],
'target_total' => $targetTotal
]);
}
$this->logInfo("Normalized weights for " . count($serviceIds) . " services to total {$targetTotal}");
return $normalizedWeights;
}
/**
* Get weight distribution.
*/
public function getWeightDistribution(array $serviceIds = null): array
{
$serviceIds = $serviceIds ?? array_keys($this->weights);
$distribution = [];
$totalWeight = 0;
foreach ($serviceIds as $serviceId) {
$weight = $this->getWeight($serviceId);
$distribution[$serviceId] = $weight;
$totalWeight += $weight;
}
// Calculate percentages
$percentages = [];
foreach ($distribution as $serviceId => $weight) {
$percentages[$serviceId] = $totalWeight > 0 ? ($weight / $totalWeight) * 100 : 0;
}
return [
'weights' => $distribution,
'percentages' => $percentages,
'total_weight' => $totalWeight,
'service_count' => count($distribution)
];
}
/**
* Get weight statistics.
*/
public function getStatistics(): array
{
if (empty($this->weights)) {
return [
'total_services' => 0,
'total_weight' => 0,
'average_weight' => 0,
'min_weight' => 0,
'max_weight' => 0,
'weight_distribution' => []
];
}
$weights = array_values($this->weights);
$totalWeight = array_sum($weights);
$averageWeight = $totalWeight / count($weights);
$minWeight = min($weights);
$maxWeight = max($weights);
// Weight distribution ranges
$distribution = [
'1-10' => 0,
'11-50' => 0,
'51-100' => 0,
'101-500' => 0,
'500+' => 0
];
foreach ($weights as $weight) {
if ($weight <= 10) $distribution['1-10']++;
elseif ($weight <= 50) $distribution['11-50']++;
elseif ($weight <= 100) $distribution['51-100']++;
elseif ($weight <= 500) $distribution['101-500']++;
else $distribution['500+']++;
}
return [
'total_services' => count($this->weights),
'total_weight' => $totalWeight,
'average_weight' => $averageWeight,
'min_weight' => $minWeight,
'max_weight' => $maxWeight,
'weight_distribution' => $distribution,
'adjustment_history_count' => count($this->adjustmentHistory)
];
}
/**
* Get adjustment history.
*/
public function getAdjustmentHistory(string $serviceId = null, int $limit = 100): array
{
$history = $this->adjustmentHistory;
if ($serviceId) {
$history = array_filter($history, function($record) use ($serviceId) {
return $record['service_id'] === $serviceId;
});
}
// Sort by timestamp (newest first)
usort($history, function($a, $b) {
return $b['timestamp'] <=> $a['timestamp'];
});
return array_slice($history, 0, $limit);
}
/**
* Export weights.
*/
public function exportWeights(string $format = 'json'): string
{
$data = [
'weights' => $this->weights,
'metadata' => $this->metadata,
'adjustment_history' => $this->adjustmentHistory,
'statistics' => $this->getStatistics(),
'exported_at' => date('Y-m-d H:i:s'),
'version' => $this->config['version'] ?? '1.0'
];
switch ($format) {
case 'json':
return json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
case 'php':
return '<?php return ' . var_export($data, true) . ';';
case 'csv':
return $this->exportToCSV($data);
default:
throw new \InvalidArgumentException("Unsupported export format: {$format}");
}
}
/**
* Import weights.
*/
public function importWeights(string $data, string $format = 'json'): void
{
switch ($format) {
case 'json':
$imported = json_decode($data, true);
break;
case 'php':
$imported = include 'data://text/plain;base64,' . base64_encode($data);
break;
default:
throw new \InvalidArgumentException("Unsupported import format: {$format}");
}
if (!$imported) {
throw new \InvalidArgumentException("Invalid import data");
}
if (isset($imported['weights'])) {
foreach ($imported['weights'] as $serviceId => $weight) {
$metadata = $imported['metadata'][$serviceId] ?? [];
$this->setWeight($serviceId, $weight, $metadata);
}
}
if (isset($imported['adjustment_history'])) {
$this->adjustmentHistory = array_merge($this->adjustmentHistory, $imported['adjustment_history']);
}
$this->logInfo("Weights imported successfully");
}
/**
* Reset all weights.
*/
public function resetWeights(): void
{
$this->weights = [];
$this->metadata = [];
$this->adjustmentHistory = [];
$this->storage->clear();
$this->logInfo("All weights reset");
}
/**
* Validate weight configuration.
*/
public function validateConfiguration(): array
{
$errors = [];
$warnings = [];
// Check weight ranges
foreach ($this->weights as $serviceId => $weight) {
if ($weight < $this->config['min_weight']) {
$errors[] = "Weight for {$serviceId} ({$weight}) is below minimum ({$this->config['min_weight']})";
}
if ($weight > $this->config['max_weight']) {
$errors[] = "Weight for {$serviceId} ({$weight}) is above maximum ({$this->config['max_weight']})";
}
}
// Check for zero weights
$zeroWeights = array_filter($this->weights, fn($w) => $w === 0);
if (!empty($zeroWeights)) {
$warnings[] = count($zeroWeights) . " services have zero weight and will not receive traffic";
}
// Check weight distribution
$stats = $this->getStatistics();
if ($stats['total_weight'] === 0) {
$errors[] = "Total weight is zero - no traffic will be distributed";
}
// Check for extreme weight ratios
if ($stats['max_weight'] > 0 && $stats['min_weight'] > 0) {
$ratio = $stats['max_weight'] / $stats['min_weight'];
if ($ratio > 100) {
$warnings[] = "Extreme weight ratio detected ({$ratio}:1) - consider rebalancing";
}
}
return [
'valid' => empty($errors),
'errors' => $errors,
'warnings' => $warnings
];
}
/**
* Record weight adjustment.
*/
protected function recordAdjustment(string $serviceId, int $previousWeight, int $newWeight, string $type, array $metadata = []): void
{
$this->adjustmentHistory[] = [
'service_id' => $serviceId,
'previous_weight' => $previousWeight,
'new_weight' => $newWeight,
'change' => $newWeight - $previousWeight,
'type' => $type,
'metadata' => $metadata,
'timestamp' => time()
];
// Limit history size
$maxHistory = $this->config['max_adjustment_history'] ?? 1000;
if (count($this->adjustmentHistory) > $maxHistory) {
$this->adjustmentHistory = array_slice($this->adjustmentHistory, -$maxHistory);
}
}
/**
* Export to CSV format.
*/
protected function exportToCSV(array $data): string
{
$csv = "Service ID,Weight,Metadata,Updated At\n";
foreach ($data['weights'] as $serviceId => $weight) {
$metadata = json_encode($data['metadata'][$serviceId] ?? []);
$updatedAt = $data['metadata'][$serviceId]['updated_at'] ?? '';
$csv .= "{$serviceId},{$weight},\"{$metadata}\",{$updatedAt}\n";
}
return $csv;
}
/**
* Initialize weight manager.
*/
protected function initialize(): void
{
// Load existing weights from storage
$this->loadWeights();
// Start background tasks if configured
if ($this->config['auto_adjust']) {
$this->startAutoAdjustment();
}
$this->logInfo("Weight manager initialized");
}
/**
* Load weights from storage.
*/
protected function loadWeights(): void
{
$stored = $this->storage->loadAll();
foreach ($stored as $serviceId => $data) {
$this->weights[$serviceId] = $data['weight'];
$this->metadata[$serviceId] = $data['metadata'] ?? [];
}
$this->logInfo("Loaded " . count($stored) . " weights from storage");
}
/**
* Start auto-adjustment task.
*/
protected function startAutoAdjustment(): void
{
// This would typically be run as a background process
// For now, we'll just log that it would start
$this->logInfo("Auto-adjustment task started");
}
/**
* Log info message.
*/
protected function logInfo(string $message): void
{
if ($this->config['logging_enabled']) {
error_log("[WeightManager] {$message}");
}
}
/**
* Get default configuration.
*/
protected function getDefaultConfig(): array
{
return [
'min_weight' => 1,
'max_weight' => 1000,
'default_weight' => 1,
'default_total_weight' => 100,
'auto_adjust' => false,
'adjustment_interval' => 300, // 5 minutes
'max_adjustment_history' => 1000,
'logging_enabled' => true,
'storage' => [
'type' => 'file',
'path' => __DIR__ . '/../../../storage/weights'
],
'calculator' => [
'response_time_weight' => 0.4,
'error_rate_weight' => 0.3,
'throughput_weight' => 0.3
],
'version' => '1.0'
];
}
/**
* Get configuration.
*/
public function getConfig(): array
{
return $this->config;
}
/**
* Set configuration.
*/
public function setConfig(array $config): void
{
$this->config = array_merge($this->config, $config);
}
/**
* Create weight manager instance.
*/
public static function create(array $config = []): self
{
return new self($config);
}
/**
* Create for production.
*/
public static function forProduction(): self
{
return new self([
'min_weight' => 1,
'max_weight' => 100,
'default_weight' => 10,
'auto_adjust' => true,
'adjustment_interval' => 300,
'logging_enabled' => false,
'storage' => [
'type' => 'redis',
'host' => 'localhost',
'port' => 6379
]
]);
}
/**
* Create for development.
*/
public static function forDevelopment(): self
{
return new self([
'min_weight' => 1,
'max_weight' => 10,
'default_weight' => 1,
'auto_adjust' => false,
'logging_enabled' => true
]);
}
}

View File

@@ -0,0 +1,813 @@
<?php
declare(strict_types=1);
namespace Fendx\Service\Metadata;
use Fendx\Service\Metadata\Storage\MetadataStorage;
use Fendx\Service\Metadata\Validator\MetadataValidator;
use Fendx\Service\Metadata\Serializer\MetadataSerializer;
class MetadataManager
{
protected MetadataStorage $storage;
protected MetadataValidator $validator;
protected MetadataSerializer $serializer;
protected array $config = [];
protected array $metadata = [];
protected array $schemas = [];
protected array $connections = [];
public function __construct(array $config = [])
{
$this->config = array_merge($this->getDefaultConfig(), $config);
$this->storage = new MetadataStorage($this->config);
$this->validator = new MetadataValidator($this->config);
$this->serializer = new MetadataSerializer($this->config);
$this->initialize();
}
/**
* Set metadata for a service.
*/
public function setMetadata(string $serviceId, array $metadata): bool
{
// Validate metadata
$validation = $this->validator->validate($metadata);
if (!$validation['valid']) {
throw new \InvalidArgumentException("Invalid metadata: " . implode(', ', $validation['errors']));
}
// Add timestamps
$metadata['created_at'] = time();
$metadata['updated_at'] = time();
// Store metadata
$this->metadata[$serviceId] = $metadata;
// Persist to storage
$this->storage->store($serviceId, $metadata);
$this->logInfo("Metadata set for service: {$serviceId}");
return true;
}
/**
* Get metadata for a service.
*/
public function getMetadata(string $serviceId): ?array
{
if (!isset($this->metadata[$serviceId])) {
// Try to load from storage
$loaded = $this->storage->load($serviceId);
if ($loaded) {
$this->metadata[$serviceId] = $loaded;
}
}
return $this->metadata[$serviceId] ?? null;
}
/**
* Update metadata for a service.
*/
public function updateMetadata(string $serviceId, array $updates): bool
{
$existing = $this->getMetadata($serviceId);
if (!$existing) {
return false;
}
// Merge updates
$updated = array_merge($existing, $updates);
$updated['updated_at'] = time();
// Validate updated metadata
$validation = $this->validator->validate($updated);
if (!$validation['valid']) {
throw new \InvalidArgumentException("Invalid metadata updates: " . implode(', ', $validation['errors']));
}
// Store updated metadata
$this->metadata[$serviceId] = $updated;
// Persist to storage
$this->storage->store($serviceId, $updated);
$this->logInfo("Metadata updated for service: {$serviceId}");
return true;
}
/**
* Delete metadata for a service.
*/
public function deleteMetadata(string $serviceId): bool
{
if (!isset($this->metadata[$serviceId])) {
return false;
}
unset($this->metadata[$serviceId]);
// Remove from storage
$this->storage->delete($serviceId);
$this->logInfo("Metadata deleted for service: {$serviceId}");
return true;
}
/**
* Get specific metadata field.
*/
public function getField(string $serviceId, string $field, $default = null)
{
$metadata = $this->getMetadata($serviceId);
if (!$metadata) {
return $default;
}
return $this->getNestedValue($metadata, $field, $default);
}
/**
* Set specific metadata field.
*/
public function setField(string $serviceId, string $field, $value): bool
{
$metadata = $this->getMetadata($serviceId) ?? [];
$this->setNestedValue($metadata, $field, $value);
return $this->setMetadata($serviceId, $metadata);
}
/**
* Update specific metadata field.
*/
public function updateField(string $serviceId, string $field, $value): bool
{
return $this->setField($serviceId, $field, $value);
}
/**
* Delete specific metadata field.
*/
public function deleteField(string $serviceId, string $field): bool
{
$metadata = $this->getMetadata($serviceId);
if (!$metadata) {
return false;
}
$this->unsetNestedValue($metadata, $field);
return $this->setMetadata($serviceId, $metadata);
}
/**
* Search services by metadata criteria.
*/
public function search(array $criteria): array
{
$results = [];
foreach ($this->metadata as $serviceId => $metadata) {
if ($this->matchesCriteria($metadata, $criteria)) {
$results[$serviceId] = $metadata;
}
}
return $results;
}
/**
* Find services by tags.
*/
public function findByTags(array $tags): array
{
return $this->search(['tags' => ['$all' => $tags]]);
}
/**
* Find services by version.
*/
public function findByVersion(string $version): array
{
return $this->search(['version' => $version]);
}
/**
* Find services by environment.
*/
public function findByEnvironment(string $environment): array
{
return $this->search(['environment' => $environment]);
}
/**
* Find services by custom attribute.
*/
public function findByAttribute(string $attribute, $value): array
{
return $this->search([$attribute => $value]);
}
/**
* Get all metadata.
*/
public function getAllMetadata(): array
{
return $this->metadata;
}
/**
* Get metadata statistics.
*/
public function getStatistics(): array
{
$totalServices = count($this->metadata);
$totalFields = 0;
$fieldCounts = [];
$tagCounts = [];
$versionCounts = [];
$environmentCounts = [];
foreach ($this->metadata as $serviceId => $metadata) {
$totalFields += count($metadata);
// Count field usage
foreach (array_keys($metadata) as $field) {
$fieldCounts[$field] = ($fieldCounts[$field] ?? 0) + 1;
}
// Count tags
if (isset($metadata['tags']) && is_array($metadata['tags'])) {
foreach ($metadata['tags'] as $tag) {
$tagCounts[$tag] = ($tagCounts[$tag] ?? 0) + 1;
}
}
// Count versions
if (isset($metadata['version'])) {
$version = $metadata['version'];
$versionCounts[$version] = ($versionCounts[$version] ?? 0) + 1;
}
// Count environments
if (isset($metadata['environment'])) {
$env = $metadata['environment'];
$environmentCounts[$env] = ($environmentCounts[$env] ?? 0) + 1;
}
}
return [
'total_services' => $totalServices,
'total_fields' => $totalFields,
'average_fields_per_service' => $totalServices > 0 ? $totalFields / $totalServices : 0,
'field_counts' => $fieldCounts,
'tag_counts' => $tagCounts,
'version_counts' => $versionCounts,
'environment_counts' => $environmentCounts,
'most_common_fields' => $this->getTopItems($fieldCounts, 10),
'most_common_tags' => $this->getTopItems($tagCounts, 10)
];
}
/**
* Register metadata schema.
*/
public function registerSchema(string $name, array $schema): void
{
$this->schemas[$name] = $schema;
$this->logInfo("Registered metadata schema: {$name}");
}
/**
* Validate metadata against schema.
*/
public function validateAgainstSchema(string $serviceId, string $schemaName): array
{
$metadata = $this->getMetadata($serviceId);
if (!$metadata) {
return ['valid' => false, 'errors' => ['Service not found']];
}
if (!isset($this->schemas[$schemaName])) {
return ['valid' => false, 'errors' => ['Schema not found: ' . $schemaName]];
}
return $this->validator->validateAgainstSchema($metadata, $this->schemas[$schemaName]);
}
/**
* Get all registered schemas.
*/
public function getSchemas(): array
{
return $this->schemas;
}
/**
* Export metadata.
*/
public function export(string $format = 'json'): string
{
$data = [
'metadata' => $this->metadata,
'schemas' => $this->schemas,
'exported_at' => date('Y-m-d H:i:s'),
'version' => $this->config['version'] ?? '1.0'
];
return $this->serializer->serialize($data, $format);
}
/**
* Import metadata.
*/
public function import(string $data, string $format = 'json'): void
{
$imported = $this->serializer->deserialize($data, $format);
if (!$imported) {
throw new \InvalidArgumentException("Invalid import data");
}
if (isset($imported['metadata'])) {
foreach ($imported['metadata'] as $serviceId => $metadata) {
$this->setMetadata($serviceId, $metadata);
}
}
if (isset($imported['schemas'])) {
foreach ($imported['schemas'] as $name => $schema) {
$this->registerSchema($name, $schema);
}
}
$this->logInfo("Metadata imported successfully");
}
/**
* Track service connections.
*/
public function trackConnection(string $serviceId, string $targetServiceId, array $connectionData = []): void
{
if (!isset($this->connections[$serviceId])) {
$this->connections[$serviceId] = [];
}
$connectionId = $this->generateConnectionId($serviceId, $targetServiceId);
$this->connections[$serviceId][$connectionId] = [
'id' => $connectionId,
'source_service' => $serviceId,
'target_service' => $targetServiceId,
'connection_data' => $connectionData,
'created_at' => time(),
'last_used' => time(),
'usage_count' => 1
];
// Update connection usage
$this->updateConnectionUsage($connectionId);
}
/**
* Get service connections.
*/
public function getConnections(string $serviceId): array
{
return $this->connections[$serviceId] ?? [];
}
/**
* Get connection count for service.
*/
public function getConnectionCount(string $serviceId): int
{
return count($this->getConnections($serviceId));
}
/**
* Update connection usage.
*/
public function updateConnectionUsage(string $connectionId): void
{
foreach ($this->connections as $serviceId => $connections) {
if (isset($connections[$connectionId])) {
$this->connections[$serviceId][$connectionId]['last_used'] = time();
$this->connections[$serviceId][$connectionId]['usage_count']++;
break;
}
}
}
/**
* Get connection statistics.
*/
public function getConnectionStatistics(): array
{
$totalConnections = 0;
$serviceConnections = [];
$mostConnectedServices = [];
foreach ($this->connections as $serviceId => $connections) {
$count = count($connections);
$totalConnections += $count;
$serviceConnections[$serviceId] = $count;
}
// Sort by connection count
arsort($serviceConnections);
$mostConnectedServices = array_slice($serviceConnections, 0, 10, true);
return [
'total_connections' => $totalConnections,
'services_with_connections' => count($this->connections),
'service_connections' => $serviceConnections,
'most_connected_services' => $mostConnectedServices,
'average_connections_per_service' => count($this->connections) > 0 ?
$totalConnections / count($this->connections) : 0
];
}
/**
* Cleanup old metadata.
*/
public function cleanup(int $maxAge = 86400): array
{
$now = time();
$removed = [];
foreach ($this->metadata as $serviceId => $metadata) {
if (isset($metadata['updated_at']) && ($now - $metadata['updated_at']) > $maxAge) {
$this->deleteMetadata($serviceId);
$removed[] = $serviceId;
}
}
$this->logInfo("Cleaned up " . count($removed) . " old metadata entries");
return $removed;
}
/**
* Backup metadata.
*/
public function backup(string $path): bool
{
$data = $this->export('json');
$result = file_put_contents($path, $data);
if ($result !== false) {
$this->logInfo("Metadata backed up to: {$path}");
return true;
}
return false;
}
/**
* Restore metadata from backup.
*/
public function restore(string $path): bool
{
if (!file_exists($path)) {
return false;
}
$data = file_get_contents($path);
if ($data === false) {
return false;
}
try {
$this->import($data, 'json');
$this->logInfo("Metadata restored from: {$path}");
return true;
} catch (\Exception $e) {
$this->logError("Failed to restore metadata: " . $e->getMessage());
return false;
}
}
/**
* Check if metadata matches criteria.
*/
protected function matchesCriteria(array $metadata, array $criteria): bool
{
foreach ($criteria as $field => $expected) {
$value = $this->getNestedValue($metadata, $field);
if (!$this->matchesValue($value, $expected)) {
return false;
}
}
return true;
}
/**
* Check if value matches expected criteria.
*/
protected function matchesValue($value, $expected): bool
{
if (is_array($expected)) {
// Handle operators
if (isset($expected['$eq'])) {
return $value === $expected['$eq'];
}
if (isset($expected['$ne'])) {
return $value !== $expected['$ne'];
}
if (isset($expected['$gt'])) {
return $value > $expected['$gt'];
}
if (isset($expected['$gte'])) {
return $value >= $expected['$gte'];
}
if (isset($expected['$lt'])) {
return $value < $expected['$lt'];
}
if (isset($expected['$lte'])) {
return $value <= $expected['$lte'];
}
if (isset($expected['$in'])) {
return in_array($value, $expected['$in']);
}
if (isset($expected['$nin'])) {
return !in_array($value, $expected['$nin']);
}
if (isset($expected['$all'])) {
return is_array($value) && count(array_intersect($value, $expected['$all'])) === count($expected['$all']);
}
if (isset($expected['$exists'])) {
$hasValue = $value !== null;
return $expected['$exists'] ? $hasValue : !$hasValue;
}
if (isset($expected['$regex'])) {
return preg_match($expected['$regex'], (string) $value) === 1;
}
}
return $value === $expected;
}
/**
* Get nested value from array.
*/
protected function getNestedValue(array $array, string $key, $default = null)
{
$keys = explode('.', $key);
$value = $array;
foreach ($keys as $k) {
if (!is_array($value) || !array_key_exists($k, $value)) {
return $default;
}
$value = $value[$k];
}
return $value;
}
/**
* Set nested value in array.
*/
protected function setNestedValue(array &$array, string $key, $value): void
{
$keys = explode('.', $key);
$current = &$array;
foreach ($keys as $k) {
if (!isset($current[$k]) || !is_array($current[$k])) {
$current[$k] = [];
}
$current = &$current[$k];
}
$current = $value;
}
/**
* Unset nested value in array.
*/
protected function unsetNestedValue(array &$array, string $key): void
{
$keys = explode('.', $key);
$lastKey = array_pop($keys);
$current = &$array;
foreach ($keys as $k) {
if (!isset($current[$k]) || !is_array($current[$k])) {
return;
}
$current = &$current[$k];
}
unset($current[$lastKey]);
}
/**
* Get top items from array.
*/
protected function getTopItems(array $items, int $limit): array
{
arsort($items);
return array_slice($items, 0, $limit, true);
}
/**
* Generate connection ID.
*/
protected function generateConnectionId(string $source, string $target): string
{
return $source . '->' . $target;
}
/**
* Initialize metadata manager.
*/
protected function initialize(): void
{
// Load existing metadata from storage
$this->loadMetadata();
// Load connections from storage
$this->loadConnections();
// Register default schemas
$this->registerDefaultSchemas();
$this->logInfo("Metadata manager initialized");
}
/**
* Load metadata from storage.
*/
protected function loadMetadata(): void
{
$metadata = $this->storage->loadAll();
$this->metadata = $metadata;
$this->logInfo("Loaded " . count($metadata) . " metadata entries from storage");
}
/**
* Load connections from storage.
*/
protected function loadConnections(): void
{
$connections = $this->storage->loadConnections();
$this->connections = $connections;
$this->logInfo("Loaded " . count($connections) . " service connections from storage");
}
/**
* Register default schemas.
*/
protected function registerDefaultSchemas(): void
{
// Basic service schema
$this->registerSchema('basic', [
'type' => 'object',
'required' => ['name', 'version'],
'properties' => [
'name' => ['type' => 'string'],
'version' => ['type' => 'string'],
'description' => ['type' => 'string'],
'tags' => ['type' => 'array', 'items' => ['type' => 'string']],
'environment' => ['type' => 'string'],
'owner' => ['type' => 'string'],
'contact' => ['type' => 'string']
]
]);
// Extended service schema
$this->registerSchema('extended', [
'type' => 'object',
'required' => ['name', 'version', 'environment'],
'properties' => [
'name' => ['type' => 'string'],
'version' => ['type' => 'string'],
'description' => ['type' => 'string'],
'tags' => ['type' => 'array', 'items' => ['type' => 'string']],
'environment' => ['type' => 'string', 'enum' => ['development', 'staging', 'production']],
'owner' => ['type' => 'string'],
'contact' => ['type' => 'string'],
'repository' => ['type' => 'string'],
'documentation' => ['type' => 'string'],
'dependencies' => ['type' => 'array', 'items' => ['type' => 'string']],
'metrics' => ['type' => 'object'],
'health_check' => ['type' => 'object']
]
]);
}
/**
* Log info message.
*/
protected function logInfo(string $message): void
{
if ($this->config['logging_enabled']) {
error_log("[MetadataManager] {$message}");
}
}
/**
* Log error message.
*/
protected function logError(string $message): void
{
if ($this->config['logging_enabled']) {
error_log("[MetadataManager] ERROR: {$message}");
}
}
/**
* Get default configuration.
*/
protected function getDefaultConfig(): array
{
return [
'logging_enabled' => true,
'auto_backup' => true,
'backup_interval' => 86400, // 24 hours
'max_backups' => 7,
'storage' => [
'type' => 'file',
'path' => __DIR__ . '/../../../storage/metadata'
],
'validation' => [
'strict' => true,
'required_fields' => ['name', 'version'],
'max_field_count' => 100,
'max_field_length' => 1000
],
'version' => '1.0'
];
}
/**
* Get configuration.
*/
public function getConfig(): array
{
return $this->config;
}
/**
* Set configuration.
*/
public function setConfig(array $config): void
{
$this->config = array_merge($this->config, $config);
}
/**
* Create metadata manager instance.
*/
public static function create(array $config = []): self
{
return new self($config);
}
/**
* Create for development.
*/
public static function forDevelopment(): self
{
return new self([
'logging_enabled' => true,
'auto_backup' => false,
'validation' => [
'strict' => false
]
]);
}
/**
* Create for production.
*/
public static function forProduction(): self
{
return new self([
'logging_enabled' => false,
'auto_backup' => true,
'backup_interval' => 86400,
'validation' => [
'strict' => true
],
'storage' => [
'type' => 'redis',
'host' => 'localhost',
'port' => 6379
]
]);
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,624 @@
<?php
declare(strict_types=1);
namespace Fendx\Service\Quality\Code;
use Fendx\Service\Quality\Code\Standard\PSR12Standard;
use Fendx\Service\Quality\Code\Standard\CustomStandard;
use Fendx\Service\Quality\Code\Formatter\CodeFormatter;
use Fendx\Service\Quality\Code\Reporter\StyleReporter;
class CodeStyleChecker
{
protected array $config = [];
protected array $standards = [];
protected CodeFormatter $formatter;
protected StyleReporter $reporter;
protected array $checkResults = [];
protected array $fileCache = [];
public function __construct(array $config = [])
{
$this->config = array_merge($this->getDefaultConfig(), $config);
$this->formatter = new CodeFormatter($this->config['formatter'] ?? []);
$this->reporter = new StyleReporter($this->config['reporter'] ?? []);
$this->initializeStandards();
}
/**
* Check code style for a file.
*/
public function checkFile(string $filePath): array
{
if (!file_exists($filePath)) {
throw new \InvalidArgumentException("File not found: {$filePath}");
}
// Check cache
$cacheKey = $this->getCacheKey($filePath);
if (isset($this->fileCache[$cacheKey])) {
return $this->fileCache[$cacheKey];
}
$content = file_get_contents($filePath);
if ($content === false) {
throw new \RuntimeException("Failed to read file: {$filePath}");
}
$result = [
'file' => $filePath,
'size' => strlen($content),
'lines' => substr_count($content, "\n") + 1,
'checks' => [],
'errors' => 0,
'warnings' => 0,
'info' => 0,
'score' => 100,
'timestamp' => microtime(true)
];
// Apply all standards
foreach ($this->standards as $name => $standard) {
$checkResult = $standard->check($content, $filePath);
$result['checks'][$name] = $checkResult;
$result['errors'] += $checkResult['errors'];
$result['warnings'] += $checkResult['warnings'];
$result['info'] += $checkResult['info'];
// Add detailed issues
if (!empty($checkResult['issues'])) {
foreach ($checkResult['issues'] as $issue) {
$issue['standard'] = $name;
$result['issues'][] = $issue;
}
}
}
// Calculate score
$totalIssues = $result['errors'] + $result['warnings'] + $result['info'];
if ($totalIssues > 0) {
$errorWeight = $this->config['scoring']['error_weight'] ?? 10;
$warningWeight = $this->config['scoring']['warning_weight'] ?? 3;
$infoWeight = $this->config['scoring']['info_weight'] ?? 1;
$weightedScore = ($result['errors'] * $errorWeight) +
($result['warnings'] * $warningWeight) +
($result['info'] * $infoWeight);
$result['score'] = max(0, 100 - $weightedScore);
}
// Cache result
$this->fileCache[$cacheKey] = $result;
$this->limitCacheSize();
return $result;
}
/**
* Check code style for directory.
*/
public function checkDirectory(string $directory, array $options = []): array
{
if (!is_dir($directory)) {
throw new \InvalidArgumentException("Directory not found: {$directory}");
}
$results = [
'directory' => $directory,
'files_checked' => 0,
'total_files' => 0,
'total_errors' => 0,
'total_warnings' => 0,
'total_info' => 0,
'average_score' => 0,
'files' => [],
'summary' => [],
'timestamp' => microtime(true)
];
// Find PHP files
$files = $this->findPhpFiles($directory, $options);
$results['total_files'] = count($files);
if (empty($files)) {
return $results;
}
$totalScore = 0;
foreach ($files as $file) {
try {
$fileResult = $this->checkFile($file);
$results['files'][$file] = $fileResult;
$results['files_checked']++;
$results['total_errors'] += $fileResult['errors'];
$results['total_warnings'] += $fileResult['warnings'];
$results['total_info'] += $fileResult['info'];
$totalScore += $fileResult['score'];
} catch (\Exception $e) {
$results['files'][$file] = [
'file' => $file,
'error' => $e->getMessage(),
'score' => 0
];
}
}
if ($results['files_checked'] > 0) {
$results['average_score'] = $totalScore / $results['files_checked'];
}
// Generate summary
$results['summary'] = $this->generateDirectorySummary($results);
return $results;
}
/**
* Fix code style issues.
*/
public function fixFile(string $filePath, array $options = []): array
{
if (!file_exists($filePath)) {
throw new \InvalidArgumentException("File not found: {$filePath}");
}
$content = file_get_contents($filePath);
if ($content === false) {
throw new \RuntimeException("Failed to read file: {$filePath}");
}
$originalContent = $content;
$fixes = [];
// Apply fixes from all standards
foreach ($this->standards as $name => $standard) {
$fixResult = $standard->fix($content, $filePath, $options);
if ($fixResult['fixed']) {
$content = $fixResult['content'];
$fixes[$name] = $fixResult['fixes'];
}
}
$result = [
'file' => $filePath,
'fixed' => $content !== $originalContent,
'original_size' => strlen($originalContent),
'fixed_size' => strlen($content),
'fixes' => $fixes,
'total_fixes' => array_sum(array_map('count', $fixes))
];
if ($result['fixed'] && ($options['save'] ?? false)) {
if (file_put_contents($filePath, $content) === false) {
throw new \RuntimeException("Failed to write file: {$filePath}");
}
$result['saved'] = true;
}
return $result;
}
/**
* Fix code style issues in directory.
*/
public function fixDirectory(string $directory, array $options = []): array
{
$results = [
'directory' => $directory,
'files_processed' => 0,
'files_fixed' => 0,
'total_fixes' => 0,
'files' => [],
'timestamp' => microtime(true)
];
$files = $this->findPhpFiles($directory, $options);
foreach ($files as $file) {
try {
$fileResult = $this->fixFile($file, $options);
$results['files'][$file] = $fileResult;
$results['files_processed']++;
if ($fileResult['fixed']) {
$results['files_fixed']++;
$results['total_fixes'] += $fileResult['total_fixes'];
}
} catch (\Exception $e) {
$results['files'][$file] = [
'file' => $file,
'error' => $e->getMessage()
];
}
}
return $results;
}
/**
* Get code style report.
*/
public function getReport(array $results, string $format = 'text'): string
{
return $this->reporter->generate($results, $format);
}
/**
* Add custom standard.
*/
public function addStandard(string $name, object $standard): void
{
$this->standards[$name] = $standard;
$this->logInfo("Added code style standard: {$name}");
}
/**
* Remove standard.
*/
public function removeStandard(string $name): bool
{
if (!isset($this->standards[$name])) {
return false;
}
unset($this->standards[$name]);
$this->logInfo("Removed code style standard: {$name}");
return true;
}
/**
* Get available standards.
*/
public function getStandards(): array
{
return array_keys($this->standards);
}
/**
* Enable/disable standard.
*/
public function setStandardEnabled(string $name, bool $enabled): void
{
if (isset($this->standards[$name])) {
$this->standards[$name]->setEnabled($enabled);
}
}
/**
* Get check statistics.
*/
public function getStatistics(): array
{
$stats = [
'standards_count' => count($this->standards),
'cache_size' => count($this->fileCache),
'enabled_standards' => 0,
'total_checks_run' => 0,
'average_check_time' => 0
];
foreach ($this->standards as $name => $standard) {
if ($standard->isEnabled()) {
$stats['enabled_standards']++;
}
$stats['total_checks_run'] += $standard->getCheckCount();
}
return $stats;
}
/**
* Clear cache.
*/
public function clearCache(): void
{
$this->fileCache = [];
$this->logInfo("Code style checker cache cleared");
}
/**
* Validate configuration.
*/
public function validateConfig(): array
{
$issues = [];
// Check required configuration
$required = ['standards', 'scoring'];
foreach ($required as $key) {
if (!isset($this->config[$key])) {
$issues[] = "Missing required configuration: {$key}";
}
}
// Validate standards
foreach ($this->config['standards'] as $name => $config) {
if (!isset($config['enabled']) || !is_bool($config['enabled'])) {
$issues[] = "Invalid enabled setting for standard: {$name}";
}
}
// Validate scoring
if (isset($this->config['scoring'])) {
$scoring = $this->config['scoring'];
foreach (['error_weight', 'warning_weight', 'info_weight'] as $key) {
if (isset($scoring[$key]) && (!is_numeric($scoring[$key]) || $scoring[$key] < 0)) {
$issues[] = "Invalid scoring weight: {$key}";
}
}
}
return $issues;
}
/**
* Find PHP files in directory.
*/
protected function findPhpFiles(string $directory, array $options = []): array
{
$files = [];
$extensions = $options['extensions'] ?? ['php'];
$exclude = $options['exclude'] ?? ['vendor', 'node_modules', '.git'];
$recursive = $options['recursive'] ?? true;
$iterator = $recursive ?
new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($directory)) :
new \DirectoryIterator($directory);
foreach ($iterator as $file) {
if ($file->isDot() || !$file->isFile()) {
continue;
}
$fileName = $file->getFilename();
$filePath = $file->getPathname();
// Check extension
$extension = strtolower(pathinfo($fileName, PATHINFO_EXTENSION));
if (!in_array($extension, $extensions)) {
continue;
}
// Check exclude patterns
$excluded = false;
foreach ($exclude as $pattern) {
if (strpos($filePath, $pattern) !== false) {
$excluded = true;
break;
}
}
if (!$excluded) {
$files[] = $filePath;
}
}
sort($files);
return $files;
}
/**
* Generate directory summary.
*/
protected function generateDirectorySummary(array $results): array
{
$summary = [
'grade' => 'A',
'status' => 'excellent',
'recommendations' => []
];
$score = $results['average_score'];
// Determine grade
if ($score >= 90) {
$summary['grade'] = 'A';
$summary['status'] = 'excellent';
} elseif ($score >= 80) {
$summary['grade'] = 'B';
$summary['status'] = 'good';
} elseif ($score >= 70) {
$summary['grade'] = 'C';
$summary['status'] = 'fair';
} elseif ($score >= 60) {
$summary['grade'] = 'D';
$summary['status'] = 'poor';
} else {
$summary['grade'] = 'F';
$summary['status'] = 'failing';
}
// Generate recommendations
if ($results['total_errors'] > 0) {
$summary['recommendations'][] = "Fix {$results['total_errors']} error(s) immediately";
}
if ($results['total_warnings'] > 10) {
$summary['recommendations'][] = "Address {$results['total_warnings']} warning(s) to improve code quality";
}
if ($results['average_score'] < 80) {
$summary['recommendations'][] = "Consider running auto-fix to resolve common style issues";
}
if ($results['files_checked'] < $results['total_files']) {
$summary['recommendations'][] = "Some files could not be checked due to errors";
}
return $summary;
}
/**
* Get cache key for file.
*/
protected function getCacheKey(string $filePath): string
{
$mtime = filemtime($filePath);
return md5($filePath . $mtime);
}
/**
* Limit cache size.
*/
protected function limitCacheSize(): void
{
$maxSize = $this->config['cache_size'] ?? 1000;
if (count($this->fileCache) > $maxSize) {
$this->fileCache = array_slice($this->fileCache, -$maxSize, null, true);
}
}
/**
* Initialize standards.
*/
protected function initializeStandards(): void
{
// Add PSR-12 standard
$this->standards['psr12'] = new PSR12Standard($this->config['standards']['psr12'] ?? []);
// Add custom standard
$this->standards['custom'] = new CustomStandard($this->config['standards']['custom'] ?? []);
$this->logInfo("Initialized " . count($this->standards) . " code style standards");
}
/**
* Log info message.
*/
protected function logInfo(string $message): void
{
if ($this->config['logging_enabled']) {
error_log("[CodeStyleChecker] {$message}");
}
}
/**
* Get default configuration.
*/
protected function getDefaultConfig(): array
{
return [
'standards' => [
'psr12' => [
'enabled' => true,
'strict' => false
],
'custom' => [
'enabled' => true,
'rules' => []
]
],
'scoring' => [
'error_weight' => 10,
'warning_weight' => 3,
'info_weight' => 1
],
'formatter' => [
'indent_size' => 4,
'indent_style' => 'space',
'line_ending' => "\n"
],
'reporter' => [
'format' => 'text',
'include_details' => true
],
'cache_size' => 1000,
'logging_enabled' => true
];
}
/**
* Get configuration.
*/
public function getConfig(): array
{
return $this->config;
}
/**
* Set configuration.
*/
public function setConfig(array $config): void
{
$this->config = array_merge($this->config, $config);
}
/**
* Create code style checker instance.
*/
public static function create(array $config = []): self
{
return new self($config);
}
/**
* Create for development.
*/
public static function forDevelopment(): self
{
return new self([
'standards' => [
'psr12' => [
'enabled' => true,
'strict' => false
],
'custom' => [
'enabled' => true,
'rules' => [
'max_line_length' => 120,
'require_docblocks' => false
]
]
],
'scoring' => [
'error_weight' => 5,
'warning_weight' => 2,
'info_weight' => 1
],
'logging_enabled' => true
]);
}
/**
* Create for production.
*/
public static function forProduction(): self
{
return new self([
'standards' => [
'psr12' => [
'enabled' => true,
'strict' => true
],
'custom' => [
'enabled' => true,
'rules' => [
'max_line_length' => 100,
'require_docblocks' => true,
'require_type_hints' => true
]
]
],
'scoring' => [
'error_weight' => 10,
'warning_weight' => 5,
'info_weight' => 2
],
'logging_enabled' => false
]);
}
}

View File

@@ -0,0 +1,997 @@
<?php
declare(strict_types=1);
namespace Fendx\Service\Quality\Code;
use Fendx\Service\Quality\Code\Complexity\CyclomaticComplexity;
use Fendx\Service\Quality\Code\Complexity\CognitiveComplexity;
use Fendx\Service\Quality\Code\Complexity\HalsteadComplexity;
use Fendx\Service\Quality\Code\Parser\CodeParser;
class ComplexityDetector
{
protected array $config = [];
protected CodeParser $parser;
protected CyclomaticComplexity $cyclomaticAnalyzer;
protected CognitiveComplexity $cognitiveAnalyzer;
protected HalsteadComplexity $halsteadAnalyzer;
protected array $complexityCache = [];
protected array $thresholds = [];
public function __construct(array $config = [])
{
$this->config = array_merge($this->getDefaultConfig(), $config);
$this->parser = new CodeParser($this->config['parser'] ?? []);
$this->cyclomaticAnalyzer = new CyclomaticComplexity($this->config['cyclomatic'] ?? []);
$this->cognitiveAnalyzer = new CognitiveComplexity($this->config['cognitive'] ?? []);
$this->halsteadAnalyzer = new HalsteadComplexity($this->config['halstead'] ?? []);
$this->initializeThresholds();
}
/**
* Analyze complexity of a file.
*/
public function analyzeFile(string $filePath): array
{
if (!file_exists($filePath)) {
throw new \InvalidArgumentException("File not found: {$filePath}");
}
// Check cache
$cacheKey = $this->getCacheKey($filePath);
if (isset($this->complexityCache[$cacheKey])) {
return $this->complexityCache[$cacheKey];
}
$content = file_get_contents($filePath);
if ($content === false) {
throw new \RuntimeException("Failed to read file: {$filePath}");
}
// Parse code
$ast = $this->parser->parse($content, $filePath);
$result = [
'file' => $filePath,
'lines' => substr_count($content, "\n") + 1,
'complexity' => [
'cyclomatic' => 0,
'cognitive' => 0,
'halstead' => [
'vocabulary' => 0,
'length' => 0,
'volume' => 0,
'difficulty' => 0,
'effort' => 0,
'time' => 0,
'bugs' => 0
]
],
'methods' => [],
'classes' => [],
'functions' => [],
'overall_score' => 0,
'risk_level' => 'low',
'recommendations' => [],
'threshold_violations' => [],
'timestamp' => microtime(true)
];
// Analyze overall complexity
$result['complexity']['cyclomatic'] = $this->cyclomaticAnalyzer->analyze($ast);
$result['complexity']['cognitive'] = $this->cognitiveAnalyzer->analyze($ast);
$result['complexity']['halstead'] = $this->halsteadAnalyzer->analyze($ast);
// Analyze individual methods
$result['methods'] = $this->analyzeMethods($ast);
// Analyze classes
$result['classes'] = $this->analyzeClasses($ast);
// Analyze functions
$result['functions'] = $this->analyzeFunctions($ast);
// Calculate overall score
$result['overall_score'] = $this->calculateOverallScore($result);
// Determine risk level
$result['risk_level'] = $this->determineRiskLevel($result);
// Generate recommendations
$result['recommendations'] = $this->generateRecommendations($result);
// Check threshold violations
$result['threshold_violations'] = $this->checkThresholdViolations($result);
// Cache result
$this->complexityCache[$cacheKey] = $result;
$this->limitCacheSize();
return $result;
}
/**
* Analyze complexity of directory.
*/
public function analyzeDirectory(string $directory, array $options = []): array
{
if (!is_dir($directory)) {
throw new \InvalidArgumentException("Directory not found: {$directory}");
}
$results = [
'directory' => $directory,
'files_analyzed' => 0,
'total_files' => 0,
'complexity_summary' => [
'average_cyclomatic' => 0,
'average_cognitive' => 0,
'max_cyclomatic' => 0,
'max_cognitive' => 0,
'total_methods' => 0,
'complex_methods' => 0,
'very_complex_methods' => 0
],
'risk_distribution' => [
'low' => 0,
'medium' => 0,
'high' => 0,
'very_high' => 0
],
'files' => [],
'hotspots' => [],
'recommendations' => [],
'timestamp' => microtime(true)
];
// Find PHP files
$files = $this->findPhpFiles($directory, $options);
$results['total_files'] = count($files);
if (empty($files)) {
return $results;
}
$totalCyclomatic = 0;
$totalCognitive = 0;
$maxCyclomatic = 0;
$maxCognitive = 0;
$totalMethods = 0;
$complexMethods = 0;
$veryComplexMethods = 0;
foreach ($files as $file) {
try {
$fileResult = $this->analyzeFile($file);
$results['files'][$file] = $fileResult;
$results['files_analyzed']++;
// Update summary
$totalCyclomatic += $fileResult['complexity']['cyclomatic'];
$totalCognitive += $fileResult['complexity']['cognitive'];
$maxCyclomatic = max($maxCyclomatic, $fileResult['complexity']['cyclomatic']);
$maxCognitive = max($maxCognitive, $fileResult['complexity']['cognitive']);
// Count methods
$totalMethods += count($fileResult['methods']);
foreach ($fileResult['methods'] as $method) {
if ($method['complexity']['cyclomatic'] > 10) {
$complexMethods++;
}
if ($method['complexity']['cyclomatic'] > 20) {
$veryComplexMethods++;
}
}
// Update risk distribution
$riskLevel = $fileResult['risk_level'];
$results['risk_distribution'][$riskLevel]++;
} catch (\Exception $e) {
$results['files'][$file] = [
'file' => $file,
'error' => $e->getMessage()
];
}
}
// Calculate averages
if ($results['files_analyzed'] > 0) {
$results['complexity_summary']['average_cyclomatic'] = $totalCyclomatic / $results['files_analyzed'];
$results['complexity_summary']['average_cognitive'] = $totalCognitive / $results['files_analyzed'];
}
$results['complexity_summary']['max_cyclomatic'] = $maxCyclomatic;
$results['complexity_summary']['max_cognitive'] = $maxCognitive;
$results['complexity_summary']['total_methods'] = $totalMethods;
$results['complexity_summary']['complex_methods'] = $complexMethods;
$results['complexity_summary']['very_complex_methods'] = $veryComplexMethods;
// Identify hotspots
$results['hotspots'] = $this->identifyHotspots($results['files']);
// Generate recommendations
$results['recommendations'] = $this->generateDirectoryRecommendations($results);
return $results;
}
/**
* Analyze specific method complexity.
*/
public function analyzeMethod(string $filePath, string $methodName): array
{
$fileResult = $this->analyzeFile($filePath);
// Find the method
foreach ($fileResult['methods'] as $method) {
if ($method['name'] === $methodName) {
return $method;
}
}
throw new \InvalidArgumentException("Method not found: {$methodName}");
}
/**
* Compare complexity between files.
*/
public function compareComplexity(array $filePaths): array
{
$comparison = [
'files' => [],
'rankings' => [
'most_complex' => [],
'least_complex' => [],
'largest' => [],
'smallest' => []
],
'statistics' => [],
'timestamp' => microtime(true)
];
$fileData = [];
foreach ($filePaths as $filePath) {
try {
$result = $this->analyzeFile($filePath);
$fileData[$filePath] = $result;
$comparison['files'][$filePath] = [
'cyclomatic' => $result['complexity']['cyclomatic'],
'cognitive' => $result['complexity']['cognitive'],
'lines' => $result['lines'],
'methods' => count($result['methods']),
'overall_score' => $result['overall_score']
];
} catch (\Exception $e) {
$comparison['files'][$filePath] = [
'error' => $e->getMessage()
];
}
}
// Generate rankings
$comparison['rankings'] = $this->generateRankings($fileData);
// Calculate statistics
$comparison['statistics'] = $this->calculateComparisonStatistics($fileData);
return $comparison;
}
/**
* Track complexity trends over time.
*/
public function trackTrends(string $directory, array $historicalData = []): array
{
$currentData = $this->analyzeDirectory($directory);
$trends = [
'current' => $currentData,
'historical' => $historicalData,
'trends' => [],
'projections' => [],
'insights' => [],
'timestamp' => microtime(true)
];
if (!empty($historicalData)) {
$trends['trends'] = $this->calculateTrends($historicalData, $currentData);
$trends['projections'] = $this->generateProjections($historicalData, $currentData);
$trends['insights'] = $this->generateTrendInsights($trends['trends']);
}
return $trends;
}
/**
* Get complexity report.
*/
public function getReport(array $results, string $format = 'html'): string
{
switch ($format) {
case 'json':
return json_encode($results, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
case 'text':
return $this->generateTextReport($results);
case 'html':
return $this->generateHtmlReport($results);
default:
throw new \InvalidArgumentException("Unsupported report format: {$format}");
}
}
/**
* Set custom thresholds.
*/
public function setThresholds(array $thresholds): void
{
$this->thresholds = array_merge($this->thresholds, $thresholds);
}
/**
* Get current thresholds.
*/
public function getThresholds(): array
{
return $this->thresholds;
}
/**
* Get complexity statistics.
*/
public function getStatistics(): array
{
return [
'files_analyzed' => count($this->complexityCache),
'cache_size' => count($this->complexityCache),
'threshold_violations' => $this->countThresholdViolations(),
'average_complexity' => $this->calculateAverageComplexity()
];
}
/**
* Clear cache.
*/
public function clearCache(): void
{
$this->complexityCache = [];
}
/**
* Analyze methods in AST.
*/
protected function analyzeMethods($ast): array
{
$methods = [];
// This would extract methods from AST and analyze each
// For now, return empty array
return $methods;
}
/**
* Analyze classes in AST.
*/
protected function analyzeClasses($ast): array
{
$classes = [];
// This would extract classes from AST and analyze each
// For now, return empty array
return $classes;
}
/**
* Analyze functions in AST.
*/
protected function analyzeFunctions($ast): array
{
$functions = [];
// This would extract functions from AST and analyze each
// For now, return empty array
return $functions;
}
/**
* Calculate overall complexity score.
*/
protected function calculateOverallScore(array $result): int
{
$score = 100;
// Deduct points based on cyclomatic complexity
$cyclomatic = $result['complexity']['cyclomatic'];
if ($cyclomatic > 10) {
$score -= min(30, ($cyclomatic - 10) * 2);
}
// Deduct points based on cognitive complexity
$cognitive = $result['complexity']['cognitive'];
if ($cognitive > 15) {
$score -= min(25, ($cognitive - 15) * 1.5);
}
// Deduct points for complex methods
$complexMethods = 0;
foreach ($result['methods'] as $method) {
if ($method['complexity']['cyclomatic'] > 15) {
$complexMethods++;
}
}
if ($complexMethods > 0) {
$score -= min(20, $complexMethods * 5);
}
// Bonus for low complexity
if ($cyclomatic <= 5 && $cognitive <= 8) {
$score += 10;
}
return max(0, min(100, $score));
}
/**
* Determine risk level.
*/
protected function determineRiskLevel(array $result): string
{
$cyclomatic = $result['complexity']['cyclomatic'];
$cognitive = $result['complexity']['cognitive'];
if ($cyclomatic > 20 || $cognitive > 30) {
return 'very_high';
} elseif ($cyclomatic > 15 || $cognitive > 20) {
return 'high';
} elseif ($cyclomatic > 10 || $cognitive > 15) {
return 'medium';
} else {
return 'low';
}
}
/**
* Generate recommendations.
*/
protected function generateRecommendations(array $result): array
{
$recommendations = [];
$cyclomatic = $result['complexity']['cyclomatic'];
$cognitive = $result['complexity']['cognitive'];
if ($cyclomatic > 20) {
$recommendations[] = "Very high cyclomatic complexity ({$cyclomatic}). Consider breaking down into smaller methods.";
} elseif ($cyclomatic > 15) {
$recommendations[] = "High cyclomatic complexity ({$cyclomatic}). Look for opportunities to simplify logic.";
}
if ($cognitive > 30) {
$recommendations[] = "Very high cognitive complexity ({$cognitive}). Reduce nesting and simplify control flow.";
} elseif ($cognitive > 20) {
$recommendations[] = "High cognitive complexity ({$cognitive}). Consider early returns and guard clauses.";
}
// Check for complex methods
$complexMethods = array_filter($result['methods'], function($method) {
return $method['complexity']['cyclomatic'] > 10;
});
if (!empty($complexMethods)) {
$recommendations[] = count($complexMethods) . " method(s) have high complexity. Consider refactoring.";
}
return $recommendations;
}
/**
* Check threshold violations.
*/
protected function checkThresholdViolations(array $result): array
{
$violations = [];
foreach ($this->thresholds as $metric => $threshold) {
$value = $this->getMetricValue($result, $metric);
if ($value > $threshold) {
$violations[] = [
'metric' => $metric,
'value' => $value,
'threshold' => $threshold,
'severity' => $this->getViolationSeverity($value, $threshold)
];
}
}
return $violations;
}
/**
* Find PHP files.
*/
protected function findPhpFiles(string $directory, array $options = []): array
{
$files = [];
$extensions = $options['extensions'] ?? ['php'];
$exclude = $options['exclude'] ?? ['vendor', 'node_modules', '.git'];
$recursive = $options['recursive'] ?? true;
$iterator = $recursive ?
new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($directory)) :
new \DirectoryIterator($directory);
foreach ($iterator as $file) {
if ($file->isDot() || !$file->isFile()) {
continue;
}
$fileName = $file->getFilename();
$filePath = $file->getPathname();
// Check extension
$extension = strtolower(pathinfo($fileName, PATHINFO_EXTENSION));
if (!in_array($extension, $extensions)) {
continue;
}
// Check exclude patterns
$excluded = false;
foreach ($exclude as $pattern) {
if (strpos($filePath, $pattern) !== false) {
$excluded = true;
break;
}
}
if (!$excluded) {
$files[] = $filePath;
}
}
sort($files);
return $files;
}
/**
* Identify complexity hotspots.
*/
protected function identifyHotspots(array $files): array
{
$hotspots = [
'most_complex_files' => [],
'most_complex_methods' => [],
'large_files' => []
];
$fileComplexity = [];
$methodComplexity = [];
$fileSizes = [];
foreach ($files as $filePath => $fileData) {
if (isset($fileData['error'])) {
continue;
}
$fileComplexity[$filePath] = $fileData['complexity']['cyclomatic'];
$fileSizes[$filePath] = $fileData['lines'];
foreach ($fileData['methods'] as $method) {
$methodComplexity[] = [
'file' => $filePath,
'method' => $method['name'],
'complexity' => $method['complexity']['cyclomatic']
];
}
}
// Sort and get top files
arsort($fileComplexity);
$hotspots['most_complex_files'] = array_slice($fileComplexity, 0, 10, true);
// Sort and get top methods
usort($methodComplexity, function($a, $b) {
return $b['complexity'] <=> $a['complexity'];
});
$hotspots['most_complex_methods'] = array_slice($methodComplexity, 0, 10);
// Sort and get largest files
arsort($fileSizes);
$hotspots['large_files'] = array_slice($fileSizes, 0, 10, true);
return $hotspots;
}
/**
* Generate directory recommendations.
*/
protected function generateDirectoryRecommendations(array $results): array
{
$recommendations = [];
$summary = $results['complexity_summary'];
if ($summary['average_cyclomatic'] > 15) {
$recommendations[] = "Average cyclomatic complexity is high ({$summary['average_cyclomatic']}). Consider code refactoring.";
}
if ($summary['average_cognitive'] > 20) {
$recommendations[] = "Average cognitive complexity is high ({$summary['average_cognitive']}). Simplify control flow.";
}
if ($summary['complex_methods'] > $summary['total_methods'] * 0.2) {
$recommendations[] = "More than 20% of methods are complex. Focus on refactoring complex methods.";
}
if ($summary['very_complex_methods'] > 0) {
$recommendations[] = "{$summary['very_complex_methods']} method(s) are very complex. Immediate attention required.";
}
$highRiskFiles = $results['risk_distribution']['high'] + $results['risk_distribution']['very_high'];
if ($highRiskFiles > $results['files_analyzed'] * 0.3) {
$recommendations[] = "More than 30% of files have high complexity risk.";
}
return $recommendations;
}
/**
* Generate rankings for comparison.
*/
protected function generateRankings(array $fileData): array
{
$rankings = [];
// Most complex
uasort($fileData, function($a, $b) {
return ($b['cyclomatic'] ?? 0) <=> ($a['cyclomatic'] ?? 0);
});
$rankings['most_complex'] = array_slice($fileData, 0, 5, true);
// Least complex
uasort($fileData, function($a, $b) {
return ($a['cyclomatic'] ?? PHP_INT_MAX) <=> ($b['cyclomatic'] ?? PHP_INT_MAX);
});
$rankings['least_complex'] = array_slice($fileData, 0, 5, true);
// Largest
uasort($fileData, function($a, $b) {
return ($b['lines'] ?? 0) <=> ($a['lines'] ?? 0);
});
$rankings['largest'] = array_slice($fileData, 0, 5, true);
// Smallest
uasort($fileData, function($a, $b) {
return ($a['lines'] ?? PHP_INT_MAX) <=> ($b['lines'] ?? PHP_INT_MAX);
});
$rankings['smallest'] = array_slice($fileData, 0, 5, true);
return $rankings;
}
/**
* Calculate comparison statistics.
*/
protected function calculateComparisonStatistics(array $fileData): array
{
$stats = [
'total_files' => count($fileData),
'average_cyclomatic' => 0,
'average_cognitive' => 0,
'average_lines' => 0,
'max_cyclomatic' => 0,
'min_cyclomatic' => PHP_INT_MAX,
'complexity_distribution' => [
'low' => 0, 'medium' => 0, 'high' => 0, 'very_high' => 0
]
];
$totalCyclomatic = 0;
$totalCognitive = 0;
$totalLines = 0;
$validFiles = 0;
foreach ($fileData as $data) {
if (isset($data['error'])) {
continue;
}
$cyclomatic = $data['cyclomatic'] ?? 0;
$cognitive = $data['cognitive'] ?? 0;
$lines = $data['lines'] ?? 0;
$totalCyclomatic += $cyclomatic;
$totalCognitive += $cognitive;
$totalLines += $lines;
$validFiles++;
$stats['max_cyclomatic'] = max($stats['max_cyclomatic'], $cyclomatic);
$stats['min_cyclomatic'] = min($stats['min_cyclomatic'], $cyclomatic);
// Complexity distribution
if ($cyclomatic <= 5) {
$stats['complexity_distribution']['low']++;
} elseif ($cyclomatic <= 10) {
$stats['complexity_distribution']['medium']++;
} elseif ($cyclomatic <= 20) {
$stats['complexity_distribution']['high']++;
} else {
$stats['complexity_distribution']['very_high']++;
}
}
if ($validFiles > 0) {
$stats['average_cyclomatic'] = $totalCyclomatic / $validFiles;
$stats['average_cognitive'] = $totalCognitive / $validFiles;
$stats['average_lines'] = $totalLines / $validFiles;
}
if ($stats['min_cyclomatic'] === PHP_INT_MAX) {
$stats['min_cyclomatic'] = 0;
}
return $stats;
}
/**
* Generate text report.
*/
protected function generateTextReport(array $results): string
{
$report = "Complexity Analysis Report\n";
$report .= "========================\n\n";
if (isset($results['directory'])) {
// Directory report
$report .= "Directory: {$results['directory']}\n";
$report .= "Files analyzed: {$results['files_analyzed']}/{$results['total_files']}\n\n";
$summary = $results['complexity_summary'];
$report .= "Summary:\n";
$report .= "- Average cyclomatic complexity: " . number_format($summary['average_cyclomatic'], 2) . "\n";
$report .= "- Average cognitive complexity: " . number_format($summary['average_cognitive'], 2) . "\n";
$report .= "- Maximum cyclomatic complexity: {$summary['max_cyclomatic']}\n";
$report .= "- Maximum cognitive complexity: {$summary['max_cognitive']}\n";
$report .= "- Total methods: {$summary['total_methods']}\n";
$report .= "- Complex methods: {$summary['complex_methods']}\n\n";
} else {
// File report
$report .= "File: {$results['file']}\n";
$report .= "Lines: {$results['lines']}\n";
$report .= "Cyclomatic complexity: {$results['complexity']['cyclomatic']}\n";
$report .= "Cognitive complexity: {$results['complexity']['cognitive']}\n";
$report .= "Overall score: {$results['overall_score']}\n";
$report .= "Risk level: {$results['risk_level']}\n\n";
if (!empty($results['recommendations'])) {
$report .= "Recommendations:\n";
foreach ($results['recommendations'] as $rec) {
$report .= "- {$rec}\n";
}
}
}
return $report;
}
/**
* Generate HTML report.
*/
protected function generateHtmlReport(array $results): string
{
$html = "<html><head><title>Complexity Analysis Report</title>";
$html .= "<style>body{font-family:Arial,sans-serif;margin:20px;}";
$html .= ".header{background:#f5f5f5;padding:10px;border-radius:5px;}";
$html .= ".metric{margin:10px 0;}";
$html .= ".high{color:#d32f2f;}.medium{color:#f57c00;}.low{color:#388e3c;}</style></head><body>";
$html .= "<div class='header'><h1>Complexity Analysis Report</h1>";
if (isset($results['directory'])) {
$html .= "<p>Directory: <strong>{$results['directory']}</strong></p>";
$html .= "<p>Files analyzed: <strong>{$results['files_analyzed']}/{$results['total_files']}</strong></p>";
} else {
$html .= "<p>File: <strong>{$results['file']}</strong></p>";
$html .= "<p>Lines: <strong>{$results['lines']}</strong></p>";
}
$html .= "</div>";
// Add metrics and recommendations
// ... (detailed HTML generation)
$html .= "</body></html>";
return $html;
}
/**
* Get metric value from result.
*/
protected function getMetricValue(array $result, string $metric): float
{
switch ($metric) {
case 'cyclomatic':
return $result['complexity']['cyclomatic'];
case 'cognitive':
return $result['complexity']['cognitive'];
case 'lines':
return $result['lines'];
default:
return 0;
}
}
/**
* Get violation severity.
*/
protected function getViolationSeverity(float $value, float $threshold): string
{
$ratio = $value / $threshold;
if ($ratio >= 2) {
return 'critical';
} elseif ($ratio >= 1.5) {
return 'high';
} elseif ($ratio >= 1.2) {
return 'medium';
} else {
return 'low';
}
}
/**
* Get cache key.
*/
protected function getCacheKey(string $filePath): string
{
$mtime = filemtime($filePath);
return md5($filePath . $mtime);
}
/**
* Limit cache size.
*/
protected function limitCacheSize(): void
{
$maxSize = $this->config['cache_size'] ?? 1000;
if (count($this->complexityCache) > $maxSize) {
$this->complexityCache = array_slice($this->complexityCache, -$maxSize, null, true);
}
}
/**
* Initialize thresholds.
*/
protected function initializeThresholds(): void
{
$this->thresholds = [
'cyclomatic' => 10,
'cognitive' => 15,
'lines' => 500,
'method_cyclomatic' => 10,
'method_cognitive' => 15
];
}
/**
* Count threshold violations.
*/
protected function countThresholdViolations(): int
{
$count = 0;
foreach ($this->complexityCache as $result) {
$count += count($result['threshold_violations'] ?? []);
}
return $count;
}
/**
* Calculate average complexity.
*/
protected function calculateAverageComplexity(): float
{
if (empty($this->complexityCache)) {
return 0;
}
$total = 0;
$count = 0;
foreach ($this->complexityCache as $result) {
$total += $result['complexity']['cyclomatic'];
$count++;
}
return $count > 0 ? $total / $count : 0;
}
/**
* Get default configuration.
*/
protected function getDefaultConfig(): array
{
return [
'cyclomatic' => [
'enabled' => true,
'threshold' => 10
],
'cognitive' => [
'enabled' => true,
'threshold' => 15
],
'halstead' => [
'enabled' => false
],
'parser' => [
'tolerant' => true
],
'cache_size' => 1000
];
}
/**
* Get configuration.
*/
public function getConfig(): array
{
return $this->config;
}
/**
* Set configuration.
*/
public function setConfig(array $config): void
{
$this->config = array_merge($this->config, $config);
}
/**
* Create complexity detector instance.
*/
public static function create(array $config = []): self
{
return new self($config);
}
/**
* Create for development.
*/
public static function forDevelopment(): self
{
return new self([
'cyclomatic' => [
'threshold' => 15 // More lenient for development
],
'cognitive' => [
'threshold' => 20
]
]);
}
/**
* Create for production.
*/
public static function forProduction(): self
{
return new self([
'cyclomatic' => [
'threshold' => 8 // Stricter for production
],
'cognitive' => [
'threshold' => 12
]
]);
}
}

View File

@@ -0,0 +1,966 @@
<?php
declare(strict_types=1);
namespace Fendx\Service\Quality\Code;
use Fendx\Service\Quality\Code\Duplicate\Tokenizer;
use Fendx\Service\Quality\Code\Duplicate\HashGenerator;
use Fendx\Service\Quality\Code\Duplicate\SimilarityCalculator;
use Fendx\Service\Quality\Code\Parser\CodeParser;
class DuplicateDetector
{
protected array $config = [];
protected Tokenizer $tokenizer;
protected HashGenerator $hashGenerator;
protected SimilarityCalculator $similarityCalculator;
protected CodeParser $parser;
protected array $duplicateCache = [];
protected array $hashIndex = [];
protected array $fileHashes = [];
public function __construct(array $config = [])
{
$this->config = array_merge($this->getDefaultConfig(), $config);
$this->tokenizer = new Tokenizer($this->config['tokenizer'] ?? []);
$this->hashGenerator = new HashGenerator($this->config['hash'] ?? []);
$this->similarityCalculator = new SimilarityCalculator($this->config['similarity'] ?? []);
$this->parser = new CodeParser($this->config['parser'] ?? []);
}
/**
* Find duplicates in a file.
*/
public function findDuplicatesInFile(string $filePath): array
{
if (!file_exists($filePath)) {
throw new \InvalidArgumentException("File not found: {$filePath}");
}
$content = file_get_contents($filePath);
if ($content === false) {
throw new \RuntimeException("Failed to read file: {$filePath}");
}
return $this->analyzeFile($filePath, $content);
}
/**
* Find duplicates across multiple files.
*/
public function findDuplicatesInFiles(array $filePaths): array
{
$results = [
'files_analyzed' => 0,
'total_files' => count($filePaths),
'duplicates' => [],
'duplicate_groups' => [],
'statistics' => [],
'timestamp' => microtime(true)
];
// Process all files and build hash index
foreach ($filePaths as $filePath) {
try {
$fileResult = $this->findDuplicatesInFile($filePath);
$results['files_analyzed']++;
// Add to global results
if (!empty($fileResult['duplicates'])) {
$results['duplicates'] = array_merge($results['duplicates'], $fileResult['duplicates']);
}
} catch (\Exception $e) {
$results['errors'][] = [
'file' => $filePath,
'error' => $e->getMessage()
];
}
}
// Group duplicates by similarity
$results['duplicate_groups'] = $this->groupDuplicates($results['duplicates']);
// Calculate statistics
$results['statistics'] = $this->calculateStatistics($results);
return $results;
}
/**
* Find duplicates in directory.
*/
public function findDuplicatesInDirectory(string $directory, array $options = []): array
{
if (!is_dir($directory)) {
throw new \InvalidArgumentException("Directory not found: {$directory}");
}
$filePaths = $this->findPhpFiles($directory, $options);
return $this->findDuplicatesInFiles($filePaths);
}
/**
* Detect similar code blocks.
*/
public function detectSimilarBlocks(string $filePath, float $threshold = 0.8): array
{
$fileResult = $this->findDuplicatesInFile($filePath);
$similarBlocks = [];
foreach ($fileResult['blocks'] as $block) {
if ($block['similarity'] >= $threshold) {
$similarBlocks[] = $block;
}
}
return [
'file' => $filePath,
'threshold' => $threshold,
'similar_blocks' => $similarBlocks,
'total_blocks' => count($similarBlocks),
'average_similarity' => $this->calculateAverageSimilarity($similarBlocks)
];
}
/**
* Compare two files for duplicates.
*/
public function compareFiles(string $file1, string $file2): array
{
$result1 = $this->findDuplicatesInFile($file1);
$result2 = $this->findDuplicatesInFile($file2);
$comparison = [
'file1' => $file1,
'file2' => $file2,
'similar_blocks' => [],
'overall_similarity' => 0,
'duplicate_percentage' => 0,
'recommendations' => []
];
// Find similar blocks between files
foreach ($result1['blocks'] as $block1) {
foreach ($result2['blocks'] as $block2) {
$similarity = $this->similarityCalculator->calculate(
$block1['tokens'],
$block2['tokens']
);
if ($similarity >= ($this->config['similarity_threshold'] ?? 0.7)) {
$comparison['similar_blocks'][] = [
'block1' => $block1,
'block2' => $block2,
'similarity' => $similarity,
'type' => $this->determineDuplicationType($block1, $block2)
];
}
}
}
// Calculate overall metrics
$comparison['overall_similarity'] = $this->calculateFileSimilarity($result1, $result2);
$comparison['duplicate_percentage'] = $this->calculateDuplicatePercentage($comparison);
// Generate recommendations
$comparison['recommendations'] = $this->generateComparisonRecommendations($comparison);
return $comparison;
}
/**
* Find duplicate methods across codebase.
*/
public function findDuplicateMethods(string $directory): array
{
$filePaths = $this->findPhpFiles($directory);
$allMethods = [];
foreach ($filePaths as $filePath) {
try {
$methods = $this->extractMethods($filePath);
foreach ($methods as $method) {
$method['file'] = $filePath;
$allMethods[] = $method;
}
} catch (\Exception $e) {
// Skip files that can't be parsed
}
}
$duplicateGroups = [];
// Compare methods for duplicates
for ($i = 0; $i < count($allMethods); $i++) {
for ($j = $i + 1; $j < count($allMethods); $j++) {
$method1 = $allMethods[$i];
$method2 = $allMethods[$j];
$similarity = $this->similarityCalculator->calculate(
$method1['tokens'],
$method2['tokens']
);
if ($similarity >= ($this->config['method_similarity_threshold'] ?? 0.8)) {
$duplicateGroups[] = [
'methods' => [$method1, $method2],
'similarity' => $similarity,
'recommendation' => 'Extract common logic to shared method or trait'
];
}
}
}
return [
'total_methods' => count($allMethods),
'duplicate_groups' => $duplicateGroups,
'duplicate_percentage' => count($duplicateGroups) / max(1, count($allMethods)) * 100
];
}
/**
* Detect copy-paste programming.
*/
public function detectCopyPaste(string $directory, int $minLines = 5): array
{
$filePaths = $this->findPhpFiles($directory);
$codeBlocks = [];
// Extract code blocks from all files
foreach ($filePaths as $filePath) {
try {
$blocks = $this->extractCodeBlocks($filePath, $minLines);
foreach ($blocks as $block) {
$block['file'] = $filePath;
$codeBlocks[] = $block;
}
} catch (\Exception $e) {
// Skip files that can't be parsed
}
}
$copyPasteGroups = [];
// Find identical or very similar blocks
for ($i = 0; $i < count($codeBlocks); $i++) {
for ($j = $i + 1; $j < count($codeBlocks); $j++) {
$block1 = $codeBlocks[$i];
$block2 = $codeBlocks[$j];
// Skip blocks from the same file
if ($block1['file'] === $block2['file']) {
continue;
}
$similarity = $this->similarityCalculator->calculate(
$block1['tokens'],
$block2['tokens']
);
if ($similarity >= ($this->config['copy_paste_threshold'] ?? 0.9)) {
$copyPasteGroups[] = [
'blocks' => [$block1, $block2],
'similarity' => $similarity,
'type' => $similarity >= 0.98 ? 'identical' : 'very_similar',
'recommendation' => 'Extract to shared function or class'
];
}
}
}
return [
'total_blocks' => count($codeBlocks),
'copy_paste_groups' => $copyPasteGroups,
'copy_paste_percentage' => count($copyPasteGroups) / max(1, count($codeBlocks)) * 100,
'most_common_patterns' => $this->analyzeCommonPatterns($copyPasteGroups)
];
}
/**
* Get duplication report.
*/
public function getReport(array $results, string $format = 'html'): string
{
switch ($format) {
case 'json':
return json_encode($results, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
case 'text':
return $this->generateTextReport($results);
case 'html':
return $this->generateHtmlReport($results);
default:
throw new \InvalidArgumentException("Unsupported report format: {$format}");
}
}
/**
* Set similarity threshold.
*/
public function setSimilarityThreshold(float $threshold): void
{
$this->config['similarity_threshold'] = max(0, min(1, $threshold));
}
/**
* Get similarity threshold.
*/
public function getSimilarityThreshold(): float
{
return $this->config['similarity_threshold'] ?? 0.7;
}
/**
* Get detection statistics.
*/
public function getStatistics(): array
{
return [
'files_processed' => count($this->fileHashes),
'hash_index_size' => count($this->hashIndex),
'cache_size' => count($this->duplicateCache),
'total_duplicates_found' => $this->countTotalDuplicates()
];
}
/**
* Clear cache and indexes.
*/
public function clearCache(): void
{
$this->duplicateCache = [];
$this->hashIndex = [];
$this->fileHashes = [];
}
/**
* Analyze file for duplicates.
*/
protected function analyzeFile(string $filePath, string $content): array
{
// Check cache
$cacheKey = $this->getCacheKey($filePath, $content);
if (isset($this->duplicateCache[$cacheKey])) {
return $this->duplicateCache[$cacheKey];
}
$result = [
'file' => $filePath,
'size' => strlen($content),
'lines' => substr_count($content, "\n") + 1,
'blocks' => [],
'duplicates' => [],
'duplication_percentage' => 0,
'timestamp' => microtime(true)
];
// Tokenize content
$tokens = $this->tokenizer->tokenize($content);
// Split into blocks
$blocks = $this->splitIntoBlocks($tokens, $filePath);
$result['blocks'] = $blocks;
// Generate hashes for blocks
foreach ($blocks as $block) {
$hash = $this->hashGenerator->generate($block['tokens']);
$block['hash'] = $hash;
// Add to hash index
if (!isset($this->hashIndex[$hash])) {
$this->hashIndex[$hash] = [];
}
$this->hashIndex[$hash][] = $block;
}
// Find duplicates using hash index
$result['duplicates'] = $this->findBlockDuplicates($blocks);
// Calculate duplication percentage
$result['duplication_percentage'] = $this->calculateDuplicationPercentage($result);
// Cache result
$this->duplicateCache[$cacheKey] = $result;
$this->limitCacheSize();
return $result;
}
/**
* Split tokens into blocks.
*/
protected function splitIntoBlocks(array $tokens, string $filePath): array
{
$blocks = [];
$blockSize = $this->config['block_size'] ?? 10;
$minBlockSize = $this->config['min_block_size'] ?? 5;
for ($i = 0; $i < count($tokens); $i += $blockSize) {
$blockTokens = array_slice($tokens, $i, $blockSize);
if (count($blockTokens) >= $minBlockSize) {
$blocks[] = [
'start_line' => $this->getLineNumber($tokens, $i),
'end_line' => $this->getLineNumber($tokens, $i + count($blockTokens) - 1),
'tokens' => $blockTokens,
'size' => count($blockTokens)
];
}
}
return $blocks;
}
/**
* Find duplicates for blocks.
*/
protected function findBlockDuplicates(array $blocks): array
{
$duplicates = [];
foreach ($blocks as $block) {
$hash = $block['hash'];
if (isset($this->hashIndex[$hash]) && count($this->hashIndex[$hash]) > 1) {
// Found potential duplicates
foreach ($this->hashIndex[$hash] as $otherBlock) {
if ($otherBlock !== $block) {
$similarity = $this->similarityCalculator->calculate(
$block['tokens'],
$otherBlock['tokens']
);
if ($similarity >= $this->getSimilarityThreshold()) {
$duplicates[] = [
'block' => $block,
'duplicate' => $otherBlock,
'similarity' => $similarity,
'type' => $this->determineDuplicationType($block, $otherBlock)
];
}
}
}
}
}
return $duplicates;
}
/**
* Group duplicates by similarity.
*/
protected function groupDuplicates(array $duplicates): array
{
$groups = [];
foreach ($duplicates as $duplicate) {
$key = $duplicate['block']['hash'];
if (!isset($groups[$key])) {
$groups[$key] = [
'hash' => $key,
'blocks' => [],
'files' => [],
'average_similarity' => 0,
'type' => 'unknown'
];
}
$groups[$key]['blocks'][] = $duplicate['block'];
$groups[$key]['blocks'][] = $duplicate['duplicate'];
$file1 = $duplicate['block']['file'] ?? 'unknown';
$file2 = $duplicate['duplicate']['file'] ?? 'unknown';
if (!in_array($file1, $groups[$key]['files'])) {
$groups[$key]['files'][] = $file1;
}
if (!in_array($file2, $groups[$key]['files'])) {
$groups[$key]['files'][] = $file2;
}
$groups[$key]['average_similarity'] += $duplicate['similarity'];
$groups[$key]['type'] = $duplicate['type'];
}
// Calculate average similarities
foreach ($groups as &$group) {
if (!empty($group['blocks'])) {
$group['average_similarity'] /= count($group['blocks']) / 2;
}
}
return array_values($groups);
}
/**
* Calculate statistics.
*/
protected function calculateStatistics(array $results): array
{
$stats = [
'total_duplicates' => count($results['duplicates']),
'duplicate_groups' => count($results['duplicate_groups']),
'files_with_duplicates' => 0,
'average_similarity' => 0,
'duplication_distribution' => [
'low' => 0, 'medium' => 0, 'high' => 0, 'very_high' => 0
]
];
$filesWithDuplicates = [];
$totalSimilarity = 0;
foreach ($results['duplicates'] as $duplicate) {
$file1 = $duplicate['block']['file'] ?? 'unknown';
$file2 = $duplicate['duplicate']['file'] ?? 'unknown';
$filesWithDuplicates[] = $file1;
$filesWithDuplicates[] = $file2;
$totalSimilarity += $duplicate['similarity'];
// Distribution
$similarity = $duplicate['similarity'];
if ($similarity >= 0.95) {
$stats['duplication_distribution']['very_high']++;
} elseif ($similarity >= 0.85) {
$stats['duplication_distribution']['high']++;
} elseif ($similarity >= 0.75) {
$stats['duplication_distribution']['medium']++;
} else {
$stats['duplication_distribution']['low']++;
}
}
$stats['files_with_duplicates'] = count(array_unique($filesWithDuplicates));
if ($stats['total_duplicates'] > 0) {
$stats['average_similarity'] = $totalSimilarity / $stats['total_duplicates'];
}
return $stats;
}
/**
* Extract methods from file.
*/
protected function extractMethods(string $filePath): array
{
$content = file_get_contents($filePath);
if ($content === false) {
return [];
}
$ast = $this->parser->parse($content, $filePath);
$methods = [];
// This would extract methods from AST
// For now, return empty array
return $methods;
}
/**
* Extract code blocks from file.
*/
protected function extractCodeBlocks(string $filePath, int $minLines): array
{
$content = file_get_contents($filePath);
if ($content === false) {
return [];
}
$tokens = $this->tokenizer->tokenize($content);
$blocks = [];
// Extract blocks with minimum lines
$lines = explode("\n", $content);
for ($i = 0; $i < count($lines); $i++) {
$blockLines = [];
for ($j = $i; $j < min($i + $minLines * 2, count($lines)); $j++) {
$blockLines[] = $lines[$j];
}
if (count($blockLines) >= $minLines) {
$blockContent = implode("\n", $blockLines);
$blockTokens = $this->tokenizer->tokenize($blockContent);
$blocks[] = [
'start_line' => $i + 1,
'end_line' => $i + count($blockLines),
'content' => $blockContent,
'tokens' => $blockTokens,
'size' => count($blockTokens)
];
}
}
return $blocks;
}
/**
* Determine duplication type.
*/
protected function determineDuplicationType(array $block1, array $block2): string
{
if ($block1['hash'] === $block2['hash']) {
return 'identical';
} elseif ($block1['size'] === $block2['size']) {
return 'structural';
} else {
return 'partial';
}
}
/**
* Calculate file similarity.
*/
protected function calculateFileSimilarity(array $result1, array $result2): float
{
if (empty($result1['blocks']) || empty($result2['blocks'])) {
return 0;
}
$totalSimilarity = 0;
$comparisons = 0;
foreach ($result1['blocks'] as $block1) {
foreach ($result2['blocks'] as $block2) {
$similarity = $this->similarityCalculator->calculate(
$block1['tokens'],
$block2['tokens']
);
$totalSimilarity += $similarity;
$comparisons++;
}
}
return $comparisons > 0 ? $totalSimilarity / $comparisons : 0;
}
/**
* Calculate duplicate percentage.
*/
protected function calculateDuplicatePercentage(array $result): float
{
if (empty($result['blocks'])) {
return 0;
}
$duplicateBlocks = 0;
foreach ($result['blocks'] as $block) {
$hash = $block['hash'];
if (isset($this->hashIndex[$hash]) && count($this->hashIndex[$hash]) > 1) {
$duplicateBlocks++;
}
}
return ($duplicateBlocks / count($result['blocks'])) * 100;
}
/**
* Calculate duplicate percentage for comparison.
*/
protected function calculateDuplicatePercentage(array $comparison): float
{
if (empty($comparison['similar_blocks'])) {
return 0;
}
return count($comparison['similar_blocks']) * 2; // Each similarity involves 2 blocks
}
/**
* Calculate average similarity.
*/
protected function calculateAverageSimilarity(array $blocks): float
{
if (empty($blocks)) {
return 0;
}
$total = 0;
foreach ($blocks as $block) {
$total += $block['similarity'];
}
return $total / count($blocks);
}
/**
* Generate comparison recommendations.
*/
protected function generateComparisonRecommendations(array $comparison): array
{
$recommendations = [];
if ($comparison['duplicate_percentage'] > 30) {
$recommendations[] = "High duplication detected ({$comparison['duplicate_percentage']}%). Consider refactoring.";
}
if ($comparison['overall_similarity'] > 0.8) {
$recommendations[] = "Files are very similar. Consider merging or extracting common code.";
}
foreach ($comparison['similar_blocks'] as $block) {
if ($block['similarity'] > 0.9) {
$recommendations[] = "Near-identical code blocks found. Extract to shared function.";
}
}
return array_unique($recommendations);
}
/**
* Analyze common patterns in copy-paste.
*/
protected function analyzeCommonPatterns(array $copyPasteGroups): array
{
$patterns = [];
// This would analyze common patterns in copy-paste groups
// For now, return empty array
return $patterns;
}
/**
* Find PHP files.
*/
protected function findPhpFiles(string $directory, array $options = []): array
{
$files = [];
$extensions = $options['extensions'] ?? ['php'];
$exclude = $options['exclude'] ?? ['vendor', 'node_modules', '.git'];
$recursive = $options['recursive'] ?? true;
$iterator = $recursive ?
new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($directory)) :
new \DirectoryIterator($directory);
foreach ($iterator as $file) {
if ($file->isDot() || !$file->isFile()) {
continue;
}
$fileName = $file->getFilename();
$filePath = $file->getPathname();
// Check extension
$extension = strtolower(pathinfo($fileName, PATHINFO_EXTENSION));
if (!in_array($extension, $extensions)) {
continue;
}
// Check exclude patterns
$excluded = false;
foreach ($exclude as $pattern) {
if (strpos($filePath, $pattern) !== false) {
$excluded = true;
break;
}
}
if (!$excluded) {
$files[] = $filePath;
}
}
sort($files);
return $files;
}
/**
* Get line number from token position.
*/
protected function getLineNumber(array $tokens, int $position): int
{
if ($position >= count($tokens)) {
return count($tokens);
}
$line = 1;
for ($i = 0; $i <= $position; $i++) {
if (isset($tokens[$i]['type']) && $tokens[$i]['type'] === 'T_NEWLINE') {
$line++;
}
}
return $line;
}
/**
* Generate text report.
*/
protected function generateTextReport(array $results): string
{
$report = "Duplicate Code Detection Report\n";
$report .= "================================\n\n";
if (isset($results['directory'])) {
$report .= "Directory: {$results['directory']}\n";
}
$report .= "Files analyzed: {$results['files_analyzed']}/{$results['total_files']}\n\n";
if (isset($results['statistics'])) {
$stats = $results['statistics'];
$report .= "Statistics:\n";
$report .= "- Total duplicates: {$stats['total_duplicates']}\n";
$report .= "- Duplicate groups: {$stats['duplicate_groups']}\n";
$report .= "- Files with duplicates: {$stats['files_with_duplicates']}\n";
$report .= "- Average similarity: " . number_format($stats['average_similarity'], 2) . "\n\n";
$report .= "Duplication distribution:\n";
foreach ($stats['duplication_distribution'] as $level => $count) {
$report .= "- {$level}: {$count}\n";
}
}
return $report;
}
/**
* Generate HTML report.
*/
protected function generateHtmlReport(array $results): string
{
$html = "<html><head><title>Duplicate Code Detection Report</title>";
$html .= "<style>body{font-family:Arial,sans-serif;margin:20px;}";
$html .= ".header{background:#f5f5f5;padding:10px;border-radius:5px;}";
$html .= ".duplicate{background:#ffebee;margin:10px 0;padding:10px;border-radius:3px;}";
$html .= ".similarity{font-weight:bold;color:#d32f2f;}</style></head><body>";
$html .= "<div class='header'><h1>Duplicate Code Detection Report</h1>";
if (isset($results['directory'])) {
$html .= "<p>Directory: <strong>{$results['directory']}</strong></p>";
}
$html .= "<p>Files analyzed: <strong>{$results['files_analyzed']}/{$results['total_files']}</strong></p>";
$html .= "</div>";
// Add duplicate details
// ... (detailed HTML generation)
$html .= "</body></html>";
return $html;
}
/**
* Get cache key.
*/
protected function getCacheKey(string $filePath, string $content): string
{
return md5($filePath . strlen($content) . filemtime($filePath));
}
/**
* Limit cache size.
*/
protected function limitCacheSize(): void
{
$maxSize = $this->config['cache_size'] ?? 1000;
if (count($this->duplicateCache) > $maxSize) {
$this->duplicateCache = array_slice($this->duplicateCache, -$maxSize, null, true);
}
}
/**
* Count total duplicates.
*/
protected function countTotalDuplicates(): int
{
$total = 0;
foreach ($this->duplicateCache as $result) {
$total += count($result['duplicates'] ?? []);
}
return $total;
}
/**
* Get default configuration.
*/
protected function getDefaultConfig(): array
{
return [
'similarity_threshold' => 0.7,
'method_similarity_threshold' => 0.8,
'copy_paste_threshold' => 0.9,
'block_size' => 10,
'min_block_size' => 5,
'tokenizer' => [
'ignore_whitespace' => true,
'ignore_comments' => true
],
'hash' => [
'algorithm' => 'md5'
],
'similarity' => [
'algorithm' => 'jaccard'
],
'parser' => [
'tolerant' => true
],
'cache_size' => 1000
];
}
/**
* Get configuration.
*/
public function getConfig(): array
{
return $this->config;
}
/**
* Set configuration.
*/
public function setConfig(array $config): void
{
$this->config = array_merge($this->config, $config);
}
/**
* Create duplicate detector instance.
*/
public static function create(array $config = []): self
{
return new self($config);
}
/**
* Create for development.
*/
public static function forDevelopment(): self
{
return new self([
'similarity_threshold' => 0.6, // More lenient for development
'block_size' => 8,
'min_block_size' => 4
]);
}
/**
* Create for production.
*/
public static function forProduction(): self
{
return new self([
'similarity_threshold' => 0.8, // Stricter for production
'block_size' => 12,
'min_block_size' => 6
]);
}
}

View File

@@ -0,0 +1,869 @@
<?php
declare(strict_types=1);
namespace Fendx\Service\Quality\Code;
use Fendx\Service\Quality\Code\Analyzer\SecurityAnalyzer;
use Fendx\Service\Quality\Code\Analyzer\PerformanceAnalyzer;
use Fendx\Service\Quality\Code\Analyzer\MaintainabilityAnalyzer;
use Fendx\Service\Quality\Code\Parser\CodeParser;
use Fendx\Service\Quality\Code\Reporter\AnalysisReporter;
class StaticAnalyzer
{
protected array $config = [];
protected array $analyzers = [];
protected CodeParser $parser;
protected AnalysisReporter $reporter;
protected array $analysisCache = [];
protected array $projectMetrics = [];
public function __construct(array $config = [])
{
$this->config = array_merge($this->getDefaultConfig(), $config);
$this->parser = new CodeParser($this->config['parser'] ?? []);
$this->reporter = new AnalysisReporter($this->config['reporter'] ?? []);
$this->initializeAnalyzers();
}
/**
* Analyze a PHP file.
*/
public function analyzeFile(string $filePath): array
{
if (!file_exists($filePath)) {
throw new \InvalidArgumentException("File not found: {$filePath}");
}
// Check cache
$cacheKey = $this->getCacheKey($filePath);
if (isset($this->analysisCache[$cacheKey])) {
return $this->analysisCache[$cacheKey];
}
$content = file_get_contents($filePath);
if ($content === false) {
throw new \RuntimeException("Failed to read file: {$filePath}");
}
// Parse code
$ast = $this->parser->parse($content, $filePath);
$result = [
'file' => $filePath,
'size' => strlen($content),
'lines' => substr_count($content, "\n") + 1,
'complexity' => 0,
'maintainability_index' => 0,
'security_issues' => [],
'performance_issues' => [],
'code_smells' => [],
'metrics' => [],
'violations' => [],
'score' => 100,
'timestamp' => microtime(true)
];
// Run all analyzers
foreach ($this->analyzers as $name => $analyzer) {
$analysisResult = $analyzer->analyze($ast, $content, $filePath);
$result['metrics'][$name] = $analysisResult['metrics'] ?? [];
$result['violations'] = array_merge($result['violations'], $analysisResult['violations'] ?? []);
// Collect specific issues
if ($name === 'security') {
$result['security_issues'] = $analysisResult['issues'] ?? [];
} elseif ($name === 'performance') {
$result['performance_issues'] = $analysisResult['issues'] ?? [];
} elseif ($name === 'maintainability') {
$result['code_smells'] = $analysisResult['issues'] ?? [];
$result['maintainability_index'] = $analysisResult['maintainability_index'] ?? 0;
}
}
// Calculate overall complexity
$result['complexity'] = $this->calculateComplexity($result['metrics']);
// Calculate score
$result['score'] = $this->calculateScore($result);
// Cache result
$this->analysisCache[$cacheKey] = $result;
$this->limitCacheSize();
return $result;
}
/**
* Analyze a directory.
*/
public function analyzeDirectory(string $directory, array $options = []): array
{
if (!is_dir($directory)) {
throw new \InvalidArgumentException("Directory not found: {$directory}");
}
$results = [
'directory' => $directory,
'files_analyzed' => 0,
'total_files' => 0,
'total_violations' => 0,
'security_issues' => 0,
'performance_issues' => 0,
'code_smells' => 0,
'average_complexity' => 0,
'average_maintainability' => 0,
'average_score' => 0,
'files' => [],
'summary' => [],
'recommendations' => [],
'timestamp' => microtime(true)
];
// Find PHP files
$files = $this->findPhpFiles($directory, $options);
$results['total_files'] = count($files);
if (empty($files)) {
return $results;
}
$totalComplexity = 0;
$totalMaintainability = 0;
$totalScore = 0;
foreach ($files as $file) {
try {
$fileResult = $this->analyzeFile($file);
$results['files'][$file] = $fileResult;
$results['files_analyzed']++;
$results['total_violations'] += count($fileResult['violations']);
$results['security_issues'] += count($fileResult['security_issues']);
$results['performance_issues'] += count($fileResult['performance_issues']);
$results['code_smells'] += count($fileResult['code_smells']);
$totalComplexity += $fileResult['complexity'];
$totalMaintainability += $fileResult['maintainability_index'];
$totalScore += $fileResult['score'];
} catch (\Exception $e) {
$results['files'][$file] = [
'file' => $file,
'error' => $e->getMessage(),
'score' => 0
];
}
}
if ($results['files_analyzed'] > 0) {
$results['average_complexity'] = $totalComplexity / $results['files_analyzed'];
$results['average_maintainability'] = $totalMaintainability / $results['files_analyzed'];
$results['average_score'] = $totalScore / $results['files_analyzed'];
}
// Generate summary and recommendations
$results['summary'] = $this->generateDirectorySummary($results);
$results['recommendations'] = $this->generateRecommendations($results);
return $results;
}
/**
* Analyze project structure.
*/
public function analyzeProject(string $rootPath): array
{
if (!is_dir($rootPath)) {
throw new \InvalidArgumentException("Project root not found: {$rootPath}");
}
$analysis = [
'project_root' => $rootPath,
'structure' => [],
'dependencies' => [],
'architecture' => [],
'metrics' => [],
'hotspots' => [],
'trends' => [],
'timestamp' => microtime(true)
];
// Analyze directory structure
$analysis['structure'] = $this->analyzeProjectStructure($rootPath);
// Analyze dependencies
$analysis['dependencies'] = $this->analyzeDependencies($rootPath);
// Analyze architecture
$analysis['architecture'] = $this->analyzeArchitecture($rootPath);
// Calculate project metrics
$analysis['metrics'] = $this->calculateProjectMetrics($rootPath);
// Identify hotspots
$analysis['hotspots'] = $this->identifyHotspots($rootPath);
return $analysis;
}
/**
* Detect code smells.
*/
public function detectCodeSmells(string $filePath): array
{
$result = $this->analyzeFile($filePath);
return [
'file' => $filePath,
'code_smells' => $result['code_smells'],
'total_smells' => count($result['code_smells']),
'smell_types' => $this->categorizeCodeSmells($result['code_smells']),
'recommendations' => $this->generateSmellRecommendations($result['code_smells'])
];
}
/**
* Detect security vulnerabilities.
*/
public function detectSecurityIssues(string $filePath): array
{
$result = $this->analyzeFile($filePath);
return [
'file' => $filePath,
'security_issues' => $result['security_issues'],
'total_issues' => count($result['security_issues']),
'severity_distribution' => $this->categorizeSecurityIssues($result['security_issues']),
'recommendations' => $this->generateSecurityRecommendations($result['security_issues'])
];
}
/**
* Analyze code complexity.
*/
public function analyzeComplexity(string $filePath): array
{
$result = $this->analyzeFile($filePath);
return [
'file' => $filePath,
'cyclomatic_complexity' => $result['metrics']['maintainability']['cyclomatic_complexity'] ?? 0,
'cognitive_complexity' => $result['metrics']['maintainability']['cognitive_complexity'] ?? 0,
'overall_complexity' => $result['complexity'],
'complexity_distribution' => $this->analyzeComplexityDistribution($result),
'recommendations' => $this->generateComplexityRecommendations($result)
];
}
/**
* Get analysis report.
*/
public function getReport(array $results, string $format = 'html'): string
{
return $this->reporter->generate($results, $format);
}
/**
* Add custom analyzer.
*/
public function addAnalyzer(string $name, object $analyzer): void
{
$this->analyzers[$name] = $analyzer;
$this->logInfo("Added static analyzer: {$name}");
}
/**
* Remove analyzer.
*/
public function removeAnalyzer(string $name): bool
{
if (!isset($this->analyzers[$name])) {
return false;
}
unset($this->analyzers[$name]);
$this->logInfo("Removed static analyzer: {$name}");
return true;
}
/**
* Get available analyzers.
*/
public function getAnalyzers(): array
{
return array_keys($this->analyzers);
}
/**
* Get analysis statistics.
*/
public function getStatistics(): array
{
$stats = [
'analyzers_count' => count($this->analyzers),
'cache_size' => count($this->analysisCache),
'files_analyzed' => 0,
'total_violations_found' => 0,
'average_analysis_time' => 0
];
foreach ($this->analyzers as $name => $analyzer) {
$analyzerStats = $analyzer->getStatistics();
$stats['files_analyzed'] += $analyzerStats['files_analyzed'] ?? 0;
$stats['total_violations_found'] += $analyzerStats['violations_found'] ?? 0;
}
return $stats;
}
/**
* Clear cache.
*/
public function clearCache(): void
{
$this->analysisCache = [];
$this->logInfo("Static analyzer cache cleared");
}
/**
* Calculate code complexity.
*/
protected function calculateComplexity(array $metrics): int
{
$complexity = 0;
if (isset($metrics['maintainability']['cyclomatic_complexity'])) {
$complexity += $metrics['maintainability']['cyclomatic_complexity'];
}
if (isset($metrics['maintainability']['cognitive_complexity'])) {
$complexity += $metrics['maintainability']['cognitive_complexity'];
}
return $complexity;
}
/**
* Calculate overall score.
*/
protected function calculateScore(array $result): int
{
$score = 100;
// Deduct points for violations
$violationCount = count($result['violations']);
if ($violationCount > 0) {
$score -= min(50, $violationCount * 2); // Max 50 points deduction
}
// Deduct points for high complexity
if ($result['complexity'] > 10) {
$score -= min(30, ($result['complexity'] - 10) * 2);
}
// Deduct points for low maintainability
if ($result['maintainability_index'] < 70) {
$score -= min(20, (70 - $result['maintainability_index']) / 2);
}
// Bonus for no security issues
if (empty($result['security_issues'])) {
$score += 5;
}
return max(0, min(100, $score));
}
/**
* Find PHP files in directory.
*/
protected function findPhpFiles(string $directory, array $options = []): array
{
$files = [];
$extensions = $options['extensions'] ?? ['php'];
$exclude = $options['exclude'] ?? ['vendor', 'node_modules', '.git', 'tests'];
$recursive = $options['recursive'] ?? true;
$iterator = $recursive ?
new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($directory)) :
new \DirectoryIterator($directory);
foreach ($iterator as $file) {
if ($file->isDot() || !$file->isFile()) {
continue;
}
$fileName = $file->getFilename();
$filePath = $file->getPathname();
// Check extension
$extension = strtolower(pathinfo($fileName, PATHINFO_EXTENSION));
if (!in_array($extension, $extensions)) {
continue;
}
// Check exclude patterns
$excluded = false;
foreach ($exclude as $pattern) {
if (strpos($filePath, $pattern) !== false) {
$excluded = true;
break;
}
}
if (!$excluded) {
$files[] = $filePath;
}
}
sort($files);
return $files;
}
/**
* Generate directory summary.
*/
protected function generateDirectorySummary(array $results): array
{
return [
'grade' => $this->calculateGrade($results['average_score']),
'quality_level' => $this->getQualityLevel($results['average_score']),
'complexity_level' => $this->getComplexityLevel($results['average_complexity']),
'maintainability_level' => $this->getMaintainabilityLevel($results['average_maintainability']),
'security_risk' => $this->getSecurityRisk($results['security_issues']),
'performance_risk' => $this->getPerformanceRisk($results['performance_issues'])
];
}
/**
* Generate recommendations.
*/
protected function generateRecommendations(array $results): array
{
$recommendations = [];
if ($results['security_issues'] > 0) {
$recommendations[] = "Address {$results['security_issues']} security issue(s) immediately";
}
if ($results['performance_issues'] > 5) {
$recommendations[] = "Optimize {$results['performance_issues']} performance issue(s)";
}
if ($results['code_smells'] > 10) {
$recommendations[] = "Refactor {$results['code_smells']} code smell(s) to improve maintainability";
}
if ($results['average_complexity'] > 15) {
$recommendations[] = "Reduce average complexity (current: " .
number_format($results['average_complexity'], 1) . ")";
}
if ($results['average_maintainability'] < 70) {
$recommendations[] = "Improve code maintainability (current: " .
number_format($results['average_maintainability'], 1) . ")";
}
if ($results['average_score'] < 80) {
$recommendations[] = "Overall code quality needs improvement (score: " .
number_format($results['average_score'], 1) . ")";
}
return $recommendations;
}
/**
* Analyze project structure.
*/
protected function analyzeProjectStructure(string $rootPath): array
{
$structure = [
'directories' => [],
'files' => [],
'depth' => 0,
'organization_score' => 0
];
// This would analyze the directory structure
// For now, return basic structure
return $structure;
}
/**
* Analyze dependencies.
*/
protected function analyzeDependencies(string $rootPath): array
{
return [
'internal_dependencies' => [],
'external_dependencies' => [],
'dependency_graph' => [],
'circular_dependencies' => []
];
}
/**
* Analyze architecture.
*/
protected function analyzeArchitecture(string $rootPath): array
{
return [
'layers' => [],
'patterns' => [],
'violations' => [],
'coupling' => [],
'cohesion' => []
];
}
/**
* Calculate project metrics.
*/
protected function calculateProjectMetrics(string $rootPath): array
{
return [
'lines_of_code' => 0,
'classes' => 0,
'methods' => 0,
'average_class_size' => 0,
'average_method_size' => 0,
'duplication_percentage' => 0
];
}
/**
* Identify hotspots.
*/
protected function identifyHotspots(string $rootPath): array
{
return [
'complex_files' => [],
'large_files' => [],
'frequently_changed' => [],
'bug_prone' => []
];
}
/**
* Categorize code smells.
*/
protected function categorizeCodeSmells(array $smells): array
{
$categories = [
'design' => 0,
'implementation' => 0,
'naming' => 0,
'formatting' => 0
];
foreach ($smells as $smell) {
$type = $smell['category'] ?? 'implementation';
$categories[$type] = ($categories[$type] ?? 0) + 1;
}
return $categories;
}
/**
* Categorize security issues.
*/
protected function categorizeSecurityIssues(array $issues): array
{
$severity = [
'critical' => 0,
'high' => 0,
'medium' => 0,
'low' => 0
];
foreach ($issues as $issue) {
$level = $issue['severity'] ?? 'medium';
$severity[$level] = ($severity[$level] ?? 0) + 1;
}
return $severity;
}
/**
* Analyze complexity distribution.
*/
protected function analyzeComplexityDistribution(array $result): array
{
return [
'low' => 0,
'medium' => 0,
'high' => 0,
'very_high' => 0
];
}
/**
* Generate smell recommendations.
*/
protected function generateSmellRecommendations(array $smells): array
{
$recommendations = [];
foreach ($smells as $smell) {
$recommendations[] = $smell['recommendation'] ?? "Address code smell: {$smell['type']}";
}
return array_unique($recommendations);
}
/**
* Generate security recommendations.
*/
protected function generateSecurityRecommendations(array $issues): array
{
$recommendations = [];
foreach ($issues as $issue) {
$recommendations[] = $issue['recommendation'] ?? "Fix security issue: {$issue['type']}";
}
return array_unique($recommendations);
}
/**
* Generate complexity recommendations.
*/
protected function generateComplexityRecommendations(array $result): array
{
$recommendations = [];
if ($result['complexity'] > 20) {
$recommendations[] = "Consider breaking down complex methods";
}
if ($result['complexity'] > 10) {
$recommendations[] = "Reduce cyclomatic complexity";
}
return $recommendations;
}
/**
* Calculate grade from score.
*/
protected function calculateGrade(float $score): string
{
if ($score >= 90) return 'A';
if ($score >= 80) return 'B';
if ($score >= 70) return 'C';
if ($score >= 60) return 'D';
return 'F';
}
/**
* Get quality level from score.
*/
protected function getQualityLevel(float $score): string
{
if ($score >= 90) return 'excellent';
if ($score >= 80) return 'good';
if ($score >= 70) return 'fair';
if ($score >= 60) return 'poor';
return 'failing';
}
/**
* Get complexity level.
*/
protected function getComplexityLevel(float $complexity): string
{
if ($complexity <= 5) return 'low';
if ($complexity <= 10) return 'moderate';
if ($complexity <= 20) return 'high';
return 'very_high';
}
/**
* Get maintainability level.
*/
protected function getMaintainabilityLevel(float $index): string
{
if ($index >= 85) return 'excellent';
if ($index >= 70) return 'good';
if ($index >= 50) return 'moderate';
return 'poor';
}
/**
* Get security risk level.
*/
protected function getSecurityRisk(int $issues): string
{
if ($issues === 0) return 'none';
if ($issues <= 2) return 'low';
if ($issues <= 5) return 'medium';
return 'high';
}
/**
* Get performance risk level.
*/
protected function getPerformanceRisk(int $issues): string
{
if ($issues === 0) return 'none';
if ($issues <= 3) return 'low';
if ($issues <= 8) return 'medium';
return 'high';
}
/**
* Get cache key for file.
*/
protected function getCacheKey(string $filePath): string
{
$mtime = filemtime($filePath);
return md5($filePath . $mtime);
}
/**
* Limit cache size.
*/
protected function limitCacheSize(): void
{
$maxSize = $this->config['cache_size'] ?? 1000;
if (count($this->analysisCache) > $maxSize) {
$this->analysisCache = array_slice($this->analysisCache, -$maxSize, null, true);
}
}
/**
* Initialize analyzers.
*/
protected function initializeAnalyzers(): void
{
$this->analyzers['security'] = new SecurityAnalyzer($this->config['analyzers']['security'] ?? []);
$this->analyzers['performance'] = new PerformanceAnalyzer($this->config['analyzers']['performance'] ?? []);
$this->analyzers['maintainability'] = new MaintainabilityAnalyzer($this->config['analyzers']['maintainability'] ?? []);
$this->logInfo("Initialized " . count($this->analyzers) . " static analyzers");
}
/**
* Log info message.
*/
protected function logInfo(string $message): void
{
if ($this->config['logging_enabled']) {
error_log("[StaticAnalyzer] {$message}");
}
}
/**
* Get default configuration.
*/
protected function getDefaultConfig(): array
{
return [
'analyzers' => [
'security' => [
'enabled' => true,
'rules' => ['sql_injection', 'xss', 'csrf', 'path_traversal']
],
'performance' => [
'enabled' => true,
'rules' => ['slow_queries', 'memory_leaks', 'inefficient_loops']
],
'maintainability' => [
'enabled' => true,
'rules' => ['complexity', 'duplication', 'long_methods']
]
],
'parser' => [
'tolerant' => true,
'track_positions' => true
],
'reporter' => [
'format' => 'html',
'include_metrics' => true
],
'cache_size' => 1000,
'logging_enabled' => true
];
}
/**
* Get configuration.
*/
public function getConfig(): array
{
return $this->config;
}
/**
* Set configuration.
*/
public function setConfig(array $config): void
{
$this->config = array_merge($this->config, $config);
}
/**
* Create static analyzer instance.
*/
public static function create(array $config = []): self
{
return new self($config);
}
/**
* Create for development.
*/
public static function forDevelopment(): self
{
return new self([
'analyzers' => [
'security' => [
'enabled' => true,
'strict_mode' => false
],
'performance' => [
'enabled' => true,
'thresholds' => ['complexity' => 15, 'method_length' => 50]
],
'maintainability' => [
'enabled' => true,
'tolerance' => 'medium'
]
],
'logging_enabled' => true
]);
}
/**
* Create for production.
*/
public static function forProduction(): self
{
return new self([
'analyzers' => [
'security' => [
'enabled' => true,
'strict_mode' => true
],
'performance' => [
'enabled' => true,
'thresholds' => ['complexity' => 10, 'method_length' => 30]
],
'maintainability' => [
'enabled' => true,
'tolerance' => 'low'
]
],
'logging_enabled' => false
]);
}
}

View File

@@ -0,0 +1,789 @@
<?php
declare(strict_types=1);
namespace Fendx\Service\Registry;
use Fendx\Service\Registry\Storage\RegistryStorage;
use Fendx\Service\Registry\Health\HealthChecker;
use Fendx\Service\Registry\Discovery\ServiceDiscovery;
use Fendx\Service\Registry\Metadata\MetadataManager;
class ServiceRegistry
{
protected RegistryStorage $storage;
protected HealthChecker $healthChecker;
protected ServiceDiscovery $discovery;
protected MetadataManager $metadataManager;
protected array $config = [];
protected array $services = [];
protected array $instances = [];
public function __construct(array $config = [])
{
$this->config = array_merge($this->getDefaultConfig(), $config);
$this->storage = new RegistryStorage($this->config);
$this->healthChecker = new HealthChecker($this->config);
$this->discovery = new ServiceDiscovery($this->config);
$this->metadataManager = new MetadataManager($this->config);
$this->initialize();
}
/**
* Register a service.
*/
public function registerService(string $serviceName, array $serviceConfig): bool
{
$this->validateServiceConfig($serviceConfig);
$service = [
'name' => $serviceName,
'id' => $serviceConfig['id'] ?? $this->generateServiceId($serviceName),
'host' => $serviceConfig['host'],
'port' => $serviceConfig['port'],
'protocol' => $serviceConfig['protocol'] ?? 'http',
'path' => $serviceConfig['path'] ?? '/',
'metadata' => $serviceConfig['metadata'] ?? [],
'tags' => $serviceConfig['tags'] ?? [],
'weight' => $serviceConfig['weight'] ?? 1,
'enabled' => $serviceConfig['enabled'] ?? true,
'health_check' => $serviceConfig['health_check'] ?? null,
'registered_at' => time(),
'updated_at' => time(),
'last_heartbeat' => time()
];
// Store service
$this->services[$serviceName][$service['id']] = $service;
$this->instances[$service['id']] = $service;
// Persist to storage
$this->storage->storeService($service);
// Start health checking if configured
if ($service['health_check']) {
$this->healthChecker->startMonitoring($service['id'], $service['health_check']);
}
$this->logInfo("Service registered: {$serviceName} ({$service['id']})");
return true;
}
/**
* Unregister a service.
*/
public function unregisterService(string $serviceName, string $serviceId = null): bool
{
if ($serviceId === null) {
// Remove all instances of the service
if (isset($this->services[$serviceName])) {
foreach ($this->services[$serviceName] as $id => $service) {
$this->removeServiceInstance($id);
}
unset($this->services[$serviceName]);
}
} else {
// Remove specific instance
if (isset($this->services[$serviceName][$serviceId])) {
$this->removeServiceInstance($serviceId);
unset($this->services[$serviceName][$serviceId]);
// Remove service entry if no instances left
if (empty($this->services[$serviceName])) {
unset($this->services[$serviceName]);
}
}
}
$this->logInfo("Service unregistered: {$serviceName}" . ($serviceId ? " ({$serviceId})" : ""));
return true;
}
/**
* Get service instances.
*/
public function getServiceInstances(string $serviceName, array $filters = []): array
{
if (!isset($this->services[$serviceName])) {
return [];
}
$instances = $this->services[$serviceName];
// Apply filters
if (!empty($filters)) {
$instances = $this->filterInstances($instances, $filters);
}
// Sort by weight and health
uasort($instances, function ($a, $b) {
// Healthy instances first
$aHealthy = $this->healthChecker->isHealthy($a['id']);
$bHealthy = $this->healthChecker->isHealthy($b['id']);
if ($aHealthy !== $bHealthy) {
return $bHealthy <=> $aHealthy;
}
// Then by weight
return $b['weight'] <=> $a['weight'];
});
return array_values($instances);
}
/**
* Get a single service instance (for load balancing).
*/
public function getServiceInstance(string $serviceName, array $filters = []): ?array
{
$instances = $this->getServiceInstances($serviceName, $filters);
if (empty($instances)) {
return null;
}
// Load balancing strategy
$strategy = $this->config['load_balancing_strategy'] ?? 'round_robin';
return match ($strategy) {
'random' => $instances[array_rand($instances)],
'round_robin' => $this->selectRoundRobin($serviceName, $instances),
'weighted' => $this->selectWeighted($instances),
'least_connections' => $this->selectLeastConnections($instances),
default => $instances[0]
};
}
/**
* Update service metadata.
*/
public function updateServiceMetadata(string $serviceId, array $metadata): bool
{
if (!isset($this->instances[$serviceId])) {
return false;
}
$this->instances[$serviceId]['metadata'] = array_merge(
$this->instances[$serviceId]['metadata'],
$metadata
);
$this->instances[$serviceId]['updated_at'] = time();
// Update in all service collections
foreach ($this->services as $serviceName => $instances) {
if (isset($instances[$serviceId])) {
$this->services[$serviceName][$serviceId] = $this->instances[$serviceId];
}
}
// Persist to storage
$this->storage->updateService($serviceId, $this->instances[$serviceId]);
return true;
}
/**
* Update service health status.
*/
public function updateServiceHealth(string $serviceId, bool $healthy, string $message = ''): void
{
if (!isset($this->instances[$serviceId])) {
return;
}
$this->instances[$serviceId]['healthy'] = $healthy;
$this->instances[$serviceId]['health_message'] = $message;
$this->instances[$serviceId]['updated_at'] = time();
// Update in all service collections
foreach ($this->services as $serviceName => $instances) {
if (isset($instances[$serviceId])) {
$this->services[$serviceName][$serviceId] = $this->instances[$serviceId];
}
}
// Persist to storage
$this->storage->updateService($serviceId, $this->instances[$serviceId]);
}
/**
* Send heartbeat for service.
*/
public function heartbeat(string $serviceId): bool
{
if (!isset($this->instances[$serviceId])) {
return false;
}
$this->instances[$serviceId]['last_heartbeat'] = time();
// Update in all service collections
foreach ($this->services as $serviceName => $instances) {
if (isset($instances[$serviceId])) {
$this->services[$serviceName][$serviceId] = $this->instances[$serviceId];
}
}
// Persist to storage
$this->storage->updateService($serviceId, $this->instances[$serviceId]);
return true;
}
/**
* Get all registered services.
*/
public function getAllServices(): array
{
$services = [];
foreach ($this->services as $serviceName => $instances) {
$services[$serviceName] = [
'name' => $serviceName,
'instances' => count($instances),
'healthy_instances' => count(array_filter($instances, function($instance) {
return $this->healthChecker->isHealthy($instance['id']);
})),
'enabled_instances' => count(array_filter($instances, function($instance) {
return $instance['enabled'];
}))
];
}
return $services;
}
/**
* Get service details.
*/
public function getServiceDetails(string $serviceName): array
{
if (!isset($this->services[$serviceName])) {
return [];
}
$instances = $this->services[$serviceName];
$healthyCount = 0;
$enabledCount = 0;
foreach ($instances as $instance) {
if ($this->healthChecker->isHealthy($instance['id'])) {
$healthyCount++;
}
if ($instance['enabled']) {
$enabledCount++;
}
}
return [
'name' => $serviceName,
'instances' => $instances,
'total_instances' => count($instances),
'healthy_instances' => $healthyCount,
'enabled_instances' => $enabledCount,
'tags' => array_unique(array_merge(...array_column($instances, 'tags')))
];
}
/**
* Find services by tags.
*/
public function findServicesByTags(array $tags): array
{
$matchingServices = [];
foreach ($this->services as $serviceName => $instances) {
foreach ($instances as $instance) {
if (count(array_intersect($tags, $instance['tags'])) > 0) {
$matchingServices[$serviceName][] = $instance;
}
}
}
return $matchingServices;
}
/**
* Find services by metadata.
*/
public function findServicesByMetadata(array $metadata): array
{
$matchingServices = [];
foreach ($this->services as $serviceName => $instances) {
foreach ($instances as $instance) {
if ($this->matchesMetadata($instance['metadata'], $metadata)) {
$matchingServices[$serviceName][] = $instance;
}
}
}
return $matchingServices;
}
/**
* Get service statistics.
*/
public function getStatistics(): array
{
$totalServices = count($this->services);
$totalInstances = count($this->instances);
$healthyInstances = 0;
$enabledInstances = 0;
$servicesByTag = [];
$servicesByProtocol = [];
foreach ($this->instances as $instance) {
if ($this->healthChecker->isHealthy($instance['id'])) {
$healthyInstances++;
}
if ($instance['enabled']) {
$enabledInstances++;
}
// Count by tags
foreach ($instance['tags'] as $tag) {
$servicesByTag[$tag] = ($servicesByTag[$tag] ?? 0) + 1;
}
// Count by protocol
$protocol = $instance['protocol'];
$servicesByProtocol[$protocol] = ($servicesByProtocol[$protocol] ?? 0) + 1;
}
return [
'total_services' => $totalServices,
'total_instances' => $totalInstances,
'healthy_instances' => $healthyInstances,
'enabled_instances' => $enabledInstances,
'unhealthy_instances' => $totalInstances - $healthyInstances,
'disabled_instances' => $totalInstances - $enabledInstances,
'health_percentage' => $totalInstances > 0 ? ($healthyInstances / $totalInstances) * 100 : 0,
'services_by_tag' => $servicesByTag,
'services_by_protocol' => $servicesByProtocol,
'registry_uptime' => time() - $this->config['start_time']
];
}
/**
* Cleanup stale services.
*/
public function cleanupStaleServices(): array
{
$now = time();
$staleThreshold = $this->config['stale_threshold'] ?? 300; // 5 minutes
$removed = [];
foreach ($this->instances as $serviceId => $instance) {
if ($now - $instance['last_heartbeat'] > $staleThreshold) {
$serviceName = $this->findServiceNameById($serviceId);
if ($serviceName) {
$this->unregisterService($serviceName, $serviceId);
$removed[] = $serviceId;
}
}
}
$this->logInfo("Cleaned up " . count($removed) . " stale services");
return $removed;
}
/**
* Enable/disable service.
*/
public function enableService(string $serviceId, bool $enabled = true): bool
{
if (!isset($this->instances[$serviceId])) {
return false;
}
$this->instances[$serviceId]['enabled'] = $enabled;
$this->instances[$serviceId]['updated_at'] = time();
// Update in all service collections
foreach ($this->services as $serviceName => $instances) {
if (isset($instances[$serviceId])) {
$this->services[$serviceName][$serviceId] = $this->instances[$serviceId];
}
}
// Persist to storage
$this->storage->updateService($serviceId, $this->instances[$serviceId]);
$this->logInfo("Service " . ($enabled ? 'enabled' : 'disabled') . ": {$serviceId}");
return true;
}
/**
* Disable service.
*/
public function disableService(string $serviceId): bool
{
return $this->enableService($serviceId, false);
}
/**
* Get service URL.
*/
public function getServiceUrl(string $serviceName, array $filters = []): ?string
{
$instance = $this->getServiceInstance($serviceName, $filters);
if (!$instance) {
return null;
}
return $this->buildServiceUrl($instance);
}
/**
* Build service URL from instance.
*/
protected function buildServiceUrl(array $instance): string
{
$protocol = $instance['protocol'];
$host = $instance['host'];
$port = $instance['port'];
$path = $instance['path'];
$url = "{$protocol}://{$host}";
// Add port if not default
if (($protocol === 'http' && $port != 80) || ($protocol === 'https' && $port != 443)) {
$url .= ":{$port}";
}
$url .= $path;
return $url;
}
/**
* Filter service instances.
*/
protected function filterInstances(array $instances, array $filters): array
{
return array_filter($instances, function ($instance) use ($filters) {
// Filter by tags
if (isset($filters['tags']) && !empty($filters['tags'])) {
if (count(array_intersect($filters['tags'], $instance['tags'])) === 0) {
return false;
}
}
// Filter by protocol
if (isset($filters['protocol']) && $instance['protocol'] !== $filters['protocol']) {
return false;
}
// Filter by enabled status
if (isset($filters['enabled']) && $instance['enabled'] !== $filters['enabled']) {
return false;
}
// Filter by health status
if (isset($filters['healthy'])) {
$isHealthy = $this->healthChecker->isHealthy($instance['id']);
if ($isHealthy !== $filters['healthy']) {
return false;
}
}
// Filter by metadata
if (isset($filters['metadata']) && !$this->matchesMetadata($instance['metadata'], $filters['metadata'])) {
return false;
}
return true;
});
}
/**
* Check if metadata matches filters.
*/
protected function matchesMetadata(array $metadata, array $filters): bool
{
foreach ($filters as $key => $value) {
if (!isset($metadata[$key]) || $metadata[$key] !== $value) {
return false;
}
}
return true;
}
/**
* Select instance using round-robin.
*/
protected function selectRoundRobin(string $serviceName, array $instances): array
{
static $counters = [];
if (!isset($counters[$serviceName])) {
$counters[$serviceName] = 0;
}
$index = $counters[$serviceName] % count($instances);
$counters[$serviceName]++;
return $instances[$index];
}
/**
* Select instance using weighted random.
*/
protected function selectWeighted(array $instances): array
{
$totalWeight = array_sum(array_column($instances, 'weight'));
if ($totalWeight === 0) {
return $instances[0];
}
$random = mt_rand(1, $totalWeight);
$currentWeight = 0;
foreach ($instances as $instance) {
$currentWeight += $instance['weight'];
if ($random <= $currentWeight) {
return $instance;
}
}
return $instances[0];
}
/**
* Select instance with least connections.
*/
protected function selectLeastConnections(array $instances): array
{
$minConnections = PHP_INT_MAX;
$selected = $instances[0];
foreach ($instances as $instance) {
$connections = $this->metadataManager->getConnections($instance['id']) ?? 0;
if ($connections < $minConnections) {
$minConnections = $connections;
$selected = $instance;
}
}
return $selected;
}
/**
* Generate service ID.
*/
protected function generateServiceId(string $serviceName): string
{
return $serviceName . '_' . uniqid();
}
/**
* Find service name by ID.
*/
protected function findServiceNameById(string $serviceId): ?string
{
foreach ($this->services as $serviceName => $instances) {
if (isset($instances[$serviceId])) {
return $serviceName;
}
}
return null;
}
/**
* Remove service instance.
*/
protected function removeServiceInstance(string $serviceId): void
{
// Stop health checking
$this->healthChecker->stopMonitoring($serviceId);
// Remove from storage
$this->storage->removeService($serviceId);
// Remove from instances
unset($this->instances[$serviceId]);
}
/**
* Validate service configuration.
*/
protected function validateServiceConfig(array $config): void
{
$required = ['host', 'port'];
foreach ($required as $field) {
if (!isset($config[$field])) {
throw new \InvalidArgumentException("Missing required field: {$field}");
}
}
if (!is_string($config['host'])) {
throw new \InvalidArgumentException("Host must be a string");
}
if (!is_int($config['port']) || $config['port'] < 1 || $config['port'] > 65535) {
throw new \InvalidArgumentException("Port must be an integer between 1 and 65535");
}
if (isset($config['protocol']) && !in_array($config['protocol'], ['http', 'https', 'tcp', 'udp'])) {
throw new \InvalidArgumentException("Invalid protocol: {$config['protocol']}");
}
if (isset($config['weight']) && (!is_int($config['weight']) || $config['weight'] < 1)) {
throw new \InvalidArgumentException("Weight must be a positive integer");
}
}
/**
* Initialize registry.
*/
protected function initialize(): void
{
$this->config['start_time'] = time();
// Load existing services from storage
$this->loadServicesFromStorage();
// Start health checking for existing services
$this->startHealthChecking();
// Start cleanup task
if ($this->config['auto_cleanup']) {
$this->startCleanupTask();
}
$this->logInfo("Service registry initialized");
}
/**
* Load services from storage.
*/
protected function loadServicesFromStorage(): void
{
$services = $this->storage->loadAllServices();
foreach ($services as $service) {
$this->instances[$service['id']] = $service;
$this->services[$service['name']][$service['id']] = $service;
}
$this->logInfo("Loaded " . count($services) . " services from storage");
}
/**
* Start health checking.
*/
protected function startHealthChecking(): void
{
foreach ($this->instances as $serviceId => $service) {
if ($service['health_check']) {
$this->healthChecker->startMonitoring($serviceId, $service['health_check']);
}
}
}
/**
* Start cleanup task.
*/
protected function startCleanupTask(): void
{
// This would typically be run as a background task
// For now, we'll just log that it would start
$this->logInfo("Cleanup task started");
}
/**
* Log info message.
*/
protected function logInfo(string $message): void
{
if ($this->config['logging_enabled']) {
error_log("[ServiceRegistry] {$message}");
}
}
/**
* Get default configuration.
*/
protected function getDefaultConfig(): array
{
return [
'load_balancing_strategy' => 'round_robin',
'health_check_interval' => 30,
'stale_threshold' => 300,
'auto_cleanup' => true,
'cleanup_interval' => 60,
'logging_enabled' => true,
'storage' => [
'type' => 'file',
'path' => __DIR__ . '/../../../storage/registry'
],
'start_time' => time()
];
}
/**
* Get configuration.
*/
public function getConfig(): array
{
return $this->config;
}
/**
* Set configuration.
*/
public function setConfig(array $config): void
{
$this->config = array_merge($this->config, $config);
}
/**
* Create registry instance.
*/
public static function create(array $config = []): self
{
return new self($config);
}
/**
* Create for development.
*/
public static function forDevelopment(): self
{
return new self([
'health_check_interval' => 10,
'stale_threshold' => 60,
'auto_cleanup' => true,
'logging_enabled' => true
]);
}
/**
* Create for production.
*/
public static function forProduction(): self
{
return new self([
'health_check_interval' => 30,
'stale_threshold' => 300,
'auto_cleanup' => true,
'logging_enabled' => false,
'storage' => [
'type' => 'redis',
'host' => 'localhost',
'port' => 6379
]
]);
}
}

View File

@@ -0,0 +1,876 @@
<?php
declare(strict_types=1);
namespace Fendx\Service\Security;
use Fendx\Service\Security\Dependency\ComposerAnalyzer;
use Fendx\Service\Security\Dependency\PackageChecker;
use Fendx\Service\Security\Dependency\VulnerabilityDatabase;
use Fendx\Service\Security\Dependency\LicenseChecker;
use Fendx\Service\Security\Reporter\DependencyReporter;
class DependencyChecker
{
protected array $config = [];
protected ComposerAnalyzer $composerAnalyzer;
protected PackageChecker $packageChecker;
protected VulnerabilityDatabase $vulnerabilityDb;
protected LicenseChecker $licenseChecker;
protected DependencyReporter $reporter;
protected array $checkResults = [];
protected array $packageCache = [];
public function __construct(array $config = [])
{
$this->config = array_merge($this->getDefaultConfig(), $config);
$this->composerAnalyzer = new ComposerAnalyzer($this->config['composer_analyzer'] ?? []);
$this->packageChecker = new PackageChecker($this->config['package_checker'] ?? []);
$this->vulnerabilityDb = new VulnerabilityDatabase($this->config['vulnerability_db'] ?? []);
$this->licenseChecker = new LicenseChecker($this->config['license_checker'] ?? []);
$this->reporter = new DependencyReporter($this->config['reporter'] ?? []);
}
/**
* Check composer dependencies for security issues.
*/
public function checkComposerDependencies(string $projectPath, array $options = []): array
{
$checkConfig = array_merge($this->config['composer_check'] ?? [], $options);
$result = [
'check_type' => 'composer_dependencies',
'project_path' => $projectPath,
'total_packages' => 0,
'vulnerable_packages' => 0,
'outdated_packages' => 0,
'license_issues' => 0,
'packages' => [],
'vulnerabilities' => [],
'security_score' => 100,
'recommendations' => [],
'check_duration' => 0,
'timestamp' => microtime(true)
];
$startTime = microtime(true);
// Analyze composer files
$composerAnalysis = $this->composerAnalyzer->analyze($projectPath, $checkConfig);
if (!$composerAnalysis['valid']) {
$result['error'] = $composerAnalysis['error'];
return $result;
}
$packages = $composerAnalysis['packages'];
$result['total_packages'] = count($packages);
$allVulnerabilities = [];
$packageResults = [];
foreach ($packages as $package => $packageInfo) {
$packageResult = $this->checkSinglePackage($package, $packageInfo, $checkConfig);
$packageResults[$package] = $packageResult;
// Collect vulnerabilities
if (!empty($packageResult['vulnerabilities'])) {
$allVulnerabilities = array_merge($allVulnerabilities, $packageResult['vulnerabilities']);
}
// Count outdated packages
if ($packageResult['outdated']) {
$result['outdated_packages']++;
}
// Count license issues
if ($packageResult['license_issue']) {
$result['license_issues']++;
}
}
$result['packages'] = $packageResults;
$result['vulnerabilities'] = $this->analyzeVulnerabilities($allVulnerabilities);
$result['vulnerable_packages'] = count(array_unique(array_column($allVulnerabilities, 'package')));
// Calculate security score
$result['security_score'] = $this->calculateDependencySecurityScore($result);
// Generate recommendations
$result['recommendations'] = $this->generateDependencyRecommendations($result);
$result['check_duration'] = microtime(true) - $startTime;
// Store result
$this->checkResults[] = $result;
return $result;
}
/**
* Check specific package for vulnerabilities.
*/
public function checkPackage(string $packageName, string $version, array $options = []): array
{
$checkConfig = array_merge($this->config['package_check'] ?? [], $options);
$result = [
'package' => $packageName,
'version' => $version,
'vulnerabilities' => [],
'vulnerability_count' => 0,
'severity_distribution' => [
'critical' => 0,
'high' => 0,
'medium' => 0,
'low' => 0
],
'outdated' => false,
'latest_version' => $version,
'license_compliant' => true,
'license_issue' => null,
'security_score' => 100,
'recommendations' => [],
'timestamp' => microtime(true)
];
// Check cache first
$cacheKey = $packageName . ':' . $version;
if (isset($this->packageCache[$cacheKey])) {
return $this->packageCache[$cacheKey];
}
// Check for vulnerabilities
$vulnerabilities = $this->vulnerabilityDb->checkPackage($packageName, $version);
$result['vulnerabilities'] = $vulnerabilities;
$result['vulnerability_count'] = count($vulnerabilities);
// Calculate severity distribution
foreach ($vulnerabilities as $vuln) {
$severity = $vuln['severity'];
$result['severity_distribution'][$severity]++;
}
// Check if package is outdated
$latestVersion = $this->packageChecker->getLatestVersion($packageName);
$result['latest_version'] = $latestVersion;
$result['outdated'] = version_compare($version, $latestVersion, '<');
// Check license compliance
$licenseCheck = $this->licenseChecker->checkLicense($packageName, $version, $checkConfig);
$result['license_compliant'] = $licenseCheck['compliant'];
$result['license_issue'] = $licenseCheck['issue'] ?? null;
// Calculate security score
$result['security_score'] = $this->calculatePackageSecurityScore($result);
// Generate recommendations
$result['recommendations'] = $this->generatePackageRecommendations($result);
// Cache result
$this->packageCache[$cacheKey] = $result;
$this->limitCacheSize();
return $result;
}
/**
* Check for outdated dependencies.
*/
public function checkOutdatedDependencies(string $projectPath, array $options = []): array
{
$checkConfig = array_merge($this->config['outdated_check'] ?? [], $options);
$result = [
'check_type' => 'outdated_dependencies',
'project_path' => $projectPath,
'total_packages' => 0,
'outdated_packages' => 0,
'packages' => [],
'update_recommendations' => [],
'check_duration' => 0,
'timestamp' => microtime(true)
];
$startTime = microtime(true);
// Get installed packages
$packages = $this->composerAnalyzer->getInstalledPackages($projectPath);
$result['total_packages'] = count($packages);
$outdatedPackages = [];
foreach ($packages as $package => $version) {
$latestVersion = $this->packageChecker->getLatestVersion($package);
if (version_compare($version, $latestVersion, '<')) {
$outdatedPackages[] = [
'package' => $package,
'current_version' => $version,
'latest_version' => $latestVersion,
'update_available' => true,
'security_update' => $this->isSecurityUpdate($package, $version, $latestVersion),
'breaking_changes' => $this->hasBreakingChanges($package, $version, $latestVersion)
];
$result['outdated_packages']++;
}
}
$result['packages'] = $outdatedPackages;
$result['update_recommendations'] = $this->generateUpdateRecommendations($outdatedPackages);
$result['check_duration'] = microtime(true) - $startTime;
return $result;
}
/**
* Check license compliance.
*/
public function checkLicenseCompliance(string $projectPath, array $options = []): array
{
$checkConfig = array_merge($this->config['license_check'] ?? [], $options);
$result = [
'check_type' => 'license_compliance',
'project_path' => $projectPath,
'total_packages' => 0,
'compliant_packages' => 0,
'non_compliant_packages' => 0,
'packages' => [],
'license_summary' => [],
'compliance_score' => 100,
'recommendations' => [],
'check_duration' => 0,
'timestamp' => microtime(true)
];
$startTime = microtime(true);
// Get installed packages
$packages = $this->composerAnalyzer->getInstalledPackages($projectPath);
$result['total_packages'] = count($packages);
$licenseResults = [];
$licenseSummary = [];
foreach ($packages as $package => $version) {
$licenseCheck = $this->licenseChecker->checkLicense($package, $version, $checkConfig);
$licenseResults[$package] = [
'package' => $package,
'version' => $version,
'license' => $licenseCheck['license'],
'compliant' => $licenseCheck['compliant'],
'issue' => $licenseCheck['issue'] ?? null,
'risk_level' => $licenseCheck['risk_level'] ?? 'low'
];
if ($licenseCheck['compliant']) {
$result['compliant_packages']++;
} else {
$result['non_compliant_packages']++;
}
// Update license summary
$license = $licenseCheck['license'];
if (!isset($licenseSummary[$license])) {
$licenseSummary[$license] = [
'count' => 0,
'compliant' => 0,
'non_compliant' => 0
];
}
$licenseSummary[$license]['count']++;
if ($licenseCheck['compliant']) {
$licenseSummary[$license]['compliant']++;
} else {
$licenseSummary[$license]['non_compliant']++;
}
}
$result['packages'] = $licenseResults;
$result['license_summary'] = $licenseSummary;
$result['compliance_score'] = $this->calculateLicenseComplianceScore($result);
$result['recommendations'] = $this->generateLicenseRecommendations($result);
$result['check_duration'] = microtime(true) - $startTime;
return $result;
}
/**
* Check for supply chain security.
*/
public function checkSupplyChainSecurity(string $projectPath, array $options = []): array
{
$checkConfig = array_merge($this->config['supply_chain_check'] ?? [], $options);
$result = [
'check_type' => 'supply_chain_security',
'project_path' => $projectPath,
'total_packages' => 0,
'risk_packages' => 0,
'packages' => [],
'supply_chain_risks' => [],
'risk_score' => 0,
'recommendations' => [],
'check_duration' => 0,
'timestamp' => microtime(true)
];
$startTime = microtime(true);
// Get installed packages
$packages = $this->composerAnalyzer->getInstalledPackages($projectPath);
$result['total_packages'] = count($packages);
$packageRisks = [];
$supplyChainRisks = [];
foreach ($packages as $package => $version) {
$riskAnalysis = $this->analyzePackageRisk($package, $version, $checkConfig);
$packageRisks[$package] = [
'package' => $package,
'version' => $version,
'risk_score' => $riskAnalysis['risk_score'],
'risk_factors' => $riskAnalysis['risk_factors'],
'recommendations' => $riskAnalysis['recommendations']
];
if ($riskAnalysis['risk_score'] > 50) {
$result['risk_packages']++;
$supplyChainRisks[] = [
'package' => $package,
'risk_type' => 'high_risk_package',
'risk_score' => $riskAnalysis['risk_score'],
'description' => "Package {$package} poses supply chain security risks"
];
}
}
$result['packages'] = $packageRisks;
$result['supply_chain_risks'] = $supplyChainRisks;
$result['risk_score'] = $this->calculateSupplyChainRiskScore($packageRisks);
$result['recommendations'] = $this->generateSupplyChainRecommendations($result);
$result['check_duration'] = microtime(true) - $startTime;
return $result;
}
/**
* Generate security report.
*/
public function getDependencyReport(array $results, string $format = 'html'): string
{
return $this->reporter->generate($results, $format);
}
/**
* Get check statistics.
*/
public function getStatistics(): array
{
return [
'total_checks' => count($this->checkResults),
'packages_checked' => $this->countTotalPackagesChecked(),
'vulnerabilities_found' => $this->countTotalVulnerabilities(),
'average_security_score' => $this->calculateAverageSecurityScore(),
'cache_size' => count($this->packageCache)
];
}
/**
* Clear cache and results.
*/
public function clearCache(): void
{
$this->packageCache = [];
$this->checkResults = [];
}
/**
* Check single package.
*/
protected function checkSinglePackage(string $package, array $packageInfo, array $config): array
{
return $this->checkPackage($package, $packageInfo['version'], $config);
}
/**
* Analyze vulnerabilities.
*/
protected function analyzeVulnerabilities(array $vulnerabilities): array
{
$analyzed = [];
foreach ($vulnerabilities as $vuln) {
$analyzed[] = [
'package' => $vuln['package'],
'version' => $vuln['version'],
'advisory_id' => $vuln['advisory_id'],
'title' => $vuln['title'],
'severity' => $vuln['severity'],
'cve_id' => $vuln['cve_id'] ?? null,
'cvss_score' => $vuln['cvss_score'] ?? null,
'description' => $vuln['description'],
'affected_versions' => $vuln['affected_versions'] ?? [],
'patched_versions' => $vuln['patched_versions'] ?? [],
'recommendation' => $vuln['recommendation']
];
}
return $analyzed;
}
/**
* Calculate dependency security score.
*/
protected function calculateDependencySecurityScore(array $result): int
{
$score = 100;
// Deduct points for vulnerabilities
foreach ($result['vulnerabilities'] as $vuln) {
switch ($vuln['severity']) {
case 'critical':
$score -= 20;
break;
case 'high':
$score -= 15;
break;
case 'medium':
$score -= 8;
break;
case 'low':
$score -= 3;
break;
}
}
// Deduct points for outdated packages
$score -= min(15, $result['outdated_packages'] * 2);
// Deduct points for license issues
$score -= min(10, $result['license_issues'] * 3);
return max(0, $score);
}
/**
* Calculate package security score.
*/
protected function calculatePackageSecurityScore(array $packageResult): int
{
$score = 100;
// Deduct points for vulnerabilities
foreach ($packageResult['vulnerabilities'] as $vuln) {
switch ($vuln['severity']) {
case 'critical':
$score -= 30;
break;
case 'high':
$score -= 20;
break;
case 'medium':
$score -= 10;
break;
case 'low':
$score -= 5;
break;
}
}
// Deduct points for being outdated
if ($packageResult['outdated']) {
$score -= 10;
}
// Deduct points for license issues
if (!$packageResult['license_compliant']) {
$score -= 15;
}
return max(0, $score);
}
/**
* Calculate license compliance score.
*/
protected function calculateLicenseComplianceScore(array $result): int
{
if ($result['total_packages'] === 0) {
return 100;
}
$complianceRate = $result['compliant_packages'] / $result['total_packages'];
return (int) round($complianceRate * 100);
}
/**
* Calculate supply chain risk score.
*/
protected function calculateSupplyChainRiskScore(array $packageRisks): int
{
if (empty($packageRisks)) {
return 0;
}
$totalRisk = 0;
foreach ($packageRisks as $risk) {
$totalRisk += $risk['risk_score'];
}
return (int) round($totalRisk / count($packageRisks));
}
/**
* Check if update is security update.
*/
protected function isSecurityUpdate(string $package, string $currentVersion, string $latestVersion): bool
{
// Check if there are security vulnerabilities in current version
$vulnerabilities = $this->vulnerabilityDb->checkPackage($package, $currentVersion);
foreach ($vulnerabilities as $vuln) {
if (isset($vuln['patched_versions'])) {
foreach ($vuln['patched_versions'] as $patchedVersion) {
if (version_compare($latestVersion, $patchedVersion, '>=')) {
return true;
}
}
}
}
return false;
}
/**
* Check if update has breaking changes.
*/
protected function hasBreakingChanges(string $package, string $currentVersion, string $latestVersion): bool
{
// This would check package changelog or semantic versioning
// For now, assume major version changes have breaking changes
$currentMajor = explode('.', $currentVersion)[0];
$latestMajor = explode('.', $latestVersion)[0];
return $currentMajor !== $latestMajor;
}
/**
* Analyze package risk.
*/
protected function analyzePackageRisk(string $package, string $version, array $config): array
{
$riskScore = 0;
$riskFactors = [];
$recommendations = [];
// Check for vulnerabilities
$vulnerabilities = $this->vulnerabilityDb->checkPackage($package, $version);
if (!empty($vulnerabilities)) {
$riskScore += 30;
$riskFactors[] = 'known_vulnerabilities';
$recommendations[] = 'Update to patched version';
}
// Check package popularity (less popular packages may be riskier)
$popularity = $this->packageChecker->getPopularity($package);
if ($popularity < 1000) {
$riskScore += 15;
$riskFactors[] = 'low_popularity';
$recommendations[] = 'Consider more popular alternative';
}
// Check maintenance status
$lastUpdate = $this->packageChecker->getLastUpdate($package);
$daysSinceUpdate = (time() - strtotime($lastUpdate)) / 86400;
if ($daysSinceUpdate > 365) {
$riskScore += 20;
$riskFactors[] = 'inactive_maintenance';
$recommendations[] = 'Package appears unmaintained';
}
// Check number of dependencies
$dependencies = $this->packageChecker->getDependencies($package, $version);
if (count($dependencies) > 50) {
$riskScore += 10;
$riskFactors[] = 'high_dependency_count';
$recommendations[] = 'Package has many dependencies increasing attack surface';
}
return [
'risk_score' => min(100, $riskScore),
'risk_factors' => $riskFactors,
'recommendations' => $recommendations
];
}
/**
* Generate dependency recommendations.
*/
protected function generateDependencyRecommendations(array $result): array
{
$recommendations = [];
if ($result['vulnerable_packages'] > 0) {
$recommendations[] = 'Update vulnerable packages to secure versions';
}
if ($result['outdated_packages'] > 0) {
$recommendations[] = 'Update outdated packages to latest stable versions';
}
if ($result['license_issues'] > 0) {
$recommendations[] = 'Review and resolve license compliance issues';
}
$recommendations[] = 'Implement automated dependency scanning in CI/CD';
$recommendations[] = 'Subscribe to security advisories for used packages';
return array_unique($recommendations);
}
/**
* Generate package recommendations.
*/
protected function generatePackageRecommendations(array $packageResult): array
{
$recommendations = [];
if (!empty($packageResult['vulnerabilities'])) {
$recommendations[] = 'Update to patched version to fix vulnerabilities';
}
if ($packageResult['outdated']) {
$recommendations[] = "Update from {$packageResult['version']} to {$packageResult['latest_version']}";
}
if (!$packageResult['license_compliant']) {
$recommendations[] = 'Review license compliance requirements';
}
return $recommendations;
}
/**
* Generate update recommendations.
*/
protected function generateUpdateRecommendations(array $outdatedPackages): array
{
$recommendations = [];
$securityUpdates = [];
$regularUpdates = [];
foreach ($outdatedPackages as $package) {
if ($package['security_update']) {
$securityUpdates[] = $package['package'];
} else {
$regularUpdates[] = $package['package'];
}
}
if (!empty($securityUpdates)) {
$recommendations[] = 'URGENT: Update security packages: ' . implode(', ', $securityUpdates);
}
if (!empty($regularUpdates)) {
$recommendations[] = 'Update outdated packages: ' . implode(', ', array_slice($regularUpdates, 0, 10));
if (count($regularUpdates) > 10) {
$recommendations[] = '... and ' . (count($regularUpdates) - 10) . ' more packages';
}
}
return $recommendations;
}
/**
* Generate license recommendations.
*/
protected function generateLicenseRecommendations(array $result): array
{
$recommendations = [];
if ($result['non_compliant_packages'] > 0) {
$recommendations[] = 'Review non-compliant package licenses';
$recommendations[] = 'Consider alternative packages with compatible licenses';
}
$recommendations[] = 'Document all package licenses in project documentation';
$recommendations[] = 'Implement license checking in CI/CD pipeline';
return $recommendations;
}
/**
* Generate supply chain recommendations.
*/
protected function generateSupplyChainRecommendations(array $result): array
{
$recommendations = [];
if ($result['risk_packages'] > 0) {
$recommendations[] = 'Review high-risk packages and consider alternatives';
}
$recommendations[] = 'Implement package signing verification';
$recommendations[] = 'Use lock files to prevent dependency confusion attacks';
$recommendations[] = 'Monitor package repositories for security updates';
return $recommendations;
}
/**
* Limit cache size.
*/
protected function limitCacheSize(): void
{
$maxSize = $this->config['cache_size'] ?? 1000;
if (count($this->packageCache) > $maxSize) {
$this->packageCache = array_slice($this->packageCache, -$maxSize, null, true);
}
}
/**
* Count total packages checked.
*/
protected function countTotalPackagesChecked(): int
{
$total = 0;
foreach ($this->checkResults as $result) {
$total += $result['total_packages'] ?? 0;
}
return $total;
}
/**
* Count total vulnerabilities.
*/
protected function countTotalVulnerabilities(): int
{
$total = 0;
foreach ($this->checkResults as $result) {
$total += $result['vulnerable_packages'] ?? 0;
}
return $total;
}
/**
* Calculate average security score.
*/
protected function calculateAverageSecurityScore(): float
{
if (empty($this->checkResults)) {
return 100;
}
$total = 0;
$count = 0;
foreach ($this->checkResults as $result) {
$scoreKey = $result['check_type'] === 'license_compliance' ? 'compliance_score' : 'security_score';
$total += $result[$scoreKey] ?? 100;
$count++;
}
return $count > 0 ? $total / $count : 100;
}
/**
* Get default configuration.
*/
protected function getDefaultConfig(): array
{
return [
'composer_check' => [
'check_dev_dependencies' => false,
'severity_threshold' => 'medium'
],
'package_check' => [
'include_license_check' => true,
'check_outdated' => true
],
'outdated_check' => [
'include_pre_releases' => false
],
'license_check' => [
'allowed_licenses' => ['MIT', 'Apache-2.0', 'BSD-3-Clause'],
'forbidden_licenses' => ['GPL-3.0', 'AGPL-3.0']
],
'supply_chain_check' => [
'check_popularity' => true,
'check_maintenance' => true,
'check_dependencies' => true
],
'cache_size' => 1000,
'composer_analyzer' => [],
'package_checker' => [],
'vulnerability_db' => [],
'license_checker' => [],
'reporter' => []
];
}
/**
* Get configuration.
*/
public function getConfig(): array
{
return $this->config;
}
/**
* Set configuration.
*/
public function setConfig(array $config): void
{
$this->config = array_merge($this->config, $config);
}
/**
* Create dependency checker instance.
*/
public static function create(array $config = []): self
{
return new self($config);
}
/**
* Create for development.
*/
public static function forDevelopment(): self
{
return new self([
'composer_check' => [
'severity_threshold' => 'high'
],
'license_check' => [
'allowed_licenses' => ['MIT', 'Apache-2.0', 'BSD-3-Clause', 'GPL-3.0']
]
]);
}
/**
* Create for production.
*/
public static function forProduction(): self
{
return new self([
'composer_check' => [
'severity_threshold' => 'low',
'check_dev_dependencies' => true
],
'supply_chain_check' => [
'strict_mode' => true
]
]);
}
}

View File

@@ -0,0 +1,869 @@
<?php
declare(strict_types=1);
namespace Fendx\Service\Security;
use Fendx\Service\Security\Validation\XssValidator;
use Fendx\Service\Security\Validation\SqlInjectionValidator;
use Fendx\Service\Security\Validation\PathTraversalValidator;
use Fendx\Service\Security\Validation\CommandInjectionValidator;
use Fendx\Service\Security\Validation\FileUploadValidator;
use Fendx\Service\Security\Reporter\ValidationReporter;
class InputValidator
{
protected array $config = [];
protected XssValidator $xssValidator;
protected SqlInjectionValidator $sqlValidator;
protected PathTraversalValidator $pathValidator;
protected CommandInjectionValidator $commandValidator;
protected FileUploadValidator $fileValidator;
protected ValidationReporter $reporter;
protected array $validationResults = [];
protected array $testCases = [];
public function __construct(array $config = [])
{
$this->config = array_merge($this->getDefaultConfig(), $config);
$this->xssValidator = new XssValidator($this->config['xss_validator'] ?? []);
$this->sqlValidator = new SqlInjectionValidator($this->config['sql_validator'] ?? []);
$this->pathValidator = new PathTraversalValidator($this->config['path_validator'] ?? []);
$this->commandValidator = new CommandInjectionValidator($this->config['command_validator'] ?? []);
$this->fileValidator = new FileUploadValidator($this->config['file_validator'] ?? []);
$this->reporter = new ValidationReporter($this->config['reporter'] ?? []);
$this->initializeTestCases();
}
/**
* Validate input against multiple security checks.
*/
public function validateInput(string $input, array $options = []): array
{
$validationConfig = array_merge($this->config['input_validation'] ?? [], $options);
$result = [
'input' => $input,
'input_length' => strlen($input),
'validation_type' => 'comprehensive',
'vulnerabilities_found' => 0,
'vulnerabilities' => [],
'severity_distribution' => [
'critical' => 0,
'high' => 0,
'medium' => 0,
'low' => 0
],
'validation_passed' => true,
'sanitized_input' => $input,
'recommendations' => [],
'timestamp' => microtime(true)
];
$vulnerabilities = [];
$sanitizedInput = $input;
// XSS validation
if ($validationConfig['check_xss'] ?? true) {
$xssResult = $this->xssValidator->validate($input, $validationConfig);
if (!$xssResult['safe']) {
$vulnerabilities[] = [
'type' => 'xss',
'severity' => $xssResult['severity'],
'description' => $xssResult['description'],
'pattern_detected' => $xssResult['pattern'] ?? null,
'recommendation' => $xssResult['recommendation']
];
$sanitizedInput = $xssResult['sanitized'] ?? $sanitizedInput;
}
}
// SQL Injection validation
if ($validationConfig['check_sql_injection'] ?? true) {
$sqlResult = $this->sqlValidator->validate($input, $validationConfig);
if (!$sqlResult['safe']) {
$vulnerabilities[] = [
'type' => 'sql_injection',
'severity' => $sqlResult['severity'],
'description' => $sqlResult['description'],
'pattern_detected' => $sqlResult['pattern'] ?? null,
'recommendation' => $sqlResult['recommendation']
];
$sanitizedInput = $sqlResult['sanitized'] ?? $sanitizedInput;
}
}
// Path Traversal validation
if ($validationConfig['check_path_traversal'] ?? true) {
$pathResult = $this->pathValidator->validate($input, $validationConfig);
if (!$pathResult['safe']) {
$vulnerabilities[] = [
'type' => 'path_traversal',
'severity' => $pathResult['severity'],
'description' => $pathResult['description'],
'pattern_detected' => $pathResult['pattern'] ?? null,
'recommendation' => $pathResult['recommendation']
];
$sanitizedInput = $pathResult['sanitized'] ?? $sanitizedInput;
}
}
// Command Injection validation
if ($validationConfig['check_command_injection'] ?? true) {
$commandResult = $this->commandValidator->validate($input, $validationConfig);
if (!$commandResult['safe']) {
$vulnerabilities[] = [
'type' => 'command_injection',
'severity' => $commandResult['severity'],
'description' => $commandResult['description'],
'pattern_detected' => $commandResult['pattern'] ?? null,
'recommendation' => $commandResult['recommendation']
];
$sanitizedInput = $commandResult['sanitized'] ?? $sanitizedInput;
}
}
$result['vulnerabilities'] = $vulnerabilities;
$result['vulnerabilities_found'] = count($vulnerabilities);
$result['validation_passed'] = empty($vulnerabilities);
$result['sanitized_input'] = $sanitizedInput;
// Calculate severity distribution
foreach ($vulnerabilities as $vuln) {
$severity = $vuln['severity'];
$result['severity_distribution'][$severity]++;
}
// Generate recommendations
$result['recommendations'] = $this->generateValidationRecommendations($vulnerabilities);
// Store result
$this->validationResults[] = $result;
return $result;
}
/**
* Test input validation with predefined test cases.
*/
public function testInputValidation(array $options = []): array
{
$testConfig = array_merge($this->config['validation_testing'] ?? [], $options);
$result = [
'test_type' => 'input_validation_security',
'test_cases_run' => 0,
'vulnerabilities_detected' => 0,
'test_results' => [],
'vulnerability_types' => [],
'security_score' => 100,
'recommendations' => [],
'timestamp' => microtime(true)
};
$testResults = [];
$allVulnerabilities = [];
$vulnerabilityTypes = [];
foreach ($this->testCases as $testCase) {
if (isset($testConfig['categories']) && !in_array($testCase['category'], $testConfig['categories'])) {
continue;
}
$testResult = $this->runTestCase($testCase, $testConfig);
$testResults[] = $testResult;
if (!$testResult['validation_passed']) {
$allVulnerabilities = array_merge($allVulnerabilities, $testResult['vulnerabilities']);
foreach ($testResult['vulnerabilities'] as $vuln) {
$vulnerabilityTypes[$vuln['type']] = ($vulnerabilityTypes[$vuln['type']] ?? 0) + 1;
}
}
}
$result['test_cases_run'] = count($testResults);
$result['vulnerabilities_detected'] = count($allVulnerabilities);
$result['test_results'] = $testResults;
$result['vulnerability_types'] = $vulnerabilityTypes;
$result['security_score'] = $this->calculateTestSecurityScore($testResults);
$result['recommendations'] = $this->generateTestRecommendations($allVulnerabilities);
return $result;
}
/**
* Validate file upload security.
*/
public function validateFileUpload(array $fileData, array $options = []): array
{
$validationConfig = array_merge($this->config['file_validation'] ?? [], $options);
$result = [
'validation_type' => 'file_upload_security',
'file_info' => [
'name' => $fileData['name'] ?? '',
'size' => $fileData['size'] ?? 0,
'type' => $fileData['type'] ?? '',
'tmp_name' => $fileData['tmp_name'] ?? ''
],
'security_issues' => 0,
'issues' => [],
'validation_passed' => true,
'recommendations' => [],
'timestamp' => microtime(true)
];
$fileResult = $this->fileValidator->validate($fileData, $validationConfig);
$result['issues'] = $fileResult['issues'];
$result['security_issues'] = count($fileResult['issues']);
$result['validation_passed'] = $fileResult['safe'];
$result['recommendations'] = $fileResult['recommendations'] ?? [];
return $result;
}
/**
* Test API endpoint input validation.
*/
public function testApiEndpoint(string $endpoint, array $testPayloads, array $options = []): array
{
$testConfig = array_merge($this->config['api_testing'] ?? [], $options);
$result = [
'test_type' => 'api_input_validation',
'endpoint' => $endpoint,
'payloads_tested' => count($testPayloads),
'vulnerabilities_found' => 0,
'test_results' => [],
'security_score' => 100,
'recommendations' => [],
'timestamp' => microtime(true)
];
$testResults = [];
$allVulnerabilities = [];
foreach ($testPayloads as $payload) {
$apiResult = $this->testApiPayload($endpoint, $payload, $testConfig);
$testResults[] = $apiResult;
if (!$apiResult['validation_passed']) {
$allVulnerabilities = array_merge($allVulnerabilities, $apiResult['vulnerabilities']);
}
}
$result['test_results'] = $testResults;
$result['vulnerabilities_found'] = count($allVulnerabilities);
$result['security_score'] = $this->calculateApiSecurityScore($testResults);
$result['recommendations'] = $this->generateApiRecommendations($allVulnerabilities);
return $result;
}
/**
* Validate form data security.
*/
public function validateFormData(array $formData, array $fieldRules = [], array $options = []): array
{
$validationConfig = array_merge($this->config['form_validation'] ?? [], $options);
$result = [
'validation_type' => 'form_data_security',
'fields_validated' => count($formData),
'vulnerabilities_found' => 0,
'field_results' => [],
'validation_passed' => true,
'recommendations' => [],
'timestamp' => microtime(true)
];
$fieldResults = [];
$allVulnerabilities = [];
foreach ($formData as $fieldName => $fieldValue) {
if (!is_string($fieldValue)) {
continue;
}
$fieldConfig = $fieldRules[$fieldName] ?? [];
$fieldValidation = array_merge($validationConfig, $fieldConfig);
$validationResult = $this->validateInput($fieldValue, $fieldValidation);
$validationResult['field_name'] = $fieldName;
$fieldResults[$fieldName] = $validationResult;
if (!$validationResult['validation_passed']) {
$allVulnerabilities = array_merge($allVulnerabilities, $validationResult['vulnerabilities']);
}
}
$result['field_results'] = $fieldResults;
$result['vulnerabilities_found'] = count($allVulnerabilities);
$result['validation_passed'] = empty($allVulnerabilities);
$result['recommendations'] = $this->generateFormRecommendations($fieldResults);
return $result;
}
/**
* Generate validation report.
*/
public function getValidationReport(array $results, string $format = 'html'): string
{
return $this->reporter->generate($results, $format);
}
/**
* Get validation statistics.
*/
public function getStatistics(): array
{
return [
'total_validations' => count($this->validationResults),
'vulnerabilities_detected' => $this->countTotalVulnerabilities(),
'vulnerability_types' => $this->getVulnerabilityTypes(),
'average_security_score' => $this->calculateAverageSecurityScore(),
'test_cases_available' => count($this->testCases)
];
}
/**
* Add custom test case.
*/
public function addTestCase(array $testCase): void
{
$this->testCases[] = $testCase;
}
/**
* Clear validation results.
*/
public function clearResults(): void
{
$this->validationResults = [];
}
/**
* Run single test case.
*/
protected function runTestCase(array $testCase, array $config): array
{
$result = [
'test_name' => $testCase['name'],
'category' => $testCase['category'],
'input' => $testCase['input'],
'expected_result' => $testCase['expected'] ?? 'vulnerable',
'validation_passed' => false,
'vulnerabilities' => [],
'timestamp' => microtime(true)
];
$validationResult = $this->validateInput($testCase['input'], $config);
$result['validation_passed'] = $validationResult['validation_passed'];
$result['vulnerabilities'] = $validationResult['vulnerabilities'];
// Check if test result matches expectation
if ($testCase['expected'] === 'safe') {
$result['test_passed'] = $validationResult['validation_passed'];
} else {
$result['test_passed'] = !$validationResult['validation_passed'];
}
return $result;
}
/**
* Test API payload.
*/
protected function testApiPayload(string $endpoint, array $payload, array $config): array
{
$result = [
'payload' => $payload,
'validation_passed' => true,
'vulnerabilities' => [],
'response_status' => null,
'response_body' => null,
'timestamp' => microtime(true)
];
try {
// Make API request
$response = $this->makeApiRequest($endpoint, $payload, $config);
$result['response_status'] = $response['status_code'];
$result['response_body'] = $response['body'];
// Validate response for security indicators
if ($response['status_code'] >= 500) {
$result['vulnerabilities'][] = [
'type' => 'server_error',
'severity' => 'medium',
'description' => 'Server error may indicate injection attempt',
'recommendation' => 'Review server logs for injection attempts'
];
$result['validation_passed'] = false;
}
// Validate payload fields
foreach ($payload as $key => $value) {
if (is_string($value)) {
$validationResult = $this->validateInput($value, $config);
if (!$validationResult['validation_passed']) {
$result['vulnerabilities'] = array_merge($result['vulnerabilities'], $validationResult['vulnerabilities']);
$result['validation_passed'] = false;
}
}
}
} catch (\Exception $e) {
$result['vulnerabilities'][] = [
'type' => 'request_error',
'severity' => 'low',
'description' => 'Request failed: ' . $e->getMessage(),
'recommendation' => 'Check API endpoint configuration'
];
}
return $result;
}
/**
* Make API request.
*/
protected function makeApiRequest(string $endpoint, array $payload, array $config): array
{
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $endpoint,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($payload),
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Accept: application/json'
],
CURLOPT_TIMEOUT => $config['timeout'] ?? 30,
CURLOPT_SSL_VERIFYPEER => $config['verify_ssl'] ?? true
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
if ($error) {
throw new \RuntimeException("API request failed: {$error}");
}
return [
'status_code' => $status,
'body' => $response
];
}
/**
* Calculate test security score.
*/
protected function calculateTestSecurityScore(array $testResults): int
{
if (empty($testResults)) {
return 100;
}
$totalScore = 0;
$passedTests = 0;
foreach ($testResults as $test) {
if ($test['test_passed'] ?? false) {
$passedTests++;
}
}
return (int) round(($passedTests / count($testResults)) * 100);
}
/**
* Calculate API security score.
*/
protected function calculateApiSecurityScore(array $testResults): int
{
if (empty($testResults)) {
return 100;
}
$secureTests = 0;
foreach ($testResults as $test) {
if ($test['validation_passed']) {
$secureTests++;
}
}
return (int) round(($secureTests / count($testResults)) * 100);
}
/**
* Generate validation recommendations.
*/
protected function generateValidationRecommendations(array $vulnerabilities): array
{
$recommendations = [];
foreach ($vulnerabilities as $vuln) {
if (isset($vuln['recommendation'])) {
$recommendations[] = $vuln['recommendation'];
}
}
// Add general recommendations based on vulnerability types
$types = array_unique(array_column($vulnerabilities, 'type'));
if (in_array('xss', $types)) {
$recommendations[] = 'Implement proper output encoding to prevent XSS';
}
if (in_array('sql_injection', $types)) {
$recommendations[] = 'Use parameterized queries to prevent SQL injection';
}
if (in_array('path_traversal', $types)) {
$recommendations[] = 'Validate and sanitize file paths to prevent directory traversal';
}
if (in_array('command_injection', $types)) {
$recommendations[] = 'Avoid executing user input as system commands';
}
return array_unique($recommendations);
}
/**
* Generate test recommendations.
*/
protected function generateTestRecommendations(array $vulnerabilities): array
{
$recommendations = [];
if (!empty($vulnerabilities)) {
$recommendations[] = 'Implement comprehensive input validation';
$recommendations[] = 'Use security-focused validation libraries';
$recommendations[] = 'Regularly test input validation with security test cases';
}
$recommendations[] = 'Implement automated security testing in CI/CD pipeline';
$recommendations[] = 'Train developers on secure coding practices';
return array_unique($recommendations);
}
/**
* Generate API recommendations.
*/
protected function generateApiRecommendations(array $vulnerabilities): array
{
$recommendations = [];
if (!empty($vulnerabilities)) {
$recommendations[] = 'Implement API input validation middleware';
$recommendations[] = 'Use API security testing tools';
$recommendations[] = 'Implement rate limiting to prevent brute force attacks';
}
$recommendations[] = 'Use API gateway with security features';
$recommendations[] = 'Implement proper API authentication and authorization';
return array_unique($recommendations);
}
/**
* Generate form recommendations.
*/
protected function generateFormRecommendations(array $fieldResults): array
{
$recommendations = [];
$vulnerableFields = [];
foreach ($fieldResults as $fieldName => $fieldResult) {
if (!$fieldResult['validation_passed']) {
$vulnerableFields[] = $fieldName;
}
}
if (!empty($vulnerableFields)) {
$recommendations[] = 'Fix validation issues in fields: ' . implode(', ', $vulnerableFields);
$recommendations[] = 'Implement client-side and server-side validation';
}
$recommendations[] = 'Use CSRF protection for forms';
$recommendations[] = 'Implement proper form field sanitization';
return array_unique($recommendations);
}
/**
* Count total vulnerabilities.
*/
protected function countTotalVulnerabilities(): int
{
$total = 0;
foreach ($this->validationResults as $result) {
$total += $result['vulnerabilities_found'] ?? 0;
}
return $total;
}
/**
* Get vulnerability types.
*/
protected function getVulnerabilityTypes(): array
{
$types = [];
foreach ($this->validationResults as $result) {
if (isset($result['vulnerabilities'])) {
foreach ($result['vulnerabilities'] as $vuln) {
$types[$vuln['type']] = ($types[$vuln['type']] ?? 0) + 1;
}
}
}
return $types;
}
/**
* Calculate average security score.
*/
protected function calculateAverageSecurityScore(): float
{
if (empty($this->validationResults)) {
return 100;
}
$totalScore = 0;
$count = 0;
foreach ($this->validationResults as $result) {
if (isset($result['security_score'])) {
$totalScore += $result['security_score'];
$count++;
}
}
return $count > 0 ? $totalScore / $count : 100;
}
/**
* Initialize test cases.
*/
protected function initializeTestCases(): void
{
$this->testCases = [
// XSS test cases
[
'name' => 'Basic XSS Script Tag',
'category' => 'xss',
'input' => '<script>alert("XSS")</script>',
'expected' => 'vulnerable'
],
[
'name' => 'XSS with Event Handler',
'category' => 'xss',
'input' => '<img src="x" onerror="alert(\'XSS\')">',
'expected' => 'vulnerable'
],
[
'name' => 'XSS with JavaScript Protocol',
'category' => 'xss',
'input' => 'javascript:alert("XSS")',
'expected' => 'vulnerable'
],
// SQL Injection test cases
[
'name' => 'Basic SQL Injection',
'category' => 'sql_injection',
'input' => "' OR '1'='1",
'expected' => 'vulnerable'
],
[
'name' => 'SQL Injection with UNION',
'category' => 'sql_injection',
'input' => "' UNION SELECT * FROM users--",
'expected' => 'vulnerable'
],
[
'name' => 'SQL Injection with Comment',
'category' => 'sql_injection',
'input' => "admin'--",
'expected' => 'vulnerable'
],
// Path Traversal test cases
[
'name' => 'Basic Path Traversal',
'category' => 'path_traversal',
'input' => '../../../etc/passwd',
'expected' => 'vulnerable'
],
[
'name' => 'Path Traversal with URL Encoding',
'category' => 'path_traversal',
'input' => '..%2F..%2F..%2Fetc%2Fpasswd',
'expected' => 'vulnerable'
],
[
'name' => 'Path Traversal with Null Byte',
'category' => 'path_traversal',
'input' => '../../../etc/passwd%00',
'expected' => 'vulnerable'
],
// Command Injection test cases
[
'name' => 'Basic Command Injection',
'category' => 'command_injection',
'input' => 'file.txt; ls -la',
'expected' => 'vulnerable'
],
[
'name' => 'Command Injection with Pipe',
'category' => 'command_injection',
'input' => 'file.txt | cat /etc/passwd',
'expected' => 'vulnerable'
],
[
'name' => 'Command Injection with Backticks',
'category' => 'command_injection',
'input' => '`cat /etc/passwd`',
'expected' => 'vulnerable'
],
// Safe inputs
[
'name' => 'Safe Text Input',
'category' => 'safe',
'input' => 'Hello, World!',
'expected' => 'safe'
],
[
'name' => 'Safe Email Input',
'category' => 'safe',
'input' => 'user@example.com',
'expected' => 'safe'
],
[
'name' => 'Safe Numeric Input',
'category' => 'safe',
'input' => '12345',
'expected' => 'safe'
]
];
}
/**
* Get default configuration.
*/
protected function getDefaultConfig(): array
{
return [
'input_validation' => [
'check_xss' => true,
'check_sql_injection' => true,
'check_path_traversal' => true,
'check_command_injection' => true,
'sanitize_output' => true
],
'validation_testing' => [
'categories' => ['xss', 'sql_injection', 'path_traversal', 'command_injection', 'safe']
],
'file_validation' => [
'allowed_extensions' => ['jpg', 'png', 'gif', 'pdf', 'doc', 'docx'],
'max_file_size' => 10 * 1024 * 1024, // 10MB
'check_mime_type' => true,
'scan_for_malware' => false
],
'api_testing' => [
'timeout' => 30,
'verify_ssl' => true,
'follow_redirects' => true
],
'form_validation' => [
'require_csrf_token' => true,
'validate_all_fields' => true
],
'xss_validator' => [],
'sql_validator' => [],
'path_validator' => [],
'command_validator' => [],
'file_validator' => [],
'reporter' => []
];
}
/**
* Get configuration.
*/
public function getConfig(): array
{
return $this->config;
}
/**
* Set configuration.
*/
public function setConfig(array $config): void
{
$this->config = array_merge($this->config, $config);
}
/**
* Create input validator instance.
*/
public static function create(array $config = []): self
{
return new self($config);
}
/**
* Create for development.
*/
public static function forDevelopment(): self
{
return new self([
'input_validation' => [
'sanitize_output' => false // Keep original input for debugging
],
'file_validation' => [
'max_file_size' => 50 * 1024 * 1024 // 50MB for development
]
]);
}
/**
* Create for production.
*/
public static function forProduction(): self
{
return new self([
'input_validation' => [
'sanitize_output' => true,
'strict_mode' => true
],
'file_validation' => [
'max_file_size' => 5 * 1024 * 1024, // 5MB for production
'scan_for_malware' => true
],
'validation_testing' => [
'strict_mode' => true
]
]);
}
}

View File

@@ -0,0 +1,973 @@
<?php
declare(strict_types=1);
namespace Fendx\Service\Security;
use Fendx\Service\Security\Permission\RoleValidator;
use Fendx\Service\Security\Permission\AccessControlValidator;
use Fendx\Service\Security\Permission\AuthenticationValidator;
use Fendx\Service\Security\Permission\AuthorizationValidator;
use Fendx\Service\Security\Permission\SessionValidator;
use Fendx\Service\Security\Reporter\PermissionReporter;
class PermissionValidator
{
protected array $config = [];
protected RoleValidator $roleValidator;
protected AccessControlValidator $accessValidator;
protected AuthenticationValidator $authValidator;
protected AuthorizationValidator $authorizationValidator;
protected SessionValidator $sessionValidator;
protected PermissionReporter $reporter;
protected array $validationResults = [];
protected array $userRoles = [];
protected array $permissions = [];
public function __construct(array $config = [])
{
$this->config = array_merge($this->getDefaultConfig(), $config);
$this->roleValidator = new RoleValidator($this->config['role_validator'] ?? []);
$this->accessValidator = new AccessControlValidator($this->config['access_validator'] ?? []);
$this->authValidator = new AuthenticationValidator($this->config['auth_validator'] ?? []);
$this->authorizationValidator = new AuthorizationValidator($this->config['authorization_validator'] ?? []);
$this->sessionValidator = new SessionValidator($this->config['session_validator'] ?? []);
$this->reporter = new PermissionReporter($this->config['reporter'] ?? []);
$this->initializePermissions();
}
/**
* Validate user permissions.
*/
public function validatePermissions(int $userId, string $resource, string $action, array $options = []): array
{
$validationConfig = array_merge($this->config['permission_validation'] ?? [], $options);
$result = [
'validation_type' => 'user_permission',
'user_id' => $userId,
'resource' => $resource,
'action' => $action,
'access_granted' => false,
'validation_steps' => [],
'security_issues' => [],
'user_roles' => [],
'effective_permissions' => [],
'recommendations' => [],
'timestamp' => microtime(true)
];
$validationSteps = [];
$securityIssues = [];
// Step 1: Validate user authentication
$authResult = $this->authValidator->validateUser($userId, $validationConfig);
$validationSteps['authentication'] = $authResult;
if (!$authResult['authenticated']) {
$result['access_granted'] = false;
$securityIssues[] = [
'type' => 'authentication_failed',
'severity' => 'critical',
'description' => $authResult['reason'] ?? 'User not authenticated',
'recommendation' => 'Ensure proper user authentication before permission check'
];
}
// Step 2: Get user roles
$userRoles = $this->roleValidator->getUserRoles($userId, $validationConfig);
$result['user_roles'] = $userRoles;
$validationSteps['roles'] = ['roles' => $userRoles, 'count' => count($userRoles)];
// Step 3: Validate session
$sessionResult = $this->sessionValidator->validateSession($userId, $validationConfig);
$validationSteps['session'] = $sessionResult;
if (!$sessionResult['valid']) {
$securityIssues[] = [
'type' => 'session_invalid',
'severity' => 'high',
'description' => $sessionResult['reason'] ?? 'Invalid session',
'recommendation' => 'Implement proper session validation'
];
}
// Step 4: Check access control
if ($authResult['authenticated'] && $sessionResult['valid']) {
$accessResult = $this->accessValidator->checkAccess($userId, $resource, $action, $userRoles, $validationConfig);
$validationSteps['access_control'] = $accessResult;
$result['access_granted'] = $accessResult['granted'];
$result['effective_permissions'] = $accessResult['effective_permissions'] ?? [];
if (!$accessResult['granted']) {
$securityIssues[] = [
'type' => 'access_denied',
'severity' => 'medium',
'description' => $accessResult['reason'] ?? 'Access denied',
'recommendation' => 'Review user permissions and role assignments'
];
}
}
// Step 5: Validate authorization
if ($result['access_granted']) {
$authzResult = $this->authorizationValidator->validateAuthorization($userId, $resource, $action, $validationConfig);
$validationSteps['authorization'] = $authzResult;
if (!$authzResult['authorized']) {
$result['access_granted'] = false;
$securityIssues[] = [
'type' => 'authorization_failed',
'severity' => 'high',
'description' => $authzResult['reason'] ?? 'Authorization failed',
'recommendation' => 'Review authorization policies and implementations'
];
}
}
$result['validation_steps'] = $validationSteps;
$result['security_issues'] = $securityIssues;
$result['recommendations'] = $this->generatePermissionRecommendations($securityIssues);
// Store result
$this->validationResults[] = $result;
return $result;
}
/**
* Test role-based access control.
*/
public function testRoleBasedAccessControl(array $testCases, array $options = []): array
{
$testConfig = array_merge($this->config['rbac_testing'] ?? [], $options);
$result = [
'test_type' => 'role_based_access_control',
'test_cases_run' => count($testCases),
'tests_passed' => 0,
'tests_failed' => 0,
'security_issues' => [],
'test_results' => [],
'role_coverage' => [],
'security_score' => 100,
'recommendations' => [],
'timestamp' => microtime(true)
];
$testResults = [];
$securityIssues = [];
$roleCoverage = [];
$passedTests = 0;
foreach ($testCases as $testCase) {
$testResult = $this->runRbacTestCase($testCase, $testConfig);
$testResults[] = $testResult;
if ($testResult['test_passed']) {
$passedTests++;
} else {
$securityIssues = array_merge($securityIssues, $testResult['security_issues']);
}
// Track role coverage
if (isset($testCase['role'])) {
$role = $testCase['role'];
if (!isset($roleCoverage[$role])) {
$roleCoverage[$role] = ['tested' => 0, 'passed' => 0];
}
$roleCoverage[$role]['tested']++;
if ($testResult['test_passed']) {
$roleCoverage[$role]['passed']++;
}
}
}
$result['tests_passed'] = $passedTests;
$result['tests_failed'] = count($testCases) - $passedTests;
$result['security_issues'] = $securityIssues;
$result['test_results'] = $testResults;
$result['role_coverage'] = $roleCoverage;
$result['security_score'] = $this->calculateRbacSecurityScore($testResults);
$result['recommendations'] = $this->generateRbacRecommendations($securityIssues, $roleCoverage);
return $result;
}
/**
* Validate API endpoint permissions.
*/
public function validateApiPermissions(string $endpoint, string $method, int $userId, array $options = []): array
{
$validationConfig = array_merge($this->config['api_permission_validation'] ?? [], $options);
$result = [
'validation_type' => 'api_permission',
'endpoint' => $endpoint,
'method' => $method,
'user_id' => $userId,
'access_granted' => false,
'validation_steps' => [],
'security_issues' => [],
'rate_limit_status' => [],
'cors_status' => [],
'recommendations' => [],
'timestamp' => microtime(true)
};
$validationSteps = [];
$securityIssues = [];
// Step 1: Validate API authentication
$apiAuthResult = $this->authValidator->validateApiAuthentication($endpoint, $method, $userId, $validationConfig);
$validationSteps['api_authentication'] = $apiAuthResult;
if (!$apiAuthResult['authenticated']) {
$securityIssues[] = [
'type' => 'api_authentication_failed',
'severity' => 'critical',
'description' => $apiAuthResult['reason'] ?? 'API authentication failed',
'recommendation' => 'Implement proper API authentication mechanisms'
];
}
// Step 2: Check rate limiting
$rateLimitResult = $this->accessValidator->checkRateLimit($userId, $endpoint, $validationConfig);
$validationSteps['rate_limit'] = $rateLimitResult;
$result['rate_limit_status'] = $rateLimitResult;
if (!$rateLimitResult['allowed']) {
$securityIssues[] = [
'type' => 'rate_limit_exceeded',
'severity' => 'medium',
'description' => 'Rate limit exceeded',
'recommendation' => 'Implement appropriate rate limiting for API endpoints'
];
}
// Step 3: Validate CORS
$corsResult = $this->accessValidator->validateCors($endpoint, $method, $validationConfig);
$validationSteps['cors'] = $corsResult;
$result['cors_status'] = $corsResult;
if (!$corsResult['valid']) {
$securityIssues[] = [
'type' => 'cors_invalid',
'severity' => 'low',
'description' => $corsResult['reason'] ?? 'CORS validation failed',
'recommendation' => 'Configure proper CORS policies'
];
}
// Step 4: Check endpoint permissions
if ($apiAuthResult['authenticated'] && $rateLimitResult['allowed']) {
$resource = $this->extractResourceFromEndpoint($endpoint);
$permissionResult = $this->validatePermissions($userId, $resource, $method, $validationConfig);
$validationSteps['endpoint_permissions'] = $permissionResult;
$result['access_granted'] = $permissionResult['access_granted'];
if (!$permissionResult['access_granted']) {
$securityIssues = array_merge($securityIssues, $permissionResult['security_issues']);
}
}
$result['validation_steps'] = $validationSteps;
$result['security_issues'] = $securityIssues;
$result['recommendations'] = $this->generateApiPermissionRecommendations($securityIssues);
return $result;
}
/**
* Test privilege escalation vulnerabilities.
*/
public function testPrivilegeEscalation(array $testScenarios, array $options = []): array
{
$testConfig = array_merge($this->config['privilege_escalation_testing'] ?? [], $options);
$result = [
'test_type' => 'privilege_escalation',
'scenarios_tested' => count($testScenarios),
'vulnerabilities_found' => 0,
'test_results' => [],
'escalation_vectors' => [],
'security_score' => 100,
'recommendations' => [],
'timestamp' => microtime(true)
];
$testResults = [];
$escalationVectors = [];
$vulnerabilitiesFound = 0;
foreach ($testScenarios as $scenario) {
$testResult = $this->runPrivilegeEscalationTest($scenario, $testConfig);
$testResults[] = $testResult;
if ($testResult['vulnerability_detected']) {
$vulnerabilitiesFound++;
$escalationVectors[] = [
'scenario' => $scenario['name'],
'vector' => $testResult['escalation_vector'],
'severity' => $testResult['severity'],
'description' => $testResult['description']
];
}
}
$result['vulnerabilities_found'] = $vulnerabilitiesFound;
$result['test_results'] = $testResults;
$result['escalation_vectors'] = $escalationVectors;
$result['security_score'] = $this->calculatePrivilegeEscalationScore($testResults);
$result['recommendations'] = $this->generatePrivilegeEscalationRecommendations($escalationVectors);
return $result;
}
/**
* Validate session security.
*/
public function validateSessionSecurity(int $userId, string $sessionId, array $options = []): array
{
$validationConfig = array_merge($this->config['session_security_validation'] ?? [], $options);
$result = [
'validation_type' => 'session_security',
'user_id' => $userId,
'session_id' => $sessionId,
'session_valid' => false,
'security_issues' => [],
'session_details' => [],
'recommendations' => [],
'timestamp' => microtime(true)
];
$sessionResult = $this->sessionValidator->validateSessionSecurity($userId, $sessionId, $validationConfig);
$result['session_valid'] = $sessionResult['valid'];
$result['session_details'] = $sessionResult['details'];
$result['security_issues'] = $sessionResult['security_issues'];
$result['recommendations'] = $this->generateSessionSecurityRecommendations($sessionResult['security_issues']);
return $result;
}
/**
* Test multi-factor authentication.
*/
public function testMultiFactorAuthentication(array $testCases, array $options = []): array
{
$testConfig = array_merge($this->config['mfa_testing'] ?? [], $options);
$result = [
'test_type' => 'multi_factor_authentication',
'test_cases_run' => count($testCases),
'mfa_enforced' => true,
'bypass_methods' => [],
'security_issues' => [],
'test_results' => [],
'security_score' => 100,
'recommendations' => [],
'timestamp' => microtime(true)
];
$testResults = [];
$securityIssues = [];
$bypassMethods = [];
foreach ($testCases as $testCase) {
$testResult = $this->runMfaTestCase($testCase, $testConfig);
$testResults[] = $testResult;
if (!$testResult['mfa_required']) {
$result['mfa_enforced'] = false;
$bypassMethods[] = $testCase['name'];
}
if (!empty($testResult['security_issues'])) {
$securityIssues = array_merge($securityIssues, $testResult['security_issues']);
}
}
$result['bypass_methods'] = $bypassMethods;
$result['security_issues'] = $securityIssues;
$result['test_results'] = $testResults;
$result['security_score'] = $this->calculateMfaSecurityScore($testResults);
$result['recommendations'] = $this->generateMfaRecommendations($securityIssues, $bypassMethods);
return $result;
}
/**
* Generate permission validation report.
*/
public function getPermissionReport(array $results, string $format = 'html'): string
{
return $this->reporter->generate($results, $format);
}
/**
* Get validation statistics.
*/
public function getStatistics(): array
{
return [
'total_validations' => count($this->validationResults),
'access_granted_count' => $this->countAccessGranted(),
'access_denied_count' => $this->countAccessDenied(),
'security_issues_count' => $this->countSecurityIssues(),
'average_security_score' => $this->calculateAverageSecurityScore()
];
}
/**
* Clear validation results.
*/
public function clearResults(): void
{
$this->validationResults = [];
}
/**
* Run RBAC test case.
*/
protected function runRbacTestCase(array $testCase, array $config): array
{
$result = [
'test_name' => $testCase['name'],
'user_id' => $testCase['user_id'],
'role' => $testCase['role'] ?? null,
'resource' => $testCase['resource'],
'action' => $testCase['action'],
'expected_result' => $testCase['expected'] ?? 'deny',
'test_passed' => false,
'access_granted' => false,
'security_issues' => [],
'timestamp' => microtime(true)
];
try {
$validationResult = $this->validatePermissions(
$testCase['user_id'],
$testCase['resource'],
$testCase['action'],
$config
);
$result['access_granted'] = $validationResult['access_granted'];
$result['security_issues'] = $validationResult['security_issues'];
// Check if test result matches expectation
$expectedAccess = $testCase['expected'] === 'allow';
$result['test_passed'] = $result['access_granted'] === $expectedAccess;
} catch (\Exception $e) {
$result['security_issues'][] = [
'type' => 'test_error',
'severity' => 'high',
'description' => 'Test execution failed: ' . $e->getMessage(),
'recommendation' => 'Review test configuration and implementation'
];
}
return $result;
}
/**
* Run privilege escalation test.
*/
protected function runPrivilegeEscalationTest(array $scenario, array $config): array
{
$result = [
'scenario_name' => $scenario['name'],
'vulnerability_detected' => false,
'escalation_vector' => '',
'severity' => 'low',
'description' => '',
'timestamp' => microtime(true)
];
// Simulate privilege escalation testing
switch ($scenario['type']) {
case 'role_manipulation':
$result = $this->testRoleManipulation($scenario, $config);
break;
case 'parameter_pollution':
$result = $this->testParameterPollution($scenario, $config);
break;
case 'session_fixation':
$result = $this->testSessionFixation($scenario, $config);
break;
case 'direct_object_reference':
$result = $this->testDirectObjectReference($scenario, $config);
break;
default:
$result['description'] = 'Unknown escalation test type';
}
return $result;
}
/**
* Test role manipulation.
*/
protected function testRoleManipulation(array $scenario, array $config): array
{
// Mock test for role manipulation
return [
'scenario_name' => $scenario['name'],
'vulnerability_detected' => false,
'escalation_vector' => 'role_parameter_manipulation',
'severity' => 'high',
'description' => 'Test for role parameter manipulation vulnerabilities'
];
}
/**
* Test parameter pollution.
*/
protected function testParameterPollution(array $scenario, array $config): array
{
// Mock test for parameter pollution
return [
'scenario_name' => $scenario['name'],
'vulnerability_detected' => false,
'escalation_vector' => 'parameter_pollution',
'severity' => 'medium',
'description' => 'Test for HTTP parameter pollution vulnerabilities'
];
}
/**
* Test session fixation.
*/
protected function testSessionFixation(array $scenario, array $config): array
{
// Mock test for session fixation
return [
'scenario_name' => $scenario['name'],
'vulnerability_detected' => false,
'escalation_vector' => 'session_fixation',
'severity' => 'high',
'description' => 'Test for session fixation vulnerabilities'
];
}
/**
* Test direct object reference.
*/
protected function testDirectObjectReference(array $scenario, array $config): array
{
// Mock test for direct object reference
return [
'scenario_name' => $scenario['name'],
'vulnerability_detected' => false,
'escalation_vector' => 'insecure_direct_object_reference',
'severity' => 'medium',
'description' => 'Test for insecure direct object reference vulnerabilities'
];
}
/**
* Run MFA test case.
*/
protected function runMfaTestCase(array $testCase, array $config): array
{
$result = [
'test_name' => $testCase['name'],
'user_id' => $testCase['user_id'],
'mfa_required' => true,
'bypass_possible' => false,
'security_issues' => [],
'timestamp' => microtime(true)
];
// Mock MFA testing
$mfaResult = $this->authValidator->validateMfa($testCase['user_id'], $config);
$result['mfa_required'] = $mfaResult['required'];
$result['bypass_possible'] = $mfaResult['bypass_possible'];
$result['security_issues'] = $mfaResult['security_issues'] ?? [];
return $result;
}
/**
* Extract resource from endpoint.
*/
protected function extractResourceFromEndpoint(string $endpoint): string
{
// Simple extraction - in real implementation would be more sophisticated
$parts = explode('/', trim($endpoint, '/'));
return $parts[0] ?? 'unknown';
}
/**
* Calculate RBAC security score.
*/
protected function calculateRbacSecurityScore(array $testResults): int
{
if (empty($testResults)) {
return 100;
}
$passedTests = 0;
foreach ($testResults as $result) {
if ($result['test_passed']) {
$passedTests++;
}
}
return (int) round(($passedTests / count($testResults)) * 100);
}
/**
* Calculate privilege escalation score.
*/
protected function calculatePrivilegeEscalationScore(array $testResults): int
{
if (empty($testResults)) {
return 100;
}
$vulnerabilities = 0;
foreach ($testResults as $result) {
if ($result['vulnerability_detected']) {
$vulnerabilities++;
}
}
$scoreDeduction = ($vulnerabilities / count($testResults)) * 100;
return max(0, 100 - (int) $scoreDeduction);
}
/**
* Calculate MFA security score.
*/
protected function calculateMfaSecurityScore(array $testResults): int
{
if (empty($testResults)) {
return 100;
}
$score = 100;
foreach ($testResults as $result) {
if (!$result['mfa_required']) {
$score -= 30;
}
if ($result['bypass_possible']) {
$score -= 20;
}
}
return max(0, $score);
}
/**
* Generate permission recommendations.
*/
protected function generatePermissionRecommendations(array $securityIssues): array
{
$recommendations = [];
foreach ($securityIssues as $issue) {
if (isset($issue['recommendation'])) {
$recommendations[] = $issue['recommendation'];
}
}
return array_unique($recommendations);
}
/**
* Generate RBAC recommendations.
*/
protected function generateRbacRecommendations(array $securityIssues, array $roleCoverage): array
{
$recommendations = [];
if (!empty($securityIssues)) {
$recommendations[] = 'Review and fix RBAC implementation issues';
}
// Check role coverage
foreach ($roleCoverage as $role => $coverage) {
$passRate = $coverage['passed'] / $coverage['tested'];
if ($passRate < 0.8) {
$recommendations[] = "Improve role '{$role}' permission definitions and test coverage";
}
}
$recommendations[] = 'Implement regular RBAC security testing';
$recommendations[] = 'Document role hierarchies and permission matrices';
return array_unique($recommendations);
}
/**
* Generate API permission recommendations.
*/
protected function generateApiPermissionRecommendations(array $securityIssues): array
{
$recommendations = [];
foreach ($securityIssues as $issue) {
if (isset($issue['recommendation'])) {
$recommendations[] = $issue['recommendation'];
}
}
$recommendations[] = 'Implement API gateway with centralized permission checking';
$recommendations[] = 'Use API keys and tokens for authentication';
$recommendations[] = 'Implement proper API rate limiting and throttling';
return array_unique($recommendations);
}
/**
* Generate privilege escalation recommendations.
*/
protected function generatePrivilegeEscalationRecommendations(array $escalationVectors): array
{
$recommendations = [];
if (!empty($escalationVectors)) {
$recommendations[] = 'Fix identified privilege escalation vulnerabilities';
$recommendations[] = 'Implement proper input validation for role parameters';
$recommendations[] = 'Use secure session management with session regeneration';
}
$recommendations[] = 'Implement principle of least privilege';
$recommendations[] = 'Regularly audit user permissions and role assignments';
$recommendations[] = 'Implement secure indirect object references';
return array_unique($recommendations);
}
/**
* Generate session security recommendations.
*/
protected function generateSessionSecurityRecommendations(array $securityIssues): array
{
$recommendations = [];
foreach ($securityIssues as $issue) {
if (isset($issue['recommendation'])) {
$recommendations[] = $issue['recommendation'];
}
}
$recommendations[] = 'Use secure, random session IDs';
$recommendations[] = 'Implement session timeout and expiration';
$recommendations[] = 'Regenerate session IDs on privilege escalation';
return array_unique($recommendations);
}
/**
* Generate MFA recommendations.
*/
protected function generateMfaRecommendations(array $securityIssues, array $bypassMethods): array
{
$recommendations = [];
if (!empty($bypassMethods)) {
$recommendations[] = 'Fix MFA bypass vulnerabilities in: ' . implode(', ', $bypassMethods);
}
if (!empty($securityIssues)) {
$recommendations[] = 'Address MFA implementation security issues';
}
$recommendations[] = 'Enforce MFA for all privileged operations';
$recommendations[] = 'Implement multiple MFA methods (TOTP, SMS, hardware tokens)';
$recommendations[] = 'Regularly test MFA bypass scenarios';
return array_unique($recommendations);
}
/**
* Count access granted validations.
*/
protected function countAccessGranted(): int
{
$count = 0;
foreach ($this->validationResults as $result) {
if ($result['access_granted']) {
$count++;
}
}
return $count;
}
/**
* Count access denied validations.
*/
protected function countAccessDenied(): int
{
$count = 0;
foreach ($this->validationResults as $result) {
if (!$result['access_granted']) {
$count++;
}
}
return $count;
}
/**
* Count security issues.
*/
protected function countSecurityIssues(): int
{
$count = 0;
foreach ($this->validationResults as $result) {
$count += count($result['security_issues']);
}
return $count;
}
/**
* Calculate average security score.
*/
protected function calculateAverageSecurityScore(): float
{
if (empty($this->validationResults)) {
return 100;
}
$totalScore = 0;
$count = 0;
foreach ($this->validationResults as $result) {
if (isset($result['security_score'])) {
$totalScore += $result['security_score'];
$count++;
}
}
return $count > 0 ? $totalScore / $count : 100;
}
/**
* Initialize permissions.
*/
protected function initializePermissions(): void
{
// This would load permissions from database or configuration
$this->permissions = [
'admin' => ['*'],
'user' => ['read:profile', 'update:profile'],
'guest' => ['read:public']
];
}
/**
* Get default configuration.
*/
protected function getDefaultConfig(): array
{
return [
'permission_validation' => [
'strict_mode' => true,
'cache_permissions' => true,
'audit_access' => true
],
'rbac_testing' => [
'test_all_roles' => true,
'test_edge_cases' => true
],
'api_permission_validation' => [
'validate_rate_limit' => true,
'validate_cors' => true,
'strict_api_mode' => true
],
'privilege_escalation_testing' => [
'test_role_manipulation' => true,
'test_parameter_pollution' => true,
'test_session_fixation' => true,
'test_direct_object_reference' => true
],
'session_security_validation' => [
'validate_session_hijacking' => true,
'validate_session_fixation' => true,
'validate_session_timeout' => true
],
'mfa_testing' => [
'test_backup_codes' => true,
'test_recovery_methods' => true,
'test_bypass_attempts' => true
],
'role_validator' => [],
'access_validator' => [],
'auth_validator' => [],
'authorization_validator' => [],
'session_validator' => [],
'reporter' => []
];
}
/**
* Get configuration.
*/
public function getConfig(): array
{
return $this->config;
}
/**
* Set configuration.
*/
public function setConfig(array $config): void
{
$this->config = array_merge($this->config, $config);
}
/**
* Create permission validator instance.
*/
public static function create(array $config = []): self
{
return new self($config);
}
/**
* Create for development.
*/
public static function forDevelopment(): self
{
return new self([
'permission_validation' => [
'strict_mode' => false,
'audit_access' => false
],
'rbac_testing' => [
'test_edge_cases' => false
]
]);
}
/**
* Create for production.
*/
public static function forProduction(): self
{
return new self([
'permission_validation' => [
'strict_mode' => true,
'cache_permissions' => true,
'audit_access' => true
],
'privilege_escalation_testing' => [
'comprehensive_testing' => true
],
'session_security_validation' => [
'strict_session_validation' => true
]
]);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,642 @@
<?php
declare(strict_types=1);
namespace Fendx\Service\ServiceMesh;
/**
* Envoy 代理配置管理
*/
class EnvoyProxy
{
private array $config;
private array $listeners = [];
private array $clusters = [];
private array $routes = [];
public function __construct(array $config = [])
{
$this->config = array_merge([
'admin_port' => 9901,
'stats_port' => 9902,
'log_level' => 'info',
'drain_time' => '30s',
'parent_shutdown_time' => '60s',
], $config);
}
/**
* 生成 Envoy 配置
*/
public function generateConfig(): array
{
return [
'static_resources' => [
'listeners' => $this->generateListeners(),
'clusters' => $this->generateClusters(),
'secrets' => $this->generateSecrets(),
],
'dynamic_resources' => [
'lds_config' => [
'ads' => [],
],
'cds_config' => [
'ads' => [],
],
'ads_config' => [
'api_type' => 'GRPC',
'grpc_services' => [
[
'envoy_grpc' => [
'cluster_name' => 'xds_cluster',
],
],
],
],
],
'admin' => [
'access_log_path' => '/dev/null',
'address' => [
'socket_address' => [
'address' => '0.0.0.0',
'port_value' => $this->config['admin_port'],
],
],
],
'layered_runtime' => [
'layers' => [
[
'name' => 'static_layer_0',
'static_layer' => [
'overload' => [
'global_downstream_max_connections' => [
'value' => 50000,
],
],
],
],
],
],
];
}
/**
* 生成监听器配置
*/
private function generateListeners(): array
{
return [
$this->createHttpListener(),
$this->createHttpsListener(),
$this->createMetricsListener(),
];
}
/**
* 创建 HTTP 监听器
*/
private function createHttpListener(): array
{
return [
'name' => 'listener_0',
'address' => [
'socket_address' => [
'address' => '0.0.0.0',
'port_value' => 8080,
],
],
'filter_chains' => [
[
'filters' => [
[
'name' => 'envoy.filters.network.http_connection_manager',
'typed_config' => [
'@type' => 'type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager',
'stat_prefix' => 'ingress_http',
'route_config' => [
'name' => 'local_route',
'virtual_hosts' => [
[
'name' => 'local_service',
'domains' => ['*'],
'routes' => $this->routes,
],
],
],
'http_filters' => [
[
'name' => 'envoy.filters.http.router',
'typed_config' => [
'@type' => 'type.googleapis.com/envoy.extensions.filters.http.router.v3.Router',
],
],
[
'name' => 'envoy.filters.http.cors',
'typed_config' => [
'@type' => 'type.googleapis.com/envoy.extensions.filters.http.cors.v3.Cors',
'cors_policy' => [
'allow_origin_string_match' => [
[
'prefix' => '*',
],
],
'allow_methods' => 'GET, POST, PUT, DELETE, OPTIONS',
'allow_headers' => 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization',
'expose_headers' => 'Content-Length,Content-Range',
'max_age' => '1728000',
],
],
],
[
'name' => 'envoy.filters.http.fault',
'typed_config' => [
'@type' => 'type.googleapis.com/envoy.extensions.filters.http.fault.v3.HTTPFault',
'delay' => [
'percentage' => [
'numerator' => 0,
'denominator' => 'HUNDRED',
],
'fixed_delay' => '5s',
],
],
],
],
'access_log' => [
[
'name' => 'envoy.access_loggers.file',
'typed_config' => [
'@type' => 'type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog',
'path' => '/dev/stdout',
'format' => '{"start_time":"%START_TIME%","method":"%REQ(:METHOD)%","x-envoy-origin-path":"%REQ(X-ENVOY-ORIGINAL-PATH?:PATH)%","protocol":"%PROTOCOL%","response_code":"%RESPONSE_CODE%","response_flags":"%RESPONSE_FLAGS%","bytes_received":"%BYTES_RECEIVED%","bytes_sent":"%BYTES_SENT%","duration":"%DURATION%","x-forwarded-for":"%REQ(X-FORWARDED-FOR)%","user-agent":"%REQ(USER-AGENT)%","x-request-id":"%REQ(X-REQUEST-ID)%",":authority":"%REQ(:AUTHORITY)%","upstream_host":"%UPSTREAM_HOST%","upstream_cluster":"%UPSTREAM_CLUSTER%","upstream_local_address":"%UPSTREAM_LOCAL_ADDRESS%","upstream_request_time_ms":"%UPSTREAM_REQUEST_TIME%","upstream_transport_failure_reason":"%UPSTREAM_TRANSPORT_FAILURE_REASON%","upstream_latency":"%UPSTREAM_LATENCY%","x-envoy-upstream-service-time":"%REQ(X-ENVOY-UPSTREAM-SERVICE-TIME)%"}%"\n',
],
],
],
'use_remote_address' => true,
'xff_num_trusted_hops' => 1,
],
],
],
],
],
'traffic_direction' => 'OUTBOUND',
];
}
/**
* 创建 HTTPS 监听器
*/
private function createHttpsListener(): array
{
return [
'name' => 'listener_1',
'address' => [
'socket_address' => [
'address' => '0.0.0.0',
'port_value' => 8443,
],
],
'filter_chains' => [
[
'transport_socket' => [
'name' => 'envoy.transport_sockets.tls',
'typed_config' => [
'@type' => 'type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext',
'common_tls_context' => [
'tls_certificates' => [
[
'certificate_chain' => [
'filename' => '/etc/ssl/certs/server.crt',
],
'private_key' => [
'filename' => '/etc/ssl/private/server.key',
],
],
],
'alpn_protocols' => ['h2', 'http/1.1'],
],
],
],
'filters' => [
[
'name' => 'envoy.filters.network.http_connection_manager',
'typed_config' => [
'@type' => 'type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager',
'stat_prefix' => 'ingress_https',
'route_config' => [
'name' => 'local_route_https',
'virtual_hosts' => [
[
'name' => 'local_service_https',
'domains' => ['*'],
'routes' => $this->routes,
],
],
],
'http_filters' => [
[
'name' => 'envoy.filters.http.router',
'typed_config' => [
'@type' => 'type.googleapis.com/envoy.extensions.filters.http.router.v3.Router',
],
],
],
],
],
],
],
],
'traffic_direction' => 'OUTBOUND',
];
}
/**
* 创建指标监听器
*/
private function createMetricsListener(): array
{
return [
'name' => 'listener_metrics',
'address' => [
'socket_address' => [
'address' => '0.0.0.0',
'port_value' => $this->config['stats_port'],
],
],
'filter_chains' => [
[
'filters' => [
[
'name' => 'envoy.filters.network.http_connection_manager',
'typed_config' => [
'@type' => 'type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager',
'stat_prefix' => 'stats',
'route_config' => [
'name' => 'stats_route',
'virtual_hosts' => [
[
'name' => 'stats_service',
'domains' => ['*'],
'routes' => [
[
'match' => [
'prefix' => '/',
],
'route' => [
'cluster' => 'stats_service',
],
],
],
],
],
],
'http_filters' => [
[
'name' => 'envoy.filters.http.router',
'typed_config' => [
'@type' => 'type.googleapis.com/envoy.extensions.filters.http.router.v3.Router',
],
],
],
],
],
],
],
],
];
}
/**
* 生成集群配置
*/
private function generateClusters(): array
{
return [
$this->createServiceCluster(),
$this->createXdsCluster(),
$this->createStatsCluster(),
$this->createZipkinCluster(),
];
}
/**
* 创建服务集群
*/
private function createServiceCluster(): array
{
return [
'name' => 'service_cluster',
'type' => 'STRICT_DNS',
'connect_timeout' => '5s',
'load_assignment' => [
'cluster_name' => 'service_cluster',
'endpoints' => [
[
'lb_endpoints' => [
[
'endpoint' => [
'address' => [
'socket_address' => [
'address' => 'fendx-php-service',
'port_value' => 9000,
],
],
],
],
],
],
],
],
'type' => 'EDS',
'eds_cluster_config' => [
'eds_config' => [
'ads' => [],
],
],
'transport_socket' => [
'name' => 'envoy.transport_sockets.tls',
'typed_config' => [
'@type' => 'type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext',
'sni' => 'fendx-php-service',
],
],
'common_lb_config' => [
'consistent_hashing_lb_config' => [
'use_source_address' => true,
],
],
];
}
/**
* 创建 XDS 集群
*/
private function createXdsCluster(): array
{
return [
'name' => 'xds_cluster',
'type' => 'STRICT_DNS',
'connect_timeout' => '5s',
'load_assignment' => [
'cluster_name' => 'xds_cluster',
'endpoints' => [
[
'lb_endpoints' => [
[
'endpoint' => [
'address' => [
'socket_address' => [
'address' => 'istiod',
'port_value' => 15012,
],
],
],
],
],
],
],
],
'transport_socket' => [
'name' => 'envoy.transport_sockets.tls',
'typed_config' => [
'@type' => 'type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext',
'sni' => 'istiod',
],
],
];
}
/**
* 创建统计集群
*/
private function createStatsCluster(): array
{
return [
'name' => 'stats_service',
'type' => 'STATIC',
'connect_timeout' => '5s',
'load_assignment' => [
'cluster_name' => 'stats_service',
'endpoints' => [
[
'lb_endpoints' => [
[
'endpoint' => [
'address' => [
'socket_address' => [
'address' => '127.0.0.1',
'port_value' => 9100,
],
],
],
],
],
],
],
],
];
}
/**
* 创建 Zipkin 集群
*/
private function createZipkinCluster(): array
{
return [
'name' => 'zipkin_cluster',
'type' => 'STRICT_DNS',
'connect_timeout' => '5s',
'load_assignment' => [
'cluster_name' => 'zipkin_cluster',
'endpoints' => [
[
'lb_endpoints' => [
[
'endpoint' => [
'address' => [
'socket_address' => [
'address' => 'zipkin',
'port_value' => 9411,
],
],
],
],
],
],
],
],
];
}
/**
* 生成密钥配置
*/
private function generateSecrets(): array
{
return [
[
'name' => 'tls_cert',
'tls_certificate' => [
'certificate_chain' => [
'filename' => '/etc/ssl/certs/server.crt',
],
'private_key' => [
'filename' => '/etc/ssl/private/server.key',
],
],
],
];
}
/**
* 配置追踪
*/
public function configureTracing(array $config): void
{
$this->config['tracing'] = array_merge([
'driver' => 'zipkin',
'sample_rate' => 100,
'service_name' => 'fendx-php',
], $config);
}
/**
* 添加路由
*/
public function addRoute(array $route): void
{
$this->routes[] = $route;
}
/**
* 添加集群
*/
public function addCluster(array $cluster): void
{
$this->clusters[] = $cluster;
}
/**
* 获取配置
*/
public function getConfig(): array
{
return $this->generateConfig();
}
/**
* 保存配置到文件
*/
public function saveConfig(string $path): void
{
$config = $this->generateConfig();
$yaml = json_encode($config, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
$this->ensureDirectory(dirname($path));
file_put_contents($path, $yaml);
}
/**
* 确保目录存在
*/
private function ensureDirectory(string $dir): void
{
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
}
/**
* 热重载配置
*/
public function reloadConfig(): bool
{
$adminUrl = "http://localhost:{$this->config['admin_port']}";
// 触发配置重载
$context = stream_context_create([
'http' => [
'method' => 'POST',
'header' => 'Content-Type: application/json',
'content' => '{}',
],
]);
$response = @file_get_contents("{$adminUrl}/config_dump", false, $context);
return $response !== false;
}
/**
* 获取统计信息
*/
public function getStats(): array
{
$adminUrl = "http://localhost:{$this->config['admin_port']}";
$statsUrl = "{$adminUrl}/stats";
$stats = @file_get_contents($statsUrl);
if ($stats === false) {
return [];
}
$lines = explode("\n", $stats);
$result = [];
foreach ($lines as $line) {
if (empty(trim($line))) {
continue;
}
$parts = explode(':', $line, 2);
if (count($parts) === 2) {
$key = trim($parts[0]);
$value = trim($parts[1]);
// 解析嵌套的统计键
$keys = explode('.', $key);
$current = &$result;
foreach ($keys as $k) {
if (!isset($current[$k])) {
$current[$k] = [];
}
$current = &$current[$k];
}
$current = is_numeric($value) ? (int) $value : $value;
}
}
return $result;
}
/**
* 健康检查
*/
public function healthCheck(): bool
{
$adminUrl = "http://localhost:{$this->config['admin_port']}";
$healthUrl = "{$adminUrl}/clusters";
$response = @file_get_contents($healthUrl);
if ($response === false) {
return false;
}
$clusters = json_decode($response, true);
// 检查所有集群是否健康
foreach ($clusters['cluster_statuses'] ?? [] as $cluster) {
if ($cluster['locality'][0]['priority'] === 0) {
foreach ($cluster['locality'][0]['endpoints'] ?? [] as $endpoint) {
if ($endpoint['health_status']['value'] !== 'HEALTHY') {
return false;
}
}
}
}
return true;
}
}

View File

@@ -0,0 +1,636 @@
<?php
declare(strict_types=1);
namespace Fendx\Service\ServiceMesh;
/**
* 服务网格管理器
* 集成 Istio + Envoy 服务网格解决方案
*/
class ServiceMeshManager
{
private EnvoyProxy $envoy;
private IstioConfig $istio;
private array $config;
public function __construct(array $config = [])
{
$this->config = array_merge([
'enabled' => false,
'istio_version' => '1.18.0',
'envoy_version' => '1.26.0',
'namespace' => 'fendx',
'auto_injection' => true,
], $config);
$this->envoy = new EnvoyProxy($this->config['envoy'] ?? []);
$this->istio = new IstioConfig($this->config['istio'] ?? []);
}
/**
* 启用服务网格
*/
public function enableServiceMesh(): void
{
if (!$this->config['enabled']) {
return;
}
// 自动注入 sidecar
$this->injectSidecar();
// 配置流量管理
$this->configureTrafficManagement();
// 启用安全策略
$this->enableSecurityPolicies();
// 配置可观测性
$this->configureObservability();
}
/**
* 注入 Sidecar 代理
*/
private function injectSidecar(): void
{
if (!$this->config['auto_injection']) {
return;
}
// Kubernetes 自动注入配置
$this->createAutoInjectionConfig();
// Docker sidecar 模式配置
$this->configureDockerSidecar();
}
/**
* 创建自动注入配置
*/
private function createAutoInjectionConfig(): void
{
$config = [
'apiVersion' => 'admissionregistration.k8s.io/v1',
'kind' => 'MutatingWebhookConfiguration',
'metadata' => [
'name' => 'istio-sidecar-injector',
'namespace' => $this->config['namespace'],
],
'webhooks' => [
[
'name' => 'sidecar-injector.istio.io',
'clientConfig' => [
'service' => [
'name' => 'istiod',
'namespace' => $this->config['namespace'],
'path' => '/inject',
],
'caBundle' => $this->getIstioCaBundle(),
],
'rules' => [
[
'operations' => ['CREATE'],
'resources' => ['pods'],
],
],
'failurePolicy' => 'Fail',
'matchPolicy' => 'Exact',
'sideEffects' => 'None',
'timeoutSeconds' => 10,
'admissionReviewVersions' => ['v1', 'v1beta1'],
'reinvocationPolicy' => 'Never',
],
],
];
$this->applyKubernetesConfig($config, 'sidecar-injector.yaml');
}
/**
* 配置 Docker Sidecar
*/
private function configureDockerSidecar(): void
{
$dockerCompose = [
'version' => '3.8',
'services' => [
'app' => [
'image' => 'fendx/php:latest',
'environment' => [
'ISTIO_META_CONFIG_NAMESPACE' => $this->config['namespace'],
],
'networks' => ['istio-mesh'],
],
'istio-proxy' => [
'image' => "istio/proxyv2:{$this->config['istio_version']}",
'command' => [
'istio-proxy',
'proxy',
'sidecar',
'--domain',
'$(POD_NAMESPACE).svc.cluster.local',
],
'environment' => [
'POD_NAME' => '$(HOSTNAME)',
'POD_NAMESPACE' => $this->config['namespace'],
'INSTANCE_IP' => '$(POD_IP)',
],
'volumes' => [
'/var/run/secrets/istio:/var/run/secrets/istio:ro',
'/var/lib/istio/data:/var/lib/istio/data',
],
'networks' => ['istio-mesh'],
'depends_on' => ['app'],
],
],
'networks' => [
'istio-mesh' => [
'external' => true,
],
],
];
$this->saveDockerCompose($dockerCompose);
}
/**
* 配置流量管理
*/
private function configureTrafficManagement(): void
{
// 创建 VirtualService
$this->createVirtualServices();
// 创建 DestinationRule
$this->createDestinationRules();
// 创建 Gateway
$this->createGateways();
// 创建 ServiceEntry
$this->createServiceEntries();
}
/**
* 创建虚拟服务
*/
private function createVirtualServices(): void
{
$virtualService = [
'apiVersion' => 'networking.istio.io/v1beta1',
'kind' => 'VirtualService',
'metadata' => [
'name' => 'fendx-php-service',
'namespace' => $this->config['namespace'],
],
'spec' => [
'hosts' => ['fendx-php-service'],
'gateways' => ['fendx-gateway'],
'http' => [
[
'match' => [
[
'uri' => [
'prefix' => '/api',
],
],
],
'route' => [
[
'destination' => [
'host' => 'fendx-php-service',
'subset' => 'v1',
],
'weight' => 90,
],
[
'destination' => [
'host' => 'fendx-php-service',
'subset' => 'v2',
],
'weight' => 10,
],
],
'fault' => [
'delay' => [
'percentage' => [
'value' => 0.1,
],
'fixedDelay' => '5s',
],
],
'retries' => [
'attempts' => 3,
'perTryTimeout' => '2s',
'retryOn' => 'gateway-error,connect-failure,refused-stream',
],
'timeout' => '10s',
],
],
],
];
$this->applyKubernetesConfig($virtualService, 'virtual-service.yaml');
}
/**
* 创建目标规则
*/
private function createDestinationRules(): void
{
$destinationRule = [
'apiVersion' => 'networking.istio.io/v1beta1',
'kind' => 'DestinationRule',
'metadata' => [
'name' => 'fendx-php-service',
'namespace' => $this->config['namespace'],
],
'spec' => [
'host' => 'fendx-php-service',
'trafficPolicy' => [
'loadBalancer' => [
'simple' => 'LEAST_CONN',
],
'connectionPool' => [
'tcp' => [
'maxConnections' => 100,
],
'http' => [
'http1MaxPendingRequests' => 50,
'maxRequestsPerConnection' => 10,
],
],
'circuitBreaker' => [
'consecutiveErrors' => 3,
'interval' => '30s',
'baseEjectionTime' => '30s',
'maxEjectionPercent' => 50,
],
],
'subsets' => [
[
'name' => 'v1',
'labels' => [
'version' => 'v1',
],
],
[
'name' => 'v2',
'labels' => [
'version' => 'v2',
],
],
],
],
];
$this->applyKubernetesConfig($destinationRule, 'destination-rule.yaml');
}
/**
* 创建网关
*/
private function createGateways(): void
{
$gateway = [
'apiVersion' => 'networking.istio.io/v1beta1',
'kind' => 'Gateway',
'metadata' => [
'name' => 'fendx-gateway',
'namespace' => $this->config['namespace'],
],
'spec' => [
'selector' => [
'istio' => 'ingressgateway',
],
'servers' => [
[
'port' => [
'number' => 80,
'name' => 'http',
'protocol' => 'HTTP',
],
'hosts' => ['*'],
],
[
'port' => [
'number' => 443,
'name' => 'https',
'protocol' => 'HTTPS',
],
'tls' => [
'mode' => 'SIMPLE',
'credentialName' => 'fendx-tls',
],
'hosts' => ['fendx.example.com'],
],
],
],
];
$this->applyKubernetesConfig($gateway, 'gateway.yaml');
}
/**
* 启用安全策略
*/
private function enableSecurityPolicies(): void
{
// 创建 PeerAuthentication
$this->createPeerAuthentication();
// 创建 AuthorizationPolicy
$this->createAuthorizationPolicies();
// 创建 RequestAuthentication
$this->createRequestAuthentication();
}
/**
* 创建对等认证策略
*/
private function createPeerAuthentication(): void
{
$peerAuth = [
'apiVersion' => 'security.istio.io/v1beta1',
'kind' => 'PeerAuthentication',
'metadata' => [
'name' => 'default',
'namespace' => $this->config['namespace'],
],
'spec' => [
'mtls' => [
'mode' => 'STRICT',
],
],
];
$this->applyKubernetesConfig($peerAuth, 'peer-authentication.yaml');
}
/**
* 创建授权策略
*/
private function createAuthorizationPolicies(): void
{
$authPolicy = [
'apiVersion' => 'security.istio.io/v1beta1',
'kind' => 'AuthorizationPolicy',
'metadata' => [
'name' => 'fendx-authz',
'namespace' => $this->config['namespace'],
],
'spec' => [
'selector' => [
'matchLabels' => [
'app' => 'fendx-php',
],
],
'rules' => [
[
'from' => [
[
'source' => [
'principals' => ['cluster.local/ns/istio-system/sa/istio-ingressgateway-service-account'],
],
],
],
'to' => [
[
'operation' => [
'methods' => ['GET', 'POST'],
'paths' => ['/api/*'],
],
],
],
],
],
],
];
$this->applyKubernetesConfig($authPolicy, 'authorization-policy.yaml');
}
/**
* 配置可观测性
*/
private function configureObservability(): void
{
// 创建 Telemetry 配置
$this->createTelemetryConfig();
// 创建 ServiceMonitor
$this->createServiceMonitors();
// 配置分布式追踪
$this->configureTracing();
}
/**
* 创建遥测配置
*/
private function createTelemetryConfig(): void
{
$telemetry = [
'apiVersion' => 'telemetry.istio.io/v1alpha1',
'kind' => 'Telemetry',
'metadata' => [
'name' => 'default',
'namespace' => $this->config['namespace'],
],
'spec' => [
'tracing' => [
[
'randomSamplingPercentage' => 100,
'customTags' => [
'service_name' => [
'literal' => [
'value' => 'fendx-php',
],
],
],
],
],
'metrics' => [
[
'providers' => [
[
'name' => 'prometheus',
],
],
'overrides' => [
[
'match' => [
'metric' => 'ALL_METRICS',
],
'tagOverrides' => [
'destination_service' => [
'operation' => 'UPSERT',
'value' => 'fendx-php-service',
],
],
],
],
],
],
'accessLogging' => [
[
'providers' => [
[
'name' => 'envoy',
],
],
'filter' => [
'expression' => 'response.code >= 400',
],
],
],
],
];
$this->applyKubernetesConfig($telemetry, 'telemetry.yaml');
}
/**
* 配置分布式追踪
*/
private function configureTracing(): void
{
$this->envoy->configureTracing([
'driver' => 'zipkin',
'config' => [
'zipkin_address' => 'zipkin.istio-system:9411',
'sample_rate' => 100,
],
]);
}
/**
* 获取 Istio CA 证书
*/
private function getIstioCaBundle(): string
{
// 从 Kubernetes secret 获取 CA 证书
$caCert = shell_exec('kubectl get secret istio-ca-secret -n istio-system -o jsonpath="{.data.ca\\.crt}"');
return base64_decode($caCert ?: '');
}
/**
* 应用 Kubernetes 配置
*/
private function applyKubernetesConfig(array $config, string $filename): void
{
$yaml = $this->arrayToYaml($config);
$configPath = runtime_path('k8s/' . $filename);
$this->ensureDirectory(dirname($configPath));
file_put_contents($configPath, $yaml);
// 自动应用到 Kubernetes 集群
if ($this->config['auto_apply'] ?? false) {
shell_exec("kubectl apply -f {$configPath}");
}
}
/**
* 保存 Docker Compose 配置
*/
private function saveDockerCompose(array $config): void
{
$yaml = $this->arrayToYaml($config);
$composePath = runtime_path('docker/docker-compose.mesh.yml');
$this->ensureDirectory(dirname($composePath));
file_put_contents($composePath, $yaml);
}
/**
* 确保目录存在
*/
private function ensureDirectory(string $dir): void
{
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
}
/**
* 数组转 YAML
*/
private function arrayToYaml(array $array): string
{
// 简化的 YAML 转换,实际项目中建议使用 symfony/yaml
return json_encode($array, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
}
/**
* 获取服务网格状态
*/
public function getStatus(): array
{
return [
'enabled' => $this->config['enabled'],
'istio_version' => $this->config['istio_version'],
'envoy_version' => $this->config['envoy_version'],
'namespace' => $this->config['namespace'],
'auto_injection' => $this->config['auto_injection'],
'components' => [
'istiod' => $this->checkIstiodStatus(),
'ingress_gateway' => $this->checkIngressGatewayStatus(),
'egress_gateway' => $this->checkEgressGatewayStatus(),
],
];
}
/**
* 检查 Istiod 状态
*/
private function checkIstiodStatus(): array
{
$output = shell_exec("kubectl get pod -n {$this->config['namespace']} -l app=istiod -o json");
$pods = json_decode($output, true);
return [
'running' => count($pods['items'] ?? []),
'ready' => count(array_filter($pods['items'] ?? [], fn($pod) =>
$pod['status']['phase'] === 'Running' &&
$pod['status']['containerStatuses'][0]['ready'] ?? false
)),
];
}
/**
* 检查入口网关状态
*/
private function checkIngressGatewayStatus(): array
{
$output = shell_exec("kubectl get pod -n {$this->config['namespace']} -l app=istio-ingressgateway -o json");
$pods = json_decode($output, true);
return [
'running' => count($pods['items'] ?? []),
'ready' => count(array_filter($pods['items'] ?? [], fn($pod) =>
$pod['status']['phase'] === 'Running' &&
($pod['status']['containerStatuses'][0]['ready'] ?? false)
)),
];
}
/**
* 检查出口网关状态
*/
private function checkEgressGatewayStatus(): array
{
$output = shell_exec("kubectl get pod -n {$this->config['namespace']} -l app=istio-egressgateway -o json");
$pods = json_decode($output, true);
return [
'running' => count($pods['items'] ?? []),
'ready' => count(array_filter($pods['items'] ?? [], fn($pod) =>
$pod['status']['phase'] === 'Running' &&
($pod['status']['containerStatuses'][0]['ready'] ?? false)
)),
];
}
}

View File

@@ -0,0 +1,926 @@
<?php
declare(strict_types=1);
namespace Fendx\Service\Testing;
use Fendx\Service\Testing\Coverage\CoverageAnalyzer;
use Fendx\Service\Testing\Coverage\UnitTestAnalyzer;
use Fendx\Service\Testing\Coverage\IntegrationTestAnalyzer;
use Fendx\Service\Testing\Coverage\ApiTestAnalyzer;
use Fendx\Service\Testing\Coverage\E2ETestAnalyzer;
use Fendx\Service\Testing\Reporter\CoverageReporter;
class TestCoverageChecker
{
protected array $config = [];
protected CoverageAnalyzer $coverageAnalyzer;
protected UnitTestAnalyzer $unitTestAnalyzer;
protected IntegrationTestAnalyzer $integrationTestAnalyzer;
protected ApiTestAnalyzer $apiTestAnalyzer;
protected E2ETestAnalyzer $e2eTestAnalyzer;
protected CoverageReporter $reporter;
protected array $coverageResults = [];
protected array $testMetrics = [];
public function __construct(array $config = [])
{
$this->config = array_merge($this->getDefaultConfig(), $config);
$this->coverageAnalyzer = new CoverageAnalyzer($this->config['coverage_analyzer'] ?? []);
$this->unitTestAnalyzer = new UnitTestAnalyzer($this->config['unit_test_analyzer'] ?? []);
$this->integrationTestAnalyzer = new IntegrationTestAnalyzer($this->config['integration_test_analyzer'] ?? []);
$this->apiTestAnalyzer = new ApiTestAnalyzer($this->config['api_test_analyzer'] ?? []);
$this->e2eTestAnalyzer = new E2ETestAnalyzer($this->config['e2e_test_analyzer'] ?? []);
$this->reporter = new CoverageReporter($this->config['reporter'] ?? []);
}
/**
* Check overall test coverage.
*/
public function checkTestCoverage(string $projectPath, array $options = []): array
{
$checkConfig = array_merge($this->config['coverage_check'] ?? [], $options);
$result = [
'check_type' => 'test_coverage',
'project_path' => $projectPath,
'total_coverage' => 0,
'line_coverage' => 0,
'branch_coverage' => 0,
'method_coverage' => 0,
'class_coverage' => 0,
'test_types' => [],
'coverage_by_type' => [],
'uncovered_code' => [],
'quality_score' => 0,
'recommendations' => [],
'timestamp' => microtime(true)
];
// Analyze different test types
$unitCoverage = $this->checkUnitTestCoverage($projectPath, $checkConfig);
$integrationCoverage = $this->checkIntegrationTestCoverage($projectPath, $checkConfig);
$apiCoverage = $this->checkApiTestCoverage($projectPath, $checkConfig);
$e2eCoverage = $this->checkE2ETestCoverage($projectPath, $checkConfig);
$result['test_types'] = [
'unit_tests' => $unitCoverage['exists'],
'integration_tests' => $integrationCoverage['exists'],
'api_tests' => $apiCoverage['exists'],
'e2e_tests' => $e2eCoverage['exists']
];
$result['coverage_by_type'] = [
'unit' => $unitCoverage,
'integration' => $integrationCoverage,
'api' => $apiCoverage,
'e2e' => $e2eCoverage
];
// Calculate overall coverage
$coverages = array_filter([
$unitCoverage['line_coverage'] ?? 0,
$integrationCoverage['line_coverage'] ?? 0,
$apiCoverage['line_coverage'] ?? 0,
$e2eCoverage['line_coverage'] ?? 0
]);
$result['total_coverage'] = count($coverages) > 0 ?
round(array_sum($coverages) / count($coverages), 2) : 0;
// Get detailed coverage metrics
$detailedCoverage = $this->coverageAnalyzer->analyze($projectPath, $checkConfig);
$result['line_coverage'] = $detailedCoverage['line_coverage'] ?? 0;
$result['branch_coverage'] = $detailedCoverage['branch_coverage'] ?? 0;
$result['method_coverage'] = $detailedCoverage['method_coverage'] ?? 0;
$result['class_coverage'] = $detailedCoverage['class_coverage'] ?? 0;
$result['uncovered_code'] = $detailedCoverage['uncovered_files'] ?? [];
$result['quality_score'] = $this->calculateCoverageQualityScore($result);
$result['recommendations'] = $this->generateCoverageRecommendations($result);
// Store result
$this->coverageResults[] = $result;
return $result;
}
/**
* Check unit test coverage.
*/
public function checkUnitTestCoverage(string $projectPath, array $options = []): array
{
$checkConfig = array_merge($this->config['unit_test_check'] ?? [], $options);
$result = [
'check_type' => 'unit_test_coverage',
'project_path' => $projectPath,
'exists' => false,
'test_files_found' => 0,
'test_classes_found' => 0,
'test_methods_found' => 0,
'assertions_found' => 0,
'line_coverage' => 0,
'method_coverage' => 0,
'class_coverage' => 0,
'coverage_details' => [],
'quality_issues' => [],
'recommendations' => [],
'timestamp' => microtime(true)
];
// Find unit test files
$testFiles = $this->findTestFiles($projectPath, 'unit', $checkConfig);
$result['test_files_found'] = count($testFiles);
$result['exists'] = !empty($testFiles);
if (empty($testFiles)) {
$result['quality_issues'][] = [
'type' => 'no_unit_tests',
'severity' => 'critical',
'message' => 'No unit test files found',
'recommendation' => 'Create unit tests for core functionality'
];
return $result;
}
// Analyze test files
$testAnalysis = $this->unitTestAnalyzer->analyze($testFiles, $checkConfig);
$result['test_classes_found'] = $testAnalysis['test_classes'];
$result['test_methods_found'] = $testAnalysis['test_methods'];
$result['assertions_found'] = $testAnalysis['assertions'];
$result['coverage_details'] = $testAnalysis['coverage_details'];
$result['quality_issues'] = $testAnalysis['quality_issues'];
// Get coverage metrics
$coverageMetrics = $this->coverageAnalyzer->analyzeUnitTests($projectPath, $checkConfig);
$result['line_coverage'] = $coverageMetrics['line_coverage'] ?? 0;
$result['method_coverage'] = $coverageMetrics['method_coverage'] ?? 0;
$result['class_coverage'] = $coverageMetrics['class_coverage'] ?? 0;
$result['recommendations'] = $this->generateUnitTestRecommendations($result);
return $result;
}
/**
* Check integration test coverage.
*/
public function checkIntegrationTestCoverage(string $projectPath, array $options = []): array
{
$checkConfig = array_merge($this->config['integration_test_check'] ?? [], $options);
$result = [
'check_type' => 'integration_test_coverage',
'project_path' => $projectPath,
'exists' => false,
'test_files_found' => 0,
'integration_scenarios' => 0,
'database_tests' => 0,
'cache_tests' => 0,
'external_service_tests' => 0,
'line_coverage' => 0,
'coverage_details' => [],
'quality_issues' => [],
'recommendations' => [],
'timestamp' => microtime(true)
];
// Find integration test files
$testFiles = $this->findTestFiles($projectPath, 'integration', $checkConfig);
$result['test_files_found'] = count($testFiles);
$result['exists'] = !empty($testFiles);
if (empty($testFiles)) {
$result['quality_issues'][] = [
'type' => 'no_integration_tests',
'severity' => 'high',
'message' => 'No integration test files found',
'recommendation' => 'Create integration tests for component interactions'
];
return $result;
}
// Analyze test files
$testAnalysis = $this->integrationTestAnalyzer->analyze($testFiles, $checkConfig);
$result['integration_scenarios'] = $testAnalysis['scenarios'];
$result['database_tests'] = $testAnalysis['database_tests'];
$result['cache_tests'] = $testAnalysis['cache_tests'];
$result['external_service_tests'] = $testAnalysis['external_service_tests'];
$result['coverage_details'] = $testAnalysis['coverage_details'];
$result['quality_issues'] = $testAnalysis['quality_issues'];
// Get coverage metrics
$coverageMetrics = $this->coverageAnalyzer->analyzeIntegrationTests($projectPath, $checkConfig);
$result['line_coverage'] = $coverageMetrics['line_coverage'] ?? 0;
$result['recommendations'] = $this->generateIntegrationTestRecommendations($result);
return $result;
}
/**
* Check API test coverage.
*/
public function checkApiTestCoverage(string $projectPath, array $options = []): array
{
$checkConfig = array_merge($this->config['api_test_check'] ?? [], $options);
$result = [
'check_type' => 'api_test_coverage',
'project_path' => $projectPath,
'exists' => false,
'test_files_found' => 0,
'endpoints_tested' => 0,
'http_methods_covered' => [],
'status_codes_tested' => [],
'request_response_tests' => 0,
'authentication_tests' => 0,
'line_coverage' => 0,
'coverage_details' => [],
'quality_issues' => [],
'recommendations' => [],
'timestamp' => microtime(true)
];
// Find API test files
$testFiles = $this->findTestFiles($projectPath, 'api', $checkConfig);
$result['test_files_found'] = count($testFiles);
$result['exists'] = !empty($testFiles);
if (empty($testFiles)) {
$result['quality_issues'][] = [
'type' => 'no_api_tests',
'severity' => 'high',
'message' => 'No API test files found',
'recommendation' => 'Create API tests for all endpoints'
];
return $result;
}
// Analyze test files
$testAnalysis = $this->apiTestAnalyzer->analyze($testFiles, $checkConfig);
$result['endpoints_tested'] = $testAnalysis['endpoints_tested'];
$result['http_methods_covered'] = $testAnalysis['http_methods'];
$result['status_codes_tested'] = $testAnalysis['status_codes'];
$result['request_response_tests'] = $testAnalysis['request_response_tests'];
$result['authentication_tests'] = $testAnalysis['authentication_tests'];
$result['coverage_details'] = $testAnalysis['coverage_details'];
$result['quality_issues'] = $testAnalysis['quality_issues'];
// Get coverage metrics
$coverageMetrics = $this->coverageAnalyzer->analyzeApiTests($projectPath, $checkConfig);
$result['line_coverage'] = $coverageMetrics['line_coverage'] ?? 0;
$result['recommendations'] = $this->generateApiTestRecommendations($result);
return $result;
}
/**
* Check end-to-end test coverage.
*/
public function checkE2ETestCoverage(string $projectPath, array $options = []): array
{
$checkConfig = array_merge($this->config['e2e_test_check'] ?? [], $options);
$result = [
'check_type' => 'e2e_test_coverage',
'project_path' => $projectPath,
'exists' => false,
'test_files_found' => 0,
'user_workflows_tested' => 0,
'browser_tests' => 0,
'mobile_tests' => 0,
'critical_paths_covered' => 0,
'line_coverage' => 0,
'coverage_details' => [],
'quality_issues' => [],
'recommendations' => [],
'timestamp' => microtime(true)
];
// Find E2E test files
$testFiles = $this->findTestFiles($projectPath, 'e2e', $checkConfig);
$result['test_files_found'] = count($testFiles);
$result['exists'] = !empty($testFiles);
if (empty($testFiles)) {
$result['quality_issues'][] = [
'type' => 'no_e2e_tests',
'severity' => 'medium',
'message' => 'No end-to-end test files found',
'recommendation' => 'Create E2E tests for critical user workflows'
];
return $result;
}
// Analyze test files
$testAnalysis = $this->e2eTestAnalyzer->analyze($testFiles, $checkConfig);
$result['user_workflows_tested'] = $testAnalysis['workflows'];
$result['browser_tests'] = $testAnalysis['browser_tests'];
$result['mobile_tests'] = $testAnalysis['mobile_tests'];
$result['critical_paths_covered'] = $testAnalysis['critical_paths'];
$result['coverage_details'] = $testAnalysis['coverage_details'];
$result['quality_issues'] = $testAnalysis['quality_issues'];
// Get coverage metrics
$coverageMetrics = $this->coverageAnalyzer->analyzeE2ETests($projectPath, $checkConfig);
$result['line_coverage'] = $coverageMetrics['line_coverage'] ?? 0;
$result['recommendations'] = $this->generateE2ETestRecommendations($result);
return $result;
}
/**
* Analyze test quality metrics.
*/
public function analyzeTestQuality(string $projectPath, array $options = []): array
{
$checkConfig = array_merge($this->config['quality_analysis'] ?? [], $options);
$result = [
'check_type' => 'test_quality_analysis',
'project_path' => $projectPath,
'test_files_analyzed' => 0,
'quality_metrics' => [
'test_complexity' => 0,
'assertion_density' => 0,
'test_isolation' => 0,
'mock_usage' => 0,
'data_driven_tests' => 0
],
'quality_issues' => [],
'best_practices_violations' => [],
'quality_score' => 0,
'recommendations' => [],
'timestamp' => microtime(true)
];
// Analyze all test types
$unitTests = $this->findTestFiles($projectPath, 'unit', $checkConfig);
$integrationTests = $this->findTestFiles($projectPath, 'integration', $checkConfig);
$apiTests = $this->findTestFiles($projectPath, 'api', $checkConfig);
$e2eTests = $this->findTestFiles($projectPath, 'e2e', $checkConfig);
$allTestFiles = array_merge($unitTests, $integrationTests, $apiTests, $e2eTests);
$result['test_files_analyzed'] = count($allTestFiles);
if (empty($allTestFiles)) {
$result['quality_issues'][] = [
'type' => 'no_tests_found',
'severity' => 'critical',
'message' => 'No test files found in the project'
];
return $result;
}
// Analyze test quality
$qualityAnalysis = $this->coverageAnalyzer->analyzeQuality($allTestFiles, $checkConfig);
$result['quality_metrics'] = $qualityAnalysis['metrics'];
$result['quality_issues'] = $qualityAnalysis['issues'];
$result['best_practices_violations'] = $qualityAnalysis['violations'];
$result['quality_score'] = $qualityAnalysis['score'];
$result['recommendations'] = $this->generateQualityRecommendations($result);
return $result;
}
/**
* Generate comprehensive test coverage report.
*/
public function generateCoverageReport(array $results, string $format = 'html'): string
{
return $this->reporter->generate($results, $format);
}
/**
* Get coverage statistics.
*/
public function getStatistics(): array
{
return [
'total_checks' => count($this->coverageResults),
'average_coverage' => $this->calculateAverageCoverage(),
'coverage_trends' => $this->getCoverageTrends(),
'test_distribution' => $this->getTestDistribution(),
'quality_scores' => $this->getQualityScores()
];
}
/**
* Clear coverage results.
*/
public function clearResults(): void
{
$this->coverageResults = [];
$this->testMetrics = [];
}
/**
* Find test files by type.
*/
protected function findTestFiles(string $projectPath, string $testType, array $config): array
{
$patterns = $config['file_patterns'][$testType] ?? $this->getDefaultPatterns($testType);
$files = [];
foreach ($patterns as $pattern) {
$iterator = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($projectPath)
);
foreach ($iterator as $file) {
if ($file->isDot() || !$file->isFile()) {
continue;
}
$filePath = $file->getPathname();
$fileName = basename($filePath);
if (fnmatch($pattern, $fileName)) {
$exclude = $config['exclude'] ?? ['vendor', 'node_modules'];
$excluded = false;
foreach ($exclude as $excludePattern) {
if (strpos($filePath, $excludePattern) !== false) {
$excluded = true;
break;
}
}
if (!$excluded) {
$files[] = $filePath;
}
}
}
}
return array_unique($files);
}
/**
* Get default file patterns for test types.
*/
protected function getDefaultPatterns(string $testType): array
{
$patterns = [
'unit' => ['*Test.php', '*UnitTest.php', 'tests/unit/*Test.php'],
'integration' => ['*IntegrationTest.php', 'tests/integration/*Test.php'],
'api' => ['*ApiTest.php', '*EndpointTest.php', 'tests/api/*Test.php'],
'e2e' => ['*E2ETest.php', '*EndToEndTest.php', 'tests/e2e/*Test.php', '*FeatureTest.php']
];
return $patterns[$testType] ?? ['*Test.php'];
}
/**
* Calculate coverage quality score.
*/
protected function calculateCoverageQualityScore(array $result): int
{
$score = 0;
// Line coverage (40%)
$score += ($result['line_coverage'] ?? 0) * 0.4;
// Branch coverage (20%)
$score += ($result['branch_coverage'] ?? 0) * 0.2;
// Method coverage (20%)
$score += ($result['method_coverage'] ?? 0) * 0.2;
// Class coverage (20%)
$score += ($result['class_coverage'] ?? 0) * 0.2;
// Test type diversity bonus
$testTypes = array_filter($result['test_types'] ?? []);
$diversityBonus = (count($testTypes) / 4) * 10;
$score += $diversityBonus;
return (int) min(100, round($score));
}
/**
* Generate coverage recommendations.
*/
protected function generateCoverageRecommendations(array $result): array
{
$recommendations = [];
if ($result['line_coverage'] < 80) {
$recommendations[] = 'Increase line coverage to at least 80%';
}
if ($result['branch_coverage'] < 70) {
$recommendations[] = 'Improve branch coverage by testing conditional logic';
}
if ($result['method_coverage'] < 85) {
$recommendations[] = 'Add tests for uncovered methods';
}
if ($result['class_coverage'] < 80) {
$recommendations[] = 'Ensure all classes have test coverage';
}
// Check test type diversity
$missingTypes = array_keys(array_filter($result['test_types'] ?? [], fn($v) => !$v));
if (!empty($missingTypes)) {
$typeNames = [
'unit_tests' => 'unit tests',
'integration_tests' => 'integration tests',
'api_tests' => 'API tests',
'e2e_tests' => 'end-to-end tests'
];
$missingNames = array_map(fn($t) => $typeNames[$t] ?? $t, $missingTypes);
$recommendations[] = 'Add missing test types: ' . implode(', ', $missingNames);
}
if (!empty($result['uncovered_code'])) {
$recommendations[] = 'Write tests for uncovered code files';
}
$recommendations[] = 'Set up automated coverage reporting in CI/CD';
$recommendations[] = 'Establish coverage gates for pull requests';
return array_unique($recommendations);
}
/**
* Generate unit test recommendations.
*/
protected function generateUnitTestRecommendations(array $result): array
{
$recommendations = [];
if (!$result['exists']) {
$recommendations[] = 'Create unit test suite for the project';
return $recommendations;
}
if ($result['line_coverage'] < 80) {
$recommendations[] = 'Improve unit test line coverage';
}
if ($result['test_methods_found'] < $result['test_classes_found'] * 3) {
$recommendations[] = 'Add more test methods per test class';
}
if ($result['assertions_found'] < $result['test_methods_found'] * 2) {
$recommendations[] = 'Add more assertions to test methods';
}
$recommendations[] = 'Follow AAA pattern (Arrange, Act, Assert)';
$recommendations[] = 'Use descriptive test method names';
return array_unique($recommendations);
}
/**
* Generate integration test recommendations.
*/
protected function generateIntegrationTestRecommendations(array $result): array
{
$recommendations = [];
if (!$result['exists']) {
$recommendations[] = 'Create integration test suite';
return $recommendations;
}
if ($result['database_tests'] === 0) {
$recommendations[] = 'Add database integration tests';
}
if ($result['cache_tests'] === 0) {
$recommendations[] = 'Add cache integration tests';
}
if ($result['external_service_tests'] === 0) {
$recommendations[] = 'Add external service integration tests';
}
$recommendations[] = 'Use test containers for integration testing';
$recommendations[] = 'Test error scenarios and edge cases';
return array_unique($recommendations);
}
/**
* Generate API test recommendations.
*/
protected function generateApiTestRecommendations(array $result): array
{
$recommendations = [];
if (!$result['exists']) {
$recommendations[] = 'Create API test suite';
return $recommendations;
}
$expectedMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'];
$missingMethods = array_diff($expectedMethods, $result['http_methods_covered']);
if (!empty($missingMethods)) {
$recommendations[] = 'Test missing HTTP methods: ' . implode(', ', $missingMethods);
}
$expectedStatusCodes = [200, 201, 400, 401, 403, 404, 500];
$missingCodes = array_diff($expectedStatusCodes, $result['status_codes_tested']);
if (!empty($missingCodes)) {
$recommendations[] = 'Test missing status codes: ' . implode(', ', $missingCodes);
}
if ($result['authentication_tests'] === 0) {
$recommendations[] = 'Add authentication and authorization tests';
}
$recommendations[] = 'Test API request validation';
$recommendations[] = 'Include performance and load testing';
return array_unique($recommendations);
}
/**
* Generate E2E test recommendations.
*/
protected function generateE2ETestRecommendations(array $result): array
{
$recommendations = [];
if (!$result['exists']) {
$recommendations[] = 'Create end-to-end test suite';
return $recommendations;
}
if ($result['critical_paths_covered'] < 5) {
$recommendations[] = 'Cover more critical user paths';
}
if ($result['browser_tests'] === 0) {
$recommendations[] = 'Add browser-based E2E tests';
}
if ($result['mobile_tests'] === 0) {
$recommendations[] = 'Consider mobile E2E testing';
}
$recommendations[] = 'Test cross-browser compatibility';
$recommendations[] = 'Include accessibility testing in E2E tests';
return array_unique($recommendations);
}
/**
* Generate quality recommendations.
*/
protected function generateQualityRecommendations(array $result): array
{
$recommendations = [];
if ($result['quality_metrics']['test_complexity'] > 10) {
$recommendations[] = 'Simplify complex test methods';
}
if ($result['quality_metrics']['assertion_density'] < 2) {
$recommendations[] = 'Increase assertion density in tests';
}
if ($result['quality_metrics']['test_isolation'] < 80) {
$recommendations[] = 'Improve test isolation and independence';
}
if ($result['quality_metrics']['mock_usage'] < 50) {
$recommendations[] = 'Use mocking for external dependencies';
}
if (!empty($result['best_practices_violations'])) {
$recommendations[] = 'Address test best practices violations';
}
$recommendations[] = 'Implement test code review process';
$recommendations[] = 'Use test-driven development (TDD) practices';
return array_unique($recommendations);
}
/**
* Calculate average coverage.
*/
protected function calculateAverageCoverage(): float
{
if (empty($this->coverageResults)) {
return 0;
}
$totalCoverage = 0;
$count = 0;
foreach ($this->coverageResults as $result) {
if (isset($result['total_coverage'])) {
$totalCoverage += $result['total_coverage'];
$count++;
}
}
return $count > 0 ? $totalCoverage / $count : 0;
}
/**
* Get coverage trends.
*/
protected function getCoverageTrends(): array
{
if (count($this->coverageResults) < 2) {
return ['trend' => 'insufficient_data'];
}
$recent = array_slice($this->coverageResults, -5);
$coverages = array_column($recent, 'total_coverage');
if (count($coverages) < 2) {
return ['trend' => 'insufficient_data'];
}
$first = $coverages[0];
$last = end($coverages);
if ($last > $first + 2) {
return ['trend' => 'improving', 'change' => $last - $first];
} elseif ($last < $first - 2) {
return ['trend' => 'declining', 'change' => $first - $last];
} else {
return ['trend' => 'stable', 'change' => 0];
}
}
/**
* Get test distribution.
*/
protected function getTestDistribution(): array
{
$distribution = [
'unit' => 0,
'integration' => 0,
'api' => 0,
'e2e' => 0
];
foreach ($this->coverageResults as $result) {
if (isset($result['test_types'])) {
foreach ($result['test_types'] as $type => $exists) {
if ($exists) {
$distribution[$type]++;
}
}
}
}
return $distribution;
}
/**
* Get quality scores.
*/
protected function getQualityScores(): array
{
$scores = [];
foreach ($this->coverageResults as $result) {
if (isset($result['quality_score'])) {
$scores[] = $result['quality_score'];
}
}
return $scores;
}
/**
* Get default configuration.
*/
protected function getDefaultConfig(): array
{
return [
'coverage_check' => [
'min_line_coverage' => 80,
'min_branch_coverage' => 70,
'min_method_coverage' => 85,
'min_class_coverage' => 80
],
'unit_test_check' => [
'min_coverage' => 80,
'min_assertions_per_test' => 2
],
'integration_test_check' => [
'min_coverage' => 70,
'require_database_tests' => true
],
'api_test_check' => [
'min_coverage' => 75,
'require_auth_tests' => true
],
'e2e_test_check' => [
'min_coverage' => 60,
'require_critical_paths' => true
],
'quality_analysis' => [
'max_test_complexity' => 10,
'min_assertion_density' => 2,
'min_test_isolation' => 80
],
'file_patterns' => [
'unit' => ['*Test.php', '*UnitTest.php', 'tests/unit/*Test.php'],
'integration' => ['*IntegrationTest.php', 'tests/integration/*Test.php'],
'api' => ['*ApiTest.php', '*EndpointTest.php', 'tests/api/*Test.php'],
'e2e' => ['*E2ETest.php', '*EndToEndTest.php', 'tests/e2e/*Test.php', '*FeatureTest.php']
],
'exclude' => ['vendor', 'node_modules', '.git'],
'coverage_analyzer' => [],
'unit_test_analyzer' => [],
'integration_test_analyzer' => [],
'api_test_analyzer' => [],
'e2e_test_analyzer' => [],
'reporter' => []
];
}
/**
* Get configuration.
*/
public function getConfig(): array
{
return $this->config;
}
/**
* Set configuration.
*/
public function setConfig(array $config): void
{
$this->config = array_merge($this->config, $config);
}
/**
* Create test coverage checker instance.
*/
public static function create(array $config = []): self
{
return new self($config);
}
/**
* Create for development.
*/
public static function forDevelopment(): self
{
return new self([
'coverage_check' => [
'min_line_coverage' => 60,
'min_branch_coverage' => 50
],
'unit_test_check' => [
'min_coverage' => 60
],
'quality_analysis' => [
'max_test_complexity' => 15
]
]);
}
/**
* Create for production.
*/
public static function forProduction(): self
{
return new self([
'coverage_check' => [
'min_line_coverage' => 85,
'min_branch_coverage' => 75,
'min_method_coverage' => 90,
'min_class_coverage' => 85
],
'unit_test_check' => [
'min_coverage' => 85,
'min_assertions_per_test' => 3
],
'integration_test_check' => [
'min_coverage' => 80,
'require_database_tests' => true
],
'api_test_check' => [
'min_coverage' => 85,
'require_auth_tests' => true
],
'quality_analysis' => [
'max_test_complexity' => 8,
'min_assertion_density' => 3,
'min_test_isolation' => 90
]
]);
}
}

View File

@@ -0,0 +1,838 @@
<?php
declare(strict_types=1);
namespace Fendx\Service\Tracing\Analyzer;
use Fendx\Service\Tracing\Span\Span;
use Fendx\Service\Tracing\Analyzer\Metric\MetricCalculator;
use Fendx\Service\Tracing\Analyzer\Pattern\PatternDetector;
use Fendx\Service\Tracing\Analyzer\Anomaly\AnomalyDetector;
class PerformanceAnalyzer
{
protected MetricCalculator $metricCalculator;
protected PatternDetector $patternDetector;
protected AnomalyDetector $anomalyDetector;
protected array $config = [];
protected array $analysisCache = [];
protected array $performanceMetrics = [];
protected array $bottlenecks = [];
protected array $recommendations = [];
public function __construct(array $config = [])
{
$this->config = array_merge($this->getDefaultConfig(), $config);
$this->metricCalculator = new MetricCalculator($this->config['metrics'] ?? []);
$this->patternDetector = new PatternDetector($this->config['patterns'] ?? []);
$this->anomalyDetector = new AnomalyDetector($this->config['anomaly_detection'] ?? []);
}
/**
* Analyze trace performance.
*/
public function analyzeTrace(array $spans): array
{
$traceId = $this->extractTraceId($spans);
// Check cache
$cacheKey = 'trace_' . $traceId;
if (isset($this->analysisCache[$cacheKey])) {
return $this->analysisCache[$cacheKey];
}
$analysis = [
'trace_id' => $traceId,
'span_count' => count($spans),
'total_duration' => 0,
'critical_path' => [],
'bottlenecks' => [],
'performance_score' => 0,
'recommendations' => [],
'metrics' => [],
'patterns' => [],
'anomalies' => []
];
if (empty($spans)) {
return $analysis;
}
// Calculate basic metrics
$analysis['total_duration'] = $this->calculateTotalDuration($spans);
$analysis['metrics'] = $this->metricCalculator->calculateTraceMetrics($spans);
// Find critical path
$analysis['critical_path'] = $this->findCriticalPath($spans);
// Identify bottlenecks
$analysis['bottlenecks'] = $this->identifyBottlenecks($spans);
// Detect patterns
$analysis['patterns'] = $this->patternDetector->detectPatterns($spans);
// Detect anomalies
$analysis['anomalies'] = $this->anomalyDetector->detectAnomalies($spans);
// Calculate performance score
$analysis['performance_score'] = $this->calculatePerformanceScore($analysis);
// Generate recommendations
$analysis['recommendations'] = $this->generateRecommendations($analysis);
// Cache result
$this->analysisCache[$cacheKey] = $analysis;
// Limit cache size
$this->limitCacheSize();
return $analysis;
}
/**
* Analyze service performance.
*/
public function analyzeService(array $traces, string $serviceName): array
{
$serviceTraces = array_filter($traces, function($trace) use ($serviceName) {
return $this->isServiceTrace($trace, $serviceName);
});
$analysis = [
'service_name' => $serviceName,
'trace_count' => count($serviceTraces),
'performance_trend' => [],
'common_bottlenecks' => [],
'service_health' => 'unknown',
'sla_compliance' => 0,
'error_rate' => 0,
'average_response_time' => 0,
'recommendations' => []
];
if (empty($serviceTraces)) {
return $analysis;
}
// Calculate service metrics
$serviceMetrics = $this->calculateServiceMetrics($serviceTraces);
$analysis = array_merge($analysis, $serviceMetrics);
// Analyze performance trend
$analysis['performance_trend'] = $this->analyzePerformanceTrend($serviceTraces);
// Find common bottlenecks
$analysis['common_bottlenecks'] = $this->findCommonBottlenecks($serviceTraces);
// Assess service health
$analysis['service_health'] = $this->assessServiceHealth($analysis);
// Calculate SLA compliance
$analysis['sla_compliance'] = $this->calculateSlaCompliance($serviceTraces);
// Generate service-specific recommendations
$analysis['recommendations'] = $this->generateServiceRecommendations($analysis);
return $analysis;
}
/**
* Analyze endpoint performance.
*/
public function analyzeEndpoint(array $traces, string $endpoint): array
{
$endpointTraces = array_filter($traces, function($trace) use ($endpoint) {
return $this->isEndpointTrace($trace, $endpoint);
});
$analysis = [
'endpoint' => $endpoint,
'trace_count' => count($endpointTraces),
'performance_metrics' => [],
'error_analysis' => [],
'performance_distribution' => [],
'optimization_opportunities' => []
];
if (empty($endpointTraces)) {
return $analysis;
}
// Calculate endpoint metrics
$analysis['performance_metrics'] = $this->calculateEndpointMetrics($endpointTraces);
// Analyze errors
$analysis['error_analysis'] = $this->analyzeEndpointErrors($endpointTraces);
// Analyze performance distribution
$analysis['performance_distribution'] = $this->analyzePerformanceDistribution($endpointTraces);
// Find optimization opportunities
$analysis['optimization_opportunities'] = $this->findOptimizationOpportunities($endpointTraces);
return $analysis;
}
/**
* Compare performance between time periods.
*/
public function comparePerformance(array $baselineTraces, array $comparisonTraces): array
{
$baselineMetrics = $this->calculateAggregatedMetrics($baselineTraces);
$comparisonMetrics = $this->calculateAggregatedMetrics($comparisonTraces);
$comparison = [
'baseline_period' => [
'trace_count' => count($baselineTraces),
'metrics' => $baselineMetrics
],
'comparison_period' => [
'trace_count' => count($comparisonTraces),
'metrics' => $comparisonMetrics
],
'performance_changes' => [],
'significant_changes' => [],
'trend_analysis' => []
];
// Calculate performance changes
$comparison['performance_changes'] = $this->calculatePerformanceChanges(
$baselineMetrics,
$comparisonMetrics
);
// Identify significant changes
$comparison['significant_changes'] = $this->identifySignificantChanges(
$comparison['performance_changes']
);
// Analyze trends
$comparison['trend_analysis'] = $this->analyzeTrends($comparison['performance_changes']);
return $comparison;
}
/**
* Detect performance regressions.
*/
public function detectRegressions(array $currentTraces, array $historicalTraces): array
{
$currentMetrics = $this->calculateAggregatedMetrics($currentTraces);
$historicalMetrics = $this->calculateAggregatedMetrics($historicalTraces);
$regressions = [
'detected_regressions' => [],
'severity_levels' => [],
'affected_components' => [],
'recommendations' => []
];
// Check for regressions in key metrics
$keyMetrics = ['average_duration', 'error_rate', 'throughput'];
foreach ($keyMetrics as $metric) {
if (isset($currentMetrics[$metric]) && isset($historicalMetrics[$metric])) {
$change = ($currentMetrics[$metric] - $historicalMetrics[$metric]) / $historicalMetrics[$metric];
// Define regression thresholds
$thresholds = $this->config['regression_thresholds'][$metric] ?? [
'warning' => 0.1, // 10% increase
'critical' => 0.2 // 20% increase
];
if ($change > $thresholds['critical']) {
$severity = 'critical';
} elseif ($change > $thresholds['warning']) {
$severity = 'warning';
} else {
continue;
}
$regression = [
'metric' => $metric,
'current_value' => $currentMetrics[$metric],
'historical_value' => $historicalMetrics[$metric],
'change_percentage' => $change * 100,
'severity' => $severity,
'description' => $this->generateRegressionDescription($metric, $change)
];
$regressions['detected_regressions'][] = $regression;
$regressions['severity_levels'][$severity] = ($regressions['severity_levels'][$severity] ?? 0) + 1;
}
}
// Analyze affected components
$regressions['affected_components'] = $this->analyzeAffectedComponents($currentTraces, $regressions['detected_regressions']);
// Generate recommendations
$regressions['recommendations'] = $this->generateRegressionRecommendations($regressions);
return $regressions;
}
/**
* Generate performance report.
*/
public function generatePerformanceReport(array $traces, array $options = []): array
{
$report = [
'generated_at' => date('Y-m-d H:i:s'),
'period' => $options['period'] ?? 'last_24_hours',
'summary' => [],
'service_analysis' => [],
'endpoint_analysis' => [],
'bottleneck_analysis' => [],
'recommendations' => [],
'trends' => []
];
// Generate summary
$report['summary'] = $this->generateReportSummary($traces);
// Analyze services
$services = $this->extractServices($traces);
foreach ($services as $service) {
$report['service_analysis'][$service] = $this->analyzeService($traces, $service);
}
// Analyze endpoints
$endpoints = $this->extractEndpoints($traces);
foreach ($endpoints as $endpoint) {
$report['endpoint_analysis'][$endpoint] = $this->analyzeEndpoint($traces, $endpoint);
}
// Analyze bottlenecks
$report['bottleneck_analysis'] = $this->analyzeBottlenecks($traces);
// Generate overall recommendations
$report['recommendations'] = $this->generateOverallRecommendations($report);
// Analyze trends
$report['trends'] = $this->analyzeTrends($traces);
return $report;
}
/**
* Get performance insights.
*/
public function getInsights(array $traces): array
{
$insights = [
'key_findings' => [],
'performance_issues' => [],
'optimization_opportunities' => [],
'risk_factors' => [],
'success_factors' => []
];
// Analyze all traces
foreach ($traces as $trace) {
$analysis = $this->analyzeTrace($trace);
// Extract key findings
if ($analysis['performance_score'] < 70) {
$insights['performance_issues'][] = [
'trace_id' => $analysis['trace_id'],
'score' => $analysis['performance_score'],
'main_issues' => array_column($analysis['bottlenecks'], 'type')
];
}
// Extract optimization opportunities
foreach ($analysis['recommendations'] as $recommendation) {
if ($recommendation['priority'] === 'high') {
$insights['optimization_opportunities'][] = $recommendation;
}
}
}
// Identify risk factors
$insights['risk_factors'] = $this->identifyRiskFactors($traces);
// Identify success factors
$insights['success_factors'] = $this->identifySuccessFactors($traces);
return $insights;
}
/**
* Calculate total duration of trace.
*/
protected function calculateTotalDuration(array $spans): float
{
$startTime = null;
$endTime = null;
foreach ($spans as $span) {
if ($startTime === null || $span->getStartTime() < $startTime) {
$startTime = $span->getStartTime();
}
if ($endTime === null || $span->getEndTime() > $endTime) {
$endTime = $span->getEndTime();
}
}
return $endTime && $startTime ? $endTime - $startTime : 0;
}
/**
* Find critical path in trace.
*/
protected function findCriticalPath(array $spans): array
{
// Build span tree
$spanTree = $this->buildSpanTree($spans);
// Find longest path
$criticalPath = [];
$maxDuration = 0;
foreach ($spanTree as $spanId => $children) {
$path = $this->findLongestPath($spanId, $spanTree, $spans);
$duration = $this->calculatePathDuration($path, $spans);
if ($duration > $maxDuration) {
$maxDuration = $duration;
$criticalPath = $path;
}
}
return $criticalPath;
}
/**
* Identify bottlenecks in trace.
*/
protected function identifyBottlenecks(array $spans): array
{
$bottlenecks = [];
$thresholds = $this->config['bottleneck_thresholds'] ?? [
'duration' => 1000, // 1 second
'error_rate' => 0.05, // 5%
'cpu_usage' => 80, // 80%
'memory_usage' => 90 // 90%
];
foreach ($spans as $span) {
$duration = $span->getDuration() * 1000; // Convert to milliseconds
$tags = $span->getTags();
$status = $span->getStatus();
// Check duration bottleneck
if ($duration > $thresholds['duration']) {
$bottlenecks[] = [
'type' => 'duration',
'span_id' => $span->getContext()->getSpanId(),
'operation' => $span->getOperationName(),
'duration' => $duration,
'threshold' => $thresholds['duration'],
'severity' => $this->calculateSeverity($duration, $thresholds['duration'])
];
}
// Check error bottleneck
if (isset($status['error']) && $status['error']) {
$bottlenecks[] = [
'type' => 'error',
'span_id' => $span->getContext()->getSpanId(),
'operation' => $span->getOperationName(),
'error' => $status['message'] ?? 'Unknown error',
'severity' => 'high'
];
}
// Check resource usage bottlenecks
if (isset($tags['cpu_usage']) && $tags['cpu_usage'] > $thresholds['cpu_usage']) {
$bottlenecks[] = [
'type' => 'cpu_usage',
'span_id' => $span->getContext()->getSpanId(),
'operation' => $span->getOperationName(),
'value' => $tags['cpu_usage'],
'threshold' => $thresholds['cpu_usage'],
'severity' => $this->calculateSeverity($tags['cpu_usage'], $thresholds['cpu_usage'])
];
}
if (isset($tags['memory_usage']) && $tags['memory_usage'] > $thresholds['memory_usage']) {
$bottlenecks[] = [
'type' => 'memory_usage',
'span_id' => $span->getContext()->getSpanId(),
'operation' => $span->getOperationName(),
'value' => $tags['memory_usage'],
'threshold' => $thresholds['memory_usage'],
'severity' => $this->calculateSeverity($tags['memory_usage'], $thresholds['memory_usage'])
];
}
}
// Sort by severity
usort($bottlenecks, function($a, $b) {
$severityOrder = ['critical' => 4, 'high' => 3, 'medium' => 2, 'low' => 1];
return $severityOrder[$b['severity']] - $severityOrder[$a['severity']];
});
return $bottlenecks;
}
/**
* Calculate performance score.
*/
protected function calculatePerformanceScore(array $analysis): float
{
$score = 100;
// Deduct points for bottlenecks
foreach ($analysis['bottlenecks'] as $bottleneck) {
switch ($bottleneck['severity']) {
case 'critical':
$score -= 20;
break;
case 'high':
$score -= 10;
break;
case 'medium':
$score -= 5;
break;
case 'low':
$score -= 2;
break;
}
}
// Deduct points for anomalies
foreach ($analysis['anomalies'] as $anomaly) {
$score -= 5;
}
// Bonus for good patterns
foreach ($analysis['patterns'] as $pattern) {
if ($pattern['type'] === 'efficient') {
$score += 2;
}
}
return max(0, min(100, $score));
}
/**
* Generate recommendations based on analysis.
*/
protected function generateRecommendations(array $analysis): array
{
$recommendations = [];
// Generate recommendations for bottlenecks
foreach ($analysis['bottlenecks'] as $bottleneck) {
$recommendation = $this->generateBottleneckRecommendation($bottleneck);
if ($recommendation) {
$recommendations[] = $recommendation;
}
}
// Generate recommendations for patterns
foreach ($analysis['patterns'] as $pattern) {
if ($pattern['type'] === 'inefficient') {
$recommendations[] = [
'type' => 'pattern_optimization',
'priority' => 'medium',
'description' => "Optimize inefficient pattern: {$pattern['description']}",
'action' => $pattern['recommendation'] ?? 'Review and optimize the identified pattern'
];
}
}
// Generate recommendations for anomalies
foreach ($analysis['anomalies'] as $anomaly) {
$recommendations[] = [
'type' => 'anomaly_investigation',
'priority' => 'high',
'description' => "Investigate anomaly: {$anomaly['description']}",
'action' => 'Analyze the root cause of the detected anomaly'
];
}
// Remove duplicates and sort by priority
$recommendations = $this->deduplicateRecommendations($recommendations);
usort($recommendations, function($a, $b) {
$priorityOrder = ['high' => 3, 'medium' => 2, 'low' => 1];
return $priorityOrder[$b['priority']] - $priorityOrder[$a['priority']];
});
return array_slice($recommendations, 0, 10); // Limit to top 10 recommendations
}
/**
* Build span tree from spans.
*/
protected function buildSpanTree(array $spans): array
{
$tree = [];
foreach ($spans as $span) {
$spanId = $span->getContext()->getSpanId();
$parentContext = $span->getContext()->getParent();
if ($parentContext) {
$parentId = $parentContext->getSpanId();
$tree[$parentId][] = $spanId;
} else {
$tree[$spanId] = $tree[$spanId] ?? [];
}
}
return $tree;
}
/**
* Find longest path in span tree.
*/
protected function findLongestPath(string $spanId, array $tree, array $spans): array
{
$path = [$spanId];
if (isset($tree[$spanId])) {
$longestChildPath = [];
$maxDuration = 0;
foreach ($tree[$spanId] as $childId) {
$childPath = $this->findLongestPath($childId, $tree, $spans);
$duration = $this->calculatePathDuration($childPath, $spans);
if ($duration > $maxDuration) {
$maxDuration = $duration;
$longestChildPath = $childPath;
}
}
$path = array_merge($path, $longestChildPath);
}
return $path;
}
/**
* Calculate path duration.
*/
protected function calculatePathDuration(array $path, array $spans): float
{
$duration = 0;
foreach ($path as $spanId) {
foreach ($spans as $span) {
if ($span->getContext()->getSpanId() === $spanId) {
$duration += $span->getDuration();
break;
}
}
}
return $duration;
}
/**
* Calculate severity based on value and threshold.
*/
protected function calculateSeverity(float $value, float $threshold): string
{
$ratio = $value / $threshold;
if ($ratio >= 3) {
return 'critical';
} elseif ($ratio >= 2) {
return 'high';
} elseif ($ratio >= 1.5) {
return 'medium';
} else {
return 'low';
}
}
/**
* Generate bottleneck recommendation.
*/
protected function generateBottleneckRecommendation(array $bottleneck): ?array
{
$recommendations = [
'duration' => [
'description' => "Operation '{$bottleneck['operation']}' is taking too long",
'action' => 'Optimize the operation logic, add caching, or consider async processing'
],
'error' => [
'description' => "Operation '{$bottleneck['operation']}' is failing",
'action' => 'Investigate and fix the root cause of the error'
],
'cpu_usage' => [
'description' => "High CPU usage in '{$bottleneck['operation']}'",
'action' => 'Optimize algorithms, reduce computational complexity, or scale horizontally'
],
'memory_usage' => [
'description' => "High memory usage in '{$bottleneck['operation']}'",
'action' => 'Optimize memory usage, implement memory pooling, or increase available memory'
]
];
$type = $bottleneck['type'];
if (!isset($recommendations[$type])) {
return null;
}
return [
'type' => $type,
'priority' => $bottleneck['severity'] === 'critical' ? 'high' : 'medium',
'description' => $recommendations[$type]['description'],
'action' => $recommendations[$type]['action'],
'span_id' => $bottleneck['span_id'],
'operation' => $bottleneck['operation']
];
}
/**
* Remove duplicate recommendations.
*/
protected function deduplicateRecommendations(array $recommendations): array
{
$unique = [];
$seen = [];
foreach ($recommendations as $recommendation) {
$key = $recommendation['type'] . ':' . $recommendation['description'];
if (!isset($seen[$key])) {
$unique[] = $recommendation;
$seen[$key] = true;
}
}
return $unique;
}
/**
* Extract trace ID from spans.
*/
protected function extractTraceId(array $spans): string
{
if (!empty($spans)) {
return $spans[0]->getContext()->getTraceId();
}
return 'unknown';
}
/**
* Check if trace belongs to service.
*/
protected function isServiceTrace(array $trace, string $serviceName): bool
{
foreach ($trace as $span) {
$tags = $span->getTags();
if (isset($tags['service.name']) && $tags['service.name'] === $serviceName) {
return true;
}
}
return false;
}
/**
* Check if trace belongs to endpoint.
*/
protected function isEndpointTrace(array $trace, string $endpoint): bool
{
foreach ($trace as $span) {
$tags = $span->getTags();
if (isset($tags['http.target']) && $tags['http.target'] === $endpoint) {
return true;
}
}
return false;
}
/**
* Limit cache size.
*/
protected function limitCacheSize(): void
{
$maxSize = $this->config['cache_size'] ?? 1000;
if (count($this->analysisCache) > $maxSize) {
$this->analysisCache = array_slice($this->analysisCache, -$maxSize, null, true);
}
}
/**
* Get default configuration.
*/
protected function getDefaultConfig(): array
{
return [
'bottleneck_thresholds' => [
'duration' => 1000, // 1 second
'error_rate' => 0.05, // 5%
'cpu_usage' => 80, // 80%
'memory_usage' => 90 // 90%
],
'regression_thresholds' => [
'average_duration' => ['warning' => 0.1, 'critical' => 0.2],
'error_rate' => ['warning' => 0.05, 'critical' => 0.1],
'throughput' => ['warning' => -0.1, 'critical' => -0.2]
],
'cache_size' => 1000,
'metrics' => [],
'patterns' => [],
'anomaly_detection' => []
];
}
/**
* Get configuration.
*/
public function getConfig(): array
{
return $this->config;
}
/**
* Set configuration.
*/
public function setConfig(array $config): void
{
$this->config = array_merge($this->config, $config);
}
/**
* Create performance analyzer instance.
*/
public static function create(array $config = []): self
{
return new self($config);
}
/**
* Create for development.
*/
public static function forDevelopment(): self
{
return new self([
'bottleneck_thresholds' => [
'duration' => 500, // 500ms for development
'error_rate' => 0.1 // 10% for development
]
]);
}
/**
* Create for production.
*/
public static function forProduction(): self
{
return new self([
'bottleneck_thresholds' => [
'duration' => 2000, // 2 seconds for production
'error_rate' => 0.01 // 1% for production
]
]);
}
}

View File

@@ -0,0 +1,597 @@
<?php
declare(strict_types=1);
namespace Fendx\Service\Tracing;
use Fendx\Service\Tracing\Span\Span;
use Fendx\Service\Tracing\Span\SpanContext;
use Fendx\Service\Tracing\Sampler\Sampler;
use Fendx\Service\Tracing\Exporter\TraceExporter;
use Fendx\Service\Tracing\Propagator\TracePropagator;
class DistributedTracer
{
protected static ?DistributedTracer $instance = null;
protected array $config = [];
protected Sampler $sampler;
protected TraceExporter $exporter;
protected TracePropagator $propagator;
protected array $spans = [];
protected array $activeSpans = [];
protected array $traceContexts = [];
protected bool $enabled = true;
public function __construct(array $config = [])
{
$this->config = array_merge($this->getDefaultConfig(), $config);
$this->sampler = new Sampler($this->config['sampling'] ?? []);
$this->exporter = new TraceExporter($this->config['exporter'] ?? []);
$this->propagator = new TracePropagator($this->config['propagation'] ?? []);
$this->initialize();
}
/**
* Get singleton instance.
*/
public static function getInstance(array $config = []): self
{
if (self::$instance === null) {
self::$instance = new self($config);
}
return self::$instance;
}
/**
* Start a new span.
*/
public function startSpan(string $operationName, array $options = []): Span
{
if (!$this->enabled) {
return new Span($operationName, new SpanContext(), $options);
}
$parentContext = $options['parent'] ?? $this->getCurrentContext();
$traceId = $parentContext ? $parentContext->getTraceId() : $this->generateTraceId();
$spanId = $this->generateSpanId();
$context = new SpanContext($traceId, $spanId, $parentContext);
// Check sampling
if (!$parentContext) {
$sampled = $this->sampler->shouldSample($traceId, $operationName, $options);
$context->setSampled($sampled);
} else {
$context->setSampled($parentContext->isSampled());
}
$span = new Span($operationName, $context, $options);
// Set start time
$span->setStartTime(microtime(true));
// Add to active spans
$this->activeSpans[$spanId] = $span;
// Set as current span
$this->traceContexts[$traceId]['current_span'] = $spanId;
// Add to spans collection
$this->spans[$spanId] = $span;
$this->logDebug("Started span: {$operationName} (trace: {$traceId}, span: {$spanId})");
return $span;
}
/**
* Finish a span.
*/
public function finishSpan(Span $span, array $options = []): void
{
if (!$this->enabled) {
return;
}
$spanId = $span->getContext()->getSpanId();
if (!isset($this->activeSpans[$spanId])) {
return;
}
// Set end time
$span->setEndTime(microtime(true));
// Calculate duration
$duration = $span->getEndTime() - $span->getStartTime();
$span->setDuration($duration);
// Remove from active spans
unset($this->activeSpans[$spanId]);
// Update current span if this was the current one
$traceId = $span->getContext()->getTraceId();
if (isset($this->traceContexts[$traceId]['current_span']) &&
$this->traceContexts[$traceId]['current_span'] === $spanId) {
// Set parent as current span if available
$parentContext = $span->getContext()->getParent();
if ($parentContext) {
$this->traceContexts[$traceId]['current_span'] = $parentContext->getSpanId();
} else {
unset($this->traceContexts[$traceId]['current_span']);
}
}
// Export if configured
if ($span->getContext()->isSampled()) {
$this->exporter->export($span);
}
$this->logDebug("Finished span: {$span->getOperationName()} (duration: " .
number_format($duration * 1000, 2) . "ms)");
}
/**
* Get current span.
*/
public function getCurrentSpan(): ?Span
{
$currentContext = $this->getCurrentContext();
if (!$currentContext) {
return null;
}
$spanId = $currentContext->getSpanId();
return $this->spans[$spanId] ?? null;
}
/**
* Get current span context.
*/
public function getCurrentContext(): ?SpanContext
{
foreach ($this->traceContexts as $traceId => $context) {
if (isset($context['current_span'])) {
$spanId = $context['current_span'];
if (isset($this->spans[$spanId])) {
return $this->spans[$spanId]->getContext();
}
}
}
return null;
}
/**
* Execute function with tracing.
*/
public function trace(string $operationName, callable $callback, array $options = [])
{
$span = $this->startSpan($operationName, $options);
try {
$result = $callback($span);
// Set success status
$span->setStatus(['code' => 0, 'message' => 'OK']);
return $result;
} catch (\Exception $e) {
// Set error status
$span->setStatus([
'code' => 1,
'message' => $e->getMessage(),
'error' => true
]);
// Add error tags
$span->setTag('error', true);
$span->setTag('error.message', $e->getMessage());
$span->setTag('error.type', get_class($e));
throw $e;
} finally {
$this->finishSpan($span);
}
}
/**
* Inject trace context into headers.
*/
public function inject(SpanContext $context, array &$headers): void
{
$this->propagator->inject($context, $headers);
}
/**
* Extract trace context from headers.
*/
public function extract(array $headers): ?SpanContext
{
return $this->propagator->extract($headers);
}
/**
* Create child span from headers.
*/
public function startSpanFromHeaders(string $operationName, array $headers, array $options = []): Span
{
$parentContext = $this->extract($headers);
if ($parentContext) {
$options['parent'] = $parentContext;
}
return $this->startSpan($operationName, $options);
}
/**
* Add global tags.
*/
public function addGlobalTag(string $key, $value): void
{
$this->config['global_tags'][$key] = $value;
}
/**
* Get global tags.
*/
public function getGlobalTags(): array
{
return $this->config['global_tags'] ?? [];
}
/**
* Get span by ID.
*/
public function getSpan(string $spanId): ?Span
{
return $this->spans[$spanId] ?? null;
}
/**
* Get all spans for a trace.
*/
public function getTraceSpans(string $traceId): array
{
$traceSpans = [];
foreach ($this->spans as $spanId => $span) {
if ($span->getContext()->getTraceId() === $traceId) {
$traceSpans[$spanId] = $span;
}
}
return $traceSpans;
}
/**
* Get active spans.
*/
public function getActiveSpans(): array
{
return $this->activeSpans;
}
/**
* Get all spans.
*/
public function getAllSpans(): array
{
return $this->spans;
}
/**
* Clear finished spans.
*/
public function clearFinishedSpans(): int
{
$cleared = 0;
foreach ($this->spans as $spanId => $span) {
if ($span->isFinished()) {
unset($this->spans[$spanId]);
$cleared++;
}
}
// Clean up empty trace contexts
foreach ($this->traceContexts as $traceId => $context) {
$hasSpans = false;
foreach ($this->spans as $span) {
if ($span->getContext()->getTraceId() === $traceId) {
$hasSpans = true;
break;
}
}
if (!$hasSpans) {
unset($this->traceContexts[$traceId]);
}
}
$this->logDebug("Cleared {$cleared} finished spans");
return $cleared;
}
/**
* Flush all spans.
*/
public function flush(): void
{
foreach ($this->spans as $span) {
if ($span->getContext()->isSampled() && !$span->isFinished()) {
$this->finishSpan($span);
}
}
$this->exporter->flush();
$this->logDebug("Flushed all spans");
}
/**
* Get tracer statistics.
*/
public function getStatistics(): array
{
$totalSpans = count($this->spans);
$activeSpans = count($this->activeSpans);
$finishedSpans = $totalSpans - $activeSpans;
$sampledSpans = 0;
foreach ($this->spans as $span) {
if ($span->getContext()->isSampled()) {
$sampledSpans++;
}
}
return [
'enabled' => $this->enabled,
'total_spans' => $totalSpans,
'active_spans' => $activeSpans,
'finished_spans' => $finishedSpans,
'sampled_spans' => $sampledSpans,
'sampling_rate' => $totalSpans > 0 ? ($sampledSpans / $totalSpans) * 100 : 0,
'trace_contexts' => count($this->traceContexts),
'exporter_stats' => $this->exporter->getStatistics(),
'sampler_stats' => $this->sampler->getStatistics()
];
}
/**
* Enable/disable tracer.
*/
public function setEnabled(bool $enabled): void
{
$this->enabled = $enabled;
$this->logInfo("Tracer " . ($enabled ? 'enabled' : 'disabled'));
}
/**
* Check if tracer is enabled.
*/
public function isEnabled(): bool
{
return $this->enabled;
}
/**
* Set service name.
*/
public function setServiceName(string $serviceName): void
{
$this->config['service_name'] = $serviceName;
$this->addGlobalTag('service.name', $serviceName);
}
/**
* Get service name.
*/
public function getServiceName(): string
{
return $this->config['service_name'] ?? 'unknown';
}
/**
* Set service version.
*/
public function setServiceVersion(string $version): void
{
$this->config['service_version'] = $version;
$this->addGlobalTag('service.version', $version);
}
/**
* Get service version.
*/
public function getServiceVersion(): string
{
return $this->config['service_version'] ?? 'unknown';
}
/**
* Add baggage item to context.
*/
public function setBaggageItem(string $key, string $value): void
{
$currentContext = $this->getCurrentContext();
if ($currentContext) {
$currentContext->setBaggageItem($key, $value);
}
}
/**
* Get baggage item from context.
*/
public function getBaggageItem(string $key): ?string
{
$currentContext = $this->getCurrentContext();
return $currentContext ? $currentContext->getBaggageItem($key) : null;
}
/**
* Initialize tracer.
*/
protected function initialize(): void
{
// Set default service info
if (!isset($this->config['service_name'])) {
$this->setServiceName($this->config['service_name'] ?? 'fendx-app');
}
if (!isset($this->config['service_version'])) {
$this->setServiceVersion($this->config['service_version'] ?? '1.0.0');
}
// Add default global tags
$this->addGlobalTag('hostname', gethostname());
$this->addGlobalTag('php.version', PHP_VERSION);
// Initialize components
$this->sampler->initialize();
$this->exporter->initialize();
$this->propagator->initialize();
$this->logInfo("Distributed tracer initialized");
}
/**
* Generate trace ID.
*/
protected function generateTraceId(): string
{
return uniqid('trace_', true) . '_' . bin2hex(random_bytes(8));
}
/**
* Generate span ID.
*/
protected function generateSpanId(): string
{
return uniqid('span_', true) . '_' . bin2hex(random_bytes(4));
}
/**
* Log debug message.
*/
protected function logDebug(string $message): void
{
if ($this->config['debug_enabled']) {
error_log("[DistributedTracer] DEBUG: {$message}");
}
}
/**
* Log info message.
*/
protected function logInfo(string $message): void
{
if ($this->config['logging_enabled']) {
error_log("[DistributedTracer] {$message}");
}
}
/**
* Get default configuration.
*/
protected function getDefaultConfig(): array
{
return [
'service_name' => 'fendx-app',
'service_version' => '1.0.0',
'enabled' => true,
'debug_enabled' => false,
'logging_enabled' => true,
'global_tags' => [],
'sampling' => [
'type' => 'probabilistic',
'param' => 0.1, // 10% sampling rate
'max_traces_per_second' => 100
],
'exporter' => [
'type' => 'jaeger',
'endpoint' => 'http://localhost:14268/api/traces',
'batch_size' => 100,
'flush_interval' => 5
],
'propagation' => [
'format' => 'trace-context'
]
];
}
/**
* Get configuration.
*/
public function getConfig(): array
{
return $this->config;
}
/**
* Set configuration.
*/
public function setConfig(array $config): void
{
$this->config = array_merge($this->config, $config);
}
/**
* Reset singleton instance.
*/
public static function reset(): void
{
self::$instance = null;
}
/**
* Create tracer instance.
*/
public static function create(array $config = []): self
{
return new self($config);
}
/**
* Create for development.
*/
public static function forDevelopment(): self
{
return new self([
'service_name' => 'fendx-app-dev',
'debug_enabled' => true,
'sampling' => [
'type' => 'always_on'
],
'exporter' => [
'type' => 'console'
]
]);
}
/**
* Create for production.
*/
public static function forProduction(): self
{
return new self([
'service_name' => 'fendx-app',
'debug_enabled' => false,
'sampling' => [
'type' => 'probabilistic',
'param' => 0.01 // 1% sampling rate
],
'exporter' => [
'type' => 'jaeger',
'endpoint' => getenv('JAEGER_ENDPOINT') ?: 'http://jaeger:14268/api/traces'
]
]);
}
}

View File

@@ -0,0 +1,835 @@
<?php
declare(strict_types=1);
namespace Fendx\Service\Tracing\Service;
use Fendx\Service\Tracing\DistributedTracer;
use Fendx\Service\Tracing\Span\Span;
use Fendx\Service\Tracing\Span\SpanContext;
use Fendx\Service\Tracing\Service\Client\HttpClient;
use Fendx\Service\Tracing\Service\Client\RpcClient;
use Fendx\Service\Tracing\Service\Client\QueueClient;
use Fendx\Service\Tracing\Service\Interceptor\CallInterceptor;
class ServiceTracer
{
protected DistributedTracer $tracer;
protected array $config = [];
protected array $clients = [];
protected array $interceptors = [];
protected array $serviceMetrics = [];
public function __construct(DistributedTracer $tracer, array $config = [])
{
$this->tracer = $tracer;
$this->config = array_merge($this->getDefaultConfig(), $config);
$this->initialize();
}
/**
* Trace HTTP request.
*/
public function traceHttpRequest(string $method, string $url, array $headers = [], $body = null): array
{
$startTime = microtime(true);
// Extract incoming context
$parentContext = $this->tracer->extract($headers);
// Start span for HTTP request
$span = $this->tracer->startSpan('http.request', [
'parent' => $parentContext,
'tags' => [
'http.method' => $method,
'http.url' => $url,
'component' => 'http-client'
]
]);
try {
// Inject trace context into headers
$this->tracer->inject($span->getContext(), $headers);
// Add request tags
$span->setTag('http.method', $method);
$span->setTag('http.url', $url);
$span->setTag('http.scheme', parse_url($url, PHP_URL_SCHEME) ?? 'http');
$span->setTag('http.host', parse_url($url, PHP_URL_HOST) ?? 'unknown');
$span->setTag('http.target', parse_url($url, PHP_URL_PATH) ?? '/');
if (!empty($body)) {
$span->setTag('http.request_content_length', strlen($body));
}
// Execute HTTP request
$client = $this->getHttpClient();
$response = $client->request($method, $url, $headers, $body);
// Add response tags
$span->setTag('http.status_code', $response['status_code']);
$span->setTag('http.response_content_length', $response['content_length'] ?? 0);
// Set status based on response code
if ($response['status_code'] >= 400) {
$span->setStatus([
'code' => 1,
'message' => 'HTTP Error: ' . $response['status_code'],
'error' => true
]);
$span->setTag('error', true);
} else {
$span->setStatus(['code' => 0, 'message' => 'OK']);
}
// Record metrics
$this->recordHttpMetrics($method, $url, $response['status_code'], $startTime);
return $response;
} catch (\Exception $e) {
// Set error status
$span->setStatus([
'code' => 1,
'message' => $e->getMessage(),
'error' => true
]);
$span->setTag('error', true);
$span->setTag('error.message', $e->getMessage());
$span->setTag('error.type', get_class($e));
// Record error metrics
$this->recordErrorMetrics('http', $e);
throw $e;
} finally {
$this->tracer->finishSpan($span);
}
}
/**
* Trace RPC call.
*/
public function traceRpcCall(string $service, string $method, array $params = [], array $metadata = []): mixed
{
$startTime = microtime(true);
// Extract incoming context
$parentContext = $this->tracer->extract($metadata);
// Start span for RPC call
$span = $this->tracer->startSpan('rpc.call', [
'parent' => $parentContext,
'tags' => [
'rpc.service' => $service,
'rpc.method' => $method,
'component' => 'rpc-client'
]
]);
try {
// Inject trace context into metadata
$this->tracer->inject($span->getContext(), $metadata);
// Add RPC tags
$span->setTag('rpc.service', $service);
$span->setTag('rpc.method', $method);
$span->setTag('rpc.system', $this->config['rpc_system'] ?? 'grpc');
// Execute RPC call
$client = $this->getRpcClient();
$result = $client->call($service, $method, $params, $metadata);
// Set success status
$span->setStatus(['code' => 0, 'message' => 'OK']);
// Record metrics
$this->recordRpcMetrics($service, $method, $startTime);
return $result;
} catch (\Exception $e) {
// Set error status
$span->setStatus([
'code' => 1,
'message' => $e->getMessage(),
'error' => true
]);
$span->setTag('error', true);
$span->setTag('error.message', $e->getMessage());
$span->setTag('error.type', get_class($e));
// Record error metrics
$this->recordErrorMetrics('rpc', $e);
throw $e;
} finally {
$this->tracer->finishSpan($span);
}
}
/**
* Trace database query.
*/
public function traceDatabaseQuery(string $query, array $params = [], string $database = 'default'): mixed
{
$startTime = microtime(true);
// Start span for database query
$span = $this->tracer->startSpan('db.query', [
'tags' => [
'db.system' => $this->config['db_system'] ?? 'mysql',
'db.name' => $database,
'db.statement' => $this->sanitizeQuery($query),
'component' => 'database'
]
]);
try {
// Add database tags
$span->setTag('db.system', $this->config['db_system'] ?? 'mysql');
$span->setTag('db.name', $database);
$span->setTag('db.statement', $this->sanitizeQuery($query));
$span->setTag('db.user', $this->config['db_user'] ?? 'app');
// Execute query
$result = $this->executeQuery($query, $params, $database);
// Set success status
$span->setStatus(['code' => 0, 'message' => 'OK']);
// Add result tags
if (isset($result['row_count'])) {
$span->setTag('db.rows_affected', $result['row_count']);
}
// Record metrics
$this->recordDatabaseMetrics($database, $query, $startTime);
return $result;
} catch (\Exception $e) {
// Set error status
$span->setStatus([
'code' => 1,
'message' => $e->getMessage(),
'error' => true
]);
$span->setTag('error', true);
$span->setTag('error.message', $e->getMessage());
$span->setTag('error.type', get_class($e));
// Record error metrics
$this->recordErrorMetrics('database', $e);
throw $e;
} finally {
$this->tracer->finishSpan($span);
}
}
/**
* Trace cache operation.
*/
public function traceCacheOperation(string $operation, string $key, $value = null, string $cache = 'default'): mixed
{
$startTime = microtime(true);
// Start span for cache operation
$span = $this->tracer->startSpan('cache.operation', [
'tags' => [
'cache.system' => $this->config['cache_system'] ?? 'redis',
'cache.name' => $cache,
'cache.operation' => $operation,
'cache.key' => $key,
'component' => 'cache'
]
]);
try {
// Add cache tags
$span->setTag('cache.system', $this->config['cache_system'] ?? 'redis');
$span->setTag('cache.name', $cache);
$span->setTag('cache.operation', $operation);
$span->setTag('cache.key', $key);
// Execute cache operation
$result = $this->executeCacheOperation($operation, $key, $value, $cache);
// Set success status
$span->setStatus(['code' => 0, 'message' => 'OK']);
// Add result tags
$span->setTag('cache.hit', $result !== null);
// Record metrics
$this->recordCacheMetrics($cache, $operation, $result !== null, $startTime);
return $result;
} catch (\Exception $e) {
// Set error status
$span->setStatus([
'code' => 1,
'message' => $e->getMessage(),
'error' => true
]);
$span->setTag('error', true);
$span->setTag('error.message', $e->getMessage());
$span->setTag('error.type', get_class($e));
// Record error metrics
$this->recordErrorMetrics('cache', $e);
throw $e;
} finally {
$this->tracer->finishSpan($span);
}
}
/**
* Trace message queue operation.
*/
public function traceMessageQueue(string $operation, string $topic, array $message = [], string $queue = 'default'): mixed
{
$startTime = microtime(true);
// Start span for queue operation
$span = $this->tracer->startSpan('queue.operation', [
'tags' => [
'messaging.system' => $this->config['queue_system'] ?? 'rabbitmq',
'messaging.destination' => $topic,
'messaging.operation' => $operation,
'messaging.message_id' => $message['id'] ?? uniqid(),
'component' => 'message-queue'
]
]);
try {
// Add queue tags
$span->setTag('messaging.system', $this->config['queue_system'] ?? 'rabbitmq');
$span->setTag('messaging.destination', $topic);
$span->setTag('messaging.operation', $operation);
// Inject trace context into message
$this->tracer->inject($span->getContext(), $message['headers'] ?? []);
// Execute queue operation
$client = $this->getQueueClient();
$result = $client->$operation($topic, $message, $queue);
// Set success status
$span->setStatus(['code' => 0, 'message' => 'OK']);
// Record metrics
$this->recordQueueMetrics($queue, $operation, $startTime);
return $result;
} catch (\Exception $e) {
// Set error status
$span->setStatus([
'code' => 1,
'message' => $e->getMessage(),
'error' => true
]);
$span->setTag('error', true);
$span->setTag('error.message', $e->getMessage());
$span->setTag('error.type', get_class($e));
// Record error metrics
$this->recordErrorMetrics('queue', $e);
throw $e;
} finally {
$this->tracer->finishSpan($span);
}
}
/**
* Trace external service call.
*/
public function traceExternalService(string $serviceName, string $operation, array $params = []): mixed
{
$startTime = microtime(true);
// Start span for external service call
$span = $this->tracer->startSpan('external.service', [
'tags' => [
'external.service' => $serviceName,
'external.operation' => $operation,
'component' => 'external-client'
]
]);
try {
// Add external service tags
$span->setTag('external.service', $serviceName);
$span->setTag('external.operation', $operation);
// Execute external service call
$result = $this->callExternalService($serviceName, $operation, $params);
// Set success status
$span->setStatus(['code' => 0, 'message' => 'OK']);
// Record metrics
$this->recordExternalServiceMetrics($serviceName, $operation, $startTime);
return $result;
} catch (\Exception $e) {
// Set error status
$span->setStatus([
'code' => 1,
'message' => $e->getMessage(),
'error' => true
]);
$span->setTag('error', true);
$span->setTag('error.message', $e->getMessage());
$span->setTag('error.type', get_class($e));
// Record error metrics
$this->recordErrorMetrics('external', $e);
throw $e;
} finally {
$this->tracer->finishSpan($span);
}
}
/**
* Add service interceptor.
*/
public function addInterceptor(string $name, CallInterceptor $interceptor): void
{
$this->interceptors[$name] = $interceptor;
$this->logInfo("Added service interceptor: {$name}");
}
/**
* Remove service interceptor.
*/
public function removeInterceptor(string $name): bool
{
if (!isset($this->interceptors[$name])) {
return false;
}
unset($this->interceptors[$name]);
$this->logInfo("Removed service interceptor: {$name}");
return true;
}
/**
* Get service metrics.
*/
public function getServiceMetrics(): array
{
return $this->serviceMetrics;
}
/**
* Reset service metrics.
*/
public function resetMetrics(): void
{
$this->serviceMetrics = [];
$this->logInfo("Reset service metrics");
}
/**
* Get HTTP client.
*/
protected function getHttpClient(): HttpClient
{
if (!isset($this->clients['http'])) {
$this->clients['http'] = new HttpClient($this->config['http_client'] ?? []);
}
return $this->clients['http'];
}
/**
* Get RPC client.
*/
protected function getRpcClient(): RpcClient
{
if (!isset($this->clients['rpc'])) {
$this->clients['rpc'] = new RpcClient($this->config['rpc_client'] ?? []);
}
return $this->clients['rpc'];
}
/**
* Get queue client.
*/
protected function getQueueClient(): QueueClient
{
if (!isset($this->clients['queue'])) {
$this->clients['queue'] = new QueueClient($this->config['queue_client'] ?? []);
}
return $this->clients['queue'];
}
/**
* Execute database query.
*/
protected function executeQuery(string $query, array $params, string $database): mixed
{
// This would be implemented based on your database layer
// For now, return a mock result
return ['row_count' => 1, 'data' => []];
}
/**
* Execute cache operation.
*/
protected function executeCacheOperation(string $operation, string $key, $value, string $cache): mixed
{
// This would be implemented based on your cache layer
// For now, return a mock result
return $operation === 'get' ? 'cached_value' : true;
}
/**
* Call external service.
*/
protected function callExternalService(string $serviceName, string $operation, array $params): mixed
{
// This would be implemented based on your external service client
// For now, return a mock result
return ['result' => 'success', 'data' => $params];
}
/**
* Sanitize SQL query for logging.
*/
protected function sanitizeQuery(string $query): string
{
// Remove sensitive data from query
$query = preg_replace('/\b(password\s*=\s*[\'"][^\'"]*[\'"])\b/', 'password=***', $query);
$query = preg_replace('/\b(token\s*=\s*[\'"][^\'"]*[\'"])\b/', 'token=***', $query);
// Truncate long queries
if (strlen($query) > 500) {
$query = substr($query, 0, 497) . '...';
}
return $query;
}
/**
* Record HTTP metrics.
*/
protected function recordHttpMetrics(string $method, string $url, int $statusCode, float $startTime): void
{
$duration = (microtime(true) - $startTime) * 1000; // Convert to milliseconds
$host = parse_url($url, PHP_URL_HOST) ?? 'unknown';
$path = parse_url($url, PHP_URL_PATH) ?? '/';
if (!isset($this->serviceMetrics['http'])) {
$this->serviceMetrics['http'] = [
'total_requests' => 0,
'total_duration' => 0,
'error_count' => 0,
'status_codes' => [],
'hosts' => [],
'methods' => []
];
}
$metrics = &$this->serviceMetrics['http'];
$metrics['total_requests']++;
$metrics['total_duration'] += $duration;
if ($statusCode >= 400) {
$metrics['error_count']++;
}
$metrics['status_codes'][$statusCode] = ($metrics['status_codes'][$statusCode] ?? 0) + 1;
$metrics['hosts'][$host] = ($metrics['hosts'][$host] ?? 0) + 1;
$metrics['methods'][$method] = ($metrics['methods'][$method] ?? 0) + 1;
}
/**
* Record RPC metrics.
*/
protected function recordRpcMetrics(string $service, string $method, float $startTime): void
{
$duration = (microtime(true) - $startTime) * 1000;
if (!isset($this->serviceMetrics['rpc'])) {
$this->serviceMetrics['rpc'] = [
'total_calls' => 0,
'total_duration' => 0,
'services' => [],
'methods' => []
];
}
$metrics = &$this->serviceMetrics['rpc'];
$metrics['total_calls']++;
$metrics['total_duration'] += $duration;
$metrics['services'][$service] = ($metrics['services'][$service] ?? 0) + 1;
$metrics['methods'][$method] = ($metrics['methods'][$method] ?? 0) + 1;
}
/**
* Record database metrics.
*/
protected function recordDatabaseMetrics(string $database, string $query, float $startTime): void
{
$duration = (microtime(true) - $startTime) * 1000;
$queryType = $this->extractQueryType($query);
if (!isset($this->serviceMetrics['database'])) {
$this->serviceMetrics['database'] = [
'total_queries' => 0,
'total_duration' => 0,
'databases' => [],
'query_types' => []
];
}
$metrics = &$this->serviceMetrics['database'];
$metrics['total_queries']++;
$metrics['total_duration'] += $duration;
$metrics['databases'][$database] = ($metrics['databases'][$database] ?? 0) + 1;
$metrics['query_types'][$queryType] = ($metrics['query_types'][$queryType] ?? 0) + 1;
}
/**
* Record cache metrics.
*/
protected function recordCacheMetrics(string $cache, string $operation, bool $hit, float $startTime): void
{
$duration = (microtime(true) - $startTime) * 1000;
if (!isset($this->serviceMetrics['cache'])) {
$this->serviceMetrics['cache'] = [
'total_operations' => 0,
'total_duration' => 0,
'hits' => 0,
'misses' => 0,
'caches' => [],
'operations' => []
];
}
$metrics = &$this->serviceMetrics['cache'];
$metrics['total_operations']++;
$metrics['total_duration'] += $duration;
if ($hit) {
$metrics['hits']++;
} else {
$metrics['misses']++;
}
$metrics['caches'][$cache] = ($metrics['caches'][$cache] ?? 0) + 1;
$metrics['operations'][$operation] = ($metrics['operations'][$operation] ?? 0) + 1;
}
/**
* Record queue metrics.
*/
protected function recordQueueMetrics(string $queue, string $operation, float $startTime): void
{
$duration = (microtime(true) - $startTime) * 1000;
if (!isset($this->serviceMetrics['queue'])) {
$this->serviceMetrics['queue'] = [
'total_operations' => 0,
'total_duration' => 0,
'queues' => [],
'operations' => []
];
}
$metrics = &$this->serviceMetrics['queue'];
$metrics['total_operations']++;
$metrics['total_duration'] += $duration;
$metrics['queues'][$queue] = ($metrics['queues'][$queue] ?? 0) + 1;
$metrics['operations'][$operation] = ($metrics['operations'][$operation] ?? 0) + 1;
}
/**
* Record external service metrics.
*/
protected function recordExternalServiceMetrics(string $serviceName, string $operation, float $startTime): void
{
$duration = (microtime(true) - $startTime) * 1000;
if (!isset($this->serviceMetrics['external'])) {
$this->serviceMetrics['external'] = [
'total_calls' => 0,
'total_duration' => 0,
'services' => [],
'operations' => []
];
}
$metrics = &$this->serviceMetrics['external'];
$metrics['total_calls']++;
$metrics['total_duration'] += $duration;
$metrics['services'][$serviceName] = ($metrics['services'][$serviceName] ?? 0) + 1;
$metrics['operations'][$operation] = ($metrics['operations'][$operation] ?? 0) + 1;
}
/**
* Record error metrics.
*/
protected function recordErrorMetrics(string $component, \Exception $e): void
{
if (!isset($this->serviceMetrics['errors'])) {
$this->serviceMetrics['errors'] = [
'total_errors' => 0,
'components' => [],
'error_types' => []
];
}
$metrics = &$this->serviceMetrics['errors'];
$metrics['total_errors']++;
$metrics['components'][$component] = ($metrics['components'][$component] ?? 0) + 1;
$metrics['error_types'][get_class($e)] = ($metrics['error_types'][get_class($e)] ?? 0) + 1;
}
/**
* Extract query type from SQL.
*/
protected function extractQueryType(string $query): string
{
$query = trim(strtoupper($query));
if (strpos($query, 'SELECT') === 0) return 'SELECT';
if (strpos($query, 'INSERT') === 0) return 'INSERT';
if (strpos($query, 'UPDATE') === 0) return 'UPDATE';
if (strpos($query, 'DELETE') === 0) return 'DELETE';
return 'OTHER';
}
/**
* Initialize service tracer.
*/
protected function initialize(): void
{
// Add default interceptors
if ($this->config['enable_default_interceptors'] ?? true) {
$this->addInterceptor('metrics', new \Fendx\Service\Tracing\Service\Interceptor\MetricsInterceptor());
$this->addInterceptor('logging', new \Fendx\Service\Tracing\Service\Interceptor\LoggingInterceptor());
}
$this->logInfo("Service tracer initialized");
}
/**
* Log info message.
*/
protected function logInfo(string $message): void
{
if ($this->config['logging_enabled']) {
error_log("[ServiceTracer] {$message}");
}
}
/**
* Get default configuration.
*/
protected function getDefaultConfig(): array
{
return [
'rpc_system' => 'grpc',
'db_system' => 'mysql',
'cache_system' => 'redis',
'queue_system' => 'rabbitmq',
'enable_default_interceptors' => true,
'logging_enabled' => true,
'http_client' => [
'timeout' => 30,
'follow_redirects' => true
],
'rpc_client' => [
'timeout' => 10,
'retry_attempts' => 3
],
'queue_client' => [
'timeout' => 5,
'persistent' => true
]
];
}
/**
* Get configuration.
*/
public function getConfig(): array
{
return $this->config;
}
/**
* Set configuration.
*/
public function setConfig(array $config): void
{
$this->config = array_merge($this->config, $config);
}
/**
* Create service tracer instance.
*/
public static function create(DistributedTracer $tracer, array $config = []): self
{
return new self($tracer, $config);
}
/**
* Create for development.
*/
public static function forDevelopment(DistributedTracer $tracer): self
{
return new self($tracer, [
'logging_enabled' => true,
'enable_default_interceptors' => true
]);
}
/**
* Create for production.
*/
public static function forProduction(DistributedTracer $tracer): self
{
return new self($tracer, [
'logging_enabled' => false,
'enable_default_interceptors' => true
]);
}
}

View File

@@ -0,0 +1,952 @@
<?php
declare(strict_types=1);
namespace Fendx\Service\Tracing\Visualization;
use Fendx\Service\Tracing\Span\Span;
use Fendx\Service\Tracing\Visualization\Renderer\HtmlRenderer;
use Fendx\Service\Tracing\Visualization\Renderer\JsonRenderer;
use Fendx\Service\Tracing\Visualization\Renderer\SvgRenderer;
use Fendx\Service\Tracing\Visualization\Layout\TreeLayout;
use Fendx\Service\Tracing\Visualization\Layout\TimelineLayout;
class TraceVisualizer
{
protected array $config = [];
protected array $renderers = [];
protected array $layouts = [];
protected array $colorSchemes = [];
public function __construct(array $config = [])
{
$this->config = array_merge($this->getDefaultConfig(), $config);
$this->initializeRenderers();
$this->initializeLayouts();
$this->initializeColorSchemes();
}
/**
* Visualize trace as HTML.
*/
public function visualizeHtml(array $spans, array $options = []): string
{
$renderer = $this->renderers['html'];
$layout = $this->selectLayout($spans, $options['layout'] ?? 'timeline');
$colorScheme = $this->selectColorScheme($options['color_scheme'] ?? 'default');
$visualizationData = $this->prepareVisualizationData($spans, $layout, $colorScheme);
return $renderer->render($visualizationData, $options);
}
/**
* Visualize trace as SVG.
*/
public function visualizeSvg(array $spans, array $options = []): string
{
$renderer = $this->renderers['svg'];
$layout = $this->selectLayout($spans, $options['layout'] ?? 'timeline');
$colorScheme = $this->selectColorScheme($options['color_scheme'] ?? 'default');
$visualizationData = $this->prepareVisualizationData($spans, $layout, $colorScheme);
return $renderer->render($visualizationData, $options);
}
/**
* Visualize trace as JSON.
*/
public function visualizeJson(array $spans, array $options = []): string
{
$renderer = $this->renderers['json'];
$layout = $this->selectLayout($spans, $options['layout'] ?? 'timeline');
$visualizationData = $this->prepareVisualizationData($spans, $layout);
return $renderer->render($visualizationData, $options);
}
/**
* Generate trace timeline.
*/
public function generateTimeline(array $spans, array $options = []): array
{
$timeline = [
'trace_id' => $this->extractTraceId($spans),
'total_duration' => $this->calculateTotalDuration($spans),
'spans' => [],
'gaps' => [],
'parallelism' => [],
'waterfall' => []
];
// Sort spans by start time
usort($spans, function($a, $b) {
return $a->getStartTime() <=> $b->getStartTime();
});
$baseTime = $spans[0]->getStartTime();
foreach ($spans as $span) {
$spanData = [
'span_id' => $span->getContext()->getSpanId(),
'parent_id' => $span->getContext()->getParent()?->getSpanId(),
'operation' => $span->getOperationName(),
'start_time' => ($span->getStartTime() - $baseTime) * 1000, // milliseconds
'duration' => $span->getDuration() * 1000, // milliseconds
'end_time' => ($span->getEndTime() - $baseTime) * 1000,
'service' => $span->getTags()['service.name'] ?? 'unknown',
'status' => $span->getStatus(),
'tags' => $span->getTags(),
'level' => $this->calculateSpanLevel($span, $spans)
];
$timeline['spans'][] = $spanData;
}
// Calculate gaps
$timeline['gaps'] = $this->calculateGaps($timeline['spans']);
// Calculate parallelism
$timeline['parallelism'] = $this->calculateParallelism($timeline['spans']);
// Generate waterfall data
$timeline['waterfall'] = $this->generateWaterfall($timeline['spans']);
return $timeline;
}
/**
* Generate service map.
*/
public function generateServiceMap(array $traces): array
{
$serviceMap = [
'services' => [],
'connections' => [],
'metrics' => []
];
// Extract services and connections
foreach ($traces as $trace) {
foreach ($trace as $span) {
$serviceName = $span->getTags()['service.name'] ?? 'unknown';
// Add service
if (!isset($serviceMap['services'][$serviceName])) {
$serviceMap['services'][$serviceName] = [
'name' => $serviceName,
'span_count' => 0,
'total_duration' => 0,
'error_count' => 0,
'operations' => []
];
}
$serviceMap['services'][$serviceName]['span_count']++;
$serviceMap['services'][$serviceName]['total_duration'] += $span->getDuration();
if (isset($span->getStatus()['error']) && $span->getStatus()['error']) {
$serviceMap['services'][$serviceName]['error_count']++;
}
$operation = $span->getOperationName();
if (!in_array($operation, $serviceMap['services'][$serviceName]['operations'])) {
$serviceMap['services'][$serviceName]['operations'][] = $operation;
}
// Add connections
$parentContext = $span->getContext()->getParent();
if ($parentContext) {
$parentService = $this->findServiceBySpanId($trace, $parentContext->getSpanId());
if ($parentService && $parentService !== $serviceName) {
$connectionKey = $parentService . '->' . $serviceName;
if (!isset($serviceMap['connections'][$connectionKey])) {
$serviceMap['connections'][$connectionKey] = [
'from' => $parentService,
'to' => $serviceName,
'count' => 0,
'avg_duration' => 0
];
}
$serviceMap['connections'][$connectionKey]['count']++;
$serviceMap['connections'][$connectionKey]['avg_duration'] += $span->getDuration();
}
}
}
}
// Calculate averages
foreach ($serviceMap['connections'] as &$connection) {
$connection['avg_duration'] /= $connection['count'];
}
// Calculate metrics
$serviceMap['metrics'] = $this->calculateServiceMapMetrics($serviceMap);
return $serviceMap;
}
/**
* Generate flame graph.
*/
public function generateFlameGraph(array $spans, array $options = []): array
{
$flameGraph = [
'name' => 'root',
'value' => 0,
'children' => [],
'metadata' => [
'trace_id' => $this->extractTraceId($spans),
'total_spans' => count($spans)
]
];
// Build span tree
$spanTree = $this->buildSpanTree($spans);
// Find root spans
$rootSpans = [];
foreach ($spans as $span) {
if (!$span->getContext()->getParent()) {
$rootSpans[] = $span;
}
}
// Build flame graph
foreach ($rootSpans as $rootSpan) {
$childData = $this->buildFlameGraphNode($rootSpan, $spanTree, $spans);
$flameGraph['children'][] = $childData;
$flameGraph['value'] += $childData['value'];
}
return $flameGraph;
}
/**
* Generate Gantt chart.
*/
public function generateGanttChart(array $spans, array $options = []): array
{
$ganttChart = [
'trace_id' => $this->extractTraceId($spans),
'tasks' => [],
'dependencies' => [],
'milestones' => [],
'timeline' => []
];
// Sort spans by start time
usort($spans, function($a, $b) {
return $a->getStartTime() <=> $b->getStartTime();
});
$baseTime = $spans[0]->getStartTime();
foreach ($spans as $index => $span) {
$task = [
'id' => $span->getContext()->getSpanId(),
'name' => $span->getOperationName(),
'service' => $span->getTags()['service.name'] ?? 'unknown',
'start' => ($span->getStartTime() - $baseTime) * 1000,
'duration' => $span->getDuration() * 1000,
'end' => ($span->getEndTime() - $baseTime) * 1000,
'progress' => 100, // Completed spans
'status' => $this->getTaskStatus($span),
'dependencies' => [],
'level' => $this->calculateSpanLevel($span, $spans)
];
// Add parent dependency
$parentContext = $span->getContext()->getParent();
if ($parentContext) {
$task['dependencies'][] = $parentContext->getSpanId();
$ganttChart['dependencies'][] = [
'from' => $parentContext->getSpanId(),
'to' => $span->getContext()->getSpanId()
];
}
$ganttChart['tasks'][] = $task;
}
// Generate timeline markers
$ganttChart['timeline'] = $this->generateTimelineMarkers($spans, $baseTime);
return $ganttChart;
}
/**
* Generate performance heatmap.
*/
public function generateHeatmap(array $traces, array $options = []): array
{
$heatmap = [
'type' => $options['type'] ?? 'duration',
'data' => [],
'scale' => [],
'metadata' => []
];
// Group traces by time intervals
$interval = $options['interval'] ?? 3600; // 1 hour default
$groupedTraces = $this->groupTracesByTime($traces, $interval);
// Calculate heatmap data
foreach ($groupedTraces as $timestamp => $traceGroup) {
$value = $this->calculateHeatmapValue($traceGroup, $heatmap['type']);
$heatmap['data'][] = [
'timestamp' => $timestamp,
'value' => $value,
'trace_count' => count($traceGroup)
];
}
// Calculate scale
$values = array_column($heatmap['data'], 'value');
if (!empty($values)) {
$heatmap['scale'] = [
'min' => min($values),
'max' => max($values),
'avg' => array_sum($values) / count($values)
];
}
$heatmap['metadata'] = [
'interval' => $interval,
'total_traces' => count($traces),
'time_range' => [
'start' => min(array_keys($groupedTraces)),
'end' => max(array_keys($groupedTraces))
]
];
return $heatmap;
}
/**
* Add custom renderer.
*/
public function addRenderer(string $name, object $renderer): void
{
$this->renderers[$name] = $renderer;
}
/**
* Add custom layout.
*/
public function addLayout(string $name, object $layout): void
{
$this->layouts[$name] = $layout;
}
/**
* Add custom color scheme.
*/
public function addColorScheme(string $name, array $colors): void
{
$this->colorSchemes[$name] = $colors;
}
/**
* Export visualization data.
*/
public function exportData(array $spans, string $format = 'json'): string
{
$data = [
'trace_id' => $this->extractTraceId($spans),
'spans' => $this->serializeSpans($spans),
'timeline' => $this->generateTimeline($spans),
'flame_graph' => $this->generateFlameGraph($spans),
'gantt_chart' => $this->generateGanttChart($spans),
'exported_at' => date('Y-m-d H:i:s')
];
switch ($format) {
case 'json':
return json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
case 'php':
return '<?php return ' . var_export($data, true) . ';';
default:
throw new \InvalidArgumentException("Unsupported export format: {$format}");
}
}
/**
* Prepare visualization data.
*/
protected function prepareVisualizationData(array $spans, object $layout, array $colorScheme = null): array
{
$data = [
'spans' => $spans,
'layout' => $layout->layout($spans),
'metadata' => [
'trace_id' => $this->extractTraceId($spans),
'span_count' => count($spans),
'total_duration' => $this->calculateTotalDuration($spans),
'services' => $this->extractServices($spans)
]
];
if ($colorScheme) {
$data['colors'] = $colorScheme;
}
return $data;
}
/**
* Select layout based on spans and options.
*/
protected function selectLayout(array $spans, string $layoutType): object
{
if (isset($this->layouts[$layoutType])) {
return $this->layouts[$layoutType];
}
// Auto-select layout based on span count
if (count($spans) > 50) {
return $this->layouts['timeline'];
} else {
return $this->layouts['tree'];
}
}
/**
* Select color scheme.
*/
protected function selectColorScheme(string $schemeName): array
{
return $this->colorSchemes[$schemeName] ?? $this->colorSchemes['default'];
}
/**
* Calculate total duration.
*/
protected function calculateTotalDuration(array $spans): float
{
if (empty($spans)) {
return 0;
}
$startTime = null;
$endTime = null;
foreach ($spans as $span) {
if ($startTime === null || $span->getStartTime() < $startTime) {
$startTime = $span->getStartTime();
}
if ($endTime === null || $span->getEndTime() > $endTime) {
$endTime = $span->getEndTime();
}
}
return $endTime && $startTime ? $endTime - $startTime : 0;
}
/**
* Calculate span level in tree.
*/
protected function calculateSpanLevel(Span $span, array $spans): int
{
$level = 0;
$currentContext = $span->getContext()->getParent();
while ($currentContext) {
$level++;
$parentSpan = $this->findSpanById($spans, $currentContext->getSpanId());
$currentContext = $parentSpan ? $parentSpan->getContext()->getParent() : null;
}
return $level;
}
/**
* Build span tree.
*/
protected function buildSpanTree(array $spans): array
{
$tree = [];
foreach ($spans as $span) {
$spanId = $span->getContext()->getSpanId();
$parentContext = $span->getContext()->getParent();
if ($parentContext) {
$parentId = $parentContext->getSpanId();
$tree[$parentId][] = $span;
} else {
$tree[$spanId] = $tree[$spanId] ?? [];
}
}
return $tree;
}
/**
* Build flame graph node.
*/
protected function buildFlameGraphNode(Span $span, array $spanTree, array $spans): array
{
$node = [
'name' => $span->getOperationName(),
'value' => $span->getDuration() * 1000, // Convert to milliseconds
'children' => [],
'metadata' => [
'span_id' => $span->getContext()->getSpanId(),
'service' => $span->getTags()['service.name'] ?? 'unknown',
'status' => $span->getStatus()
]
];
$spanId = $span->getContext()->getSpanId();
if (isset($spanTree[$spanId])) {
foreach ($spanTree[$spanId] as $childSpan) {
$childNode = $this->buildFlameGraphNode($childSpan, $spanTree, $spans);
$node['children'][] = $childNode;
}
}
return $node;
}
/**
* Calculate gaps between spans.
*/
protected function calculateGaps(array $spans): array
{
$gaps = [];
for ($i = 1; $i < count($spans); $i++) {
$prevSpan = $spans[$i - 1];
$currSpan = $spans[$i];
$gap = $currSpan['start_time'] - $prevSpan['end_time'];
if ($gap > 0) {
$gaps[] = [
'start' => $prevSpan['end_time'],
'end' => $currSpan['start_time'],
'duration' => $gap,
'between_spans' => [$prevSpan['span_id'], $currSpan['span_id']]
];
}
}
return $gaps;
}
/**
* Calculate parallelism.
*/
protected function calculateParallelism(array $spans): array
{
$parallelism = [];
$timePoints = [];
// Collect all start and end points
foreach ($spans as $span) {
$timePoints[] = ['time' => $span['start_time'], 'type' => 'start', 'span_id' => $span['span_id']];
$timePoints[] = ['time' => $span['end_time'], 'type' => 'end', 'span_id' => $span['span_id']];
}
// Sort by time
usort($timePoints, function($a, $b) {
return $a['time'] <=> $b['time'];
});
// Calculate concurrent spans at each point
$concurrent = 0;
foreach ($timePoints as $point) {
if ($point['type'] === 'start') {
$concurrent++;
} else {
$concurrent--;
}
$parallelism[] = [
'time' => $point['time'],
'concurrent_spans' => $concurrent
];
}
return $parallelism;
}
/**
* Generate waterfall data.
*/
protected function generateWaterfall(array $spans): array
{
$waterfall = [];
foreach ($spans as $span) {
$waterfall[] = [
'span_id' => $span['span_id'],
'operation' => $span['operation'],
'service' => $span['service'],
'start' => $span['start_time'],
'duration' => $span['duration'],
'level' => $span['level'],
'color' => $this->getSpanColor($span)
];
}
return $waterfall;
}
/**
* Get span color based on status and duration.
*/
protected function getSpanColor(array $span): string
{
if (isset($span['status']['error']) && $span['status']['error']) {
return '#ff4444'; // Red for errors
}
$duration = $span['duration'];
if ($duration > 5000) { // > 5 seconds
return '#ff8800'; // Orange for slow
} elseif ($duration > 1000) { // > 1 second
return '#ffaa00'; // Yellow for moderate
} else {
return '#44ff44'; // Green for fast
}
}
/**
* Find span by ID.
*/
protected function findSpanById(array $spans, string $spanId): ?Span
{
foreach ($spans as $span) {
if ($span->getContext()->getSpanId() === $spanId) {
return $span;
}
}
return null;
}
/**
* Find service by span ID in trace.
*/
protected function findServiceBySpanId(array $trace, string $spanId): ?string
{
foreach ($trace as $span) {
if ($span->getContext()->getSpanId() === $spanId) {
return $span->getTags()['service.name'] ?? 'unknown';
}
}
return null;
}
/**
* Extract trace ID.
*/
protected function extractTraceId(array $spans): string
{
if (!empty($spans)) {
return $spans[0]->getContext()->getTraceId();
}
return 'unknown';
}
/**
* Extract services from spans.
*/
protected function extractServices(array $spans): array
{
$services = [];
foreach ($spans as $span) {
$serviceName = $span->getTags()['service.name'] ?? 'unknown';
if (!in_array($serviceName, $services)) {
$services[] = $serviceName;
}
}
return $services;
}
/**
* Get task status from span.
*/
protected function getTaskStatus(Span $span): string
{
$status = $span->getStatus();
if (isset($status['error']) && $status['error']) {
return 'error';
}
return 'completed';
}
/**
* Generate timeline markers.
*/
protected function generateTimelineMarkers(array $spans, float $baseTime): array
{
$markers = [];
foreach ($spans as $span) {
$markers[] = [
'time' => ($span->getStartTime() - $baseTime) * 1000,
'type' => 'start',
'span_id' => $span->getContext()->getSpanId(),
'operation' => $span->getOperationName()
];
$markers[] = [
'time' => ($span->getEndTime() - $baseTime) * 1000,
'type' => 'end',
'span_id' => $span->getContext()->getSpanId(),
'operation' => $span->getOperationName()
];
}
// Sort by time
usort($markers, function($a, $b) {
return $a['time'] <=> $b['time'];
});
return $markers;
}
/**
* Group traces by time intervals.
*/
protected function groupTracesByTime(array $traces, int $interval): array
{
$grouped = [];
foreach ($traces as $trace) {
if (!empty($trace)) {
$timestamp = floor($trace[0]->getStartTime() / $interval) * $interval;
$grouped[$timestamp][] = $trace;
}
}
return $grouped;
}
/**
* Calculate heatmap value.
*/
protected function calculateHeatmapValue(array $traces, string $type): float
{
switch ($type) {
case 'duration':
$durations = [];
foreach ($traces as $trace) {
$durations[] = $this->calculateTotalDuration($trace);
}
return !empty($durations) ? array_sum($durations) / count($durations) : 0;
case 'error_rate':
$totalSpans = 0;
$errorSpans = 0;
foreach ($traces as $trace) {
foreach ($trace as $span) {
$totalSpans++;
if (isset($span->getStatus()['error']) && $span->getStatus()['error']) {
$errorSpans++;
}
}
}
return $totalSpans > 0 ? ($errorSpans / $totalSpans) * 100 : 0;
case 'throughput':
return count($traces);
default:
return 0;
}
}
/**
* Calculate service map metrics.
*/
protected function calculateServiceMapMetrics(array $serviceMap): array
{
$metrics = [
'total_services' => count($serviceMap['services']),
'total_connections' => count($serviceMap['connections']),
'total_spans' => 0,
'total_errors' => 0,
'average_duration' => 0
];
foreach ($serviceMap['services'] as $service) {
$metrics['total_spans'] += $service['span_count'];
$metrics['total_errors'] += $service['error_count'];
$metrics['average_duration'] += $service['total_duration'];
}
if ($metrics['total_spans'] > 0) {
$metrics['average_duration'] /= $metrics['total_spans'];
}
return $metrics;
}
/**
* Serialize spans for export.
*/
protected function serializeSpans(array $spans): array
{
$serialized = [];
foreach ($spans as $span) {
$serialized[] = [
'span_id' => $span->getContext()->getSpanId(),
'trace_id' => $span->getContext()->getTraceId(),
'parent_id' => $span->getContext()->getParent()?->getSpanId(),
'operation' => $span->getOperationName(),
'start_time' => $span->getStartTime(),
'end_time' => $span->getEndTime(),
'duration' => $span->getDuration(),
'status' => $span->getStatus(),
'tags' => $span->getTags(),
'logs' => $span->getLogs()
];
}
return $serialized;
}
/**
* Initialize renderers.
*/
protected function initializeRenderers(): void
{
$this->renderers['html'] = new HtmlRenderer($this->config['html'] ?? []);
$this->renderers['svg'] = new SvgRenderer($this->config['svg'] ?? []);
$this->renderers['json'] = new JsonRenderer($this->config['json'] ?? []);
}
/**
* Initialize layouts.
*/
protected function initializeLayouts(): void
{
$this->layouts['tree'] = new TreeLayout($this->config['layouts']['tree'] ?? []);
$this->layouts['timeline'] = new TimelineLayout($this->config['layouts']['timeline'] ?? []);
}
/**
* Initialize color schemes.
*/
protected function initializeColorSchemes(): void
{
$this->colorSchemes['default'] = [
'success' => '#28a745',
'warning' => '#ffc107',
'error' => '#dc3545',
'info' => '#17a2b8',
'primary' => '#007bff',
'secondary' => '#6c757d'
];
$this->colorSchemes['dark'] = [
'success' => '#28a745',
'warning' => '#ffc107',
'error' => '#dc3545',
'info' => '#17a2b8',
'primary' => '#007bff',
'secondary' => '#6c757d'
];
}
/**
* Get default configuration.
*/
protected function getDefaultConfig(): array
{
return [
'html' => [
'template' => 'default',
'include_css' => true,
'include_js' => true
],
'svg' => [
'width' => 1200,
'height' => 600,
'margin' => 20
],
'json' => [
'pretty_print' => true,
'include_metadata' => true
],
'layouts' => [
'tree' => [
'horizontal_spacing' => 150,
'vertical_spacing' => 50
],
'timeline' => [
'row_height' => 30,
'min_width' => 800
]
]
];
}
/**
* Get configuration.
*/
public function getConfig(): array
{
return $this->config;
}
/**
* Set configuration.
*/
public function setConfig(array $config): void
{
$this->config = array_merge($this->config, $config);
}
/**
* Create trace visualizer instance.
*/
public static function create(array $config = []): self
{
return new self($config);
}
/**
* Create for development.
*/
public static function forDevelopment(): self
{
return new self([
'html' => [
'include_css' => true,
'include_js' => true
],
'debug' => true
]);
}
/**
* Create for production.
*/
public static function forProduction(): self
{
return new self([
'html' => [
'include_css' => false,
'include_js' => false
],
'debug' => false
]);
}
}