refactor(deployment): Remove WireGuard VPN dependency and restore public service access
Remove WireGuard integration from production deployment to simplify infrastructure: - Remove docker-compose-direct-access.yml (VPN-bound services) - Remove VPN-only middlewares from Grafana, Prometheus, Portainer - Remove WireGuard middleware definitions from Traefik - Remove WireGuard IPs (10.8.0.0/24) from Traefik forwarded headers All monitoring services now publicly accessible via subdomains: - grafana.michaelschiemer.de (with Grafana native auth) - prometheus.michaelschiemer.de (with Basic Auth) - portainer.michaelschiemer.de (with Portainer native auth) All services use Let's Encrypt SSL certificates via Traefik.
This commit is contained in:
@@ -182,6 +182,11 @@ final readonly class ApiGateway
|
||||
connectTimeout: min(3, $timeoutSeconds), // Connect timeout max 3s or total timeout
|
||||
);
|
||||
|
||||
// Add authentication if present
|
||||
if ($request instanceof HasAuth) {
|
||||
$options = $options->with(['auth' => $request->getAuth()]);
|
||||
}
|
||||
|
||||
// Use factory method for JSON requests if payload is present
|
||||
if ($request instanceof HasPayload) {
|
||||
return ClientRequest::json(
|
||||
|
||||
30
src/Framework/ApiGateway/HasAuth.php
Normal file
30
src/Framework/ApiGateway/HasAuth.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\ApiGateway;
|
||||
|
||||
use App\Framework\HttpClient\AuthConfig;
|
||||
|
||||
/**
|
||||
* Marker interface for API requests that require authentication
|
||||
*
|
||||
* Requests implementing this interface provide AuthConfig
|
||||
* instead of manually building Authorization headers.
|
||||
*
|
||||
* Example:
|
||||
* final readonly class AuthenticatedApiRequest implements ApiRequest, HasAuth
|
||||
* {
|
||||
* public function getAuth(): AuthConfig
|
||||
* {
|
||||
* return AuthConfig::basic($username, $password);
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
interface HasAuth
|
||||
{
|
||||
/**
|
||||
* Get authentication configuration
|
||||
*/
|
||||
public function getAuth(): AuthConfig;
|
||||
}
|
||||
@@ -33,6 +33,8 @@ enum EnvKey: string
|
||||
case RAPIDMAIL_USERNAME = 'RAPIDMAIL_USERNAME';
|
||||
case RAPIDMAIL_PASSWORD = 'RAPIDMAIL_PASSWORD';
|
||||
case RAPIDMAIL_TEST_MODE = 'RAPIDMAIL_TEST_MODE';
|
||||
case NETCUP_API_KEY = 'NETCUP_API_KEY';
|
||||
case NETCUP_API_PASSWORD = 'NETCUP_API_PASSWORD';
|
||||
|
||||
// OAuth - Spotify
|
||||
case SPOTIFY_CLIENT_ID = 'SPOTIFY_CLIENT_ID';
|
||||
|
||||
@@ -84,6 +84,11 @@ final readonly class InitializerProcessor
|
||||
if ($returnType === null || $returnType === 'void') {
|
||||
$this->container->invoker->invoke($discoveredAttribute->className, $methodName->toString());
|
||||
}
|
||||
// Handle "self" return type: Replace with the declaring class
|
||||
elseif ($returnType === 'self') {
|
||||
$returnType = $discoveredAttribute->className->toString();
|
||||
$dependencyGraph->addInitializer($returnType, $discoveredAttribute->className, $methodName);
|
||||
}
|
||||
// Service-Initializer: Konkreter Return-Type → Zum Dependency-Graph hinzufügen
|
||||
else {
|
||||
$dependencyGraph->addInitializer($returnType, $discoveredAttribute->className, $methodName);
|
||||
|
||||
@@ -10,7 +10,7 @@ use App\Framework\Core\ValueObjects\Duration;
|
||||
use App\Framework\DateTime\Clock;
|
||||
use App\Framework\ErrorAggregation\Storage\ErrorStorageInterface;
|
||||
use App\Framework\Exception\Core\ErrorSeverity;
|
||||
use App\Framework\Exception\ErrorHandlerContext;
|
||||
use App\Framework\ExceptionHandling\Context\ExceptionContextProvider;
|
||||
use App\Framework\Logging\Logger;
|
||||
use App\Framework\Queue\Queue;
|
||||
|
||||
@@ -35,18 +35,18 @@ final readonly class ErrorAggregator implements ErrorAggregatorInterface
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes a new error from ErrorHandlerContext
|
||||
* Processes a new error using unified exception pattern
|
||||
*/
|
||||
public function processError(ErrorHandlerContext $context): void
|
||||
public function processError(\Throwable $exception, ExceptionContextProvider $contextProvider, bool $isDebug = false): void
|
||||
{
|
||||
try {
|
||||
$errorEvent = ErrorEvent::fromErrorHandlerContext($context, $this->clock);
|
||||
$errorEvent = ErrorEvent::fromException($exception, $contextProvider, $this->clock, $isDebug);
|
||||
$this->processErrorEvent($errorEvent);
|
||||
} catch (\Throwable $e) {
|
||||
// Don't let error aggregation break the application
|
||||
$this->logError("Failed to process error: " . $e->getMessage(), [
|
||||
'exception' => $e,
|
||||
'context' => $context->toArray(),
|
||||
'original_exception' => $exception,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +59,43 @@ final readonly class ErrorEvent
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates ErrorEvent from Exception using ExceptionContextProvider (new unified pattern)
|
||||
*/
|
||||
public static function fromException(\Throwable $exception, \App\Framework\ExceptionHandling\Context\ExceptionContextProvider $contextProvider, \App\Framework\DateTime\Clock $clock, bool $isDebug = false): self
|
||||
{
|
||||
// Retrieve context from WeakMap
|
||||
$context = $contextProvider->get($exception);
|
||||
|
||||
// Extract ErrorCode if exception implements the interface
|
||||
$errorCode = self::extractErrorCodeFromException($exception);
|
||||
|
||||
// Extract service name from operation or component
|
||||
$service = self::extractServiceNameFromContext($context);
|
||||
|
||||
// Determine severity
|
||||
$severity = self::determineSeverityFromException($exception, $context, $errorCode);
|
||||
|
||||
return new self(
|
||||
id: new Ulid($clock),
|
||||
service: $service,
|
||||
component: $context?->component ?? 'unknown',
|
||||
operation: $context?->operation ?? 'unknown',
|
||||
errorCode: $errorCode,
|
||||
errorMessage: $exception->getMessage(),
|
||||
severity: $severity,
|
||||
occurredAt: $context?->occurredAt ?? new \DateTimeImmutable(),
|
||||
context: $context?->data ?? [],
|
||||
metadata: $context?->metadata ?? [],
|
||||
requestId: $context?->requestId,
|
||||
userId: $context?->userId,
|
||||
clientIp: $context?->clientIp,
|
||||
isSecurityEvent: $context?->metadata['security_event'] ?? false,
|
||||
stackTrace: $isDebug ? $exception->getTraceAsString() : null,
|
||||
userAgent: $context?->userAgent,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts to array for storage/transmission
|
||||
*/
|
||||
@@ -298,4 +335,74 @@ final readonly class ErrorEvent
|
||||
|
||||
return $normalized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract ErrorCode from exception (new unified pattern helper)
|
||||
*/
|
||||
private static function extractErrorCodeFromException(\Throwable $exception): ErrorCode
|
||||
{
|
||||
// Check if exception implements HasErrorCode interface
|
||||
if ($exception instanceof \App\Framework\Exception\FrameworkException) {
|
||||
$errorCode = $exception->getErrorCode();
|
||||
if ($errorCode !== null) {
|
||||
return $errorCode;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: Use SystemErrorCode::RESOURCE_EXHAUSTED as generic error
|
||||
return \App\Framework\Exception\Core\SystemErrorCode::RESOURCE_EXHAUSTED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract service name from ExceptionContextData (new unified pattern helper)
|
||||
*/
|
||||
private static function extractServiceNameFromContext(?\App\Framework\ExceptionHandling\Context\ExceptionContextData $context): string
|
||||
{
|
||||
if ($context === null) {
|
||||
return 'web';
|
||||
}
|
||||
|
||||
// Extract from operation if available (e.g., "user.create" → "user")
|
||||
if ($context->operation !== null && str_contains($context->operation, '.')) {
|
||||
$parts = explode('.', $context->operation);
|
||||
return strtolower($parts[0]);
|
||||
}
|
||||
|
||||
// Extract from component if available
|
||||
if ($context->component !== null) {
|
||||
return strtolower($context->component);
|
||||
}
|
||||
|
||||
return 'web';
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine severity from exception, context, and error code (new unified pattern helper)
|
||||
*/
|
||||
private static function determineSeverityFromException(
|
||||
\Throwable $exception,
|
||||
?\App\Framework\ExceptionHandling\Context\ExceptionContextData $context,
|
||||
ErrorCode $errorCode
|
||||
): ErrorSeverity {
|
||||
// Security events are always critical
|
||||
if ($context?->metadata['security_event'] ?? false) {
|
||||
return ErrorSeverity::CRITICAL;
|
||||
}
|
||||
|
||||
// Check explicit severity in metadata
|
||||
if ($context !== null && isset($context->metadata['severity'])) {
|
||||
$severity = ErrorSeverity::tryFrom($context->metadata['severity']);
|
||||
if ($severity !== null) {
|
||||
return $severity;
|
||||
}
|
||||
}
|
||||
|
||||
// Get severity from ErrorCode
|
||||
if (method_exists($errorCode, 'getSeverity')) {
|
||||
return $errorCode->getSeverity();
|
||||
}
|
||||
|
||||
// Fallback: ERROR for all unhandled exceptions
|
||||
return ErrorSeverity::ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ namespace App\Framework\ErrorBoundaries;
|
||||
use App\Framework\Core\ValueObjects\Duration;
|
||||
use App\Framework\Core\ValueObjects\Timestamp;
|
||||
use App\Framework\DateTime\Timer;
|
||||
use App\Framework\ErrorAggregation\ErrorAggregatorInterface;
|
||||
use App\Framework\ErrorBoundaries\CircuitBreaker\BoundaryCircuitBreakerManager;
|
||||
use App\Framework\ErrorBoundaries\Events\BoundaryEventInterface;
|
||||
use App\Framework\ErrorBoundaries\Events\BoundaryEventPublisher;
|
||||
@@ -16,6 +17,7 @@ use App\Framework\ErrorBoundaries\Events\BoundaryFallbackExecuted;
|
||||
use App\Framework\ErrorBoundaries\Events\BoundaryTimeoutOccurred;
|
||||
use App\Framework\Exception\ErrorCode;
|
||||
use App\Framework\Exception\FrameworkException;
|
||||
use App\Framework\ExceptionHandling\Context\ExceptionContextProvider;
|
||||
use App\Framework\Logging\Logger;
|
||||
use Throwable;
|
||||
|
||||
@@ -34,6 +36,8 @@ final readonly class ErrorBoundary
|
||||
private ?Logger $logger = null,
|
||||
private ?BoundaryCircuitBreakerManager $circuitBreakerManager = null,
|
||||
private ?BoundaryEventPublisher $eventPublisher = null,
|
||||
private ?ErrorAggregatorInterface $errorAggregator = null,
|
||||
private ?ExceptionContextProvider $contextProvider = null,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -319,6 +323,29 @@ final readonly class ErrorBoundary
|
||||
{
|
||||
$this->logFailure($exception, 'Operation failed, executing fallback');
|
||||
|
||||
// Dispatch to ErrorAggregator for centralized monitoring
|
||||
if ($this->errorAggregator !== null && $this->contextProvider !== null) {
|
||||
try {
|
||||
// Enrich exception context with boundary metadata
|
||||
$existingContext = $this->contextProvider->get($exception);
|
||||
if ($existingContext !== null) {
|
||||
$enrichedContext = $existingContext->withMetadata([
|
||||
'error_boundary' => $this->boundaryName,
|
||||
'boundary_failure' => true,
|
||||
]);
|
||||
$this->contextProvider->set($exception, $enrichedContext);
|
||||
}
|
||||
|
||||
// Dispatch to aggregator
|
||||
$this->errorAggregator->processError($exception, $this->contextProvider, false);
|
||||
} catch (Throwable $aggregationException) {
|
||||
// Don't let aggregation failures break boundary resilience
|
||||
$this->log('warning', 'Error aggregation failed', [
|
||||
'aggregation_error' => $aggregationException->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$result = $fallback();
|
||||
|
||||
|
||||
@@ -7,9 +7,11 @@ namespace App\Framework\ErrorBoundaries;
|
||||
use App\Framework\Core\ValueObjects\Duration;
|
||||
use App\Framework\DateTime\SystemTimer;
|
||||
use App\Framework\DateTime\Timer;
|
||||
use App\Framework\ErrorAggregation\ErrorAggregatorInterface;
|
||||
use App\Framework\ErrorBoundaries\CircuitBreaker\BoundaryCircuitBreakerManager;
|
||||
use App\Framework\ErrorBoundaries\Events\BoundaryEventPublisher;
|
||||
use App\Framework\EventBus\EventBus;
|
||||
use App\Framework\ExceptionHandling\Context\ExceptionContextProvider;
|
||||
use App\Framework\Logging\Logger;
|
||||
use App\Framework\StateManagement\StateManagerFactory;
|
||||
|
||||
@@ -25,6 +27,8 @@ final readonly class ErrorBoundaryFactory
|
||||
private ?Logger $logger = null,
|
||||
private ?StateManagerFactory $stateManagerFactory = null,
|
||||
private ?EventBus $eventBus = null,
|
||||
private ?ErrorAggregatorInterface $errorAggregator = null,
|
||||
private ?ExceptionContextProvider $contextProvider = null,
|
||||
array $routeConfigs = []
|
||||
) {
|
||||
$this->routeConfigs = array_merge($this->getDefaultRouteConfigs(), $routeConfigs);
|
||||
@@ -101,6 +105,8 @@ final readonly class ErrorBoundaryFactory
|
||||
logger: $this->logger,
|
||||
circuitBreakerManager: $circuitBreakerManager,
|
||||
eventPublisher: $eventPublisher,
|
||||
errorAggregator: $this->errorAggregator,
|
||||
contextProvider: $this->contextProvider,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -66,6 +66,57 @@ final readonly class ErrorReport
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create from Exception with WeakMap context (unified pattern)
|
||||
*
|
||||
* @param Throwable $exception Exception to report
|
||||
* @param \App\Framework\ExceptionHandling\Context\ExceptionContextProvider $contextProvider WeakMap context provider
|
||||
* @param string $level Error level (error, warning, critical, etc.)
|
||||
* @param array $additionalContext Additional context to merge with WeakMap context
|
||||
* @param string|null $environment Environment name (production, staging, etc.)
|
||||
* @return self
|
||||
*/
|
||||
public static function fromException(
|
||||
Throwable $exception,
|
||||
\App\Framework\ExceptionHandling\Context\ExceptionContextProvider $contextProvider,
|
||||
string $level = 'error',
|
||||
array $additionalContext = [],
|
||||
?string $environment = null
|
||||
): self {
|
||||
// Retrieve context from WeakMap
|
||||
$context = $contextProvider->get($exception);
|
||||
|
||||
// Merge data from WeakMap with additional context
|
||||
$mergedContext = array_merge($context?->data ?? [], $additionalContext);
|
||||
|
||||
return new self(
|
||||
id: self::generateId(),
|
||||
timestamp: $context?->occurredAt ?? new DateTimeImmutable(),
|
||||
level: $level,
|
||||
message: $exception->getMessage(),
|
||||
exception: $exception::class,
|
||||
file: $exception->getFile(),
|
||||
line: $exception->getLine(),
|
||||
trace: $exception->getTraceAsString(),
|
||||
context: $mergedContext,
|
||||
userId: $context?->userId,
|
||||
sessionId: $context?->sessionId,
|
||||
requestId: $context?->requestId,
|
||||
userAgent: $context?->userAgent,
|
||||
ipAddress: $context?->clientIp,
|
||||
tags: $context?->tags ?? [],
|
||||
environment: $environment ?? 'production',
|
||||
serverInfo: self::getServerInfo(),
|
||||
customData: array_merge(
|
||||
$context?->metadata ?? [],
|
||||
array_filter([
|
||||
'operation' => $context?->operation,
|
||||
'component' => $context?->component,
|
||||
])
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create from manual report
|
||||
*/
|
||||
|
||||
@@ -28,7 +28,21 @@ final readonly class ErrorReporter implements ErrorReporterInterface
|
||||
}
|
||||
|
||||
/**
|
||||
* Report an error from Throwable
|
||||
* Report an error from Exception with WeakMap context (unified pattern)
|
||||
*/
|
||||
public function reportException(
|
||||
Throwable $exception,
|
||||
\App\Framework\ExceptionHandling\Context\ExceptionContextProvider $contextProvider,
|
||||
string $level = 'error',
|
||||
array $additionalContext = []
|
||||
): string {
|
||||
$report = ErrorReport::fromException($exception, $contextProvider, $level, $additionalContext);
|
||||
|
||||
return $this->report($report);
|
||||
}
|
||||
|
||||
/**
|
||||
* Report an error from Throwable (legacy method)
|
||||
*/
|
||||
public function reportThrowable(
|
||||
Throwable $throwable,
|
||||
|
||||
309
src/Framework/ExceptionHandling/Context/ExceptionContextData.php
Normal file
309
src/Framework/ExceptionHandling/Context/ExceptionContextData.php
Normal file
@@ -0,0 +1,309 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\ExceptionHandling\Context;
|
||||
|
||||
use DateTimeImmutable;
|
||||
|
||||
/**
|
||||
* Exception Context Data
|
||||
*
|
||||
* Immutable value object containing rich exception context.
|
||||
* Stored externally via ExceptionContextProvider - never embedded in exceptions.
|
||||
*
|
||||
* PHP 8.5+ readonly class with asymmetric visibility for extensibility.
|
||||
*/
|
||||
final readonly class ExceptionContextData
|
||||
{
|
||||
public readonly DateTimeImmutable $occurredAt;
|
||||
|
||||
/**
|
||||
* @param string|null $operation Operation being performed (e.g., 'user.create', 'payment.process')
|
||||
* @param string|null $component Component where error occurred (e.g., 'UserService', 'PaymentGateway')
|
||||
* @param array<string, mixed> $data Domain data (e.g., user_id, order_id, amount)
|
||||
* @param array<string, mixed> $debug Debug data (queries, traces, internal state)
|
||||
* @param array<string, mixed> $metadata Additional metadata (tags, severity, fingerprint)
|
||||
* @param DateTimeImmutable|null $occurredAt When the exception occurred
|
||||
* @param string|null $userId User ID if available
|
||||
* @param string|null $requestId Request ID for tracing
|
||||
* @param string|null $sessionId Session ID if available
|
||||
* @param string|null $clientIp Client IP address for HTTP requests
|
||||
* @param string|null $userAgent User agent string for HTTP requests
|
||||
* @param array<string> $tags Tags for categorization (e.g., ['payment', 'external_api'])
|
||||
*/
|
||||
public function __construct(
|
||||
public ?string $operation = null,
|
||||
public ?string $component = null,
|
||||
public array $data = [],
|
||||
public array $debug = [],
|
||||
public array $metadata = [],
|
||||
?DateTimeImmutable $occurredAt = null,
|
||||
public ?string $userId = null,
|
||||
public ?string $requestId = null,
|
||||
public ?string $sessionId = null,
|
||||
public ?string $clientIp = null,
|
||||
public ?string $userAgent = null,
|
||||
public array $tags = [],
|
||||
) {
|
||||
$this->occurredAt ??= new DateTimeImmutable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create empty context
|
||||
*/
|
||||
public static function empty(): self
|
||||
{
|
||||
return new self();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create context with operation
|
||||
*/
|
||||
public static function forOperation(string $operation, ?string $component = null): self
|
||||
{
|
||||
return new self(
|
||||
operation: $operation,
|
||||
component: $component
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create context with data
|
||||
*/
|
||||
public static function withData(array $data): self
|
||||
{
|
||||
return new self(data: $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new instance with operation
|
||||
*/
|
||||
public function withOperation(string $operation, ?string $component = null): self
|
||||
{
|
||||
return new self(
|
||||
operation: $operation,
|
||||
component: $component ?? $this->component,
|
||||
data: $this->data,
|
||||
debug: $this->debug,
|
||||
metadata: $this->metadata,
|
||||
occurredAt: $this->occurredAt,
|
||||
userId: $this->userId,
|
||||
requestId: $this->requestId,
|
||||
sessionId: $this->sessionId,
|
||||
clientIp: $this->clientIp,
|
||||
userAgent: $this->userAgent,
|
||||
tags: $this->tags
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add data to context
|
||||
*/
|
||||
public function addData(array $data): self
|
||||
{
|
||||
return new self(
|
||||
operation: $this->operation,
|
||||
component: $this->component,
|
||||
data: array_merge($this->data, $data),
|
||||
debug: $this->debug,
|
||||
metadata: $this->metadata,
|
||||
occurredAt: $this->occurredAt,
|
||||
userId: $this->userId,
|
||||
requestId: $this->requestId,
|
||||
sessionId: $this->sessionId,
|
||||
clientIp: $this->clientIp,
|
||||
userAgent: $this->userAgent,
|
||||
tags: $this->tags
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add debug information
|
||||
*/
|
||||
public function addDebug(array $debug): self
|
||||
{
|
||||
return new self(
|
||||
operation: $this->operation,
|
||||
component: $this->component,
|
||||
data: $this->data,
|
||||
debug: array_merge($this->debug, $debug),
|
||||
metadata: $this->metadata,
|
||||
occurredAt: $this->occurredAt,
|
||||
userId: $this->userId,
|
||||
requestId: $this->requestId,
|
||||
sessionId: $this->sessionId,
|
||||
clientIp: $this->clientIp,
|
||||
userAgent: $this->userAgent,
|
||||
tags: $this->tags
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add metadata
|
||||
*/
|
||||
public function addMetadata(array $metadata): self
|
||||
{
|
||||
return new self(
|
||||
operation: $this->operation,
|
||||
component: $this->component,
|
||||
data: $this->data,
|
||||
debug: $this->debug,
|
||||
metadata: array_merge($this->metadata, $metadata),
|
||||
occurredAt: $this->occurredAt,
|
||||
userId: $this->userId,
|
||||
requestId: $this->requestId,
|
||||
sessionId: $this->sessionId,
|
||||
clientIp: $this->clientIp,
|
||||
userAgent: $this->userAgent,
|
||||
tags: $this->tags
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add user ID
|
||||
*/
|
||||
public function withUserId(string $userId): self
|
||||
{
|
||||
return new self(
|
||||
operation: $this->operation,
|
||||
component: $this->component,
|
||||
data: $this->data,
|
||||
debug: $this->debug,
|
||||
metadata: $this->metadata,
|
||||
occurredAt: $this->occurredAt,
|
||||
userId: $userId,
|
||||
requestId: $this->requestId,
|
||||
sessionId: $this->sessionId,
|
||||
clientIp: $this->clientIp,
|
||||
userAgent: $this->userAgent,
|
||||
tags: $this->tags
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add request ID
|
||||
*/
|
||||
public function withRequestId(string $requestId): self
|
||||
{
|
||||
return new self(
|
||||
operation: $this->operation,
|
||||
component: $this->component,
|
||||
data: $this->data,
|
||||
debug: $this->debug,
|
||||
metadata: $this->metadata,
|
||||
occurredAt: $this->occurredAt,
|
||||
userId: $this->userId,
|
||||
requestId: $requestId,
|
||||
sessionId: $this->sessionId,
|
||||
clientIp: $this->clientIp,
|
||||
userAgent: $this->userAgent,
|
||||
tags: $this->tags
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add session ID
|
||||
*/
|
||||
public function withSessionId(string $sessionId): self
|
||||
{
|
||||
return new self(
|
||||
operation: $this->operation,
|
||||
component: $this->component,
|
||||
data: $this->data,
|
||||
debug: $this->debug,
|
||||
metadata: $this->metadata,
|
||||
occurredAt: $this->occurredAt,
|
||||
userId: $this->userId,
|
||||
requestId: $this->requestId,
|
||||
sessionId: $sessionId,
|
||||
clientIp: $this->clientIp,
|
||||
userAgent: $this->userAgent,
|
||||
tags: $this->tags
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add client IP
|
||||
*/
|
||||
public function withClientIp(string $clientIp): self
|
||||
{
|
||||
return new self(
|
||||
operation: $this->operation,
|
||||
component: $this->component,
|
||||
data: $this->data,
|
||||
debug: $this->debug,
|
||||
metadata: $this->metadata,
|
||||
occurredAt: $this->occurredAt,
|
||||
userId: $this->userId,
|
||||
requestId: $this->requestId,
|
||||
sessionId: $this->sessionId,
|
||||
clientIp: $clientIp,
|
||||
userAgent: $this->userAgent,
|
||||
tags: $this->tags
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add user agent
|
||||
*/
|
||||
public function withUserAgent(string $userAgent): self
|
||||
{
|
||||
return new self(
|
||||
operation: $this->operation,
|
||||
component: $this->component,
|
||||
data: $this->data,
|
||||
debug: $this->debug,
|
||||
metadata: $this->metadata,
|
||||
occurredAt: $this->occurredAt,
|
||||
userId: $this->userId,
|
||||
requestId: $this->requestId,
|
||||
sessionId: $this->sessionId,
|
||||
clientIp: $this->clientIp,
|
||||
userAgent: $userAgent,
|
||||
tags: $this->tags
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add tags
|
||||
*/
|
||||
public function withTags(string ...$tags): self
|
||||
{
|
||||
return new self(
|
||||
operation: $this->operation,
|
||||
component: $this->component,
|
||||
data: $this->data,
|
||||
debug: $this->debug,
|
||||
metadata: $this->metadata,
|
||||
occurredAt: $this->occurredAt,
|
||||
userId: $this->userId,
|
||||
requestId: $this->requestId,
|
||||
sessionId: $this->sessionId,
|
||||
clientIp: $this->clientIp,
|
||||
userAgent: $this->userAgent,
|
||||
tags: array_merge($this->tags, $tags)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert to array for serialization
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'operation' => $this->operation,
|
||||
'component' => $this->component,
|
||||
'data' => $this->data,
|
||||
'debug' => $this->debug,
|
||||
'metadata' => $this->metadata,
|
||||
'occurred_at' => $this->occurredAt?->format('Y-m-d H:i:s.u'),
|
||||
'user_id' => $this->userId,
|
||||
'request_id' => $this->requestId,
|
||||
'session_id' => $this->sessionId,
|
||||
'client_ip' => $this->clientIp,
|
||||
'user_agent' => $this->userAgent,
|
||||
'tags' => $this->tags,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\ExceptionHandling\Context;
|
||||
|
||||
use WeakMap;
|
||||
|
||||
/**
|
||||
* Exception Context Provider
|
||||
*
|
||||
* Manages exception context externally using WeakMap for automatic garbage collection.
|
||||
* Context is automatically cleaned up when the exception is garbage collected.
|
||||
*
|
||||
* PHP 8.5+ WeakMap-based implementation - no memory leaks possible.
|
||||
*/
|
||||
final class ExceptionContextProvider
|
||||
{
|
||||
/** @var WeakMap<\Throwable, ExceptionContextData> */
|
||||
private WeakMap $contexts;
|
||||
|
||||
private static ?self $instance = null;
|
||||
|
||||
private function __construct()
|
||||
{
|
||||
$this->contexts = new WeakMap();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get singleton instance
|
||||
*
|
||||
* Singleton pattern ensures consistent context across the application
|
||||
*/
|
||||
public static function instance(): self
|
||||
{
|
||||
return self::$instance ??= new self();
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach context to exception
|
||||
*
|
||||
* @param \Throwable $exception The exception to attach context to
|
||||
* @param ExceptionContextData $context The context data
|
||||
*/
|
||||
public function attach(\Throwable $exception, ExceptionContextData $context): void
|
||||
{
|
||||
$this->contexts[$exception] = $context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get context for exception
|
||||
*
|
||||
* @param \Throwable $exception The exception to get context for
|
||||
* @return ExceptionContextData|null The context data or null if not found
|
||||
*/
|
||||
public function get(\Throwable $exception): ?ExceptionContextData
|
||||
{
|
||||
return $this->contexts[$exception] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if exception has context
|
||||
*
|
||||
* @param \Throwable $exception The exception to check
|
||||
* @return bool True if context exists
|
||||
*/
|
||||
public function has(\Throwable $exception): bool
|
||||
{
|
||||
return isset($this->contexts[$exception]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove context from exception
|
||||
*
|
||||
* Note: Usually not needed due to WeakMap automatic cleanup,
|
||||
* but provided for explicit control if needed.
|
||||
*
|
||||
* @param \Throwable $exception The exception to remove context from
|
||||
*/
|
||||
public function detach(\Throwable $exception): void
|
||||
{
|
||||
unset($this->contexts[$exception]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get statistics about context storage
|
||||
*
|
||||
* @return array{total_contexts: int}
|
||||
*/
|
||||
public function getStats(): array
|
||||
{
|
||||
// WeakMap doesn't provide count(), so we iterate
|
||||
$count = 0;
|
||||
foreach ($this->contexts as $_) {
|
||||
$count++;
|
||||
}
|
||||
|
||||
return [
|
||||
'total_contexts' => $count,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all contexts
|
||||
*
|
||||
* Mainly for testing purposes
|
||||
*/
|
||||
public function clear(): void
|
||||
{
|
||||
$this->contexts = new WeakMap();
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,10 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\ExceptionHandling;
|
||||
|
||||
use App\Framework\ExceptionHandling\Context\ExceptionContextProvider;
|
||||
use App\Framework\ExceptionHandling\Renderers\ResponseErrorRenderer;
|
||||
use App\Framework\ExceptionHandling\Reporter\LogReporter;
|
||||
use App\Framework\Http\Response;
|
||||
use Throwable;
|
||||
|
||||
final readonly class ErrorKernel
|
||||
@@ -25,5 +28,28 @@ final readonly class ErrorKernel
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create HTTP Response from exception without terminating execution
|
||||
*
|
||||
* This method enables middleware recovery patterns by returning a Response
|
||||
* object instead of terminating the application.
|
||||
*
|
||||
* @param Throwable $exception Exception to render
|
||||
* @param ExceptionContextProvider|null $contextProvider Optional WeakMap context provider
|
||||
* @param bool $isDebugMode Enable debug information in response
|
||||
* @return Response HTTP Response object (JSON for API, HTML for web)
|
||||
*/
|
||||
public function createHttpResponse(
|
||||
Throwable $exception,
|
||||
?ExceptionContextProvider $contextProvider = null,
|
||||
bool $isDebugMode = false
|
||||
): Response {
|
||||
// Create ResponseErrorRenderer with debug mode setting
|
||||
$renderer = new ResponseErrorRenderer($isDebugMode);
|
||||
|
||||
// Generate and return Response object
|
||||
return $renderer->createResponse($exception, $contextProvider);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\ExceptionHandling;
|
||||
|
||||
use App\Framework\DI\Initializer;
|
||||
use Fiber;
|
||||
|
||||
final class ErrorScope
|
||||
{
|
||||
private array $stack = [];
|
||||
|
||||
#[Initializer]
|
||||
public static function initialize(): ErrorScope
|
||||
{
|
||||
return new self;
|
||||
}
|
||||
|
||||
public function enter(ErrorScopeContext $context): int
|
||||
{
|
||||
$id = $this->fiberId();
|
||||
$this->stack[$id] ??= [];
|
||||
$this->stack[$id][] = $context;
|
||||
return count($this->stack[$id]);
|
||||
}
|
||||
|
||||
public function current(): ?ErrorScopeContext
|
||||
{
|
||||
$id = $this->fiberId();
|
||||
$stack = $this->stack[$id] ?? [];
|
||||
return end($stack) ?? null;
|
||||
}
|
||||
|
||||
public function leave(int $token): void
|
||||
{
|
||||
$id = $this->fiberId();
|
||||
if(!isset($this->stack[$id])) {
|
||||
return;
|
||||
}
|
||||
while(!empty($this->stack[$id]) && count($this->stack[$id]) >= $token) {
|
||||
array_pop($this->stack[$id]);
|
||||
}
|
||||
if(empty($this->stack[$id])) {
|
||||
unset($this->stack[$id]);
|
||||
}
|
||||
}
|
||||
|
||||
private function fiberId(): int
|
||||
{
|
||||
$fiber = Fiber::getCurrent();
|
||||
return $fiber ? spl_object_id($fiber) : 0;
|
||||
}
|
||||
}
|
||||
200
src/Framework/ExceptionHandling/Factory/ExceptionFactory.php
Normal file
200
src/Framework/ExceptionHandling/Factory/ExceptionFactory.php
Normal file
@@ -0,0 +1,200 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\ExceptionHandling\Factory;
|
||||
|
||||
use App\Framework\ExceptionHandling\Context\ExceptionContextData;
|
||||
use App\Framework\ExceptionHandling\Context\ExceptionContextProvider;
|
||||
use App\Framework\ExceptionHandling\Scope\ErrorScope;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Exception Factory
|
||||
*
|
||||
* Helper for creating exceptions with external context.
|
||||
* Integrates with ErrorScope for automatic context enrichment.
|
||||
*
|
||||
* PHP 8.5+ with WeakMap-based context management.
|
||||
*/
|
||||
final readonly class ExceptionFactory
|
||||
{
|
||||
public function __construct(
|
||||
private ExceptionContextProvider $contextProvider,
|
||||
private ErrorScope $errorScope
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Create exception with context
|
||||
*
|
||||
* @template T of Throwable
|
||||
* @param class-string<T> $exceptionClass
|
||||
* @param string $message
|
||||
* @param ExceptionContextData|null $context
|
||||
* @param Throwable|null $previous
|
||||
* @return T
|
||||
*/
|
||||
public function create(
|
||||
string $exceptionClass,
|
||||
string $message,
|
||||
?ExceptionContextData $context = null,
|
||||
?\Throwable $previous = null
|
||||
): Throwable {
|
||||
// Create slim exception (pure PHP)
|
||||
$exception = new $exceptionClass($message, 0, $previous);
|
||||
|
||||
// Enrich context from current scope
|
||||
$enrichedContext = $this->enrichContext($context);
|
||||
|
||||
// Attach context externally via WeakMap
|
||||
$this->contextProvider->attach($exception, $enrichedContext);
|
||||
|
||||
return $exception;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhance existing exception with context
|
||||
*
|
||||
* Useful for rethrowing exceptions with additional context
|
||||
*
|
||||
* @param Throwable $exception
|
||||
* @param ExceptionContextData $additionalContext
|
||||
* @return Throwable
|
||||
*/
|
||||
public function enhance(
|
||||
Throwable $exception,
|
||||
ExceptionContextData $additionalContext
|
||||
): Throwable {
|
||||
// Get existing context if any
|
||||
$existingContext = $this->contextProvider->get($exception);
|
||||
|
||||
// Merge contexts
|
||||
$mergedContext = $existingContext
|
||||
? $existingContext
|
||||
->addData($additionalContext->data)
|
||||
->addDebug($additionalContext->debug)
|
||||
->addMetadata($additionalContext->metadata)
|
||||
: $additionalContext;
|
||||
|
||||
// Enrich from scope
|
||||
$enrichedContext = $this->enrichContext($mergedContext);
|
||||
|
||||
// Update context
|
||||
$this->contextProvider->attach($exception, $enrichedContext);
|
||||
|
||||
return $exception;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create exception with operation context
|
||||
*
|
||||
* Convenience method for common use case
|
||||
*
|
||||
* @template T of Throwable
|
||||
* @param class-string<T> $exceptionClass
|
||||
* @param string $message
|
||||
* @param string $operation
|
||||
* @param string|null $component
|
||||
* @param array<string, mixed> $data
|
||||
* @param Throwable|null $previous
|
||||
* @return T
|
||||
*/
|
||||
public function forOperation(
|
||||
string $exceptionClass,
|
||||
string $message,
|
||||
string $operation,
|
||||
?string $component = null,
|
||||
array $data = [],
|
||||
?\Throwable $previous = null
|
||||
): Throwable {
|
||||
$context = ExceptionContextData::forOperation($operation, $component)
|
||||
->addData($data);
|
||||
|
||||
return $this->create($exceptionClass, $message, $context, $previous);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create exception with data
|
||||
*
|
||||
* Convenience method for exceptions with data payload
|
||||
*
|
||||
* @template T of Throwable
|
||||
* @param class-string<T> $exceptionClass
|
||||
* @param string $message
|
||||
* @param array<string, mixed> $data
|
||||
* @param Throwable|null $previous
|
||||
* @return T
|
||||
*/
|
||||
public function withData(
|
||||
string $exceptionClass,
|
||||
string $message,
|
||||
array $data,
|
||||
?\Throwable $previous = null
|
||||
): Throwable {
|
||||
$context = ExceptionContextData::withData($data);
|
||||
|
||||
return $this->create($exceptionClass, $message, $context, $previous);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enrich context from current error scope
|
||||
*
|
||||
* @param ExceptionContextData|null $context
|
||||
* @return ExceptionContextData
|
||||
*/
|
||||
private function enrichContext(?ExceptionContextData $context): ExceptionContextData
|
||||
{
|
||||
$scopeContext = $this->errorScope->current();
|
||||
|
||||
if ($scopeContext === null) {
|
||||
return $context ?? ExceptionContextData::empty();
|
||||
}
|
||||
|
||||
// Start with provided context or empty
|
||||
$enriched = $context ?? ExceptionContextData::empty();
|
||||
|
||||
// Enrich with scope data
|
||||
$enriched = $enriched
|
||||
->addMetadata([
|
||||
'scope_type' => $scopeContext->type->value,
|
||||
'scope_id' => $scopeContext->scopeId,
|
||||
]);
|
||||
|
||||
// Add operation/component from scope if not already set
|
||||
if ($enriched->operation === null && $scopeContext->operation !== null) {
|
||||
$enriched = $enriched->withOperation(
|
||||
$scopeContext->operation,
|
||||
$scopeContext->component
|
||||
);
|
||||
}
|
||||
|
||||
// Add user/request/session IDs from scope
|
||||
if ($scopeContext->userId !== null) {
|
||||
$enriched = $enriched->withUserId($scopeContext->userId);
|
||||
}
|
||||
|
||||
if ($scopeContext->requestId !== null) {
|
||||
$enriched = $enriched->withRequestId($scopeContext->requestId);
|
||||
}
|
||||
|
||||
if ($scopeContext->sessionId !== null) {
|
||||
$enriched = $enriched->withSessionId($scopeContext->sessionId);
|
||||
}
|
||||
|
||||
// Extract HTTP fields from scope metadata (for HTTP scopes)
|
||||
if (isset($scopeContext->metadata['ip'])) {
|
||||
$enriched = $enriched->withClientIp($scopeContext->metadata['ip']);
|
||||
}
|
||||
|
||||
if (isset($scopeContext->metadata['user_agent'])) {
|
||||
$enriched = $enriched->withUserAgent($scopeContext->metadata['user_agent']);
|
||||
}
|
||||
|
||||
// Add scope tags
|
||||
if (!empty($scopeContext->tags)) {
|
||||
$enriched = $enriched->withTags(...$scopeContext->tags);
|
||||
}
|
||||
|
||||
return $enriched;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,336 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\ExceptionHandling\Renderers;
|
||||
|
||||
use App\Framework\ExceptionHandling\Context\ExceptionContextProvider;
|
||||
use App\Framework\Http\Response;
|
||||
use App\Framework\Http\Status;
|
||||
|
||||
/**
|
||||
* HTTP Response factory for API and HTML error pages
|
||||
*
|
||||
* Extracts Response generation logic from ErrorKernel for reuse
|
||||
* in middleware recovery patterns.
|
||||
*/
|
||||
final readonly class ResponseErrorRenderer
|
||||
{
|
||||
public function __construct(
|
||||
private bool $isDebugMode = false
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Create HTTP Response from exception
|
||||
*
|
||||
* @param \Throwable $exception Exception to render
|
||||
* @param ExceptionContextProvider|null $contextProvider Optional context provider
|
||||
* @return Response HTTP Response object
|
||||
*/
|
||||
public function createResponse(
|
||||
\Throwable $exception,
|
||||
?ExceptionContextProvider $contextProvider = null
|
||||
): Response {
|
||||
// Determine if API or HTML response needed
|
||||
$isApiRequest = $this->isApiRequest();
|
||||
|
||||
if ($isApiRequest) {
|
||||
return $this->createApiResponse($exception, $contextProvider);
|
||||
}
|
||||
|
||||
return $this->createHtmlResponse($exception, $contextProvider);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create JSON API error response
|
||||
*/
|
||||
private function createApiResponse(
|
||||
\Throwable $exception,
|
||||
?ExceptionContextProvider $contextProvider
|
||||
): Response {
|
||||
$statusCode = $this->getHttpStatusCode($exception);
|
||||
|
||||
$errorData = [
|
||||
'error' => [
|
||||
'message' => $this->isDebugMode
|
||||
? $exception->getMessage()
|
||||
: 'An error occurred while processing your request.',
|
||||
'type' => $this->isDebugMode ? get_class($exception) : 'ServerError',
|
||||
'code' => $exception->getCode(),
|
||||
]
|
||||
];
|
||||
|
||||
// Add debug information if enabled
|
||||
if ($this->isDebugMode) {
|
||||
$errorData['error']['file'] = $exception->getFile();
|
||||
$errorData['error']['line'] = $exception->getLine();
|
||||
$errorData['error']['trace'] = $this->formatStackTrace($exception);
|
||||
|
||||
// Add context from WeakMap if available
|
||||
if ($contextProvider !== null) {
|
||||
$context = $contextProvider->get($exception);
|
||||
if ($context !== null) {
|
||||
$errorData['context'] = [
|
||||
'operation' => $context->operation,
|
||||
'component' => $context->component,
|
||||
'request_id' => $context->requestId,
|
||||
'occurred_at' => $context->occurredAt?->format('Y-m-d H:i:s'),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$body = json_encode($errorData, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
|
||||
|
||||
return new Response(
|
||||
status: Status::from($statusCode),
|
||||
body: $body,
|
||||
headers: [
|
||||
'Content-Type' => 'application/json',
|
||||
'X-Content-Type-Options' => 'nosniff',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create HTML error page response
|
||||
*/
|
||||
private function createHtmlResponse(
|
||||
\Throwable $exception,
|
||||
?ExceptionContextProvider $contextProvider
|
||||
): Response {
|
||||
$statusCode = $this->getHttpStatusCode($exception);
|
||||
|
||||
$html = $this->generateErrorHtml(
|
||||
$exception,
|
||||
$contextProvider,
|
||||
$statusCode
|
||||
);
|
||||
|
||||
return new Response(
|
||||
status: Status::from($statusCode),
|
||||
body: $html,
|
||||
headers: [
|
||||
'Content-Type' => 'text/html; charset=utf-8',
|
||||
'X-Content-Type-Options' => 'nosniff',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate HTML error page
|
||||
*/
|
||||
private function generateErrorHtml(
|
||||
\Throwable $exception,
|
||||
?ExceptionContextProvider $contextProvider,
|
||||
int $statusCode
|
||||
): string {
|
||||
$title = $this->getErrorTitle($statusCode);
|
||||
$message = $this->isDebugMode
|
||||
? $exception->getMessage()
|
||||
: 'An error occurred while processing your request.';
|
||||
|
||||
$debugInfo = '';
|
||||
if ($this->isDebugMode) {
|
||||
$debugInfo = $this->generateDebugSection($exception, $contextProvider);
|
||||
}
|
||||
|
||||
return <<<HTML
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{$title}</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
.error-container {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 2rem;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
h1 {
|
||||
color: #d32f2f;
|
||||
margin-top: 0;
|
||||
}
|
||||
.error-message {
|
||||
background: #fff3cd;
|
||||
border-left: 4px solid #ffc107;
|
||||
padding: 1rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
.debug-info {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 4px;
|
||||
padding: 1rem;
|
||||
margin-top: 2rem;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.debug-info pre {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
.context-item {
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
.context-label {
|
||||
font-weight: bold;
|
||||
color: #666;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="error-container">
|
||||
<h1>{$title}</h1>
|
||||
<div class="error-message">
|
||||
<p>{$message}</p>
|
||||
</div>
|
||||
{$debugInfo}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
HTML;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate debug information section
|
||||
*/
|
||||
private function generateDebugSection(
|
||||
\Throwable $exception,
|
||||
?ExceptionContextProvider $contextProvider
|
||||
): string {
|
||||
$exceptionClass = get_class($exception);
|
||||
$file = $exception->getFile();
|
||||
$line = $exception->getLine();
|
||||
$trace = $this->formatStackTrace($exception);
|
||||
|
||||
$contextHtml = '';
|
||||
if ($contextProvider !== null) {
|
||||
$context = $contextProvider->get($exception);
|
||||
if ($context !== null) {
|
||||
$contextHtml = <<<HTML
|
||||
<div class="context-item">
|
||||
<span class="context-label">Operation:</span> {$context->operation}
|
||||
</div>
|
||||
<div class="context-item">
|
||||
<span class="context-label">Component:</span> {$context->component}
|
||||
</div>
|
||||
<div class="context-item">
|
||||
<span class="context-label">Request ID:</span> {$context->requestId}
|
||||
</div>
|
||||
<div class="context-item">
|
||||
<span class="context-label">Occurred At:</span> {$context->occurredAt?->format('Y-m-d H:i:s')}
|
||||
</div>
|
||||
HTML;
|
||||
}
|
||||
}
|
||||
|
||||
return <<<HTML
|
||||
<div class="debug-info">
|
||||
<h3>Debug Information</h3>
|
||||
<div class="context-item">
|
||||
<span class="context-label">Exception:</span> {$exceptionClass}
|
||||
</div>
|
||||
<div class="context-item">
|
||||
<span class="context-label">File:</span> {$file}:{$line}
|
||||
</div>
|
||||
{$contextHtml}
|
||||
<h4>Stack Trace:</h4>
|
||||
<pre>{$trace}</pre>
|
||||
</div>
|
||||
HTML;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if current request is API request
|
||||
*/
|
||||
private function isApiRequest(): bool
|
||||
{
|
||||
// Check for JSON Accept header
|
||||
$acceptHeader = $_SERVER['HTTP_ACCEPT'] ?? '';
|
||||
if (str_contains($acceptHeader, 'application/json')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for API path prefix
|
||||
$requestUri = $_SERVER['REQUEST_URI'] ?? '';
|
||||
if (str_starts_with($requestUri, '/api/')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for AJAX requests
|
||||
$requestedWith = $_SERVER['HTTP_X_REQUESTED_WITH'] ?? '';
|
||||
if (strtolower($requestedWith) === 'xmlhttprequest') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get HTTP status code from exception
|
||||
*/
|
||||
private function getHttpStatusCode(\Throwable $exception): int
|
||||
{
|
||||
// Use exception code if it's a valid HTTP status code
|
||||
$code = $exception->getCode();
|
||||
if ($code >= 400 && $code < 600) {
|
||||
return $code;
|
||||
}
|
||||
|
||||
// Map common exceptions to status codes
|
||||
return match (true) {
|
||||
$exception instanceof \InvalidArgumentException => 400,
|
||||
$exception instanceof \RuntimeException => 500,
|
||||
$exception instanceof \LogicException => 500,
|
||||
default => 500,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user-friendly error title from status code
|
||||
*/
|
||||
private function getErrorTitle(int $statusCode): string
|
||||
{
|
||||
return match ($statusCode) {
|
||||
400 => 'Bad Request',
|
||||
401 => 'Unauthorized',
|
||||
403 => 'Forbidden',
|
||||
404 => 'Not Found',
|
||||
405 => 'Method Not Allowed',
|
||||
429 => 'Too Many Requests',
|
||||
500 => 'Internal Server Error',
|
||||
502 => 'Bad Gateway',
|
||||
503 => 'Service Unavailable',
|
||||
default => "Error {$statusCode}",
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Format stack trace for display
|
||||
*/
|
||||
private function formatStackTrace(\Throwable $exception): string
|
||||
{
|
||||
$trace = $exception->getTraceAsString();
|
||||
|
||||
// Limit trace depth in production
|
||||
if (!$this->isDebugMode) {
|
||||
$lines = explode("\n", $trace);
|
||||
$trace = implode("\n", array_slice($lines, 0, 5));
|
||||
}
|
||||
|
||||
return htmlspecialchars($trace, ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
}
|
||||
135
src/Framework/ExceptionHandling/Scope/ErrorScope.php
Normal file
135
src/Framework/ExceptionHandling/Scope/ErrorScope.php
Normal file
@@ -0,0 +1,135 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\ExceptionHandling\Scope;
|
||||
|
||||
use App\Framework\DI\Initializer;
|
||||
use Fiber;
|
||||
|
||||
/**
|
||||
* Error Scope Stack Manager
|
||||
*
|
||||
* Manages fiber-aware scope stack for error context enrichment.
|
||||
* Each fiber has its own isolated scope stack.
|
||||
*
|
||||
* PHP 8.5+ with fiber isolation and automatic cleanup.
|
||||
*/
|
||||
final class ErrorScope
|
||||
{
|
||||
/** @var array<int, array<ErrorScopeContext>> Fiber-specific scope stacks */
|
||||
private array $stack = [];
|
||||
|
||||
#[Initializer]
|
||||
public static function initialize(): self
|
||||
{
|
||||
return new self();
|
||||
}
|
||||
|
||||
/**
|
||||
* Enter a new error scope
|
||||
*
|
||||
* @param ErrorScopeContext $context Scope context to enter
|
||||
* @return int Token for leaving this scope (stack depth)
|
||||
*/
|
||||
public function enter(ErrorScopeContext $context): int
|
||||
{
|
||||
$id = $this->fiberId();
|
||||
$this->stack[$id] ??= [];
|
||||
$this->stack[$id][] = $context;
|
||||
|
||||
return count($this->stack[$id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Exit error scope(s)
|
||||
*
|
||||
* @param int $token Token from enter() - exits all scopes until this depth
|
||||
*/
|
||||
public function exit(int $token = 0): void
|
||||
{
|
||||
$id = $this->fiberId();
|
||||
|
||||
if (!isset($this->stack[$id])) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($token === 0) {
|
||||
// Exit only the most recent scope
|
||||
array_pop($this->stack[$id]);
|
||||
} else {
|
||||
// Exit all scopes until token depth
|
||||
while (!empty($this->stack[$id]) && count($this->stack[$id]) >= $token) {
|
||||
array_pop($this->stack[$id]);
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup empty stack
|
||||
if (empty($this->stack[$id])) {
|
||||
unset($this->stack[$id]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current error scope context
|
||||
*
|
||||
* @return ErrorScopeContext|null Current scope or null if no scope active
|
||||
*/
|
||||
public function current(): ?ErrorScopeContext
|
||||
{
|
||||
$id = $this->fiberId();
|
||||
$stack = $this->stack[$id] ?? [];
|
||||
|
||||
$current = end($stack);
|
||||
return $current !== false ? $current : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if any scope is active
|
||||
*/
|
||||
public function hasScope(): bool
|
||||
{
|
||||
$id = $this->fiberId();
|
||||
return !empty($this->stack[$id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get scope depth (number of nested scopes)
|
||||
*/
|
||||
public function depth(): int
|
||||
{
|
||||
$id = $this->fiberId();
|
||||
return count($this->stack[$id] ?? []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get fiber ID for isolation
|
||||
*
|
||||
* Returns 0 for main fiber, unique ID for each Fiber
|
||||
*/
|
||||
private function fiberId(): int
|
||||
{
|
||||
$fiber = Fiber::getCurrent();
|
||||
return $fiber ? spl_object_id($fiber) : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all scopes (for testing/cleanup)
|
||||
*/
|
||||
public function clear(): void
|
||||
{
|
||||
$this->stack = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get statistics for monitoring
|
||||
*/
|
||||
public function getStats(): array
|
||||
{
|
||||
return [
|
||||
'active_fibers' => count($this->stack),
|
||||
'total_scopes' => array_sum(array_map('count', $this->stack)),
|
||||
'max_depth' => !empty($this->stack) ? max(array_map('count', $this->stack)) : 0,
|
||||
];
|
||||
}
|
||||
}
|
||||
276
src/Framework/ExceptionHandling/Scope/ErrorScopeContext.php
Normal file
276
src/Framework/ExceptionHandling/Scope/ErrorScopeContext.php
Normal file
@@ -0,0 +1,276 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\ExceptionHandling\Scope;
|
||||
|
||||
use App\Framework\Http\Request;
|
||||
|
||||
/**
|
||||
* Error Scope Context
|
||||
*
|
||||
* Rich context for error scopes (HTTP, Console, Job, CLI, etc.).
|
||||
* Works with ErrorScope for fiber-aware scope stack management.
|
||||
*
|
||||
* PHP 8.5+ readonly class with factory methods for different scope types.
|
||||
*/
|
||||
final readonly class ErrorScopeContext
|
||||
{
|
||||
/**
|
||||
* @param ErrorScopeType $type Scope type (HTTP, Console, Job, etc.)
|
||||
* @param string $scopeId Unique scope identifier
|
||||
* @param string|null $operation Operation being performed
|
||||
* @param string|null $component Component executing the operation
|
||||
* @param array<string, mixed> $metadata Additional metadata
|
||||
* @param string|null $userId User ID if authenticated
|
||||
* @param string|null $requestId Request ID for HTTP scopes
|
||||
* @param string|null $sessionId Session ID if available
|
||||
* @param string|null $jobId Job ID for background job scopes
|
||||
* @param string|null $commandName Console command name
|
||||
* @param array<string> $tags Tags for categorization
|
||||
*/
|
||||
public function __construct(
|
||||
public ErrorScopeType $type,
|
||||
public string $scopeId,
|
||||
public ?string $operation = null,
|
||||
public ?string $component = null,
|
||||
public array $metadata = [],
|
||||
public ?string $userId = null,
|
||||
public ?string $requestId = null,
|
||||
public ?string $sessionId = null,
|
||||
public ?string $jobId = null,
|
||||
public ?string $commandName = null,
|
||||
public array $tags = [],
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Create HTTP scope from request
|
||||
*/
|
||||
public static function http(
|
||||
Request $request,
|
||||
?string $operation = null,
|
||||
?string $component = null
|
||||
): self {
|
||||
return new self(
|
||||
type: ErrorScopeType::HTTP,
|
||||
scopeId: $request->headers->getFirst('X-Request-ID')
|
||||
?? uniqid('http_', true),
|
||||
operation: $operation,
|
||||
component: $component,
|
||||
metadata: [
|
||||
'method' => $request->method->value,
|
||||
'path' => $request->path,
|
||||
'ip' => $request->server->getRemoteAddr(),
|
||||
'user_agent' => $request->server->getUserAgent(),
|
||||
],
|
||||
requestId: $request->headers->getFirst('X-Request-ID'),
|
||||
sessionId: property_exists($request, 'session') ? $request->session?->getId() : null,
|
||||
userId: property_exists($request, 'user') ? ($request->user?->id ?? null) : null,
|
||||
tags: ['http', 'web']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create console scope
|
||||
*/
|
||||
public static function console(
|
||||
string $commandName,
|
||||
?string $operation = null,
|
||||
?string $component = null
|
||||
): self {
|
||||
return new self(
|
||||
type: ErrorScopeType::CONSOLE,
|
||||
scopeId: uniqid('console_', true),
|
||||
operation: $operation ?? "console.{$commandName}",
|
||||
component: $component,
|
||||
metadata: [
|
||||
'command' => $commandName,
|
||||
'argv' => $_SERVER['argv'] ?? [],
|
||||
'cwd' => getcwd(),
|
||||
],
|
||||
commandName: $commandName,
|
||||
tags: ['console', 'cli']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create background job scope
|
||||
*/
|
||||
public static function job(
|
||||
string $jobId,
|
||||
string $jobClass,
|
||||
?string $operation = null,
|
||||
?string $component = null
|
||||
): self {
|
||||
return new self(
|
||||
type: ErrorScopeType::JOB,
|
||||
scopeId: $jobId,
|
||||
operation: $operation ?? "job.{$jobClass}",
|
||||
component: $component ?? $jobClass,
|
||||
metadata: [
|
||||
'job_class' => $jobClass,
|
||||
'job_id' => $jobId,
|
||||
],
|
||||
jobId: $jobId,
|
||||
tags: ['job', 'background', 'async']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create CLI scope
|
||||
*/
|
||||
public static function cli(
|
||||
string $script,
|
||||
?string $operation = null,
|
||||
?string $component = null
|
||||
): self {
|
||||
return new self(
|
||||
type: ErrorScopeType::CLI,
|
||||
scopeId: uniqid('cli_', true),
|
||||
operation: $operation ?? "cli.{$script}",
|
||||
component: $component,
|
||||
metadata: [
|
||||
'script' => $script,
|
||||
'argv' => $_SERVER['argv'] ?? [],
|
||||
],
|
||||
tags: ['cli', 'script']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create test scope
|
||||
*/
|
||||
public static function test(
|
||||
string $testName,
|
||||
?string $operation = null,
|
||||
?string $component = null
|
||||
): self {
|
||||
return new self(
|
||||
type: ErrorScopeType::TEST,
|
||||
scopeId: uniqid('test_', true),
|
||||
operation: $operation ?? "test.{$testName}",
|
||||
component: $component,
|
||||
metadata: [
|
||||
'test_name' => $testName,
|
||||
],
|
||||
tags: ['test', 'testing']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create generic scope
|
||||
*/
|
||||
public static function generic(
|
||||
string $scopeId,
|
||||
?string $operation = null,
|
||||
?string $component = null
|
||||
): self {
|
||||
return new self(
|
||||
type: ErrorScopeType::GENERIC,
|
||||
scopeId: $scopeId,
|
||||
operation: $operation,
|
||||
component: $component,
|
||||
tags: ['generic']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add operation
|
||||
*/
|
||||
public function withOperation(string $operation, ?string $component = null): self
|
||||
{
|
||||
return new self(
|
||||
type: $this->type,
|
||||
scopeId: $this->scopeId,
|
||||
operation: $operation,
|
||||
component: $component ?? $this->component,
|
||||
metadata: $this->metadata,
|
||||
userId: $this->userId,
|
||||
requestId: $this->requestId,
|
||||
sessionId: $this->sessionId,
|
||||
jobId: $this->jobId,
|
||||
commandName: $this->commandName,
|
||||
tags: $this->tags
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add metadata
|
||||
*/
|
||||
public function addMetadata(array $metadata): self
|
||||
{
|
||||
return new self(
|
||||
type: $this->type,
|
||||
scopeId: $this->scopeId,
|
||||
operation: $this->operation,
|
||||
component: $this->component,
|
||||
metadata: array_merge($this->metadata, $metadata),
|
||||
userId: $this->userId,
|
||||
requestId: $this->requestId,
|
||||
sessionId: $this->sessionId,
|
||||
jobId: $this->jobId,
|
||||
commandName: $this->commandName,
|
||||
tags: $this->tags
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add user ID
|
||||
*/
|
||||
public function withUserId(string $userId): self
|
||||
{
|
||||
return new self(
|
||||
type: $this->type,
|
||||
scopeId: $this->scopeId,
|
||||
operation: $this->operation,
|
||||
component: $this->component,
|
||||
metadata: $this->metadata,
|
||||
userId: $userId,
|
||||
requestId: $this->requestId,
|
||||
sessionId: $this->sessionId,
|
||||
jobId: $this->jobId,
|
||||
commandName: $this->commandName,
|
||||
tags: $this->tags
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add tags
|
||||
*/
|
||||
public function withTags(string ...$tags): self
|
||||
{
|
||||
return new self(
|
||||
type: $this->type,
|
||||
scopeId: $this->scopeId,
|
||||
operation: $this->operation,
|
||||
component: $this->component,
|
||||
metadata: $this->metadata,
|
||||
userId: $this->userId,
|
||||
requestId: $this->requestId,
|
||||
sessionId: $this->sessionId,
|
||||
jobId: $this->jobId,
|
||||
commandName: $this->commandName,
|
||||
tags: array_merge($this->tags, $tags)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert to array
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'type' => $this->type->value,
|
||||
'scope_id' => $this->scopeId,
|
||||
'operation' => $this->operation,
|
||||
'component' => $this->component,
|
||||
'metadata' => $this->metadata,
|
||||
'user_id' => $this->userId,
|
||||
'request_id' => $this->requestId,
|
||||
'session_id' => $this->sessionId,
|
||||
'job_id' => $this->jobId,
|
||||
'command_name' => $this->commandName,
|
||||
'tags' => $this->tags,
|
||||
];
|
||||
}
|
||||
}
|
||||
55
src/Framework/ExceptionHandling/Scope/ErrorScopeType.php
Normal file
55
src/Framework/ExceptionHandling/Scope/ErrorScopeType.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\ExceptionHandling\Scope;
|
||||
|
||||
/**
|
||||
* Error Scope Type
|
||||
*
|
||||
* Defines the different types of error scopes in the application.
|
||||
*/
|
||||
enum ErrorScopeType: string
|
||||
{
|
||||
case HTTP = 'http';
|
||||
case CONSOLE = 'console';
|
||||
case JOB = 'job';
|
||||
case CLI = 'cli';
|
||||
case TEST = 'test';
|
||||
case GENERIC = 'generic';
|
||||
|
||||
/**
|
||||
* Check if scope is web-based
|
||||
*/
|
||||
public function isWeb(): bool
|
||||
{
|
||||
return $this === self::HTTP;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if scope is CLI-based
|
||||
*/
|
||||
public function isCli(): bool
|
||||
{
|
||||
return match ($this) {
|
||||
self::CONSOLE, self::CLI => true,
|
||||
default => false
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if scope is async/background
|
||||
*/
|
||||
public function isAsync(): bool
|
||||
{
|
||||
return $this === self::JOB;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if scope is for testing
|
||||
*/
|
||||
public function isTest(): bool
|
||||
{
|
||||
return $this === self::TEST;
|
||||
}
|
||||
}
|
||||
213
src/Framework/Process/Console/AlertCommands.php
Normal file
213
src/Framework/Process/Console/AlertCommands.php
Normal file
@@ -0,0 +1,213 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Process\Console;
|
||||
|
||||
use App\Framework\Console\ConsoleCommand;
|
||||
use App\Framework\Console\ConsoleInput;
|
||||
use App\Framework\Console\ExitCode;
|
||||
use App\Framework\Process\Services\AlertService;
|
||||
|
||||
/**
|
||||
* Alert Console Commands.
|
||||
*/
|
||||
final readonly class AlertCommands
|
||||
{
|
||||
public function __construct(
|
||||
private AlertService $alertService
|
||||
) {
|
||||
}
|
||||
|
||||
#[ConsoleCommand('alert:check', 'Check all active alerts')]
|
||||
public function check(ConsoleInput $input): int
|
||||
{
|
||||
echo "Checking system alerts...\n\n";
|
||||
|
||||
$report = $this->alertService->checkAlerts();
|
||||
|
||||
echo "╔════════════════════════════════════════════════════════════╗\n";
|
||||
echo "║ ALERT CHECK REPORT ║\n";
|
||||
echo "╚════════════════════════════════════════════════════════════╝\n\n";
|
||||
|
||||
echo "┌─ SUMMARY ────────────────────────────────────────────────┐\n";
|
||||
$counts = $report->getSeverityCounts();
|
||||
echo "│ Total Alerts: " . count($report->alerts) . "\n";
|
||||
echo "│ Active Alerts: " . count($report->getActiveAlerts()) . "\n";
|
||||
echo "│ Critical Alerts: {$counts['critical']}\n";
|
||||
echo "│ Warning Alerts: {$counts['warning']}\n";
|
||||
echo "│ Info Alerts: {$counts['info']}\n";
|
||||
echo "│ Generated At: {$report->generatedAt->format('Y-m-d H:i:s')}\n";
|
||||
echo "└─────────────────────────────────────────────────────────┘\n\n";
|
||||
|
||||
$activeAlerts = $report->getActiveAlerts();
|
||||
|
||||
if (empty($activeAlerts)) {
|
||||
echo "✅ No active alerts!\n";
|
||||
|
||||
return ExitCode::SUCCESS;
|
||||
}
|
||||
|
||||
// Group by severity
|
||||
$criticalAlerts = $report->getCriticalAlerts();
|
||||
$warningAlerts = $report->getWarningAlerts();
|
||||
|
||||
if (! empty($criticalAlerts)) {
|
||||
echo "┌─ CRITICAL ALERTS ───────────────────────────────────────┐\n";
|
||||
foreach ($criticalAlerts as $alert) {
|
||||
echo "│ {$alert->severity->getIcon()} {$alert->name}\n";
|
||||
echo "│ {$alert->message}\n";
|
||||
if ($alert->description !== null) {
|
||||
echo "│ {$alert->description}\n";
|
||||
}
|
||||
if ($alert->triggeredAt !== null) {
|
||||
echo "│ Triggered: {$alert->triggeredAt->format('Y-m-d H:i:s')}\n";
|
||||
}
|
||||
echo "│\n";
|
||||
}
|
||||
echo "└─────────────────────────────────────────────────────────┘\n\n";
|
||||
}
|
||||
|
||||
if (! empty($warningAlerts)) {
|
||||
echo "┌─ WARNING ALERTS ────────────────────────────────────────┐\n";
|
||||
foreach ($warningAlerts as $alert) {
|
||||
echo "│ {$alert->severity->getIcon()} {$alert->name}\n";
|
||||
echo "│ {$alert->message}\n";
|
||||
if ($alert->description !== null) {
|
||||
echo "│ {$alert->description}\n";
|
||||
}
|
||||
if ($alert->triggeredAt !== null) {
|
||||
echo "│ Triggered: {$alert->triggeredAt->format('Y-m-d H:i:s')}\n";
|
||||
}
|
||||
echo "│\n";
|
||||
}
|
||||
echo "└─────────────────────────────────────────────────────────┘\n";
|
||||
}
|
||||
|
||||
if ($report->hasCriticalAlerts()) {
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
if (! empty($warningAlerts)) {
|
||||
return ExitCode::WARNING;
|
||||
}
|
||||
|
||||
return ExitCode::SUCCESS;
|
||||
}
|
||||
|
||||
#[ConsoleCommand('alert:list', 'Show alert history')]
|
||||
public function list(ConsoleInput $input): int
|
||||
{
|
||||
$limit = (int) ($input->getOption('limit') ?? 50);
|
||||
|
||||
echo "Retrieving alert history (limit: {$limit})...\n\n";
|
||||
|
||||
$report = $this->alertService->checkAlerts();
|
||||
|
||||
$allAlerts = $report->alerts;
|
||||
|
||||
// Sort by triggered date (newest first)
|
||||
usort($allAlerts, function ($a, $b) {
|
||||
if ($a->triggeredAt === null && $b->triggeredAt === null) {
|
||||
return 0;
|
||||
}
|
||||
if ($a->triggeredAt === null) {
|
||||
return 1;
|
||||
}
|
||||
if ($b->triggeredAt === null) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return $b->triggeredAt <=> $a->triggeredAt;
|
||||
});
|
||||
|
||||
$displayAlerts = array_slice($allAlerts, 0, $limit);
|
||||
|
||||
echo "╔════════════════════════════════════════════════════════════╗\n";
|
||||
echo "║ ALERT HISTORY ║\n";
|
||||
echo "╚════════════════════════════════════════════════════════════╝\n\n";
|
||||
|
||||
if (empty($displayAlerts)) {
|
||||
echo "ℹ️ No alerts found.\n";
|
||||
|
||||
return ExitCode::SUCCESS;
|
||||
}
|
||||
|
||||
echo "┌─ ALERTS ──────────────────────────────────────────────────┐\n";
|
||||
|
||||
foreach ($displayAlerts as $alert) {
|
||||
$statusIcon = $alert->isActive ? '🔴' : '⚪';
|
||||
echo "│ {$statusIcon} {$alert->severity->getIcon()} {$alert->name}\n";
|
||||
echo "│ {$alert->message}\n";
|
||||
|
||||
if ($alert->triggeredAt !== null) {
|
||||
echo "│ Triggered: {$alert->triggeredAt->format('Y-m-d H:i:s')}\n";
|
||||
}
|
||||
|
||||
echo "│\n";
|
||||
}
|
||||
|
||||
echo "└─────────────────────────────────────────────────────────┘\n";
|
||||
|
||||
return ExitCode::SUCCESS;
|
||||
}
|
||||
|
||||
#[ConsoleCommand('alert:config', 'Configure alert thresholds')]
|
||||
public function config(ConsoleInput $input): int
|
||||
{
|
||||
echo "╔════════════════════════════════════════════════════════════╗\n";
|
||||
echo "║ ALERT CONFIGURATION ║\n";
|
||||
echo "╚════════════════════════════════════════════════════════════╝\n\n";
|
||||
|
||||
$defaultThresholds = AlertService::getDefaultThresholds();
|
||||
|
||||
echo "┌─ DEFAULT THRESHOLDS ──────────────────────────────────────┐\n";
|
||||
|
||||
foreach ($defaultThresholds as $threshold) {
|
||||
echo "│ {$threshold->name}\n";
|
||||
echo "│ Warning: {$threshold->warningThreshold} {$threshold->unit}\n";
|
||||
echo "│ Critical: {$threshold->criticalThreshold} {$threshold->unit}\n";
|
||||
|
||||
if ($threshold->description !== null) {
|
||||
echo "│ Description: {$threshold->description}\n";
|
||||
}
|
||||
|
||||
echo "│\n";
|
||||
}
|
||||
|
||||
echo "└─────────────────────────────────────────────────────────┘\n\n";
|
||||
|
||||
echo "ℹ️ Alert thresholds are currently configured in code.\n";
|
||||
echo " To customize thresholds, modify AlertService::getDefaultThresholds()\n";
|
||||
|
||||
return ExitCode::SUCCESS;
|
||||
}
|
||||
|
||||
#[ConsoleCommand('alert:test', 'Test alert system')]
|
||||
public function test(ConsoleInput $input): int
|
||||
{
|
||||
echo "Testing alert system...\n\n";
|
||||
|
||||
$report = $this->alertService->checkAlerts();
|
||||
|
||||
echo "┌─ TEST RESULTS ────────────────────────────────────────────┐\n";
|
||||
echo "│ Alert System: ✅ Operational\n";
|
||||
echo "│ Health Checks: ✅ Connected\n";
|
||||
echo "│ Active Alerts: " . count($report->getActiveAlerts()) . "\n";
|
||||
echo "│ Critical Alerts: " . count($report->getCriticalAlerts()) . "\n";
|
||||
echo "│ Warning Alerts: " . count($report->getWarningAlerts()) . "\n";
|
||||
echo "└─────────────────────────────────────────────────────────┘\n\n";
|
||||
|
||||
if ($report->hasActiveAlerts()) {
|
||||
echo "⚠️ Active alerts detected. Run 'alert:check' for details.\n";
|
||||
|
||||
return ExitCode::WARNING;
|
||||
}
|
||||
|
||||
echo "✅ No active alerts. System is healthy.\n";
|
||||
|
||||
return ExitCode::SUCCESS;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
325
src/Framework/Process/Console/BackupCommands.php
Normal file
325
src/Framework/Process/Console/BackupCommands.php
Normal file
@@ -0,0 +1,325 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Process\Console;
|
||||
|
||||
use App\Framework\Console\ConsoleCommand;
|
||||
use App\Framework\Console\ConsoleInput;
|
||||
use App\Framework\Console\ExitCode;
|
||||
use App\Framework\Filesystem\ValueObjects\FilePath;
|
||||
use App\Framework\Process\Services\BackupService;
|
||||
use App\Framework\Process\Services\BackupVerificationService;
|
||||
|
||||
/**
|
||||
* Backup Console Commands.
|
||||
*/
|
||||
final readonly class BackupCommands
|
||||
{
|
||||
public function __construct(
|
||||
private BackupVerificationService $backupVerification,
|
||||
private BackupService $backupService
|
||||
) {
|
||||
}
|
||||
|
||||
#[ConsoleCommand('backup:list', 'List all backup files in a directory')]
|
||||
public function list(ConsoleInput $input): int
|
||||
{
|
||||
$directory = $input->getArgument('directory');
|
||||
|
||||
if ($directory === null) {
|
||||
echo "❌ Please provide a directory path.\n";
|
||||
echo "Usage: php console.php backup:list <directory> [--pattern=*.sql]\n";
|
||||
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
try {
|
||||
$dir = FilePath::create($directory);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
echo "❌ Invalid directory path: {$directory}\n";
|
||||
echo "Error: {$e->getMessage()}\n";
|
||||
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
if (! $dir->exists() || ! $dir->isDirectory()) {
|
||||
echo "❌ Directory does not exist or is not a directory: {$directory}\n";
|
||||
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
$pattern = $input->getOption('pattern') ?? '*.sql';
|
||||
|
||||
echo "Searching for backup files matching '{$pattern}' in: {$dir->toString()}\n\n";
|
||||
|
||||
$result = $this->backupVerification->verify($dir, $pattern);
|
||||
|
||||
if (empty($result->backups)) {
|
||||
echo "ℹ️ No backup files found matching pattern '{$pattern}'.\n";
|
||||
|
||||
return ExitCode::SUCCESS;
|
||||
}
|
||||
|
||||
echo "╔════════════════════════════════════════════════════════════╗\n";
|
||||
echo "║ BACKUP FILES ║\n";
|
||||
echo "╚════════════════════════════════════════════════════════════╝\n\n";
|
||||
|
||||
echo "┌─ SUMMARY ───────────────────────────────────────────────┐\n";
|
||||
echo "│ Total Backups: {$result->totalCount}\n";
|
||||
echo "│ Fresh Backups: {$result->getFreshBackupCount()}\n";
|
||||
echo "│ Old Backups: {$result->getOldBackupCount()}\n";
|
||||
|
||||
if ($result->latestBackupDate !== null) {
|
||||
echo "│ Latest Backup: {$result->latestBackupDate->format('Y-m-d H:i:s')}\n";
|
||||
}
|
||||
|
||||
echo "└─────────────────────────────────────────────────────────┘\n\n";
|
||||
|
||||
echo "┌─ BACKUP FILES ────────────────────────────────────────────┐\n";
|
||||
|
||||
foreach ($result->backups as $backup) {
|
||||
$age = $backup->getAge()->toHumanReadable();
|
||||
$freshIcon = $backup->isFresh() ? '✅' : '⏰';
|
||||
echo "│ {$freshIcon} {$backup->name}\n";
|
||||
echo "│ Size: {$backup->size->toHumanReadable()}\n";
|
||||
echo "│ Created: {$backup->createdAt->format('Y-m-d H:i:s')}\n";
|
||||
echo "│ Age: {$age}\n";
|
||||
echo "│ Path: {$backup->path->toString()}\n";
|
||||
echo "│\n";
|
||||
}
|
||||
|
||||
echo "└─────────────────────────────────────────────────────────┘\n";
|
||||
|
||||
return ExitCode::SUCCESS;
|
||||
}
|
||||
|
||||
#[ConsoleCommand('backup:verify', 'Verify backup files in a directory')]
|
||||
public function verify(ConsoleInput $input): int
|
||||
{
|
||||
$directory = $input->getArgument('directory');
|
||||
|
||||
if ($directory === null) {
|
||||
echo "❌ Please provide a directory path.\n";
|
||||
echo "Usage: php console.php backup:verify <directory> [--pattern=*.sql]\n";
|
||||
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
try {
|
||||
$dir = FilePath::create($directory);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
echo "❌ Invalid directory path: {$directory}\n";
|
||||
echo "Error: {$e->getMessage()}\n";
|
||||
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
if (! $dir->exists() || ! $dir->isDirectory()) {
|
||||
echo "❌ Directory does not exist or is not a directory: {$directory}\n";
|
||||
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
$pattern = $input->getOption('pattern') ?? '*.sql';
|
||||
|
||||
echo "Verifying backup files matching '{$pattern}' in: {$dir->toString()}\n\n";
|
||||
|
||||
$result = $this->backupVerification->verify($dir, $pattern);
|
||||
|
||||
echo "╔════════════════════════════════════════════════════════════╗\n";
|
||||
echo "║ BACKUP VERIFICATION ║\n";
|
||||
echo "╚════════════════════════════════════════════════════════════╝\n\n";
|
||||
|
||||
echo "┌─ VERIFICATION RESULTS ───────────────────────────────────┐\n";
|
||||
echo "│ Total Backups: {$result->totalCount}\n";
|
||||
echo "│ Fresh Backups: {$result->getFreshBackupCount()}\n";
|
||||
echo "│ Old Backups: {$result->getOldBackupCount()}\n";
|
||||
|
||||
if ($result->latestBackupDate !== null) {
|
||||
$latestAge = (new \DateTimeImmutable())->getTimestamp() - $result->latestBackupDate->getTimestamp();
|
||||
$latestAgeDays = (int) floor($latestAge / 86400);
|
||||
echo "│ Latest Backup: {$result->latestBackupDate->format('Y-m-d H:i:s')} ({$latestAgeDays} days ago)\n";
|
||||
}
|
||||
|
||||
if ($result->hasFreshBackup()) {
|
||||
echo "│ Status: ✅ Fresh backups available\n";
|
||||
} elseif ($result->latestBackupDate !== null) {
|
||||
echo "│ Status: ⚠️ No fresh backups (latest is older than 24h)\n";
|
||||
} else {
|
||||
echo "│ Status: ❌ No backups found\n";
|
||||
}
|
||||
|
||||
echo "└─────────────────────────────────────────────────────────┘\n";
|
||||
|
||||
if (empty($result->backups)) {
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
if (! $result->hasFreshBackup()) {
|
||||
return ExitCode::WARNING;
|
||||
}
|
||||
|
||||
return ExitCode::SUCCESS;
|
||||
}
|
||||
|
||||
#[ConsoleCommand('backup:check', 'Check integrity of a backup file')]
|
||||
public function check(ConsoleInput $input): int
|
||||
{
|
||||
$filePath = $input->getArgument('file');
|
||||
|
||||
if ($filePath === null) {
|
||||
echo "❌ Please provide a backup file path.\n";
|
||||
echo "Usage: php console.php backup:check <file>\n";
|
||||
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
try {
|
||||
$file = FilePath::create($filePath);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
echo "❌ Invalid file path: {$filePath}\n";
|
||||
echo "Error: {$e->getMessage()}\n";
|
||||
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
if (! $file->exists() || ! $file->isFile()) {
|
||||
echo "❌ File does not exist or is not a file: {$filePath}\n";
|
||||
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
// Create BackupFile from path
|
||||
$size = $file->getSize();
|
||||
$modifiedTime = $file->getModifiedTime();
|
||||
$backupFile = new \App\Framework\Process\ValueObjects\Backup\BackupFile(
|
||||
path: $file,
|
||||
size: $size,
|
||||
createdAt: new \DateTimeImmutable('@' . $modifiedTime),
|
||||
name: $file->getFilename()
|
||||
);
|
||||
|
||||
echo "Checking integrity of: {$file->toString()}\n\n";
|
||||
|
||||
$isValid = $this->backupVerification->checkIntegrity($backupFile);
|
||||
|
||||
echo "┌─ INTEGRITY CHECK ──────────────────────────────────────┐\n";
|
||||
echo "│ File: {$backupFile->name}\n";
|
||||
echo "│ Size: {$backupFile->size->toHumanReadable()}\n";
|
||||
echo "│ Created: {$backupFile->createdAt->format('Y-m-d H:i:s')}\n";
|
||||
|
||||
if ($isValid) {
|
||||
echo "│ Integrity: ✅ File is valid\n";
|
||||
} else {
|
||||
echo "│ Integrity: ❌ File is corrupted or invalid\n";
|
||||
}
|
||||
|
||||
echo "└─────────────────────────────────────────────────────────┘\n";
|
||||
|
||||
return $isValid ? ExitCode::SUCCESS : ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
#[ConsoleCommand('backup:create', 'Create a backup (database, files, or full)')]
|
||||
public function create(ConsoleInput $input): int
|
||||
{
|
||||
$type = $input->getOption('type') ?? 'database';
|
||||
|
||||
echo "Creating {$type} backup...\n\n";
|
||||
|
||||
return match ($type) {
|
||||
'database' => $this->createDatabaseBackup($input),
|
||||
'files' => $this->createFileBackup($input),
|
||||
'full' => $this->createFullBackup($input),
|
||||
default => ExitCode::FAILURE,
|
||||
};
|
||||
}
|
||||
|
||||
private function createDatabaseBackup(ConsoleInput $input): int
|
||||
{
|
||||
$database = $input->getOption('database') ?? 'default';
|
||||
$username = $input->getOption('username') ?? 'root';
|
||||
$password = $input->getOption('password') ?? '';
|
||||
$output = $input->getOption('output');
|
||||
|
||||
if ($output === null) {
|
||||
$output = sys_get_temp_dir() . "/backup_{$database}_" . date('Y-m-d_H-i-s') . '.sql';
|
||||
}
|
||||
|
||||
try {
|
||||
$outputFile = FilePath::create($output);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
echo "❌ Invalid output file: {$output}\n";
|
||||
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
if ($this->backupService->createDatabaseBackup($database, $username, $password, $outputFile)) {
|
||||
echo "✅ Database backup created: {$outputFile->toString()}\n";
|
||||
|
||||
return ExitCode::SUCCESS;
|
||||
}
|
||||
|
||||
echo "❌ Failed to create database backup.\n";
|
||||
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
private function createFileBackup(ConsoleInput $input): int
|
||||
{
|
||||
$source = $input->getOption('source') ?? '/var/www';
|
||||
$output = $input->getOption('output');
|
||||
|
||||
if ($output === null) {
|
||||
$output = sys_get_temp_dir() . '/files_backup_' . date('Y-m-d_H-i-s') . '.tar.gz';
|
||||
}
|
||||
|
||||
try {
|
||||
$sourceDir = FilePath::create($source);
|
||||
$outputFile = FilePath::create($output);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
echo "❌ Invalid path: {$e->getMessage()}\n";
|
||||
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
if ($this->backupService->createFileBackup($sourceDir, $outputFile)) {
|
||||
echo "✅ File backup created: {$outputFile->toString()}\n";
|
||||
|
||||
return ExitCode::SUCCESS;
|
||||
}
|
||||
|
||||
echo "❌ Failed to create file backup.\n";
|
||||
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
private function createFullBackup(ConsoleInput $input): int
|
||||
{
|
||||
$database = $input->getOption('database') ?? 'default';
|
||||
$username = $input->getOption('username') ?? 'root';
|
||||
$password = $input->getOption('password') ?? '';
|
||||
$source = $input->getOption('source') ?? '/var/www';
|
||||
$output = $input->getOption('output') ?? sys_get_temp_dir();
|
||||
|
||||
try {
|
||||
$sourceDir = FilePath::create($source);
|
||||
$outputDir = FilePath::create($output);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
echo "❌ Invalid path: {$e->getMessage()}\n";
|
||||
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
if ($this->backupService->createFullBackup($database, $username, $password, $sourceDir, $outputDir)) {
|
||||
echo "✅ Full backup created in: {$outputDir->toString()}\n";
|
||||
|
||||
return ExitCode::SUCCESS;
|
||||
}
|
||||
|
||||
echo "❌ Failed to create full backup.\n";
|
||||
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
233
src/Framework/Process/Console/HealthCommands.php
Normal file
233
src/Framework/Process/Console/HealthCommands.php
Normal file
@@ -0,0 +1,233 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Process\Console;
|
||||
|
||||
use App\Framework\Console\ConsoleCommand;
|
||||
use App\Framework\Console\ConsoleInput;
|
||||
use App\Framework\Console\ExitCode;
|
||||
use App\Framework\Core\ValueObjects\Duration;
|
||||
use App\Framework\Http\Url\Url;
|
||||
use App\Framework\Process\Services\SystemHealthCheckService;
|
||||
use App\Framework\Process\Services\UrlHealthCheckService;
|
||||
|
||||
/**
|
||||
* Health Console Commands.
|
||||
*/
|
||||
final readonly class HealthCommands
|
||||
{
|
||||
public function __construct(
|
||||
private SystemHealthCheckService $systemHealthCheck,
|
||||
private UrlHealthCheckService $urlHealthCheck
|
||||
) {
|
||||
}
|
||||
|
||||
#[ConsoleCommand('health:check', 'Run system health check')]
|
||||
public function check(ConsoleInput $input): int
|
||||
{
|
||||
echo "Running system health check...\n\n";
|
||||
|
||||
$report = ($this->systemHealthCheck)();
|
||||
|
||||
echo "╔════════════════════════════════════════════════════════════╗\n";
|
||||
echo "║ SYSTEM HEALTH CHECK ║\n";
|
||||
echo "╚════════════════════════════════════════════════════════════╝\n\n";
|
||||
|
||||
$overallStatus = $report->overallStatus;
|
||||
|
||||
echo "┌─ OVERALL STATUS ────────────────────────────────────────┐\n";
|
||||
$statusIcon = match ($overallStatus->value) {
|
||||
'healthy' => '✅',
|
||||
'degraded' => '⚠️',
|
||||
'unhealthy' => '❌',
|
||||
default => '❓',
|
||||
};
|
||||
echo "│ Status: {$statusIcon} {$overallStatus->value}\n";
|
||||
echo "│ Description: {$overallStatus->getDescription()}\n";
|
||||
echo "└─────────────────────────────────────────────────────────┘\n\n";
|
||||
|
||||
echo "┌─ HEALTH CHECKS ─────────────────────────────────────────┐\n";
|
||||
|
||||
foreach ($report->checks as $check) {
|
||||
$icon = match ($check->status->value) {
|
||||
'healthy' => '✅',
|
||||
'degraded' => '⚠️',
|
||||
'unhealthy' => '❌',
|
||||
default => '❓',
|
||||
};
|
||||
|
||||
echo "│ {$icon} {$check->name}\n";
|
||||
echo "│ {$check->message}\n";
|
||||
echo "│ Value: {$check->value} {$check->unit} (Threshold: {$check->threshold} {$check->unit})\n";
|
||||
echo "│\n";
|
||||
}
|
||||
|
||||
echo "└─────────────────────────────────────────────────────────┘\n";
|
||||
|
||||
if (! empty($report->getUnhealthyChecks())) {
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
if (! empty($report->getDegradedChecks())) {
|
||||
return ExitCode::WARNING;
|
||||
}
|
||||
|
||||
return ExitCode::SUCCESS->value;
|
||||
}
|
||||
|
||||
#[ConsoleCommand('health:url', 'Check health of a single URL')]
|
||||
public function url(ConsoleInput $input): int
|
||||
{
|
||||
$urlString = $input->getArgument('url');
|
||||
|
||||
if ($urlString === null) {
|
||||
echo "❌ Please provide a URL to check.\n";
|
||||
echo "Usage: php console.php health:url <url> [--timeout=5]\n";
|
||||
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
try {
|
||||
$url = Url::parse($urlString);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
echo "❌ Invalid URL: {$urlString}\n";
|
||||
echo "Error: {$e->getMessage()}\n";
|
||||
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
$timeoutSeconds = (int) ($input->getOption('timeout') ?? 5);
|
||||
$timeout = Duration::fromSeconds($timeoutSeconds);
|
||||
|
||||
echo "Checking URL: {$url->toString()}...\n\n";
|
||||
|
||||
$result = $this->urlHealthCheck->checkUrl($url, $timeout);
|
||||
|
||||
echo "┌─ URL HEALTH CHECK ──────────────────────────────────────┐\n";
|
||||
echo "│ URL: {$result->url->toString()}\n";
|
||||
|
||||
if ($result->isAccessible) {
|
||||
$statusIcon = $result->isSuccessful() ? '✅' : '⚠️';
|
||||
echo "│ Status: {$statusIcon} {$result->status->value} {$result->status->getDescription()}\n";
|
||||
echo "│ Response Time: {$result->responseTime->toHumanReadable()}\n";
|
||||
|
||||
if ($result->redirectUrl !== null) {
|
||||
echo "│ Redirect: → {$result->redirectUrl->toString()}\n";
|
||||
}
|
||||
|
||||
echo "└─────────────────────────────────────────────────────────┘\n";
|
||||
|
||||
return $result->isSuccessful() ? ExitCode::SUCCESS : ExitCode::WARNING;
|
||||
}
|
||||
|
||||
echo "│ Status: ❌ Not accessible\n";
|
||||
echo "│ Error: {$result->error}\n";
|
||||
echo "└─────────────────────────────────────────────────────────┘\n";
|
||||
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
#[ConsoleCommand('health:urls', 'Check health of multiple URLs')]
|
||||
public function urls(ConsoleInput $input): int
|
||||
{
|
||||
$urlsOption = $input->getOption('urls');
|
||||
|
||||
if ($urlsOption === null) {
|
||||
echo "❌ Please provide URLs to check.\n";
|
||||
echo "Usage: php console.php health:urls --urls=https://example.com,https://google.com [--timeout=5]\n";
|
||||
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
$urlStrings = explode(',', $urlsOption);
|
||||
$timeoutSeconds = (int) ($input->getOption('timeout') ?? 5);
|
||||
$timeout = Duration::fromSeconds($timeoutSeconds);
|
||||
|
||||
$urls = [];
|
||||
foreach ($urlStrings as $urlString) {
|
||||
$urlString = trim($urlString);
|
||||
if (empty($urlString)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$urls[] = Url::parse($urlString);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
echo "⚠️ Invalid URL skipped: {$urlString}\n";
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($urls)) {
|
||||
echo "❌ No valid URLs provided.\n";
|
||||
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
echo "Checking " . count($urls) . " URL(s)...\n\n";
|
||||
|
||||
$results = $this->urlHealthCheck->checkMultipleUrls($urls, $timeout);
|
||||
|
||||
echo "┌─ URL HEALTH CHECKS ─────────────────────────────────────┐\n";
|
||||
|
||||
$allSuccessful = true;
|
||||
|
||||
foreach ($results as $result) {
|
||||
if ($result->isAccessible) {
|
||||
$statusIcon = $result->isSuccessful() ? '✅' : '⚠️';
|
||||
echo "│ {$statusIcon} {$result->url->toString()}\n";
|
||||
echo "│ Status: {$result->status->value} ({$result->responseTime->toHumanReadable()})\n";
|
||||
|
||||
if (! $result->isSuccessful()) {
|
||||
$allSuccessful = false;
|
||||
}
|
||||
} else {
|
||||
echo "│ ❌ {$result->url->toString()}\n";
|
||||
echo "│ Error: {$result->error}\n";
|
||||
$allSuccessful = false;
|
||||
}
|
||||
|
||||
echo "│\n";
|
||||
}
|
||||
|
||||
echo "└─────────────────────────────────────────────────────────┘\n";
|
||||
|
||||
return $allSuccessful ? ExitCode::SUCCESS : ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
#[ConsoleCommand('health:services', 'Check health of common internet services')]
|
||||
public function services(ConsoleInput $input): int
|
||||
{
|
||||
echo "Checking common internet services...\n\n";
|
||||
|
||||
$results = $this->urlHealthCheck->checkCommonServices();
|
||||
|
||||
echo "┌─ COMMON SERVICES HEALTH CHECK ──────────────────────────┐\n";
|
||||
|
||||
$allSuccessful = true;
|
||||
|
||||
foreach ($results as $result) {
|
||||
if ($result->isAccessible) {
|
||||
$statusIcon = $result->isSuccessful() ? '✅' : '⚠️';
|
||||
echo "│ {$statusIcon} {$result->url->toString()}\n";
|
||||
echo "│ Status: {$result->status->value} ({$result->responseTime->toHumanReadable()})\n";
|
||||
|
||||
if (! $result->isSuccessful()) {
|
||||
$allSuccessful = false;
|
||||
}
|
||||
} else {
|
||||
echo "│ ❌ {$result->url->toString()}\n";
|
||||
echo "│ Error: {$result->error}\n";
|
||||
$allSuccessful = false;
|
||||
}
|
||||
|
||||
echo "│\n";
|
||||
}
|
||||
|
||||
echo "└─────────────────────────────────────────────────────────┘\n";
|
||||
|
||||
return $allSuccessful ? ExitCode::SUCCESS : ExitCode::FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
291
src/Framework/Process/Console/LogCommands.php
Normal file
291
src/Framework/Process/Console/LogCommands.php
Normal file
@@ -0,0 +1,291 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Process\Console;
|
||||
|
||||
use App\Framework\Console\ConsoleCommand;
|
||||
use App\Framework\Console\ConsoleInput;
|
||||
use App\Framework\Console\ExitCode;
|
||||
use App\Framework\Filesystem\ValueObjects\FilePath;
|
||||
use App\Framework\Process\Services\LogAnalysisService;
|
||||
|
||||
/**
|
||||
* Log Console Commands.
|
||||
*/
|
||||
final readonly class LogCommands
|
||||
{
|
||||
public function __construct(
|
||||
private LogAnalysisService $logAnalysis
|
||||
) {
|
||||
}
|
||||
|
||||
#[ConsoleCommand('log:tail', 'Display last N lines of a log file')]
|
||||
public function tail(ConsoleInput $input): int
|
||||
{
|
||||
$filePath = $input->getArgument('file');
|
||||
|
||||
if ($filePath === null) {
|
||||
echo "❌ Please provide a log file path.\n";
|
||||
echo "Usage: php console.php log:tail <file> [--lines=100]\n";
|
||||
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
try {
|
||||
$file = FilePath::create($filePath);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
echo "❌ Invalid file path: {$filePath}\n";
|
||||
echo "Error: {$e->getMessage()}\n";
|
||||
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
if (! $file->exists() || ! $file->isFile()) {
|
||||
echo "❌ File does not exist or is not a file: {$filePath}\n";
|
||||
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
$lines = (int) ($input->getOption('lines') ?? 100);
|
||||
|
||||
echo "Showing last {$lines} lines of: {$file->toString()}\n\n";
|
||||
echo "--- LOG OUTPUT ---\n\n";
|
||||
|
||||
$output = $this->logAnalysis->tail($file, $lines);
|
||||
echo $output;
|
||||
|
||||
if (empty($output)) {
|
||||
echo "(No content)\n";
|
||||
}
|
||||
|
||||
return ExitCode::SUCCESS;
|
||||
}
|
||||
|
||||
#[ConsoleCommand('log:errors', 'Find errors in a log file')]
|
||||
public function errors(ConsoleInput $input): int
|
||||
{
|
||||
$filePath = $input->getArgument('file');
|
||||
|
||||
if ($filePath === null) {
|
||||
echo "❌ Please provide a log file path.\n";
|
||||
echo "Usage: php console.php log:errors <file> [--lines=1000]\n";
|
||||
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
try {
|
||||
$file = FilePath::create($filePath);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
echo "❌ Invalid file path: {$filePath}\n";
|
||||
echo "Error: {$e->getMessage()}\n";
|
||||
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
if (! $file->exists() || ! $file->isFile()) {
|
||||
echo "❌ File does not exist or is not a file: {$filePath}\n";
|
||||
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
$lines = (int) ($input->getOption('lines') ?? 1000);
|
||||
|
||||
echo "Searching for errors in: {$file->toString()} (last {$lines} lines)...\n\n";
|
||||
|
||||
$result = $this->logAnalysis->findErrors($file, $lines);
|
||||
|
||||
if (empty($result->entries)) {
|
||||
echo "✅ No errors found!\n";
|
||||
|
||||
return ExitCode::SUCCESS;
|
||||
}
|
||||
|
||||
echo "┌─ ERRORS FOUND ──────────────────────────────────────────┐\n";
|
||||
echo "│ Total Errors: {$result->getErrorCount()}\n";
|
||||
echo "│ Total Lines: {$result->totalLines}\n";
|
||||
echo "└─────────────────────────────────────────────────────────┘\n\n";
|
||||
|
||||
echo "Error Entries:\n";
|
||||
foreach ($result->entries as $entry) {
|
||||
$timestamp = $entry->timestamp?->format('Y-m-d H:i:s') ?? 'N/A';
|
||||
echo " [{$timestamp}] {$entry->level}: {$entry->message}\n";
|
||||
}
|
||||
|
||||
return ExitCode::SUCCESS;
|
||||
}
|
||||
|
||||
#[ConsoleCommand('log:warnings', 'Find warnings in a log file')]
|
||||
public function warnings(ConsoleInput $input): int
|
||||
{
|
||||
$filePath = $input->getArgument('file');
|
||||
|
||||
if ($filePath === null) {
|
||||
echo "❌ Please provide a log file path.\n";
|
||||
echo "Usage: php console.php log:warnings <file> [--lines=1000]\n";
|
||||
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
try {
|
||||
$file = FilePath::create($filePath);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
echo "❌ Invalid file path: {$filePath}\n";
|
||||
echo "Error: {$e->getMessage()}\n";
|
||||
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
if (! $file->exists() || ! $file->isFile()) {
|
||||
echo "❌ File does not exist or is not a file: {$filePath}\n";
|
||||
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
$lines = (int) ($input->getOption('lines') ?? 1000);
|
||||
|
||||
echo "Searching for warnings in: {$file->toString()} (last {$lines} lines)...\n\n";
|
||||
|
||||
$result = $this->logAnalysis->findWarnings($file, $lines);
|
||||
|
||||
if (empty($result->entries)) {
|
||||
echo "✅ No warnings found!\n";
|
||||
|
||||
return ExitCode::SUCCESS;
|
||||
}
|
||||
|
||||
echo "┌─ WARNINGS FOUND ────────────────────────────────────────┐\n";
|
||||
echo "│ Total Warnings: {$result->getWarningCount()}\n";
|
||||
echo "│ Total Lines: {$result->totalLines}\n";
|
||||
echo "└─────────────────────────────────────────────────────────┘\n\n";
|
||||
|
||||
echo "Warning Entries:\n";
|
||||
foreach ($result->entries as $entry) {
|
||||
$timestamp = $entry->timestamp?->format('Y-m-d H:i:s') ?? 'N/A';
|
||||
echo " [{$timestamp}] {$entry->level}: {$entry->message}\n";
|
||||
}
|
||||
|
||||
return ExitCode::SUCCESS;
|
||||
}
|
||||
|
||||
#[ConsoleCommand('log:search', 'Search for a pattern in a log file')]
|
||||
public function search(ConsoleInput $input): int
|
||||
{
|
||||
$filePath = $input->getArgument('file');
|
||||
$pattern = $input->getArgument('pattern');
|
||||
|
||||
if ($filePath === null || $pattern === null) {
|
||||
echo "❌ Please provide a log file path and search pattern.\n";
|
||||
echo "Usage: php console.php log:search <file> <pattern> [--lines=1000]\n";
|
||||
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
try {
|
||||
$file = FilePath::create($filePath);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
echo "❌ Invalid file path: {$filePath}\n";
|
||||
echo "Error: {$e->getMessage()}\n";
|
||||
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
if (! $file->exists() || ! $file->isFile()) {
|
||||
echo "❌ File does not exist or is not a file: {$filePath}\n";
|
||||
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
$lines = (int) ($input->getOption('lines') ?? 1000);
|
||||
|
||||
echo "Searching for '{$pattern}' in: {$file->toString()} (last {$lines} lines)...\n\n";
|
||||
|
||||
$result = $this->logAnalysis->search($file, $pattern, $lines);
|
||||
|
||||
if (empty($result->entries)) {
|
||||
echo "ℹ️ No matches found for pattern: {$pattern}\n";
|
||||
|
||||
return ExitCode::SUCCESS;
|
||||
}
|
||||
|
||||
echo "┌─ SEARCH RESULTS ────────────────────────────────────────┐\n";
|
||||
echo "│ Pattern: {$pattern}\n";
|
||||
echo "│ Matches: " . count($result->entries) . "\n";
|
||||
echo "│ Total Lines: {$result->totalLines}\n";
|
||||
echo "└─────────────────────────────────────────────────────────┘\n\n";
|
||||
|
||||
echo "Matching Entries:\n";
|
||||
foreach ($result->entries as $entry) {
|
||||
$timestamp = $entry->timestamp?->format('Y-m-d H:i:s') ?? 'N/A';
|
||||
echo " [{$timestamp}] {$entry->level}: {$entry->message}\n";
|
||||
}
|
||||
|
||||
return ExitCode::SUCCESS;
|
||||
}
|
||||
|
||||
#[ConsoleCommand('log:stats', 'Show statistics for a log file')]
|
||||
public function stats(ConsoleInput $input): int
|
||||
{
|
||||
$filePath = $input->getArgument('file');
|
||||
|
||||
if ($filePath === null) {
|
||||
echo "❌ Please provide a log file path.\n";
|
||||
echo "Usage: php console.php log:stats <file> [--lines=1000]\n";
|
||||
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
try {
|
||||
$file = FilePath::create($filePath);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
echo "❌ Invalid file path: {$filePath}\n";
|
||||
echo "Error: {$e->getMessage()}\n";
|
||||
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
if (! $file->exists() || ! $file->isFile()) {
|
||||
echo "❌ File does not exist or is not a file: {$filePath}\n";
|
||||
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
$lines = (int) ($input->getOption('lines') ?? 1000);
|
||||
|
||||
echo "Analyzing log file: {$file->toString()} (last {$lines} lines)...\n\n";
|
||||
|
||||
$stats = $this->logAnalysis->getStatistics($file, $lines);
|
||||
|
||||
echo "╔════════════════════════════════════════════════════════════╗\n";
|
||||
echo "║ LOG STATISTICS ║\n";
|
||||
echo "╚════════════════════════════════════════════════════════════╝\n\n";
|
||||
|
||||
echo "┌─ OVERVIEW ──────────────────────────────────────────────┐\n";
|
||||
echo "│ Total Lines: {$stats['total_lines']}\n";
|
||||
echo "│ Errors: {$stats['error_count']}\n";
|
||||
echo "│ Warnings: {$stats['warning_count']}\n";
|
||||
echo "└─────────────────────────────────────────────────────────┘\n\n";
|
||||
|
||||
if (! empty($stats['level_distribution'])) {
|
||||
echo "┌─ LEVEL DISTRIBUTION ───────────────────────────────────┐\n";
|
||||
foreach ($stats['level_distribution'] as $level => $count) {
|
||||
echo "│ {$level}: {$count}\n";
|
||||
}
|
||||
echo "└─────────────────────────────────────────────────────────┘\n\n";
|
||||
}
|
||||
|
||||
if (! empty($stats['top_errors'])) {
|
||||
echo "┌─ TOP ERRORS ──────────────────────────────────────────┐\n";
|
||||
$rank = 1;
|
||||
foreach ($stats['top_errors'] as $message => $count) {
|
||||
echo "│ {$rank}. ({$count}x) {$message}\n";
|
||||
$rank++;
|
||||
}
|
||||
echo "└─────────────────────────────────────────────────────────┘\n";
|
||||
}
|
||||
|
||||
return ExitCode::SUCCESS;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
211
src/Framework/Process/Console/MaintenanceCommands.php
Normal file
211
src/Framework/Process/Console/MaintenanceCommands.php
Normal file
@@ -0,0 +1,211 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Process\Console;
|
||||
|
||||
use App\Framework\Console\ConsoleCommand;
|
||||
use App\Framework\Console\ConsoleInput;
|
||||
use App\Framework\Console\ExitCode;
|
||||
use App\Framework\Core\ValueObjects\Duration;
|
||||
use App\Framework\Filesystem\ValueObjects\FilePath;
|
||||
use App\Framework\Process\Services\MaintenanceService;
|
||||
|
||||
/**
|
||||
* Maintenance Console Commands.
|
||||
*/
|
||||
final readonly class MaintenanceCommands
|
||||
{
|
||||
public function __construct(
|
||||
private MaintenanceService $maintenance
|
||||
) {
|
||||
}
|
||||
|
||||
#[ConsoleCommand('maintenance:clean-temp', 'Clean old temporary files')]
|
||||
public function cleanTemp(ConsoleInput $input): int
|
||||
{
|
||||
$days = (int) ($input->getOption('days') ?? 7);
|
||||
$olderThan = Duration::fromDays($days);
|
||||
|
||||
echo "Cleaning temporary files older than {$days} days...\n";
|
||||
|
||||
$deleted = $this->maintenance->cleanTempFiles($olderThan);
|
||||
|
||||
if ($deleted > 0) {
|
||||
echo "✅ Cleaned {$deleted} temporary file(s).\n";
|
||||
|
||||
return ExitCode::SUCCESS;
|
||||
}
|
||||
|
||||
echo "ℹ️ No temporary files to clean.\n";
|
||||
|
||||
return ExitCode::SUCCESS;
|
||||
}
|
||||
|
||||
#[ConsoleCommand('maintenance:clean-logs', 'Clean old log files')]
|
||||
public function cleanLogs(ConsoleInput $input): int
|
||||
{
|
||||
$directory = $input->getArgument('directory') ?? '/var/log';
|
||||
$days = (int) ($input->getOption('days') ?? 30);
|
||||
|
||||
try {
|
||||
$logDir = FilePath::create($directory);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
echo "❌ Invalid directory: {$directory}\n";
|
||||
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
$olderThan = Duration::fromDays($days);
|
||||
|
||||
echo "Cleaning log files older than {$days} days in: {$logDir->toString()}\n";
|
||||
|
||||
$cleaned = $this->maintenance->cleanLogFiles($logDir, $olderThan);
|
||||
|
||||
echo "✅ Cleaned {$cleaned} log file(s).\n";
|
||||
|
||||
return ExitCode::SUCCESS;
|
||||
}
|
||||
|
||||
#[ConsoleCommand('maintenance:clean-cache', 'Clean cache directories')]
|
||||
public function cleanCache(ConsoleInput $input): int
|
||||
{
|
||||
$directory = $input->getArgument('directory') ?? '/tmp/cache';
|
||||
|
||||
try {
|
||||
$cacheDir = FilePath::create($directory);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
echo "❌ Invalid directory: {$directory}\n";
|
||||
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
echo "Cleaning cache directory: {$cacheDir->toString()}\n";
|
||||
|
||||
if ($this->maintenance->cleanCache($cacheDir)) {
|
||||
echo "✅ Cache cleaned successfully!\n";
|
||||
|
||||
return ExitCode::SUCCESS;
|
||||
}
|
||||
|
||||
echo "❌ Failed to clean cache.\n";
|
||||
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
#[ConsoleCommand('maintenance:clean-old-backups', 'Clean old backup files')]
|
||||
public function cleanOldBackups(ConsoleInput $input): int
|
||||
{
|
||||
$directory = $input->getArgument('directory');
|
||||
$days = (int) ($input->getOption('days') ?? 90);
|
||||
|
||||
if ($directory === null) {
|
||||
echo "❌ Please provide a backup directory.\n";
|
||||
echo "Usage: php console.php maintenance:clean-old-backups <directory> [--days=90]\n";
|
||||
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
try {
|
||||
$backupDir = FilePath::create($directory);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
echo "❌ Invalid directory: {$directory}\n";
|
||||
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
$olderThan = Duration::fromDays($days);
|
||||
|
||||
echo "Cleaning backup files older than {$days} days in: {$backupDir->toString()}\n";
|
||||
|
||||
$cleaned = $this->maintenance->cleanOldBackups($backupDir, $olderThan);
|
||||
|
||||
echo "✅ Cleaned {$cleaned} backup file(s).\n";
|
||||
|
||||
return ExitCode::SUCCESS;
|
||||
}
|
||||
|
||||
#[ConsoleCommand('maintenance:disk-space', 'Show largest directories')]
|
||||
public function diskSpace(ConsoleInput $input): int
|
||||
{
|
||||
$directory = $input->getArgument('directory') ?? '/';
|
||||
$limit = (int) ($input->getOption('limit') ?? 10);
|
||||
|
||||
try {
|
||||
$dir = FilePath::create($directory);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
echo "❌ Invalid directory: {$directory}\n";
|
||||
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
echo "Finding largest directories in: {$dir->toString()}\n\n";
|
||||
|
||||
$directories = $this->maintenance->findLargestDirectories($dir, $limit);
|
||||
|
||||
if (empty($directories)) {
|
||||
echo "ℹ️ No directories found.\n";
|
||||
|
||||
return ExitCode::SUCCESS;
|
||||
}
|
||||
|
||||
echo "┌─ LARGEST DIRECTORIES ────────────────────────────────────┐\n";
|
||||
|
||||
$rank = 1;
|
||||
foreach ($directories as $path => $size) {
|
||||
echo "│ {$rank}. {$path} ({$size})\n";
|
||||
$rank++;
|
||||
}
|
||||
|
||||
echo "└─────────────────────────────────────────────────────────┘\n";
|
||||
|
||||
return ExitCode::SUCCESS;
|
||||
}
|
||||
|
||||
#[ConsoleCommand('maintenance:find-duplicates', 'Find duplicate files')]
|
||||
public function findDuplicates(ConsoleInput $input): int
|
||||
{
|
||||
$directory = $input->getArgument('directory');
|
||||
|
||||
if ($directory === null) {
|
||||
echo "❌ Please provide a directory to search.\n";
|
||||
echo "Usage: php console.php maintenance:find-duplicates <directory>\n";
|
||||
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
try {
|
||||
$dir = FilePath::create($directory);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
echo "❌ Invalid directory: {$directory}\n";
|
||||
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
echo "Searching for duplicate files in: {$dir->toString()}\n\n";
|
||||
|
||||
$duplicates = $this->maintenance->findDuplicateFiles($dir);
|
||||
|
||||
if (empty($duplicates)) {
|
||||
echo "✅ No duplicate files found!\n";
|
||||
|
||||
return ExitCode::SUCCESS;
|
||||
}
|
||||
|
||||
echo "┌─ DUPLICATE FILES ────────────────────────────────────────┐\n";
|
||||
echo "│ Found " . count($duplicates) . " duplicate group(s)\n";
|
||||
echo "└─────────────────────────────────────────────────────────┘\n\n";
|
||||
|
||||
foreach ($duplicates as $hash => $files) {
|
||||
echo "┌─ Hash: {$hash} ────────────────────────────────────────┐\n";
|
||||
foreach ($files as $file) {
|
||||
echo "│ - {$file}\n";
|
||||
}
|
||||
echo "└─────────────────────────────────────────────────────────┘\n\n";
|
||||
}
|
||||
|
||||
return ExitCode::SUCCESS;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
228
src/Framework/Process/Console/NetworkCommands.php
Normal file
228
src/Framework/Process/Console/NetworkCommands.php
Normal file
@@ -0,0 +1,228 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Process\Console;
|
||||
|
||||
use App\Framework\Console\ConsoleCommand;
|
||||
use App\Framework\Console\ConsoleInput;
|
||||
use App\Framework\Console\ExitCode;
|
||||
use App\Framework\Process\Services\NetworkDiagnosticsService;
|
||||
use App\Framework\Process\Services\TcpPortCheckService;
|
||||
|
||||
/**
|
||||
* Network Console Commands.
|
||||
*/
|
||||
final readonly class NetworkCommands
|
||||
{
|
||||
public function __construct(
|
||||
private NetworkDiagnosticsService $networkDiagnostics,
|
||||
private TcpPortCheckService $tcpPortCheck
|
||||
) {
|
||||
}
|
||||
|
||||
#[ConsoleCommand('network:ping', 'Ping a host to check connectivity')]
|
||||
public function ping(ConsoleInput $input): int
|
||||
{
|
||||
$host = $input->getArgument('host');
|
||||
|
||||
if ($host === null) {
|
||||
echo "❌ Please provide a host to ping.\n";
|
||||
echo "Usage: php console.php network:ping <host> [--count=4]\n";
|
||||
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
$count = (int) ($input->getOption('count') ?? 4);
|
||||
|
||||
echo "Pinging {$host} ({$count} packets)...\n\n";
|
||||
|
||||
$result = $this->networkDiagnostics->ping($host, $count);
|
||||
|
||||
if ($result->isReachable) {
|
||||
echo "✅ Host is reachable!\n\n";
|
||||
echo "┌─ PING RESULTS ────────────────────────────────────────┐\n";
|
||||
echo "│ Host: {$result->host}\n";
|
||||
echo "│ Latency: {$result->latency->toHumanReadable()}\n";
|
||||
echo "│ Packets Sent: {$result->packetsSent}\n";
|
||||
echo "│ Packets Received: {$result->packetsReceived}\n";
|
||||
|
||||
if ($result->packetLoss !== null) {
|
||||
echo "│ Packet Loss: {$result->packetLoss}%\n";
|
||||
}
|
||||
|
||||
echo "└─────────────────────────────────────────────────────────┘\n";
|
||||
|
||||
return ExitCode::SUCCESS;
|
||||
}
|
||||
|
||||
echo "❌ Host is not reachable: {$result->host}\n";
|
||||
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
#[ConsoleCommand('network:dns', 'Perform DNS lookup for a hostname')]
|
||||
public function dns(ConsoleInput $input): int
|
||||
{
|
||||
$hostname = $input->getArgument('hostname');
|
||||
|
||||
if ($hostname === null) {
|
||||
echo "❌ Please provide a hostname to resolve.\n";
|
||||
echo "Usage: php console.php network:dns <hostname>\n";
|
||||
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
echo "Performing DNS lookup for {$hostname}...\n\n";
|
||||
|
||||
$result = $this->networkDiagnostics->dnsLookup($hostname);
|
||||
|
||||
if ($result->resolved) {
|
||||
echo "✅ DNS resolution successful!\n\n";
|
||||
echo "┌─ DNS RESULTS ────────────────────────────────────────┐\n";
|
||||
echo "│ Hostname: {$result->hostname}\n";
|
||||
echo "│ Resolved: ✅ Yes\n";
|
||||
echo "│ Addresses: " . count($result->addresses) . "\n\n";
|
||||
|
||||
if (! empty($result->addresses)) {
|
||||
echo "│ IP Addresses:\n";
|
||||
foreach ($result->addresses as $address) {
|
||||
$type = $address->isV4() ? 'IPv4' : 'IPv6';
|
||||
echo "│ - {$address->value} ({$type})\n";
|
||||
}
|
||||
}
|
||||
|
||||
echo "└─────────────────────────────────────────────────────────┘\n";
|
||||
|
||||
return ExitCode::SUCCESS;
|
||||
}
|
||||
|
||||
echo "❌ DNS resolution failed for: {$result->hostname}\n";
|
||||
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
#[ConsoleCommand('network:port', 'Check if a TCP port is open on a host')]
|
||||
public function port(ConsoleInput $input): int
|
||||
{
|
||||
$host = $input->getArgument('host');
|
||||
$port = $input->getArgument('port');
|
||||
|
||||
if ($host === null || $port === null) {
|
||||
echo "❌ Please provide both host and port.\n";
|
||||
echo "Usage: php console.php network:port <host> <port>\n";
|
||||
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
$portNumber = (int) $port;
|
||||
|
||||
if ($portNumber < 1 || $portNumber > 65535) {
|
||||
echo "❌ Port must be between 1 and 65535.\n";
|
||||
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
echo "Checking port {$portNumber} on {$host}...\n\n";
|
||||
|
||||
// Use both services - NetworkDiagnosticsService for detailed info, TcpPortCheckService for simple check
|
||||
$portStatus = $this->networkDiagnostics->checkPort($host, $portNumber);
|
||||
$isOpen = $this->tcpPortCheck->isPortOpen($host, $portNumber);
|
||||
|
||||
echo "┌─ PORT CHECK RESULTS ─────────────────────────────────────┐\n";
|
||||
echo "│ Host: {$host}\n";
|
||||
echo "│ Port: {$portNumber}\n";
|
||||
|
||||
if ($portStatus->isOpen) {
|
||||
echo "│ Status: ✅ Open\n";
|
||||
|
||||
if (! empty($portStatus->service)) {
|
||||
echo "│ Service: {$portStatus->service}\n";
|
||||
}
|
||||
|
||||
echo "└─────────────────────────────────────────────────────────┘\n";
|
||||
|
||||
return ExitCode::SUCCESS;
|
||||
}
|
||||
|
||||
echo "│ Status: ❌ Closed\n";
|
||||
echo "└─────────────────────────────────────────────────────────┘\n";
|
||||
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
#[ConsoleCommand('network:scan', 'Scan multiple ports on a host')]
|
||||
public function scan(ConsoleInput $input): int
|
||||
{
|
||||
$host = $input->getArgument('host');
|
||||
|
||||
if ($host === null) {
|
||||
echo "❌ Please provide a host to scan.\n";
|
||||
echo "Usage: php console.php network:scan <host> [--ports=80,443,22]\n";
|
||||
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
$portsOption = $input->getOption('ports') ?? '80,443,22,21,25,3306,5432';
|
||||
$ports = array_map('intval', explode(',', $portsOption));
|
||||
|
||||
echo "Scanning ports on {$host}...\n\n";
|
||||
|
||||
$results = $this->networkDiagnostics->scanPorts($host, $ports);
|
||||
|
||||
echo "┌─ PORT SCAN RESULTS ─────────────────────────────────────┐\n";
|
||||
echo "│ Host: {$host}\n";
|
||||
echo "│ Ports Scanned: " . count($ports) . "\n\n";
|
||||
|
||||
$openPorts = array_filter($results, fn ($r) => $r->isOpen);
|
||||
$closedPorts = array_filter($results, fn ($r) => ! $r->isOpen);
|
||||
|
||||
if (! empty($openPorts)) {
|
||||
echo "│ Open Ports:\n";
|
||||
foreach ($openPorts as $portStatus) {
|
||||
$service = ! empty($portStatus->service) ? " ({$portStatus->service})" : '';
|
||||
echo "│ ✅ {$portStatus->port}{$service}\n";
|
||||
}
|
||||
echo "│\n";
|
||||
}
|
||||
|
||||
if (! empty($closedPorts)) {
|
||||
echo "│ Closed Ports:\n";
|
||||
foreach ($closedPorts as $portStatus) {
|
||||
echo "│ ❌ {$portStatus->port}\n";
|
||||
}
|
||||
}
|
||||
|
||||
echo "└─────────────────────────────────────────────────────────┘\n";
|
||||
|
||||
return ! empty($openPorts) ? ExitCode::SUCCESS : ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
#[ConsoleCommand('network:connectivity', 'Check connectivity to common internet services')]
|
||||
public function connectivity(ConsoleInput $input): int
|
||||
{
|
||||
echo "Checking connectivity to common internet services...\n\n";
|
||||
|
||||
$results = $this->networkDiagnostics->checkConnectivity();
|
||||
|
||||
echo "┌─ CONNECTIVITY CHECK ─────────────────────────────────────┐\n";
|
||||
|
||||
$allReachable = true;
|
||||
|
||||
foreach ($results as $host => $pingResult) {
|
||||
if ($pingResult->isReachable) {
|
||||
$latency = $pingResult->latency?->toHumanReadable() ?? 'N/A';
|
||||
echo "│ ✅ {$host}: Reachable (Latency: {$latency})\n";
|
||||
} else {
|
||||
echo "│ ❌ {$host}: Not reachable\n";
|
||||
$allReachable = false;
|
||||
}
|
||||
}
|
||||
|
||||
echo "└─────────────────────────────────────────────────────────┘\n";
|
||||
|
||||
return $allReachable ? ExitCode::SUCCESS : ExitCode::FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
298
src/Framework/Process/Console/ProcessCommands.php
Normal file
298
src/Framework/Process/Console/ProcessCommands.php
Normal file
@@ -0,0 +1,298 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Process\Console;
|
||||
|
||||
use App\Framework\Console\ConsoleCommand;
|
||||
use App\Framework\Console\ConsoleInput;
|
||||
use App\Framework\Console\ExitCode;
|
||||
use App\Framework\Core\ValueObjects\Duration;
|
||||
use App\Framework\Process\Process;
|
||||
use App\Framework\Process\Services\ProcessMonitoringService;
|
||||
use App\Framework\Process\ValueObjects\Command;
|
||||
|
||||
/**
|
||||
* Process Console Commands.
|
||||
*/
|
||||
final readonly class ProcessCommands
|
||||
{
|
||||
public function __construct(
|
||||
private ProcessMonitoringService $processMonitoring,
|
||||
private Process $process
|
||||
) {
|
||||
}
|
||||
|
||||
#[ConsoleCommand('process:list', 'List running processes')]
|
||||
public function list(ConsoleInput $input): int
|
||||
{
|
||||
$filter = $input->getOption('filter');
|
||||
|
||||
echo "Listing processes" . ($filter ? " (filter: {$filter})" : '') . "...\n\n";
|
||||
|
||||
$processes = $this->processMonitoring->listProcesses($filter);
|
||||
|
||||
if (empty($processes)) {
|
||||
echo "ℹ️ No processes found.\n";
|
||||
|
||||
return ExitCode::SUCCESS;
|
||||
}
|
||||
|
||||
echo "┌─ RUNNING PROCESSES ───────────────────────────────────────┐\n";
|
||||
echo "│ Found " . count($processes) . " process(es)\n";
|
||||
echo "└─────────────────────────────────────────────────────────┘\n\n";
|
||||
|
||||
echo "┌─ PROCESSES ──────────────────────────────────────────────┐\n";
|
||||
|
||||
foreach ($processes as $proc) {
|
||||
echo "│ PID: {$proc->pid} | {$proc->command}\n";
|
||||
|
||||
if ($proc->user !== null) {
|
||||
echo "│ User: {$proc->user}\n";
|
||||
}
|
||||
|
||||
if ($proc->cpuPercent !== null) {
|
||||
echo "│ CPU: {$proc->cpuPercent}%\n";
|
||||
}
|
||||
|
||||
if ($proc->memoryUsage !== null) {
|
||||
echo "│ Memory: {$proc->memoryUsage->toHumanReadable()}\n";
|
||||
}
|
||||
|
||||
if ($proc->state !== null) {
|
||||
echo "│ State: {$proc->state}\n";
|
||||
}
|
||||
|
||||
echo "│\n";
|
||||
}
|
||||
|
||||
echo "└─────────────────────────────────────────────────────────┘\n";
|
||||
|
||||
return ExitCode::SUCCESS;
|
||||
}
|
||||
|
||||
#[ConsoleCommand('process:find', 'Find processes by name')]
|
||||
public function find(ConsoleInput $input): int
|
||||
{
|
||||
$name = $input->getArgument('name');
|
||||
|
||||
if ($name === null) {
|
||||
echo "❌ Please provide a process name to search for.\n";
|
||||
echo "Usage: php console.php process:find <name>\n";
|
||||
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
echo "Searching for processes matching: {$name}\n\n";
|
||||
|
||||
$processes = $this->processMonitoring->findProcesses($name);
|
||||
|
||||
if (empty($processes)) {
|
||||
echo "ℹ️ No processes found matching '{$name}'.\n";
|
||||
|
||||
return ExitCode::SUCCESS;
|
||||
}
|
||||
|
||||
echo "┌─ FOUND PROCESSES ────────────────────────────────────────┐\n";
|
||||
echo "│ Found " . count($processes) . " process(es) matching '{$name}'\n";
|
||||
echo "└─────────────────────────────────────────────────────────┘\n\n";
|
||||
|
||||
foreach ($processes as $proc) {
|
||||
echo "┌─ {$proc->command} (PID: {$proc->pid}) ────────────────────────────────┐\n";
|
||||
echo "│ PID: {$proc->pid}\n";
|
||||
echo "│ Command: {$proc->command}\n";
|
||||
|
||||
if ($proc->user !== null) {
|
||||
echo "│ User: {$proc->user}\n";
|
||||
}
|
||||
|
||||
if ($proc->cpuPercent !== null) {
|
||||
echo "│ CPU: {$proc->cpuPercent}%\n";
|
||||
}
|
||||
|
||||
if ($proc->memoryUsage !== null) {
|
||||
echo "│ Memory: {$proc->memoryUsage->toHumanReadable()}\n";
|
||||
}
|
||||
|
||||
if ($proc->state !== null) {
|
||||
echo "│ State: {$proc->state}\n";
|
||||
}
|
||||
|
||||
if ($proc->priority !== null) {
|
||||
echo "│ Priority: {$proc->priority}\n";
|
||||
}
|
||||
|
||||
echo "└─────────────────────────────────────────────────────────┘\n\n";
|
||||
}
|
||||
|
||||
return ExitCode::SUCCESS;
|
||||
}
|
||||
|
||||
#[ConsoleCommand('process:kill', 'Kill a process by PID')]
|
||||
public function kill(ConsoleInput $input): int
|
||||
{
|
||||
$pid = $input->getArgument('pid');
|
||||
|
||||
if ($pid === null) {
|
||||
echo "❌ Please provide a process ID to kill.\n";
|
||||
echo "Usage: php console.php process:kill <pid> [--force]\n";
|
||||
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
$pidInt = (int) $pid;
|
||||
$force = $input->hasOption('force');
|
||||
|
||||
// Check if process exists
|
||||
if (! $this->processMonitoring->isProcessRunning($pidInt)) {
|
||||
echo "❌ Process with PID {$pidInt} is not running.\n";
|
||||
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
$procDetails = $this->processMonitoring->getProcessDetails($pidInt);
|
||||
if ($procDetails !== null) {
|
||||
echo "⚠️ About to kill process:\n";
|
||||
echo " PID: {$procDetails->pid}\n";
|
||||
echo " Command: {$procDetails->command}\n";
|
||||
|
||||
if (! $force) {
|
||||
echo "\n⚠️ Use --force to proceed without confirmation.\n";
|
||||
echo " (In production, you should add confirmation prompts)\n";
|
||||
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
$signal = $force ? 'SIGKILL' : 'SIGTERM';
|
||||
$command = Command::fromArray([
|
||||
'kill',
|
||||
$force ? '-9' : '-15',
|
||||
(string) $pidInt,
|
||||
]);
|
||||
|
||||
echo "Sending {$signal} to PID {$pidInt}...\n";
|
||||
|
||||
$result = $this->process->run($command);
|
||||
|
||||
if ($result->isSuccess()) {
|
||||
echo "✅ Process {$pidInt} terminated successfully.\n";
|
||||
|
||||
return ExitCode::SUCCESS;
|
||||
}
|
||||
|
||||
echo "❌ Failed to kill process {$pidInt}.\n";
|
||||
echo " Error: {$result->stderr}\n";
|
||||
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
#[ConsoleCommand('process:tree', 'Show process tree')]
|
||||
public function tree(ConsoleInput $input): int
|
||||
{
|
||||
echo "Building process tree...\n\n";
|
||||
|
||||
$treeData = $this->processMonitoring->getProcessTree();
|
||||
|
||||
echo "┌─ PROCESS TREE ────────────────────────────────────────────┐\n";
|
||||
echo "│ Total Processes: " . count($treeData['tree']) . "\n";
|
||||
echo "│ Root Processes: " . count($treeData['roots']) . "\n";
|
||||
echo "└─────────────────────────────────────────────────────────┘\n\n";
|
||||
|
||||
// Display tree (simplified version)
|
||||
foreach ($treeData['roots'] as $rootPid) {
|
||||
if (! isset($treeData['tree'][$rootPid])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->displayTreeNode($treeData['tree'], $rootPid, 0);
|
||||
}
|
||||
|
||||
return ExitCode::SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Zeigt einen Prozess-Knoten rekursiv an.
|
||||
*
|
||||
* @param array<int, array{pid: int, command: string, children: array}> $tree
|
||||
*/
|
||||
private function displayTreeNode(array $tree, int $pid, int $depth): void
|
||||
{
|
||||
if (! isset($tree[$pid])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$node = $tree[$pid];
|
||||
$indent = str_repeat(' ', $depth);
|
||||
$prefix = $depth > 0 ? '└─ ' : '';
|
||||
|
||||
echo "{$indent}{$prefix}[{$node['pid']}] {$node['command']}\n";
|
||||
|
||||
foreach ($node['children'] as $childPid) {
|
||||
$this->displayTreeNode($tree, $childPid, $depth + 1);
|
||||
}
|
||||
}
|
||||
|
||||
#[ConsoleCommand('process:watch', 'Watch a process in real-time')]
|
||||
public function watch(ConsoleInput $input): int
|
||||
{
|
||||
$pid = $input->getArgument('pid');
|
||||
|
||||
if ($pid === null) {
|
||||
echo "❌ Please provide a process ID to watch.\n";
|
||||
echo "Usage: php console.php process:watch <pid> [--interval=2]\n";
|
||||
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
$pidInt = (int) $pid;
|
||||
$interval = (int) ($input->getOption('interval') ?? 2);
|
||||
|
||||
echo "Watching process {$pidInt} (refresh every {$interval} seconds)...\n";
|
||||
echo "Press Ctrl+C to stop.\n\n";
|
||||
|
||||
$maxIterations = 10; // Limit iterations for safety
|
||||
|
||||
for ($i = 0; $i < $maxIterations; $i++) {
|
||||
$procDetails = $this->processMonitoring->getProcessDetails($pidInt);
|
||||
|
||||
if ($procDetails === null) {
|
||||
echo "\n⚠️ Process {$pidInt} no longer exists.\n";
|
||||
|
||||
return ExitCode::SUCCESS;
|
||||
}
|
||||
|
||||
echo "\n┌─ PROCESS {$pidInt} ─────────────────────────────────────────────┐\n";
|
||||
echo "│ Command: {$procDetails->command}\n";
|
||||
|
||||
if ($procDetails->user !== null) {
|
||||
echo "│ User: {$procDetails->user}\n";
|
||||
}
|
||||
|
||||
if ($procDetails->cpuPercent !== null) {
|
||||
echo "│ CPU: {$procDetails->cpuPercent}%\n";
|
||||
}
|
||||
|
||||
if ($procDetails->memoryUsage !== null) {
|
||||
echo "│ Memory: {$procDetails->memoryUsage->toHumanReadable()}\n";
|
||||
}
|
||||
|
||||
if ($procDetails->state !== null) {
|
||||
echo "│ State: {$procDetails->state}\n";
|
||||
}
|
||||
|
||||
echo "│ Time: " . date('Y-m-d H:i:s') . "\n";
|
||||
echo "└─────────────────────────────────────────────────────────┘\n";
|
||||
|
||||
if ($i < $maxIterations - 1) {
|
||||
sleep($interval);
|
||||
}
|
||||
}
|
||||
|
||||
echo "\n✅ Monitoring stopped after {$maxIterations} iterations.\n";
|
||||
|
||||
return ExitCode::SUCCESS;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
328
src/Framework/Process/Console/SslCommands.php
Normal file
328
src/Framework/Process/Console/SslCommands.php
Normal file
@@ -0,0 +1,328 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Process\Console;
|
||||
|
||||
use App\Framework\Console\ConsoleCommand;
|
||||
use App\Framework\Console\ConsoleInput;
|
||||
use App\Framework\Console\ExitCode;
|
||||
use App\Framework\Process\Services\SslCertificateService;
|
||||
|
||||
/**
|
||||
* SSL Certificate Console Commands.
|
||||
*/
|
||||
final readonly class SslCommands
|
||||
{
|
||||
public function __construct(
|
||||
private SslCertificateService $sslService
|
||||
) {
|
||||
}
|
||||
|
||||
#[ConsoleCommand('ssl:check', 'Check SSL certificate of a domain')]
|
||||
public function check(ConsoleInput $input): int
|
||||
{
|
||||
$domain = $input->getArgument('domain');
|
||||
|
||||
if ($domain === null) {
|
||||
echo "❌ Please provide a domain to check.\n";
|
||||
echo "Usage: php console.php ssl:check <domain> [--port=443]\n";
|
||||
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
$port = (int) ($input->getOption('port') ?? 443);
|
||||
|
||||
echo "Checking SSL certificate for: {$domain}:{$port}\n\n";
|
||||
|
||||
$result = $this->sslService->checkCertificate($domain, $port);
|
||||
|
||||
echo "┌─ SSL CERTIFICATE CHECK ──────────────────────────────────┐\n";
|
||||
echo "│ Domain: {$result->hostname}\n";
|
||||
|
||||
if ($result->isValid) {
|
||||
$cert = $result->certificateInfo;
|
||||
if ($cert !== null) {
|
||||
echo "│ Status: ✅ Valid\n";
|
||||
echo "│ Subject: {$cert->subject}\n";
|
||||
echo "│ Issuer: {$cert->issuer}\n";
|
||||
echo "│ Valid From: {$cert->validFrom->format('Y-m-d H:i:s')}\n";
|
||||
echo "│ Valid To: {$cert->validTo->format('Y-m-d H:i:s')}\n";
|
||||
|
||||
$daysUntilExpiry = $cert->getDaysUntilExpiry();
|
||||
echo "│ Days Until Expiry: {$daysUntilExpiry}\n";
|
||||
|
||||
if ($cert->isExpiringSoon(30)) {
|
||||
echo "│ ⚠️ WARNING: Certificate expires soon!\n";
|
||||
}
|
||||
|
||||
if ($cert->isSelfSigned) {
|
||||
echo "│ ⚠️ WARNING: Certificate is self-signed\n";
|
||||
}
|
||||
|
||||
if (! empty($cert->subjectAltNames)) {
|
||||
echo "│ Subject Alt Names:\n";
|
||||
foreach ($cert->subjectAltNames as $san) {
|
||||
echo "│ - {$san}\n";
|
||||
}
|
||||
}
|
||||
|
||||
if ($cert->serialNumber !== null) {
|
||||
echo "│ Serial Number: {$cert->serialNumber}\n";
|
||||
}
|
||||
|
||||
if ($cert->signatureAlgorithm !== null) {
|
||||
echo "│ Signature Alg: {$cert->signatureAlgorithm}\n";
|
||||
}
|
||||
}
|
||||
} else {
|
||||
echo "│ Status: ❌ Invalid\n";
|
||||
|
||||
if (! empty($result->errors)) {
|
||||
echo "│ Errors:\n";
|
||||
foreach ($result->errors as $error) {
|
||||
echo "│ - {$error}\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($result->hasWarnings()) {
|
||||
echo "│ Warnings:\n";
|
||||
foreach ($result->warnings as $warning) {
|
||||
echo "│ - {$warning}\n";
|
||||
}
|
||||
}
|
||||
|
||||
echo "└─────────────────────────────────────────────────────────┘\n";
|
||||
|
||||
return $result->isValid ? ExitCode::SUCCESS : ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
#[ConsoleCommand('ssl:verify', 'Detailed SSL certificate verification')]
|
||||
public function verify(ConsoleInput $input): int
|
||||
{
|
||||
$domain = $input->getArgument('domain');
|
||||
|
||||
if ($domain === null) {
|
||||
echo "❌ Please provide a domain to verify.\n";
|
||||
echo "Usage: php console.php ssl:verify <domain> [--port=443]\n";
|
||||
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
$port = (int) ($input->getOption('port') ?? 443);
|
||||
|
||||
echo "Verifying SSL certificate for: {$domain}:{$port}\n\n";
|
||||
|
||||
$result = $this->sslService->checkCertificate($domain, $port);
|
||||
|
||||
echo "╔════════════════════════════════════════════════════════════╗\n";
|
||||
echo "║ SSL CERTIFICATE VERIFICATION ║\n";
|
||||
echo "╚════════════════════════════════════════════════════════════╝\n\n";
|
||||
|
||||
echo "┌─ VERIFICATION RESULTS ───────────────────────────────────┐\n";
|
||||
echo "│ Domain: {$result->hostname}\n";
|
||||
echo "│ Port: {$port}\n";
|
||||
|
||||
$statusIcon = $result->isValid ? '✅' : '❌';
|
||||
echo "│ Overall Status: {$statusIcon} " . ($result->isValid ? 'Valid' : 'Invalid') . "\n";
|
||||
echo "└─────────────────────────────────────────────────────────┘\n\n";
|
||||
|
||||
if ($result->certificateInfo !== null) {
|
||||
$cert = $result->certificateInfo;
|
||||
|
||||
echo "┌─ CERTIFICATE DETAILS ───────────────────────────────────┐\n";
|
||||
echo "│ Subject: {$cert->subject}\n";
|
||||
echo "│ Issuer: {$cert->issuer}\n";
|
||||
echo "│ Valid From: {$cert->validFrom->format('Y-m-d H:i:s')}\n";
|
||||
echo "│ Valid To: {$cert->validTo->format('Y-m-d H:i:s')}\n";
|
||||
echo "│ Days Until Expiry: {$cert->getDaysUntilExpiry()}\n";
|
||||
echo "│ Is Self-Signed: " . ($cert->isSelfSigned ? 'Yes' : 'No') . "\n";
|
||||
|
||||
if ($cert->serialNumber !== null) {
|
||||
echo "│ Serial Number: {$cert->serialNumber}\n";
|
||||
}
|
||||
|
||||
if ($cert->signatureAlgorithm !== null) {
|
||||
echo "│ Signature Alg: {$cert->signatureAlgorithm}\n";
|
||||
}
|
||||
|
||||
if (! empty($cert->subjectAltNames)) {
|
||||
echo "│\n│ Subject Alternative Names:\n";
|
||||
foreach ($cert->subjectAltNames as $san) {
|
||||
echo "│ - {$san}\n";
|
||||
}
|
||||
}
|
||||
|
||||
echo "└─────────────────────────────────────────────────────────┘\n\n";
|
||||
|
||||
// Validation checks
|
||||
echo "┌─ VALIDATION CHECKS ────────────────────────────────────┐\n";
|
||||
|
||||
$checks = [
|
||||
'Certificate is valid' => $cert->isValid(),
|
||||
'Certificate is not expired' => ! $cert->isExpired(),
|
||||
'Certificate is not expiring soon (30 days)' => ! $cert->isExpiringSoon(30),
|
||||
'Certificate is not self-signed' => ! $cert->isSelfSigned,
|
||||
];
|
||||
|
||||
foreach ($checks as $check => $passed) {
|
||||
$icon = $passed ? '✅' : '❌';
|
||||
echo "│ {$icon} {$check}\n";
|
||||
}
|
||||
|
||||
echo "└─────────────────────────────────────────────────────────┘\n";
|
||||
}
|
||||
|
||||
if (! empty($result->errors)) {
|
||||
echo "\n┌─ ERRORS ───────────────────────────────────────────────┐\n";
|
||||
foreach ($result->errors as $error) {
|
||||
echo "│ ❌ {$error}\n";
|
||||
}
|
||||
echo "└─────────────────────────────────────────────────────────┘\n";
|
||||
}
|
||||
|
||||
if ($result->hasWarnings()) {
|
||||
echo "\n┌─ WARNINGS ─────────────────────────────────────────────┐\n";
|
||||
foreach ($result->warnings as $warning) {
|
||||
echo "│ ⚠️ {$warning}\n";
|
||||
}
|
||||
echo "└─────────────────────────────────────────────────────────┘\n";
|
||||
}
|
||||
|
||||
return $result->isValid && ! $result->hasWarnings() ? ExitCode::SUCCESS : ExitCode::WARNING;
|
||||
}
|
||||
|
||||
#[ConsoleCommand('ssl:expiring', 'List domains with expiring certificates')]
|
||||
public function expiring(ConsoleInput $input): int
|
||||
{
|
||||
$domainsOption = $input->getOption('domains');
|
||||
$threshold = (int) ($input->getOption('threshold') ?? 30);
|
||||
|
||||
if ($domainsOption === null) {
|
||||
echo "❌ Please provide domains to check.\n";
|
||||
echo "Usage: php console.php ssl:expiring --domains=example.com,google.com [--threshold=30]\n";
|
||||
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
$domains = array_map('trim', explode(',', $domainsOption));
|
||||
$domains = array_filter($domains);
|
||||
|
||||
if (empty($domains)) {
|
||||
echo "❌ No valid domains provided.\n";
|
||||
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
echo "Checking {$threshold} days threshold for " . count($domains) . " domain(s)...\n\n";
|
||||
|
||||
$results = $this->sslService->findExpiringCertificates($domains, $threshold);
|
||||
|
||||
if (empty($results)) {
|
||||
echo "✅ No certificates expiring within {$threshold} days!\n";
|
||||
|
||||
return ExitCode::SUCCESS;
|
||||
}
|
||||
|
||||
echo "┌─ EXPIRING CERTIFICATES ───────────────────────────────────┐\n";
|
||||
echo "│ Found " . count($results) . " certificate(s) expiring within {$threshold} days:\n";
|
||||
echo "└─────────────────────────────────────────────────────────┘\n\n";
|
||||
|
||||
foreach ($results as $result) {
|
||||
$cert = $result->certificateInfo;
|
||||
if ($cert === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$daysUntilExpiry = $cert->getDaysUntilExpiry();
|
||||
|
||||
echo "┌─ {$result->hostname} ─────────────────────────────────────────────┐\n";
|
||||
echo "│ Days Until Expiry: {$daysUntilExpiry}\n";
|
||||
echo "│ Valid To: {$cert->validTo->format('Y-m-d H:i:s')}\n";
|
||||
echo "│ Subject: {$cert->subject}\n";
|
||||
echo "│ Issuer: {$cert->issuer}\n";
|
||||
|
||||
if (! empty($result->warnings)) {
|
||||
echo "│ Warnings:\n";
|
||||
foreach ($result->warnings as $warning) {
|
||||
echo "│ - {$warning}\n";
|
||||
}
|
||||
}
|
||||
|
||||
echo "└─────────────────────────────────────────────────────────┘\n\n";
|
||||
}
|
||||
|
||||
return ExitCode::WARNING;
|
||||
}
|
||||
|
||||
#[ConsoleCommand('ssl:info', 'Show detailed SSL certificate information')]
|
||||
public function info(ConsoleInput $input): int
|
||||
{
|
||||
$domain = $input->getArgument('domain');
|
||||
|
||||
if ($domain === null) {
|
||||
echo "❌ Please provide a domain to check.\n";
|
||||
echo "Usage: php console.php ssl:info <domain> [--port=443]\n";
|
||||
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
$port = (int) ($input->getOption('port') ?? 443);
|
||||
|
||||
echo "Retrieving SSL certificate information for: {$domain}:{$port}\n\n";
|
||||
|
||||
$cert = $this->sslService->getCertificateInfo($domain, $port);
|
||||
|
||||
if ($cert === null) {
|
||||
echo "❌ Could not retrieve certificate information for {$domain}:{$port}\n";
|
||||
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
echo "╔════════════════════════════════════════════════════════════╗\n";
|
||||
echo "║ SSL CERTIFICATE INFORMATION ║\n";
|
||||
echo "╚════════════════════════════════════════════════════════════╝\n\n";
|
||||
|
||||
echo "┌─ CERTIFICATE INFORMATION ────────────────────────────────┐\n";
|
||||
echo "│ Subject: {$cert->subject}\n";
|
||||
echo "│ Issuer: {$cert->issuer}\n";
|
||||
echo "│ Valid From: {$cert->validFrom->format('Y-m-d H:i:s')}\n";
|
||||
echo "│ Valid To: {$cert->validTo->format('Y-m-d H:i:s')}\n";
|
||||
echo "│ Days Until Expiry: {$cert->getDaysUntilExpiry()}\n";
|
||||
echo "│ Is Self-Signed: " . ($cert->isSelfSigned ? 'Yes' : 'No') . "\n";
|
||||
|
||||
if ($cert->serialNumber !== null) {
|
||||
echo "│ Serial Number: {$cert->serialNumber}\n";
|
||||
}
|
||||
|
||||
if ($cert->signatureAlgorithm !== null) {
|
||||
echo "│ Signature Alg: {$cert->signatureAlgorithm}\n";
|
||||
}
|
||||
|
||||
echo "└─────────────────────────────────────────────────────────┘\n\n";
|
||||
|
||||
echo "┌─ VALIDITY STATUS ─────────────────────────────────────────┐\n";
|
||||
$validIcon = $cert->isValid() ? '✅' : '❌';
|
||||
echo "│ Is Valid: {$validIcon} " . ($cert->isValid() ? 'Yes' : 'No') . "\n";
|
||||
|
||||
$expiredIcon = $cert->isExpired() ? '❌' : '✅';
|
||||
echo "│ Is Expired: {$expiredIcon} " . ($cert->isExpired() ? 'Yes' : 'No') . "\n";
|
||||
|
||||
$expiringIcon = $cert->isExpiringSoon(30) ? '⚠️' : '✅';
|
||||
echo "│ Expiring Soon: {$expiringIcon} " . ($cert->isExpiringSoon(30) ? 'Yes (within 30 days)' : 'No') . "\n";
|
||||
echo "└─────────────────────────────────────────────────────────┘\n\n";
|
||||
|
||||
if (! empty($cert->subjectAltNames)) {
|
||||
echo "┌─ SUBJECT ALTERNATIVE NAMES ─────────────────────────────┐\n";
|
||||
foreach ($cert->subjectAltNames as $san) {
|
||||
echo "│ - {$san}\n";
|
||||
}
|
||||
echo "└─────────────────────────────────────────────────────────┘\n";
|
||||
}
|
||||
|
||||
return ExitCode::SUCCESS;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
214
src/Framework/Process/Console/SystemCommands.php
Normal file
214
src/Framework/Process/Console/SystemCommands.php
Normal file
@@ -0,0 +1,214 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Process\Console;
|
||||
|
||||
use App\Framework\Console\ConsoleCommand;
|
||||
use App\Framework\Console\ConsoleInput;
|
||||
use App\Framework\Console\ConsoleOutput;
|
||||
use App\Framework\Console\ExitCode;
|
||||
use App\Framework\Process\Services\SystemHealthCheckService;
|
||||
use App\Framework\Process\Services\SystemInfoService;
|
||||
|
||||
/**
|
||||
* System Console Commands.
|
||||
*/
|
||||
final readonly class SystemCommands
|
||||
{
|
||||
public function __construct(
|
||||
private SystemInfoService $systemInfo,
|
||||
private SystemHealthCheckService $healthCheck
|
||||
) {
|
||||
}
|
||||
|
||||
#[ConsoleCommand('system:info', 'Display system information (uptime, load, memory, disk, CPU)')]
|
||||
public function info(ConsoleInput $input, ConsoleOutput $output): int
|
||||
{
|
||||
$info = ($this->systemInfo)();
|
||||
|
||||
echo "╔════════════════════════════════════════════════════════════╗\n";
|
||||
echo "║ SYSTEM INFORMATION ║\n";
|
||||
echo "╚════════════════════════════════════════════════════════════╝\n\n";
|
||||
|
||||
// Uptime
|
||||
echo "┌─ UPTIME ────────────────────────────────────────────────┐\n";
|
||||
echo "│ Boot Time: {$info->uptime->getBootTimeFormatted()}\n";
|
||||
echo "│ Uptime: {$info->uptime->uptime->toHumanReadable()} ({$info->uptime->getUptimeDays()} days)\n";
|
||||
echo "└─────────────────────────────────────────────────────────┘\n\n";
|
||||
|
||||
// Load Average
|
||||
echo "┌─ LOAD AVERAGE ──────────────────────────────────────────┐\n";
|
||||
$util = round($info->load->getUtilization($info->cpu->cores) * 100, 1);
|
||||
echo "│ 1 min: {$info->load->oneMinute}\n";
|
||||
echo "│ 5 min: {$info->load->fiveMinutes}\n";
|
||||
echo "│ 15 min: {$info->load->fifteenMinutes}\n";
|
||||
echo "│ CPU Usage: {$util}%\n";
|
||||
echo "└─────────────────────────────────────────────────────────┘\n\n";
|
||||
|
||||
// CPU
|
||||
echo "┌─ CPU ───────────────────────────────────────────────────┐\n";
|
||||
echo "│ Cores: {$info->cpu->cores}\n";
|
||||
echo "│ Model: {$info->cpu->getShortModel()}\n";
|
||||
echo "└─────────────────────────────────────────────────────────┘\n\n";
|
||||
|
||||
// Memory
|
||||
echo "┌─ MEMORY ────────────────────────────────────────────────┐\n";
|
||||
echo "│ Total: {$info->memory->getTotal()->toHumanReadable()}\n";
|
||||
echo "│ Used: {$info->memory->getUsed()->toHumanReadable()} ({$info->memory->getUsagePercentage()}%)\n";
|
||||
echo "│ Free: {$info->memory->getFree()->toHumanReadable()}\n";
|
||||
echo "│ Available: {$info->memory->getAvailable()->toHumanReadable()}\n";
|
||||
echo "└─────────────────────────────────────────────────────────┘\n\n";
|
||||
|
||||
// Disk
|
||||
echo "┌─ DISK ({$info->disk->mountPoint}) ─────────────────────────────────────────────┐\n";
|
||||
echo "│ Total: {$info->disk->getTotal()->toHumanReadable()}\n";
|
||||
echo "│ Used: {$info->disk->getUsed()->toHumanReadable()} ({$info->disk->getUsagePercentage()}%)\n";
|
||||
echo "│ Available: {$info->disk->getAvailable()->toHumanReadable()}\n";
|
||||
|
||||
if ($info->disk->isAlmostFull()) {
|
||||
echo "│ ⚠️ WARNING: Disk is almost full!\n";
|
||||
}
|
||||
|
||||
echo "└─────────────────────────────────────────────────────────┘\n\n";
|
||||
|
||||
// Processes
|
||||
echo "┌─ PROCESSES ─────────────────────────────────────────────┐\n";
|
||||
echo "│ Total: {$info->processes->total}\n";
|
||||
echo "│ Running: {$info->processes->running}\n";
|
||||
echo "│ Sleeping: {$info->processes->sleeping}\n";
|
||||
echo "│ Other: {$info->processes->getOther()}\n";
|
||||
echo "└─────────────────────────────────────────────────────────┘\n";
|
||||
|
||||
return ExitCode::SUCCESS->value;
|
||||
}
|
||||
|
||||
#[ConsoleCommand('system:health', 'Display system health check report')]
|
||||
public function health(ConsoleInput $input, ConsoleOutput $output): int
|
||||
{
|
||||
$report = ($this->healthCheck)();
|
||||
|
||||
echo "╔════════════════════════════════════════════════════════════╗\n";
|
||||
echo "║ SYSTEM HEALTH CHECK ║\n";
|
||||
echo "╚════════════════════════════════════════════════════════════╝\n\n";
|
||||
|
||||
$overallStatus = $report->overallStatus;
|
||||
|
||||
echo "┌─ OVERALL STATUS ────────────────────────────────────────┐\n";
|
||||
$statusIcon = match ($overallStatus->value) {
|
||||
'healthy' => '✅',
|
||||
'degraded' => '⚠️',
|
||||
'unhealthy' => '❌',
|
||||
default => '❓',
|
||||
};
|
||||
echo "│ Status: {$statusIcon} {$overallStatus->value}\n";
|
||||
echo "└─────────────────────────────────────────────────────────┘\n\n";
|
||||
|
||||
echo "┌─ HEALTH CHECKS ─────────────────────────────────────────┐\n";
|
||||
|
||||
foreach ($report->checks as $check) {
|
||||
$icon = match ($check->status->value) {
|
||||
'healthy' => '✅',
|
||||
'degraded' => '⚠️',
|
||||
'unhealthy' => '❌',
|
||||
default => '❓',
|
||||
};
|
||||
|
||||
echo "│ {$icon} {$check->name}\n";
|
||||
echo "│ {$check->message}\n";
|
||||
echo "│ Value: {$check->value} {$check->unit} (Threshold: {$check->threshold} {$check->unit})\n";
|
||||
echo "│\n";
|
||||
}
|
||||
|
||||
echo "└─────────────────────────────────────────────────────────┘\n";
|
||||
|
||||
if (! empty($report->getUnhealthyChecks())) {
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
if (! empty($report->getDegradedChecks())) {
|
||||
return ExitCode::WARNING;
|
||||
}
|
||||
|
||||
return ExitCode::SUCCESS->value;
|
||||
}
|
||||
|
||||
#[ConsoleCommand('system:uptime', 'Display system uptime information')]
|
||||
public function uptime(ConsoleInput $input, ConsoleOutput $output): int
|
||||
{
|
||||
$uptime = $this->systemInfo->getUptime();
|
||||
|
||||
echo "╔════════════════════════════════════════════════════════════╗\n";
|
||||
echo "║ SYSTEM UPTIME ║\n";
|
||||
echo "╚════════════════════════════════════════════════════════════╝\n\n";
|
||||
|
||||
echo "┌─ UPTIME ────────────────────────────────────────────────┐\n";
|
||||
echo "│ Boot Time: {$uptime->getBootTimeFormatted()}\n";
|
||||
echo "│ Uptime: {$uptime->uptime->toHumanReadable()}\n";
|
||||
echo "│ Days: {$uptime->getUptimeDays()} days\n";
|
||||
echo "│ Hours: " . round($uptime->uptime->toHours(), 1) . " hours\n";
|
||||
echo "│ Minutes: " . round($uptime->uptime->toMinutes(), 1) . " minutes\n";
|
||||
echo "│ Seconds: " . round($uptime->uptime->toSeconds(), 1) . " seconds\n";
|
||||
echo "└─────────────────────────────────────────────────────────┘\n";
|
||||
|
||||
return ExitCode::SUCCESS->value;
|
||||
}
|
||||
|
||||
#[ConsoleCommand('system:memory', 'Display system memory information')]
|
||||
public function memory(ConsoleInput $input, ConsoleOutput $output): int
|
||||
{
|
||||
$memory = $this->systemInfo->getMemoryInfo();
|
||||
|
||||
echo "╔════════════════════════════════════════════════════════════╗\n";
|
||||
echo "║ MEMORY INFORMATION ║\n";
|
||||
echo "╚════════════════════════════════════════════════════════════╝\n\n";
|
||||
|
||||
echo "┌─ MEMORY ────────────────────────────────────────────────┐\n";
|
||||
echo "│ Total: {$memory->getTotal()->toHumanReadable()}\n";
|
||||
echo "│ Used: {$memory->getUsed()->toHumanReadable()} ({$memory->getUsagePercentage()}%)\n";
|
||||
echo "│ Free: {$memory->getFree()->toHumanReadable()}\n";
|
||||
echo "│ Available: {$memory->getAvailable()->toHumanReadable()}\n";
|
||||
|
||||
$usage = $memory->getUsagePercentage();
|
||||
if ($usage >= 90) {
|
||||
echo "│ ❌ CRITICAL: Memory usage is {$usage}%!\n";
|
||||
} elseif ($usage >= 80) {
|
||||
echo "│ ⚠️ WARNING: Memory usage is {$usage}%!\n";
|
||||
} else {
|
||||
echo "│ ✅ Memory usage is normal\n";
|
||||
}
|
||||
|
||||
echo "└─────────────────────────────────────────────────────────┘\n";
|
||||
|
||||
return ExitCode::SUCCESS->value;
|
||||
}
|
||||
|
||||
#[ConsoleCommand('system:disk', 'Display system disk information')]
|
||||
public function disk(ConsoleInput $input, ConsoleOutput $output): int
|
||||
{
|
||||
$disk = $this->systemInfo->getDiskInfo();
|
||||
|
||||
echo "╔════════════════════════════════════════════════════════════╗\n";
|
||||
echo "║ DISK INFORMATION ║\n";
|
||||
echo "╚════════════════════════════════════════════════════════════╝\n\n";
|
||||
|
||||
echo "┌─ DISK ({$disk->mountPoint}) ─────────────────────────────────────────────┐\n";
|
||||
echo "│ Total: {$disk->getTotal()->toHumanReadable()}\n";
|
||||
echo "│ Used: {$disk->getUsed()->toHumanReadable()} ({$disk->getUsagePercentage()}%)\n";
|
||||
echo "│ Available: {$disk->getAvailable()->toHumanReadable()}\n";
|
||||
|
||||
$usage = $disk->getUsagePercentage();
|
||||
if ($disk->isAlmostFull()) {
|
||||
echo "│ ❌ CRITICAL: Disk is almost full ({$usage}%)!\n";
|
||||
} elseif ($usage >= 80) {
|
||||
echo "│ ⚠️ WARNING: Disk usage is {$usage}%!\n";
|
||||
} else {
|
||||
echo "│ ✅ Disk space is sufficient\n";
|
||||
}
|
||||
|
||||
echo "└─────────────────────────────────────────────────────────┘\n";
|
||||
|
||||
return ExitCode::SUCCESS->value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Process\Console;
|
||||
|
||||
use App\Framework\Console\ConsoleCommand;
|
||||
use App\Framework\Console\ConsoleInput;
|
||||
use App\Framework\Console\ConsoleOutput;
|
||||
use App\Framework\Console\ExitCode;
|
||||
use App\Framework\Process\Services\SystemInfoService;
|
||||
|
||||
/**
|
||||
* Zeigt System-Informationen an.
|
||||
*/
|
||||
final readonly class SystemInfoCommand
|
||||
{
|
||||
public function __construct(
|
||||
private SystemInfoService $systemInfo
|
||||
) {
|
||||
}
|
||||
|
||||
#[ConsoleCommand('system:info', 'Display system information (uptime, load, memory, disk, CPU)')]
|
||||
public function execute(ConsoleInput $input, ConsoleOutput $output): int
|
||||
{
|
||||
$info = ($this->systemInfo)();
|
||||
|
||||
echo "╔════════════════════════════════════════════════════════════╗\n";
|
||||
echo "║ SYSTEM INFORMATION ║\n";
|
||||
echo "╚════════════════════════════════════════════════════════════╝\n\n";
|
||||
|
||||
// Uptime
|
||||
echo "┌─ UPTIME ────────────────────────────────────────────────┐\n";
|
||||
echo "│ Boot Time: {$info->uptime->getBootTimeFormatted()}\n";
|
||||
echo "│ Uptime: {$info->uptime->uptime->toHumanReadable()} ({$info->uptime->getUptimeDays()} days)\n";
|
||||
echo "└─────────────────────────────────────────────────────────┘\n\n";
|
||||
|
||||
// Load Average
|
||||
echo "┌─ LOAD AVERAGE ──────────────────────────────────────────┐\n";
|
||||
$util = round($info->load->getUtilization($info->cpu->cores) * 100, 1);
|
||||
echo "│ 1 min: {$info->load->oneMinute}\n";
|
||||
echo "│ 5 min: {$info->load->fiveMinutes}\n";
|
||||
echo "│ 15 min: {$info->load->fifteenMinutes}\n";
|
||||
echo "│ CPU Usage: {$util}%\n";
|
||||
echo "└─────────────────────────────────────────────────────────┘\n\n";
|
||||
|
||||
// CPU
|
||||
echo "┌─ CPU ───────────────────────────────────────────────────┐\n";
|
||||
echo "│ Cores: {$info->cpu->cores}\n";
|
||||
echo "│ Model: {$info->cpu->getShortModel()}\n";
|
||||
echo "└─────────────────────────────────────────────────────────┘\n\n";
|
||||
|
||||
// Memory
|
||||
echo "┌─ MEMORY ────────────────────────────────────────────────┐\n";
|
||||
echo "│ Total: {$info->memory->getTotal()->toHumanReadable()}\n";
|
||||
echo "│ Used: {$info->memory->getUsed()->toHumanReadable()} ({$info->memory->getUsagePercentage()}%)\n";
|
||||
echo "│ Free: {$info->memory->getFree()->toHumanReadable()}\n";
|
||||
echo "│ Available: {$info->memory->getAvailable()->toHumanReadable()}\n";
|
||||
echo "└─────────────────────────────────────────────────────────┘\n\n";
|
||||
|
||||
// Disk
|
||||
echo "┌─ DISK ({$info->disk->mountPoint}) ─────────────────────────────────────────────┐\n";
|
||||
echo "│ Total: {$info->disk->getTotal()->toHumanReadable()}\n";
|
||||
echo "│ Used: {$info->disk->getUsed()->toHumanReadable()} ({$info->disk->getUsagePercentage()}%)\n";
|
||||
echo "│ Available: {$info->disk->getAvailable()->toHumanReadable()}\n";
|
||||
|
||||
if ($info->disk->isAlmostFull()) {
|
||||
echo "│ ⚠️ WARNING: Disk is almost full!\n";
|
||||
}
|
||||
|
||||
echo "└─────────────────────────────────────────────────────────┘\n\n";
|
||||
|
||||
// Processes
|
||||
echo "┌─ PROCESSES ─────────────────────────────────────────────┐\n";
|
||||
echo "│ Total: {$info->processes->total}\n";
|
||||
echo "│ Running: {$info->processes->running}\n";
|
||||
echo "│ Sleeping: {$info->processes->sleeping}\n";
|
||||
echo "│ Other: {$info->processes->getOther()}\n";
|
||||
echo "└─────────────────────────────────────────────────────────┘\n";
|
||||
|
||||
return ExitCode::SUCCESS->value;
|
||||
}
|
||||
}
|
||||
228
src/Framework/Process/Console/SystemdCommands.php
Normal file
228
src/Framework/Process/Console/SystemdCommands.php
Normal file
@@ -0,0 +1,228 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Process\Console;
|
||||
|
||||
use App\Framework\Console\ConsoleCommand;
|
||||
use App\Framework\Console\ConsoleInput;
|
||||
use App\Framework\Console\ExitCode;
|
||||
use App\Framework\Process\Services\SystemdService;
|
||||
|
||||
/**
|
||||
* Systemd Console Commands.
|
||||
*/
|
||||
final readonly class SystemdCommands
|
||||
{
|
||||
public function __construct(
|
||||
private SystemdService $systemdService
|
||||
) {
|
||||
}
|
||||
|
||||
#[ConsoleCommand('systemd:list', 'List all systemd services')]
|
||||
public function list(ConsoleInput $input): int
|
||||
{
|
||||
$all = $input->hasOption('all') || $input->hasOption('a');
|
||||
|
||||
echo "Listing systemd services" . ($all ? ' (including inactive)' : '') . "...\n\n";
|
||||
|
||||
$services = $this->systemdService->listServices($all);
|
||||
|
||||
if (empty($services)) {
|
||||
echo "ℹ️ No services found.\n";
|
||||
|
||||
return ExitCode::SUCCESS;
|
||||
}
|
||||
|
||||
echo "┌─ SYSTEMD SERVICES ───────────────────────────────────────┐\n";
|
||||
echo "│ Found " . count($services) . " service(s)\n";
|
||||
echo "└─────────────────────────────────────────────────────────┘\n\n";
|
||||
|
||||
foreach ($services as $service) {
|
||||
$statusIcon = $service['active'] ? '✅' : '⏸️';
|
||||
echo "{$statusIcon} {$service['name']} ({$service['status']})\n";
|
||||
}
|
||||
|
||||
return ExitCode::SUCCESS;
|
||||
}
|
||||
|
||||
#[ConsoleCommand('systemd:status', 'Show status of a systemd service')]
|
||||
public function status(ConsoleInput $input): int
|
||||
{
|
||||
$service = $input->getArgument('service');
|
||||
|
||||
if ($service === null) {
|
||||
echo "❌ Please provide a service name.\n";
|
||||
echo "Usage: php console.php systemd:status <service>\n";
|
||||
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
$status = $this->systemdService->getServiceStatus($service);
|
||||
|
||||
if ($status === null) {
|
||||
echo "❌ Could not retrieve status for service: {$service}\n";
|
||||
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
echo "┌─ SERVICE STATUS ────────────────────────────────────────┐\n";
|
||||
echo "│ Service: {$status['name']}\n";
|
||||
echo "│ Active: " . ($status['active'] ? '✅ Yes' : '❌ No') . "\n";
|
||||
echo "│ Enabled: " . ($status['enabled'] ? '✅ Yes' : '❌ No') . "\n";
|
||||
echo "└─────────────────────────────────────────────────────────┘\n";
|
||||
|
||||
return ExitCode::SUCCESS;
|
||||
}
|
||||
|
||||
#[ConsoleCommand('systemd:start', 'Start a systemd service')]
|
||||
public function start(ConsoleInput $input): int
|
||||
{
|
||||
$service = $input->getArgument('service');
|
||||
|
||||
if ($service === null) {
|
||||
echo "❌ Please provide a service name.\n";
|
||||
echo "Usage: php console.php systemd:start <service>\n";
|
||||
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
echo "Starting service: {$service}...\n";
|
||||
|
||||
if ($this->systemdService->startService($service)) {
|
||||
echo "✅ Service started successfully!\n";
|
||||
|
||||
return ExitCode::SUCCESS;
|
||||
}
|
||||
|
||||
echo "❌ Failed to start service.\n";
|
||||
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
#[ConsoleCommand('systemd:stop', 'Stop a systemd service')]
|
||||
public function stop(ConsoleInput $input): int
|
||||
{
|
||||
$service = $input->getArgument('service');
|
||||
|
||||
if ($service === null) {
|
||||
echo "❌ Please provide a service name.\n";
|
||||
echo "Usage: php console.php systemd:stop <service>\n";
|
||||
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
echo "Stopping service: {$service}...\n";
|
||||
|
||||
if ($this->systemdService->stopService($service)) {
|
||||
echo "✅ Service stopped successfully!\n";
|
||||
|
||||
return ExitCode::SUCCESS;
|
||||
}
|
||||
|
||||
echo "❌ Failed to stop service.\n";
|
||||
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
#[ConsoleCommand('systemd:restart', 'Restart a systemd service')]
|
||||
public function restart(ConsoleInput $input): int
|
||||
{
|
||||
$service = $input->getArgument('service');
|
||||
|
||||
if ($service === null) {
|
||||
echo "❌ Please provide a service name.\n";
|
||||
echo "Usage: php console.php systemd:restart <service>\n";
|
||||
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
echo "Restarting service: {$service}...\n";
|
||||
|
||||
if ($this->systemdService->restartService($service)) {
|
||||
echo "✅ Service restarted successfully!\n";
|
||||
|
||||
return ExitCode::SUCCESS;
|
||||
}
|
||||
|
||||
echo "❌ Failed to restart service.\n";
|
||||
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
#[ConsoleCommand('systemd:enable', 'Enable a systemd service')]
|
||||
public function enable(ConsoleInput $input): int
|
||||
{
|
||||
$service = $input->getArgument('service');
|
||||
|
||||
if ($service === null) {
|
||||
echo "❌ Please provide a service name.\n";
|
||||
echo "Usage: php console.php systemd:enable <service>\n";
|
||||
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
echo "Enabling service: {$service}...\n";
|
||||
|
||||
if ($this->systemdService->enableService($service)) {
|
||||
echo "✅ Service enabled successfully!\n";
|
||||
|
||||
return ExitCode::SUCCESS;
|
||||
}
|
||||
|
||||
echo "❌ Failed to enable service.\n";
|
||||
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
#[ConsoleCommand('systemd:disable', 'Disable a systemd service')]
|
||||
public function disable(ConsoleInput $input): int
|
||||
{
|
||||
$service = $input->getArgument('service');
|
||||
|
||||
if ($service === null) {
|
||||
echo "❌ Please provide a service name.\n";
|
||||
echo "Usage: php console.php systemd:disable <service>\n";
|
||||
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
echo "Disabling service: {$service}...\n";
|
||||
|
||||
if ($this->systemdService->disableService($service)) {
|
||||
echo "✅ Service disabled successfully!\n";
|
||||
|
||||
return ExitCode::SUCCESS;
|
||||
}
|
||||
|
||||
echo "❌ Failed to disable service.\n";
|
||||
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
#[ConsoleCommand('systemd:failed', 'List failed systemd services')]
|
||||
public function failed(ConsoleInput $input): int
|
||||
{
|
||||
echo "Checking for failed services...\n\n";
|
||||
|
||||
$failed = $this->systemdService->getFailedServices();
|
||||
|
||||
if (empty($failed)) {
|
||||
echo "✅ No failed services found!\n";
|
||||
|
||||
return ExitCode::SUCCESS;
|
||||
}
|
||||
|
||||
echo "┌─ FAILED SERVICES ───────────────────────────────────────┐\n";
|
||||
echo "│ Found " . count($failed) . " failed service(s):\n";
|
||||
echo "└─────────────────────────────────────────────────────────┘\n\n";
|
||||
|
||||
foreach ($failed as $service) {
|
||||
echo "❌ {$service}\n";
|
||||
}
|
||||
|
||||
return ExitCode::WARNING;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
150
src/Framework/Process/Services/AlertService.php
Normal file
150
src/Framework/Process/Services/AlertService.php
Normal file
@@ -0,0 +1,150 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Process\Services;
|
||||
|
||||
use App\Framework\Process\ValueObjects\Alert\Alert;
|
||||
use App\Framework\Process\ValueObjects\Alert\AlertReport;
|
||||
use App\Framework\Process\ValueObjects\Alert\AlertSeverity;
|
||||
use App\Framework\Process\ValueObjects\Alert\AlertThreshold;
|
||||
use App\Framework\Process\ValueObjects\Health\HealthReport;
|
||||
|
||||
/**
|
||||
* Alert Service.
|
||||
*
|
||||
* Verwaltet System-Alerts basierend auf Health Checks und Thresholds.
|
||||
*/
|
||||
final readonly class AlertService
|
||||
{
|
||||
/**
|
||||
* @param AlertThreshold[] $thresholds
|
||||
*/
|
||||
public function __construct(
|
||||
private SystemHealthCheckService $healthCheck,
|
||||
private array $thresholds = []
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft alle Alerts und gibt einen Report zurück.
|
||||
*/
|
||||
public function checkAlerts(): AlertReport
|
||||
{
|
||||
$healthReport = ($this->healthCheck)();
|
||||
$alerts = [];
|
||||
|
||||
// Check health checks for alerts
|
||||
foreach ($healthReport->checks as $check) {
|
||||
$threshold = $this->findThreshold($check->name);
|
||||
|
||||
if ($threshold === null) {
|
||||
// Use default thresholds based on health check status
|
||||
if ($check->status->value === 'unhealthy') {
|
||||
$alerts[] = Alert::create(
|
||||
id: $this->generateAlertId($check->name),
|
||||
name: $check->name,
|
||||
severity: AlertSeverity::CRITICAL,
|
||||
message: $check->message,
|
||||
description: "Value: {$check->value} {$check->unit} exceeds critical threshold",
|
||||
metadata: [
|
||||
'value' => $check->value,
|
||||
'unit' => $check->unit,
|
||||
'threshold' => $check->threshold,
|
||||
]
|
||||
);
|
||||
} elseif ($check->status->value === 'degraded') {
|
||||
$alerts[] = Alert::create(
|
||||
id: $this->generateAlertId($check->name),
|
||||
name: $check->name,
|
||||
severity: AlertSeverity::WARNING,
|
||||
message: $check->message,
|
||||
description: "Value: {$check->value} {$check->unit} exceeds warning threshold",
|
||||
metadata: [
|
||||
'value' => $check->value,
|
||||
'unit' => $check->unit,
|
||||
'threshold' => $check->threshold,
|
||||
]
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Use configured threshold
|
||||
$severity = $threshold->getSeverity($check->value);
|
||||
|
||||
if ($severity !== AlertSeverity::INFO) {
|
||||
$alerts[] = Alert::create(
|
||||
id: $this->generateAlertId($check->name),
|
||||
name: $check->name,
|
||||
severity: $severity,
|
||||
message: $check->message,
|
||||
description: "Value: {$check->value} {$check->unit} exceeds {$severity->value} threshold",
|
||||
metadata: [
|
||||
'value' => $check->value,
|
||||
'unit' => $check->unit,
|
||||
'threshold' => $threshold->criticalThreshold,
|
||||
'warning_threshold' => $threshold->warningThreshold,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return AlertReport::fromAlerts($alerts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Findet Threshold für einen Check-Namen.
|
||||
*/
|
||||
private function findThreshold(string $checkName): ?AlertThreshold
|
||||
{
|
||||
foreach ($this->thresholds as $threshold) {
|
||||
if ($threshold->name === $checkName) {
|
||||
return $threshold;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert eine eindeutige Alert-ID.
|
||||
*/
|
||||
private function generateAlertId(string $checkName): string
|
||||
{
|
||||
return 'alert_' . md5($checkName . time());
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt die Standard-Thresholds zurück.
|
||||
*
|
||||
* @return AlertThreshold[]
|
||||
*/
|
||||
public static function getDefaultThresholds(): array
|
||||
{
|
||||
return [
|
||||
new AlertThreshold(
|
||||
name: 'Memory Usage',
|
||||
warningThreshold: 80.0,
|
||||
criticalThreshold: 90.0,
|
||||
unit: '%',
|
||||
description: 'Memory usage percentage'
|
||||
),
|
||||
new AlertThreshold(
|
||||
name: 'Disk Usage',
|
||||
warningThreshold: 80.0,
|
||||
criticalThreshold: 90.0,
|
||||
unit: '%',
|
||||
description: 'Disk usage percentage'
|
||||
),
|
||||
new AlertThreshold(
|
||||
name: 'System Load',
|
||||
warningThreshold: 80.0,
|
||||
criticalThreshold: 120.0,
|
||||
unit: '%',
|
||||
description: 'System load percentage'
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
82
src/Framework/Process/Services/BackupService.php
Normal file
82
src/Framework/Process/Services/BackupService.php
Normal file
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Process\Services;
|
||||
|
||||
use App\Framework\Filesystem\ValueObjects\FilePath;
|
||||
use App\Framework\Process\Process;
|
||||
use App\Framework\Process\ValueObjects\Command;
|
||||
|
||||
/**
|
||||
* Backup Service.
|
||||
*
|
||||
* Erstellt Backups (Database, Files, Full).
|
||||
*/
|
||||
final readonly class BackupService
|
||||
{
|
||||
public function __construct(
|
||||
private Process $process
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt ein Database-Backup.
|
||||
*/
|
||||
public function createDatabaseBackup(
|
||||
string $database,
|
||||
string $username,
|
||||
string $password,
|
||||
FilePath $outputFile
|
||||
): bool {
|
||||
$command = Command::fromArray([
|
||||
'mysqldump',
|
||||
'-u',
|
||||
$username,
|
||||
"-p{$password}",
|
||||
$database,
|
||||
'>',
|
||||
$outputFile->toString(),
|
||||
]);
|
||||
|
||||
$result = $this->process->run($command);
|
||||
|
||||
return $result->isSuccess();
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt ein File-Backup (tar).
|
||||
*/
|
||||
public function createFileBackup(FilePath $sourceDirectory, FilePath $outputFile): bool
|
||||
{
|
||||
$command = Command::fromString(
|
||||
"tar -czf {$outputFile->toString()} -C {$sourceDirectory->toString()} ."
|
||||
);
|
||||
|
||||
$result = $this->process->run($command);
|
||||
|
||||
return $result->isSuccess();
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt ein Full-Backup (Database + Files).
|
||||
*/
|
||||
public function createFullBackup(
|
||||
string $database,
|
||||
string $dbUsername,
|
||||
string $dbPassword,
|
||||
FilePath $sourceDirectory,
|
||||
FilePath $outputDirectory
|
||||
): bool {
|
||||
$timestamp = date('Y-m-d_H-i-s');
|
||||
$dbFile = FilePath::create($outputDirectory->toString() . "/db_backup_{$timestamp}.sql");
|
||||
$filesFile = FilePath::create($outputDirectory->toString() . "/files_backup_{$timestamp}.tar.gz");
|
||||
|
||||
$dbSuccess = $this->createDatabaseBackup($database, $dbUsername, $dbPassword, $dbFile);
|
||||
$filesSuccess = $this->createFileBackup($sourceDirectory, $filesFile);
|
||||
|
||||
return $dbSuccess && $filesSuccess;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
180
src/Framework/Process/Services/MaintenanceService.php
Normal file
180
src/Framework/Process/Services/MaintenanceService.php
Normal file
@@ -0,0 +1,180 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Process\Services;
|
||||
|
||||
use App\Framework\Core\ValueObjects\Duration;
|
||||
use App\Framework\Filesystem\ValueObjects\FilePath;
|
||||
use App\Framework\Process\Process;
|
||||
use App\Framework\Process\ValueObjects\Command;
|
||||
|
||||
/**
|
||||
* Maintenance Service.
|
||||
*
|
||||
* Führt Wartungs- und Cleanup-Operationen durch.
|
||||
*/
|
||||
final readonly class MaintenanceService
|
||||
{
|
||||
public function __construct(
|
||||
private Process $process
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Löscht alte temporäre Dateien.
|
||||
*
|
||||
* @return int Anzahl gelöschter Dateien
|
||||
*/
|
||||
public function cleanTempFiles(Duration $olderThan): int
|
||||
{
|
||||
$tempDir = sys_get_temp_dir();
|
||||
$days = (int) ceil($olderThan->toDays());
|
||||
|
||||
$command = Command::fromString(
|
||||
"find {$tempDir} -type f -mtime +{$days} -delete"
|
||||
);
|
||||
|
||||
$result = $this->process->run($command);
|
||||
|
||||
// Count deleted files (approximate)
|
||||
return $result->isSuccess() ? 1 : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotiert alte Log-Dateien.
|
||||
*
|
||||
* @return int Anzahl rotierter Dateien
|
||||
*/
|
||||
public function cleanLogFiles(FilePath $logDirectory, Duration $olderThan): int
|
||||
{
|
||||
if (! $logDirectory->isDirectory()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$days = (int) ceil($olderThan->toDays());
|
||||
|
||||
$command = Command::fromString(
|
||||
"find {$logDirectory->toString()} -name '*.log' -type f -mtime +{$days} -delete"
|
||||
);
|
||||
|
||||
$result = $this->process->run($command);
|
||||
|
||||
return $result->isSuccess() ? 1 : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Leert Cache-Verzeichnisse.
|
||||
*/
|
||||
public function cleanCache(FilePath $cacheDirectory): bool
|
||||
{
|
||||
if (! $cacheDirectory->isDirectory()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$command = Command::fromString(
|
||||
"rm -rf {$cacheDirectory->toString()}/*"
|
||||
);
|
||||
|
||||
$result = $this->process->run($command);
|
||||
|
||||
return $result->isSuccess();
|
||||
}
|
||||
|
||||
/**
|
||||
* Löscht alte Backups.
|
||||
*
|
||||
* @return int Anzahl gelöschter Backups
|
||||
*/
|
||||
public function cleanOldBackups(FilePath $backupDirectory, Duration $olderThan): int
|
||||
{
|
||||
if (! $backupDirectory->isDirectory()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$days = (int) ceil($olderThan->toDays());
|
||||
|
||||
$command = Command::fromString(
|
||||
"find {$backupDirectory->toString()} -type f -name '*.sql' -o -name '*.sql.gz' -mtime +{$days} -delete"
|
||||
);
|
||||
|
||||
$result = $this->process->run($command);
|
||||
|
||||
return $result->isSuccess() ? 1 : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Findet die größten Verzeichnisse.
|
||||
*
|
||||
* @return array<string, int> Verzeichnis => Größe in Bytes
|
||||
*/
|
||||
public function findLargestDirectories(FilePath $directory, int $limit = 10): array
|
||||
{
|
||||
if (! $directory->isDirectory()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$command = Command::fromString(
|
||||
"du -h -d 1 {$directory->toString()} 2>/dev/null | sort -hr | head -{$limit}"
|
||||
);
|
||||
|
||||
$result = $this->process->run($command);
|
||||
|
||||
if (! $result->isSuccess()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$directories = [];
|
||||
$lines = explode("\n", trim($result->stdout));
|
||||
|
||||
foreach ($lines as $line) {
|
||||
$parts = preg_split('/\s+/', $line, 2);
|
||||
if (count($parts) === 2) {
|
||||
$directories[$parts[1]] = $parts[0]; // Size as string (human-readable)
|
||||
}
|
||||
}
|
||||
|
||||
return $directories;
|
||||
}
|
||||
|
||||
/**
|
||||
* Findet doppelte Dateien.
|
||||
*
|
||||
* @return array<string, array<string>> Hash => Dateipfade
|
||||
*/
|
||||
public function findDuplicateFiles(FilePath $directory): array
|
||||
{
|
||||
if (! $directory->isDirectory()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$command = Command::fromString(
|
||||
"find {$directory->toString()} -type f -exec md5sum {} \\; | sort | uniq -d -w 32"
|
||||
);
|
||||
|
||||
$result = $this->process->run($command);
|
||||
|
||||
if (! $result->isSuccess()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$duplicates = [];
|
||||
$lines = explode("\n", trim($result->stdout));
|
||||
|
||||
foreach ($lines as $line) {
|
||||
$parts = preg_split('/\s+/', $line, 2);
|
||||
if (count($parts) === 2) {
|
||||
$hash = $parts[0];
|
||||
$file = $parts[1];
|
||||
if (! isset($duplicates[$hash])) {
|
||||
$duplicates[$hash] = [];
|
||||
}
|
||||
$duplicates[$hash][] = $file;
|
||||
}
|
||||
}
|
||||
|
||||
return $duplicates;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
189
src/Framework/Process/Services/ProcessMonitoringService.php
Normal file
189
src/Framework/Process/Services/ProcessMonitoringService.php
Normal file
@@ -0,0 +1,189 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Process\Services;
|
||||
|
||||
use App\Framework\Core\ValueObjects\Byte;
|
||||
use App\Framework\Core\ValueObjects\Duration;
|
||||
use App\Framework\Process\Process;
|
||||
use App\Framework\Process\ValueObjects\Command;
|
||||
use App\Framework\Process\ValueObjects\ProcessDetails\ProcessDetails;
|
||||
|
||||
/**
|
||||
* Process Monitoring Service.
|
||||
*
|
||||
* Bietet erweiterte Prozess-Überwachung und -Verwaltung.
|
||||
*/
|
||||
final readonly class ProcessMonitoringService
|
||||
{
|
||||
public function __construct(
|
||||
private Process $process
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Listet alle laufenden Prozesse.
|
||||
*
|
||||
* @return ProcessDetails[]
|
||||
*/
|
||||
public function listProcesses(?string $filter = null): array
|
||||
{
|
||||
$command = Command::fromArray([
|
||||
'ps',
|
||||
'aux',
|
||||
]);
|
||||
|
||||
$result = $this->process->run($command);
|
||||
|
||||
if (! $result->isSuccess()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$processes = [];
|
||||
$lines = explode("\n", trim($result->stdout));
|
||||
|
||||
// Skip header line
|
||||
array_shift($lines);
|
||||
|
||||
foreach ($lines as $line) {
|
||||
if (empty(trim($line))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$parts = preg_split('/\s+/', $line, 11);
|
||||
|
||||
if (count($parts) < 11) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$pid = (int) $parts[1];
|
||||
$cpuPercent = (float) $parts[2];
|
||||
$memoryPercent = (float) $parts[3];
|
||||
$command = $parts[10] ?? '';
|
||||
|
||||
// Apply filter if provided
|
||||
if ($filter !== null && stripos($command, $filter) === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get memory usage (convert from percentage to bytes - approximate)
|
||||
// This is a simplified approach; real memory would require /proc/[pid]/status
|
||||
$memoryUsage = null; // Will be null for now, can be enhanced later
|
||||
|
||||
$processes[] = new ProcessDetails(
|
||||
pid: $pid,
|
||||
command: $command,
|
||||
user: $parts[0] ?? null,
|
||||
cpuPercent: $cpuPercent,
|
||||
memoryUsage: $memoryUsage,
|
||||
state: $parts[7] ?? null,
|
||||
priority: isset($parts[5]) ? (int) $parts[5] : null
|
||||
);
|
||||
}
|
||||
|
||||
return $processes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Findet Prozesse nach Name.
|
||||
*
|
||||
* @return ProcessDetails[]
|
||||
*/
|
||||
public function findProcesses(string $name): array
|
||||
{
|
||||
return $this->listProcesses($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt die Prozess-Hierarchie zurück.
|
||||
*
|
||||
* @return array<int, array{pid: int, command: string, children: array}>
|
||||
*/
|
||||
public function getProcessTree(): array
|
||||
{
|
||||
$processes = $this->listProcesses();
|
||||
$tree = [];
|
||||
|
||||
// Build tree structure
|
||||
foreach ($processes as $proc) {
|
||||
$tree[$proc->pid] = [
|
||||
'pid' => $proc->pid,
|
||||
'command' => $proc->command,
|
||||
'children' => [],
|
||||
];
|
||||
}
|
||||
|
||||
// Build parent-child relationships
|
||||
foreach ($processes as $proc) {
|
||||
if ($proc->ppid !== null && isset($tree[$proc->ppid])) {
|
||||
$tree[$proc->ppid]['children'][] = $proc->pid;
|
||||
}
|
||||
}
|
||||
|
||||
// Find root processes (ppid = 1 or null)
|
||||
$roots = [];
|
||||
foreach ($processes as $proc) {
|
||||
if ($proc->ppid === null || $proc->ppid === 1) {
|
||||
$roots[] = $proc->pid;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'tree' => $tree,
|
||||
'roots' => $roots,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt Details eines spezifischen Prozesses zurück.
|
||||
*/
|
||||
public function getProcessDetails(int $pid): ?ProcessDetails
|
||||
{
|
||||
$command = Command::fromArray([
|
||||
'ps',
|
||||
'-p',
|
||||
(string) $pid,
|
||||
'-o',
|
||||
'pid,comm,user,cpu,mem,etime,stat,pri',
|
||||
]);
|
||||
|
||||
$result = $this->process->run($command);
|
||||
|
||||
if (! $result->isSuccess()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$lines = explode("\n", trim($result->stdout));
|
||||
if (count($lines) < 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Skip header
|
||||
$data = preg_split('/\s+/', trim($lines[1]));
|
||||
|
||||
if (count($data) < 8) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new ProcessDetails(
|
||||
pid: (int) $data[0],
|
||||
command: $data[1] ?? '',
|
||||
user: $data[2] ?? null,
|
||||
cpuPercent: isset($data[3]) ? (float) $data[3] : null,
|
||||
memoryUsage: null, // Would need additional parsing
|
||||
state: $data[6] ?? null,
|
||||
priority: isset($data[7]) ? (int) $data[7] : null
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft, ob ein Prozess läuft.
|
||||
*/
|
||||
public function isProcessRunning(int $pid): bool
|
||||
{
|
||||
return $this->getProcessDetails($pid) !== null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
250
src/Framework/Process/Services/SslCertificateService.php
Normal file
250
src/Framework/Process/Services/SslCertificateService.php
Normal file
@@ -0,0 +1,250 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Process\Services;
|
||||
|
||||
use App\Framework\Core\ValueObjects\Duration;
|
||||
use App\Framework\Process\Process;
|
||||
use App\Framework\Process\ValueObjects\Command;
|
||||
use App\Framework\Process\ValueObjects\Ssl\CertificateInfo;
|
||||
use App\Framework\Process\ValueObjects\Ssl\CertificateValidationResult;
|
||||
|
||||
/**
|
||||
* SSL Certificate Service.
|
||||
*
|
||||
* Prüft SSL-Zertifikate von Domains und gibt strukturierte Informationen zurück.
|
||||
*/
|
||||
final readonly class SslCertificateService
|
||||
{
|
||||
public function __construct(
|
||||
private Process $process
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft SSL-Zertifikat eines Domains.
|
||||
*/
|
||||
public function checkCertificate(string $domain, int $port = 443): CertificateValidationResult
|
||||
{
|
||||
$certInfo = $this->getCertificateInfo($domain, $port);
|
||||
|
||||
if ($certInfo === null) {
|
||||
return CertificateValidationResult::failed($domain, [
|
||||
'Could not retrieve certificate information',
|
||||
]);
|
||||
}
|
||||
|
||||
$errors = [];
|
||||
$warnings = [];
|
||||
|
||||
// Check if certificate is valid
|
||||
if (! $certInfo->isValid()) {
|
||||
$errors[] = 'Certificate is not valid (expired or not yet valid)';
|
||||
}
|
||||
|
||||
// Check if certificate is expiring soon
|
||||
if ($certInfo->isExpiringSoon(30)) {
|
||||
$daysUntilExpiry = $certInfo->getDaysUntilExpiry();
|
||||
$warnings[] = "Certificate expires in {$daysUntilExpiry} days";
|
||||
}
|
||||
|
||||
// Check if certificate is self-signed
|
||||
if ($certInfo->isSelfSigned) {
|
||||
$warnings[] = 'Certificate is self-signed';
|
||||
}
|
||||
|
||||
$result = CertificateValidationResult::success($domain, $certInfo);
|
||||
|
||||
if (! empty($warnings)) {
|
||||
$result = $result->withWarnings($warnings);
|
||||
}
|
||||
|
||||
if (! empty($errors)) {
|
||||
return CertificateValidationResult::failed($domain, $errors);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt detaillierte Zertifikats-Informationen zurück.
|
||||
*/
|
||||
public function getCertificateInfo(string $domain, int $port = 443): ?CertificateInfo
|
||||
{
|
||||
$command = Command::fromArray([
|
||||
'openssl',
|
||||
's_client',
|
||||
'-servername',
|
||||
$domain,
|
||||
'-connect',
|
||||
"{$domain}:{$port}",
|
||||
'-showcerts',
|
||||
]);
|
||||
|
||||
$result = $this->process->run(
|
||||
command: $command,
|
||||
timeout: Duration::fromSeconds(10)
|
||||
);
|
||||
|
||||
if (! $result->isSuccess()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Extract certificate from output
|
||||
$certStart = strpos($result->stdout, '-----BEGIN CERTIFICATE-----');
|
||||
if ($certStart === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$certEnd = strpos($result->stdout, '-----END CERTIFICATE-----', $certStart);
|
||||
if ($certEnd === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$certPem = substr($result->stdout, $certStart, $certEnd - $certStart + strlen('-----END CERTIFICATE-----'));
|
||||
|
||||
// Parse certificate using openssl
|
||||
return $this->parseCertificate($certPem);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parst Zertifikat aus PEM-Format.
|
||||
*/
|
||||
private function parseCertificate(string $certPem): ?CertificateInfo
|
||||
{
|
||||
// Use openssl to parse certificate details
|
||||
$tempFile = tempnam(sys_get_temp_dir(), 'ssl_cert_');
|
||||
if ($tempFile === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
file_put_contents($tempFile, $certPem);
|
||||
|
||||
// Get certificate dates
|
||||
$datesResult = $this->process->run(
|
||||
Command::fromArray(['openssl', 'x509', '-in', $tempFile, '-noout', '-dates'])
|
||||
);
|
||||
|
||||
// Get certificate subject
|
||||
$subjectResult = $this->process->run(
|
||||
Command::fromArray(['openssl', 'x509', '-in', $tempFile, '-noout', '-subject'])
|
||||
);
|
||||
|
||||
// Get certificate issuer
|
||||
$issuerResult = $this->process->run(
|
||||
Command::fromArray(['openssl', 'x509', '-in', $tempFile, '-noout', '-issuer'])
|
||||
);
|
||||
|
||||
// Get subject alternative names
|
||||
$sanResult = $this->process->run(
|
||||
Command::fromArray(['openssl', 'x509', '-in', $tempFile, '-noout', '-text'])
|
||||
);
|
||||
|
||||
// Get serial number
|
||||
$serialResult = $this->process->run(
|
||||
Command::fromArray(['openssl', 'x509', '-in', $tempFile, '-noout', '-serial'])
|
||||
);
|
||||
|
||||
// Get signature algorithm
|
||||
$sigAlgResult = $this->process->run(
|
||||
Command::fromArray(['openssl', 'x509', '-in', $tempFile, '-noout', '-signature'])
|
||||
);
|
||||
|
||||
if (! $datesResult->isSuccess() || ! $subjectResult->isSuccess() || ! $issuerResult->isSuccess()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Parse dates
|
||||
$validFrom = null;
|
||||
$validTo = null;
|
||||
|
||||
if (preg_match('/notBefore=(.+)/', $datesResult->stdout, $matches)) {
|
||||
$validFrom = new \DateTimeImmutable(trim($matches[1]));
|
||||
}
|
||||
|
||||
if (preg_match('/notAfter=(.+)/', $datesResult->stdout, $matches)) {
|
||||
$validTo = new \DateTimeImmutable(trim($matches[1]));
|
||||
}
|
||||
|
||||
if ($validFrom === null || $validTo === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Parse subject
|
||||
$subject = '';
|
||||
if (preg_match('/subject=(.+)/', $subjectResult->stdout, $matches)) {
|
||||
$subject = trim($matches[1]);
|
||||
}
|
||||
|
||||
// Parse issuer
|
||||
$issuer = '';
|
||||
if (preg_match('/issuer=(.+)/', $issuerResult->stdout, $matches)) {
|
||||
$issuer = trim($matches[1]);
|
||||
}
|
||||
|
||||
// Check if self-signed
|
||||
$isSelfSigned = $subject === $issuer;
|
||||
|
||||
// Parse subject alternative names
|
||||
$subjectAltNames = [];
|
||||
if (preg_match('/Subject Alternative Name:\s*(.+)/', $sanResult->stdout, $matches)) {
|
||||
$sanString = trim($matches[1]);
|
||||
// Parse DNS: entries
|
||||
if (preg_match_all('/DNS:([^,]+)/', $sanString, $sanMatches)) {
|
||||
$subjectAltNames = $sanMatches[1];
|
||||
}
|
||||
}
|
||||
|
||||
// Parse serial number
|
||||
$serialNumber = null;
|
||||
if (preg_match('/serial=(.+)/', $serialResult->stdout, $matches)) {
|
||||
$serialNumber = trim($matches[1]);
|
||||
}
|
||||
|
||||
// Parse signature algorithm (from text output)
|
||||
$signatureAlgorithm = null;
|
||||
if (preg_match('/Signature Algorithm:\s*([^\n]+)/', $sanResult->stdout, $matches)) {
|
||||
$signatureAlgorithm = trim($matches[1]);
|
||||
}
|
||||
|
||||
return new CertificateInfo(
|
||||
subject: $subject,
|
||||
issuer: $issuer,
|
||||
validFrom: $validFrom,
|
||||
validTo: $validTo,
|
||||
subjectAltNames: $subjectAltNames,
|
||||
isSelfSigned: $isSelfSigned,
|
||||
serialNumber: $serialNumber,
|
||||
signatureAlgorithm: $signatureAlgorithm
|
||||
);
|
||||
} finally {
|
||||
@unlink($tempFile);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft mehrere Domains auf ablaufende Zertifikate.
|
||||
*
|
||||
* @param string[] $domains
|
||||
* @param int $daysThreshold Tage vor Ablauf
|
||||
* @return CertificateValidationResult[]
|
||||
*/
|
||||
public function findExpiringCertificates(array $domains, int $daysThreshold = 30): array
|
||||
{
|
||||
$results = [];
|
||||
|
||||
foreach ($domains as $domain) {
|
||||
$result = $this->checkCertificate($domain);
|
||||
|
||||
if ($result->certificateInfo !== null && $result->certificateInfo->isExpiringSoon($daysThreshold)) {
|
||||
$results[] = $result;
|
||||
}
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
219
src/Framework/Process/Services/SystemdService.php
Normal file
219
src/Framework/Process/Services/SystemdService.php
Normal file
@@ -0,0 +1,219 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Process\Services;
|
||||
|
||||
use App\Framework\Process\Process;
|
||||
use App\Framework\Process\ValueObjects\Command;
|
||||
|
||||
/**
|
||||
* Systemd Service.
|
||||
*
|
||||
* Verwaltet Systemd-Services über systemctl.
|
||||
*/
|
||||
final readonly class SystemdService
|
||||
{
|
||||
public function __construct(
|
||||
private Process $process
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Listet alle Services.
|
||||
*
|
||||
* @return array<string, array{name: string, status: string, active: bool}>
|
||||
*/
|
||||
public function listServices(bool $all = false): array
|
||||
{
|
||||
$command = Command::fromArray([
|
||||
'systemctl',
|
||||
'list-units',
|
||||
'--type=service',
|
||||
'--no-pager',
|
||||
'--no-legend',
|
||||
]);
|
||||
|
||||
if ($all) {
|
||||
$command = Command::fromArray([
|
||||
'systemctl',
|
||||
'list-units',
|
||||
'--type=service',
|
||||
'--all',
|
||||
'--no-pager',
|
||||
'--no-legend',
|
||||
]);
|
||||
}
|
||||
|
||||
$result = $this->process->run($command);
|
||||
|
||||
if (! $result->isSuccess()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$services = [];
|
||||
$lines = explode("\n", trim($result->stdout));
|
||||
|
||||
foreach ($lines as $line) {
|
||||
if (empty(trim($line))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$parts = preg_split('/\s+/', $line, 6);
|
||||
if (count($parts) < 5) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$name = $parts[0];
|
||||
$status = $parts[3] ?? 'unknown';
|
||||
$active = ($parts[3] ?? '') === 'active';
|
||||
|
||||
$services[] = [
|
||||
'name' => $name,
|
||||
'status' => $status,
|
||||
'active' => $active,
|
||||
];
|
||||
}
|
||||
|
||||
return $services;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt den Status eines Services zurück.
|
||||
*/
|
||||
public function getServiceStatus(string $service): ?array
|
||||
{
|
||||
$command = Command::fromArray([
|
||||
'systemctl',
|
||||
'status',
|
||||
$service,
|
||||
'--no-pager',
|
||||
]);
|
||||
|
||||
$result = $this->process->run($command);
|
||||
|
||||
if (! $result->isSuccess()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Parse status output
|
||||
$lines = explode("\n", $result->stdout);
|
||||
$status = [
|
||||
'name' => $service,
|
||||
'active' => false,
|
||||
'enabled' => false,
|
||||
];
|
||||
|
||||
foreach ($lines as $line) {
|
||||
if (strpos($line, 'Active:') !== false) {
|
||||
$status['active'] = strpos($line, 'active') !== false;
|
||||
}
|
||||
if (strpos($line, 'Loaded:') !== false) {
|
||||
$status['enabled'] = strpos($line, 'enabled') !== false;
|
||||
}
|
||||
}
|
||||
|
||||
return $status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Startet einen Service.
|
||||
*/
|
||||
public function startService(string $service): bool
|
||||
{
|
||||
$result = $this->process->run(
|
||||
Command::fromArray(['systemctl', 'start', $service])
|
||||
);
|
||||
|
||||
return $result->isSuccess();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stoppt einen Service.
|
||||
*/
|
||||
public function stopService(string $service): bool
|
||||
{
|
||||
$result = $this->process->run(
|
||||
Command::fromArray(['systemctl', 'stop', $service])
|
||||
);
|
||||
|
||||
return $result->isSuccess();
|
||||
}
|
||||
|
||||
/**
|
||||
* Startet einen Service neu.
|
||||
*/
|
||||
public function restartService(string $service): bool
|
||||
{
|
||||
$result = $this->process->run(
|
||||
Command::fromArray(['systemctl', 'restart', $service])
|
||||
);
|
||||
|
||||
return $result->isSuccess();
|
||||
}
|
||||
|
||||
/**
|
||||
* Aktiviert einen Service.
|
||||
*/
|
||||
public function enableService(string $service): bool
|
||||
{
|
||||
$result = $this->process->run(
|
||||
Command::fromArray(['systemctl', 'enable', $service])
|
||||
);
|
||||
|
||||
return $result->isSuccess();
|
||||
}
|
||||
|
||||
/**
|
||||
* Deaktiviert einen Service.
|
||||
*/
|
||||
public function disableService(string $service): bool
|
||||
{
|
||||
$result = $this->process->run(
|
||||
Command::fromArray(['systemctl', 'disable', $service])
|
||||
);
|
||||
|
||||
return $result->isSuccess();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt fehlgeschlagene Services zurück.
|
||||
*
|
||||
* @return array<string>
|
||||
*/
|
||||
public function getFailedServices(): array
|
||||
{
|
||||
$command = Command::fromArray([
|
||||
'systemctl',
|
||||
'list-units',
|
||||
'--type=service',
|
||||
'--state=failed',
|
||||
'--no-pager',
|
||||
'--no-legend',
|
||||
]);
|
||||
|
||||
$result = $this->process->run($command);
|
||||
|
||||
if (! $result->isSuccess()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$failed = [];
|
||||
$lines = explode("\n", trim($result->stdout));
|
||||
|
||||
foreach ($lines as $line) {
|
||||
if (empty(trim($line))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$parts = preg_split('/\s+/', $line);
|
||||
if (! empty($parts[0])) {
|
||||
$failed[] = $parts[0];
|
||||
}
|
||||
}
|
||||
|
||||
return $failed;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
101
src/Framework/Process/ValueObjects/Alert/Alert.php
Normal file
101
src/Framework/Process/ValueObjects/Alert/Alert.php
Normal file
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Process\ValueObjects\Alert;
|
||||
|
||||
/**
|
||||
* Alert Definition.
|
||||
*/
|
||||
final readonly class Alert
|
||||
{
|
||||
public function __construct(
|
||||
public string $id,
|
||||
public string $name,
|
||||
public AlertSeverity $severity,
|
||||
public string $message,
|
||||
public ?string $description = null,
|
||||
public ?\DateTimeImmutable $triggeredAt = null,
|
||||
public bool $isActive = false,
|
||||
public array $metadata = []
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt einen neuen Alert.
|
||||
*/
|
||||
public static function create(
|
||||
string $id,
|
||||
string $name,
|
||||
AlertSeverity $severity,
|
||||
string $message,
|
||||
?string $description = null,
|
||||
array $metadata = []
|
||||
): self {
|
||||
return new self(
|
||||
id: $id,
|
||||
name: $name,
|
||||
severity: $severity,
|
||||
message: $message,
|
||||
description: $description,
|
||||
triggeredAt: new \DateTimeImmutable(),
|
||||
isActive: true,
|
||||
metadata: $metadata
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Markiert Alert als inaktiv.
|
||||
*/
|
||||
public function deactivate(): self
|
||||
{
|
||||
return new self(
|
||||
id: $this->id,
|
||||
name: $this->name,
|
||||
severity: $this->severity,
|
||||
message: $this->message,
|
||||
description: $this->description,
|
||||
triggeredAt: $this->triggeredAt,
|
||||
isActive: false,
|
||||
metadata: $this->metadata
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft, ob Alert kritisch ist.
|
||||
*/
|
||||
public function isCritical(): bool
|
||||
{
|
||||
return $this->severity === AlertSeverity::CRITICAL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft, ob Alert eine Warnung ist.
|
||||
*/
|
||||
public function isWarning(): bool
|
||||
{
|
||||
return $this->severity === AlertSeverity::WARNING;
|
||||
}
|
||||
|
||||
/**
|
||||
* Konvertiert zu Array.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'name' => $this->name,
|
||||
'severity' => $this->severity->value,
|
||||
'message' => $this->message,
|
||||
'description' => $this->description,
|
||||
'triggered_at' => $this->triggeredAt?->format('Y-m-d H:i:s'),
|
||||
'is_active' => $this->isActive,
|
||||
'is_critical' => $this->isCritical(),
|
||||
'metadata' => $this->metadata,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
128
src/Framework/Process/ValueObjects/Alert/AlertReport.php
Normal file
128
src/Framework/Process/ValueObjects/Alert/AlertReport.php
Normal file
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Process\ValueObjects\Alert;
|
||||
|
||||
/**
|
||||
* Alert Report aggregiert alle aktiven Alerts.
|
||||
*/
|
||||
final readonly class AlertReport
|
||||
{
|
||||
/**
|
||||
* @param Alert[] $alerts
|
||||
*/
|
||||
public function __construct(
|
||||
public array $alerts,
|
||||
public \DateTimeImmutable $generatedAt
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt einen Alert Report aus Alerts.
|
||||
*
|
||||
* @param Alert[] $alerts
|
||||
*/
|
||||
public static function fromAlerts(array $alerts): self
|
||||
{
|
||||
return new self(
|
||||
alerts: $alerts,
|
||||
generatedAt: new \DateTimeImmutable()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt nur aktive Alerts zurück.
|
||||
*
|
||||
* @return Alert[]
|
||||
*/
|
||||
public function getActiveAlerts(): array
|
||||
{
|
||||
return array_filter(
|
||||
$this->alerts,
|
||||
fn (Alert $alert) => $alert->isActive
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt nur kritische Alerts zurück.
|
||||
*
|
||||
* @return Alert[]
|
||||
*/
|
||||
public function getCriticalAlerts(): array
|
||||
{
|
||||
return array_filter(
|
||||
$this->getActiveAlerts(),
|
||||
fn (Alert $alert) => $alert->isCritical()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt nur Warning-Alerts zurück.
|
||||
*
|
||||
* @return Alert[]
|
||||
*/
|
||||
public function getWarningAlerts(): array
|
||||
{
|
||||
return array_filter(
|
||||
$this->getActiveAlerts(),
|
||||
fn (Alert $alert) => $alert->isWarning()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt die Anzahl der Alerts pro Severity zurück.
|
||||
*
|
||||
* @return array{info: int, warning: int, critical: int}
|
||||
*/
|
||||
public function getSeverityCounts(): array
|
||||
{
|
||||
$counts = [
|
||||
'info' => 0,
|
||||
'warning' => 0,
|
||||
'critical' => 0,
|
||||
];
|
||||
|
||||
foreach ($this->getActiveAlerts() as $alert) {
|
||||
$counts[$alert->severity->value]++;
|
||||
}
|
||||
|
||||
return $counts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft, ob es aktive Alerts gibt.
|
||||
*/
|
||||
public function hasActiveAlerts(): bool
|
||||
{
|
||||
return count($this->getActiveAlerts()) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft, ob es kritische Alerts gibt.
|
||||
*/
|
||||
public function hasCriticalAlerts(): bool
|
||||
{
|
||||
return count($this->getCriticalAlerts()) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Konvertiert zu Array.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'generated_at' => $this->generatedAt->format('Y-m-d H:i:s'),
|
||||
'total_alerts' => count($this->alerts),
|
||||
'active_alerts' => count($this->getActiveAlerts()),
|
||||
'critical_alerts' => count($this->getCriticalAlerts()),
|
||||
'warning_alerts' => count($this->getWarningAlerts()),
|
||||
'severity_counts' => $this->getSeverityCounts(),
|
||||
'alerts' => array_map(fn (Alert $a) => $a->toArray(), $this->alerts),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
41
src/Framework/Process/ValueObjects/Alert/AlertSeverity.php
Normal file
41
src/Framework/Process/ValueObjects/Alert/AlertSeverity.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Process\ValueObjects\Alert;
|
||||
|
||||
/**
|
||||
* Alert Severity Level.
|
||||
*/
|
||||
enum AlertSeverity: string
|
||||
{
|
||||
case INFO = 'info';
|
||||
case WARNING = 'warning';
|
||||
case CRITICAL = 'critical';
|
||||
|
||||
/**
|
||||
* Gibt eine menschenlesbare Beschreibung zurück.
|
||||
*/
|
||||
public function getDescription(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::INFO => 'Informational alert',
|
||||
self::WARNING => 'Warning alert - attention required',
|
||||
self::CRITICAL => 'Critical alert - immediate action required',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt ein Icon für den Severity-Level zurück.
|
||||
*/
|
||||
public function getIcon(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::INFO => 'ℹ️',
|
||||
self::WARNING => '⚠️',
|
||||
self::CRITICAL => '❌',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
70
src/Framework/Process/ValueObjects/Alert/AlertThreshold.php
Normal file
70
src/Framework/Process/ValueObjects/Alert/AlertThreshold.php
Normal file
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Process\ValueObjects\Alert;
|
||||
|
||||
/**
|
||||
* Alert Threshold Configuration.
|
||||
*/
|
||||
final readonly class AlertThreshold
|
||||
{
|
||||
public function __construct(
|
||||
public string $name,
|
||||
public float $warningThreshold,
|
||||
public float $criticalThreshold,
|
||||
public string $unit = '',
|
||||
public ?string $description = null
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft, ob ein Wert den Warning-Threshold überschreitet.
|
||||
*/
|
||||
public function exceedsWarning(float $value): bool
|
||||
{
|
||||
return $value >= $this->warningThreshold;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft, ob ein Wert den Critical-Threshold überschreitet.
|
||||
*/
|
||||
public function exceedsCritical(float $value): bool
|
||||
{
|
||||
return $value >= $this->criticalThreshold;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bestimmt die Severity basierend auf dem Wert.
|
||||
*/
|
||||
public function getSeverity(float $value): AlertSeverity
|
||||
{
|
||||
if ($this->exceedsCritical($value)) {
|
||||
return AlertSeverity::CRITICAL;
|
||||
}
|
||||
|
||||
if ($this->exceedsWarning($value)) {
|
||||
return AlertSeverity::WARNING;
|
||||
}
|
||||
|
||||
return AlertSeverity::INFO;
|
||||
}
|
||||
|
||||
/**
|
||||
* Konvertiert zu Array.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'name' => $this->name,
|
||||
'warning_threshold' => $this->warningThreshold,
|
||||
'critical_threshold' => $this->criticalThreshold,
|
||||
'unit' => $this->unit,
|
||||
'description' => $this->description,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Process\ValueObjects\ProcessDetails;
|
||||
|
||||
use App\Framework\Core\ValueObjects\Byte;
|
||||
use App\Framework\Core\ValueObjects\Duration;
|
||||
|
||||
/**
|
||||
* Detailed Process Information.
|
||||
*/
|
||||
final readonly class ProcessDetails
|
||||
{
|
||||
public function __construct(
|
||||
public int $pid,
|
||||
public string $command,
|
||||
public ?int $ppid = null,
|
||||
public ?string $user = null,
|
||||
public ?float $cpuPercent = null,
|
||||
public ?Byte $memoryUsage = null,
|
||||
public ?Duration $uptime = null,
|
||||
public ?string $state = null,
|
||||
public ?int $priority = null
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Konvertiert zu Array.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'pid' => $this->pid,
|
||||
'command' => $this->command,
|
||||
'ppid' => $this->ppid,
|
||||
'user' => $this->user,
|
||||
'cpu_percent' => $this->cpuPercent,
|
||||
'memory_usage_bytes' => $this->memoryUsage?->toBytes(),
|
||||
'memory_usage_human' => $this->memoryUsage?->toHumanReadable(),
|
||||
'uptime_seconds' => $this->uptime?->toSeconds(),
|
||||
'uptime_human' => $this->uptime?->toHumanReadable(),
|
||||
'state' => $this->state,
|
||||
'priority' => $this->priority,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,9 +15,11 @@ namespace App\Framework\QrCode\ErrorCorrection;
|
||||
final class ReedSolomonEncoder
|
||||
{
|
||||
// Generator polynomial coefficients for different EC codeword counts
|
||||
// Format: [ecCodewords => [coefficients...]]
|
||||
// Note: The first coefficient is always 0 (leading term)
|
||||
private const GENERATOR_POLYNOMIALS = [
|
||||
7 => [0, 87, 229, 146, 149, 238, 102, 21], // EC Level M, Version 1
|
||||
10 => [0, 251, 67, 46, 61, 118, 70, 64, 94, 32, 45],
|
||||
7 => [0, 87, 229, 146, 149, 238, 102, 21], // 7 EC codewords
|
||||
10 => [0, 251, 67, 46, 61, 118, 70, 64, 94, 32, 45], // Version 1, Level M (10 EC codewords)
|
||||
13 => [0, 74, 152, 176, 100, 86, 100, 106, 104, 130, 218, 206, 140, 78],
|
||||
15 => [0, 8, 183, 61, 91, 202, 37, 51, 58, 58, 237, 140, 124, 5, 99, 105],
|
||||
16 => [0, 120, 104, 107, 109, 102, 161, 76, 3, 91, 191, 147, 169, 182, 194, 225, 120],
|
||||
@@ -73,15 +75,30 @@ final class ReedSolomonEncoder
|
||||
$generator = $this->getGeneratorPolynomial($ecCodewords);
|
||||
|
||||
// Initialize message polynomial (data + zero padding for EC)
|
||||
// This represents m(x) * x^t where t is the number of EC codewords
|
||||
$messagePolynomial = array_merge($data, array_fill(0, $ecCodewords, 0));
|
||||
|
||||
// Polynomial division
|
||||
// Polynomial division: divide messagePolynomial by generator
|
||||
// Standard Reed-Solomon encoding algorithm
|
||||
// For stored polynomials [0, a1, a2, ..., an], the leading coefficient is implicitly 1
|
||||
// So we treat it as a monic polynomial [1, a1, a2, ..., an]
|
||||
$messageLength = count($messagePolynomial);
|
||||
|
||||
for ($i = 0; $i < count($data); $i++) {
|
||||
$coefficient = $messagePolynomial[$i];
|
||||
|
||||
if ($coefficient !== 0) {
|
||||
for ($j = 0; $j < count($generator); $j++) {
|
||||
$messagePolynomial[$i + $j] ^= $this->gfMultiply($generator[$j], $coefficient);
|
||||
// Leading coefficient is implicitly 1 (monic polynomial)
|
||||
// So we clear the current position and apply generator coefficients
|
||||
$messagePolynomial[$i] = 0;
|
||||
|
||||
// Apply generator coefficients (skip first element which is 0)
|
||||
// Generator format: [0, a1, a2, ..., an] represents [1, a1, a2, ..., an]
|
||||
for ($j = 1; $j < count($generator); $j++) {
|
||||
$index = $i + $j;
|
||||
if ($index < $messageLength) {
|
||||
$messagePolynomial[$index] ^= $this->gfMultiply($generator[$j], $coefficient);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -106,6 +123,8 @@ final class ReedSolomonEncoder
|
||||
|
||||
/**
|
||||
* Generate generator polynomial g(x) = (x - α^0)(x - α^1)...(x - α^(n-1))
|
||||
*
|
||||
* Returns monic polynomial [1, a1, a2, ..., an] where leading coefficient is 1
|
||||
*/
|
||||
private function generateGeneratorPolynomial(int $degree): array
|
||||
{
|
||||
@@ -113,6 +132,7 @@ final class ReedSolomonEncoder
|
||||
$polynomial = [1];
|
||||
|
||||
// Multiply by (x - α^i) for i = 0 to degree-1
|
||||
// (x - α^i) = x + (-α^i) = x + (α^i in GF(256))
|
||||
for ($i = 0; $i < $degree; $i++) {
|
||||
$polynomial = $this->multiplyPolynomials(
|
||||
$polynomial,
|
||||
|
||||
@@ -24,10 +24,14 @@ use App\Framework\QrCode\ValueObjects\QrCodeMatrix;
|
||||
* Phase 2: Full Reed-Solomon error correction with mask pattern selection
|
||||
* Generates scannable QR codes compliant with ISO/IEC 18004
|
||||
*/
|
||||
final class QrCodeGenerator
|
||||
final readonly class QrCodeGenerator
|
||||
{
|
||||
public function __construct(
|
||||
private QrCodeRenderer $renderer
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Generate QR Code from data
|
||||
* Generate QR Code from data (static method for backward compatibility)
|
||||
*/
|
||||
public static function generate(string $data, ?QrCodeConfig $config = null): QrCodeMatrix
|
||||
{
|
||||
@@ -68,16 +72,18 @@ final class QrCodeGenerator
|
||||
);
|
||||
}
|
||||
|
||||
// Generate matrix
|
||||
$matrix = self::generateMatrix($data, $config);
|
||||
// Generate matrix using temporary instance
|
||||
$temporaryRenderer = new QrCodeRenderer();
|
||||
$temporaryGenerator = new self($temporaryRenderer);
|
||||
$matrix = $temporaryGenerator->generateMatrix($data, $config);
|
||||
|
||||
return $matrix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate QR Code matrix
|
||||
* Generate QR Code matrix (instance method)
|
||||
*/
|
||||
private static function generateMatrix(string $data, QrCodeConfig $config): QrCodeMatrix
|
||||
private function generateMatrix(string $data, QrCodeConfig $config): QrCodeMatrix
|
||||
{
|
||||
// 1. Create empty matrix
|
||||
$matrix = QrCodeMatrix::create($config->version);
|
||||
@@ -97,7 +103,7 @@ final class QrCodeGenerator
|
||||
$matrix = $matrix->setModuleAt($darkModuleRow, 8, Module::dark());
|
||||
|
||||
// 6. Encode data into codewords
|
||||
$dataCodewords = self::encodeData($data, $config);
|
||||
$dataCodewords = $this->encodeData($data, $config);
|
||||
|
||||
// 7. Generate error correction codewords using Reed-Solomon
|
||||
$reedSolomon = new ReedSolomonEncoder();
|
||||
@@ -109,7 +115,7 @@ final class QrCodeGenerator
|
||||
|
||||
// 8. Place data and EC codewords in matrix
|
||||
$allCodewords = array_merge($dataCodewords, $ecCodewords);
|
||||
$matrix = self::placeDataCodewords($matrix, $allCodewords);
|
||||
$matrix = $this->placeDataCodewords($matrix, $allCodewords);
|
||||
|
||||
// 9. Select best mask pattern (evaluates all 8 patterns)
|
||||
$maskEvaluator = new MaskEvaluator();
|
||||
@@ -132,7 +138,7 @@ final class QrCodeGenerator
|
||||
/**
|
||||
* Encode data into codewords (Phase 2: Byte mode with proper structure)
|
||||
*/
|
||||
private static function encodeData(string $data, QrCodeConfig $config): array
|
||||
private function encodeData(string $data, QrCodeConfig $config): array
|
||||
{
|
||||
$codewords = [];
|
||||
$bits = '';
|
||||
@@ -186,14 +192,16 @@ final class QrCodeGenerator
|
||||
/**
|
||||
* Place data codewords in matrix using zig-zag pattern
|
||||
*/
|
||||
private static function placeDataCodewords(QrCodeMatrix $matrix, array $codewords): QrCodeMatrix
|
||||
private function placeDataCodewords(QrCodeMatrix $matrix, array $codewords): QrCodeMatrix
|
||||
{
|
||||
$size = $matrix->getSize();
|
||||
$bitIndex = 0;
|
||||
|
||||
// Convert codewords to bit string
|
||||
// ISO/IEC 18004: Bits are placed MSB-first (most significant bit first)
|
||||
$bits = '';
|
||||
foreach ($codewords as $codeword) {
|
||||
// Convert byte to 8-bit binary string (MSB-first)
|
||||
$bits .= str_pad(decbin($codeword), 8, '0', STR_PAD_LEFT);
|
||||
}
|
||||
$totalBits = strlen($bits);
|
||||
@@ -212,12 +220,14 @@ final class QrCodeGenerator
|
||||
$row = $upward ? ($size - 1 - $i) : $i;
|
||||
|
||||
// Place bits in both columns of the pair
|
||||
// ISO/IEC 18004 Section 7.7.3: Within a column pair, place bits from RIGHT to LEFT
|
||||
// Right column first (col), then left column (col-1)
|
||||
for ($c = 0; $c < 2; $c++) {
|
||||
$currentCol = $col - $c;
|
||||
$position = ModulePosition::at($row, $currentCol);
|
||||
|
||||
// Skip if position is already occupied (function patterns)
|
||||
if (self::isOccupied($matrix, $position)) {
|
||||
if ($this->isOccupied($matrix, $position)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -241,7 +251,7 @@ final class QrCodeGenerator
|
||||
/**
|
||||
* Check if position is occupied by function pattern
|
||||
*/
|
||||
private static function isOccupied(QrCodeMatrix $matrix, ModulePosition $position): bool
|
||||
private function isOccupied(QrCodeMatrix $matrix, ModulePosition $position): bool
|
||||
{
|
||||
$size = $matrix->getSize();
|
||||
$row = $position->row;
|
||||
@@ -320,4 +330,98 @@ final class QrCodeGenerator
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate QR Code as SVG string
|
||||
*/
|
||||
public function generateSvg(
|
||||
string $data,
|
||||
ErrorCorrectionLevel $errorLevel = ErrorCorrectionLevel::M,
|
||||
?QrCodeVersion $version = null
|
||||
): string {
|
||||
$config = $version !== null
|
||||
? QrCodeConfig::withVersion($version->getVersionNumber(), $errorLevel)
|
||||
: QrCodeConfig::autoSize($data, $errorLevel);
|
||||
|
||||
$matrix = $this->generateMatrix($data, $config);
|
||||
|
||||
return $this->renderer->renderSvg($matrix);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate QR Code as Data URI (base64 encoded SVG)
|
||||
*/
|
||||
public function generateDataUri(
|
||||
string $data,
|
||||
ErrorCorrectionLevel $errorLevel = ErrorCorrectionLevel::M,
|
||||
?QrCodeVersion $version = null
|
||||
): string {
|
||||
$config = $version !== null
|
||||
? QrCodeConfig::withVersion($version->getVersionNumber(), $errorLevel)
|
||||
: QrCodeConfig::autoSize($data, $errorLevel);
|
||||
|
||||
$matrix = $this->generateMatrix($data, $config);
|
||||
|
||||
return $this->renderer->toDataUrl($matrix);
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze data and provide QR Code recommendations
|
||||
*
|
||||
* @return array<string, mixed> Analysis data with recommendations
|
||||
*/
|
||||
public function analyzeData(string $data): array
|
||||
{
|
||||
$dataLength = strlen($data);
|
||||
$encodingMode = EncodingMode::BYTE; // Currently only byte mode is supported
|
||||
$recommendedErrorLevel = ErrorCorrectionLevel::M; // Default
|
||||
$recommendedVersion = QrCodeVersion::fromDataLength($dataLength, $encodingMode, $recommendedErrorLevel);
|
||||
|
||||
// Determine if data looks like a URL
|
||||
$isUrl = filter_var($data, FILTER_VALIDATE_URL) !== false || str_starts_with($data, 'http://') || str_starts_with($data, 'https://');
|
||||
|
||||
// Determine if data looks like TOTP URI
|
||||
$isTotp = str_starts_with($data, 'otpauth://totp/');
|
||||
|
||||
// Calculate estimated capacity
|
||||
$capacity = $recommendedVersion->getDataCapacity($encodingMode, $recommendedErrorLevel);
|
||||
|
||||
return [
|
||||
'dataLength' => $dataLength,
|
||||
'dataType' => $isTotp ? 'totp' : ($isUrl ? 'url' : 'text'),
|
||||
'recommendedVersion' => $recommendedVersion->getVersionNumber(),
|
||||
'recommendedErrorLevel' => $recommendedErrorLevel->value,
|
||||
'encodingMode' => $encodingMode->value,
|
||||
'matrixSize' => $recommendedVersion->getMatrixSize(),
|
||||
'capacity' => $capacity,
|
||||
'efficiency' => round(($dataLength / $capacity) * 100, 2),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate TOTP QR Code with optimized settings
|
||||
*
|
||||
* TOTP URIs are typically longer, so we use a higher version for better readability
|
||||
*/
|
||||
public function generateTotpQrCode(string $totpUri): string
|
||||
{
|
||||
// TOTP URIs are typically 50-100 characters, so we use version 3 for better error correction
|
||||
$version = QrCodeVersion::fromNumber(3);
|
||||
$errorLevel = ErrorCorrectionLevel::M; // Medium error correction for TOTP
|
||||
|
||||
$config = QrCodeConfig::withVersion($version->getVersionNumber(), $errorLevel);
|
||||
|
||||
// Validate that data fits
|
||||
$dataLength = strlen($totpUri);
|
||||
$capacity = $version->getDataCapacity($config->encodingMode, $errorLevel);
|
||||
if ($dataLength > $capacity) {
|
||||
throw FrameworkException::simple(
|
||||
"TOTP URI too long: {$dataLength} bytes exceeds capacity of {$capacity} bytes"
|
||||
);
|
||||
}
|
||||
|
||||
$matrix = $this->generateMatrix($totpUri, $config);
|
||||
|
||||
return $this->renderer->renderSvg($matrix);
|
||||
}
|
||||
}
|
||||
|
||||
33
src/Framework/QrCode/QrCodeInitializer.php
Normal file
33
src/Framework/QrCode/QrCodeInitializer.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\QrCode;
|
||||
|
||||
use App\Framework\DI\Container;
|
||||
use App\Framework\DI\Initializer;
|
||||
|
||||
/**
|
||||
* QR Code Module Initializer
|
||||
*
|
||||
* Registriert QrCodeRenderer und QrCodeGenerator im DI-Container
|
||||
*/
|
||||
final readonly class QrCodeInitializer
|
||||
{
|
||||
#[Initializer]
|
||||
public function initializeQrCode(Container $container): void
|
||||
{
|
||||
// QrCodeRenderer - can be used independently
|
||||
$container->singleton(QrCodeRenderer::class, function () {
|
||||
return new QrCodeRenderer();
|
||||
});
|
||||
|
||||
// QrCodeGenerator - depends on QrCodeRenderer
|
||||
$container->singleton(QrCodeGenerator::class, function (Container $container) {
|
||||
$renderer = $container->get(QrCodeRenderer::class);
|
||||
return new QrCodeGenerator($renderer);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -152,4 +152,14 @@ final readonly class QrCodeVersion
|
||||
{
|
||||
return new self(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recommended version for TOTP URIs
|
||||
*
|
||||
* TOTP URIs are typically 50-100 characters, so version 3 is recommended
|
||||
*/
|
||||
public static function forTotp(): self
|
||||
{
|
||||
return new self(3);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user