feat(Production): Complete production deployment infrastructure

- Add comprehensive health check system with multiple endpoints
- Add Prometheus metrics endpoint
- Add production logging configurations (5 strategies)
- Add complete deployment documentation suite:
  * QUICKSTART.md - 30-minute deployment guide
  * DEPLOYMENT_CHECKLIST.md - Printable verification checklist
  * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle
  * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference
  * production-logging.md - Logging configuration guide
  * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation
  * README.md - Navigation hub
  * DEPLOYMENT_SUMMARY.md - Executive summary
- Add deployment scripts and automation
- Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment
- Update README with production-ready features

All production infrastructure is now complete and ready for deployment.
This commit is contained in:
2025-10-25 19:18:37 +02:00
parent caa85db796
commit fc3d7e6357
83016 changed files with 378904 additions and 20919 deletions

View File

@@ -0,0 +1,137 @@
<?php
declare(strict_types=1);
namespace App\Framework\Process\Console;
use App\Framework\Console\Attribute\ConsoleCommand;
use App\Framework\Console\ConsoleInput;
use App\Framework\Console\ExitCode;
use App\Framework\Process\Services\ComposerService;
/**
* Composer Console Commands.
*/
final readonly class ComposerCommands
{
public function __construct(
private ComposerService $composerService
) {
}
#[ConsoleCommand(name: 'composer:diagnose', description: 'Run Composer diagnostics')]
public function diagnose(ConsoleInput $input): int
{
echo "Running Composer diagnostics...\n\n";
$result = $this->composerService->diagnose();
echo "Composer Version: {$result->composerVersion}\n";
echo "Status: " . ($result->isHealthy ? '✅ Healthy' : '❌ Issues Found') . "\n\n";
if ($result->hasWarnings()) {
echo "⚠️ Warnings:\n";
foreach ($result->warnings as $warning) {
echo " - {$warning}\n";
}
echo "\n";
}
if ($result->hasErrors()) {
echo "❌ Errors:\n";
foreach ($result->errors as $error) {
echo " - {$error}\n";
}
echo "\n";
}
if ($result->isHealthy) {
echo "✅ All checks passed!\n";
}
return $result->isHealthy ? ExitCode::SUCCESS : ExitCode::FAILURE;
}
#[ConsoleCommand(name: 'composer:outdated', description: 'List outdated Composer packages')]
public function outdated(ConsoleInput $input): int
{
echo "Checking for outdated packages...\n\n";
$packages = $this->composerService->getOutdatedPackages();
if (empty($packages)) {
echo "✅ All packages are up to date!\n";
return ExitCode::SUCCESS;
}
echo "📦 Found " . count($packages) . " outdated package(s):\n\n";
foreach ($packages as $package) {
echo " 📌 {$package->name}\n";
echo " Current: {$package->currentVersion}\n";
if ($package->latestVersion !== null) {
echo " Latest: {$package->latestVersion}\n";
}
echo "\n";
}
return ExitCode::SUCCESS;
}
#[ConsoleCommand(name: 'composer:validate', description: 'Validate composer.json')]
public function validate(ConsoleInput $input): int
{
echo "Validating composer.json...\n\n";
$isValid = $this->composerService->validate();
if ($isValid) {
echo "✅ composer.json is valid!\n";
return ExitCode::SUCCESS;
}
echo "❌ composer.json validation failed!\n";
return ExitCode::FAILURE;
}
#[ConsoleCommand(name: 'composer:installed', description: 'List installed Composer packages')]
public function installed(ConsoleInput $input): int
{
echo "Loading installed packages...\n\n";
$packages = $this->composerService->getInstalledPackages();
if (empty($packages)) {
echo "No packages installed.\n";
return ExitCode::SUCCESS;
}
echo "📦 Installed Packages (" . count($packages) . "):\n\n";
foreach ($packages as $package) {
echo " {$package->name} ({$package->currentVersion})\n";
if ($package->description !== null) {
echo " {$package->description}\n";
}
}
return ExitCode::SUCCESS;
}
#[ConsoleCommand(name: 'composer:version', description: 'Show Composer version')]
public function version(ConsoleInput $input): int
{
$version = $this->composerService->getVersion();
echo "Composer version: {$version}\n";
return ExitCode::SUCCESS;
}
}

View File

@@ -0,0 +1,262 @@
<?php
declare(strict_types=1);
namespace App\Framework\Process\Console;
use App\Framework\Console\Attribute\ConsoleCommand;
use App\Framework\Console\ConsoleInput;
use App\Framework\Console\ExitCode;
use App\Framework\Process\Services\DockerService;
/**
* Docker Console Commands.
*/
final readonly class DockerCommands
{
public function __construct(
private DockerService $dockerService
) {
}
#[ConsoleCommand(name: 'docker:list', description: 'List Docker containers')]
public function list(ConsoleInput $input): int
{
$all = $input->hasOption('all') || $input->hasOption('a');
echo "Loading Docker containers" . ($all ? ' (including stopped)' : '') . "...\n\n";
$containers = $this->dockerService->listContainers($all);
if (empty($containers)) {
echo "No containers found.\n";
return ExitCode::SUCCESS;
}
echo "🐳 Docker Containers (" . count($containers) . "):\n\n";
foreach ($containers as $container) {
$statusIcon = $container->isRunning ? '✅' : '⏸️';
echo " {$statusIcon} {$container->name} ({$container->id})\n";
echo " Image: {$container->image}\n";
echo " Status: {$container->status}\n";
echo "\n";
}
return ExitCode::SUCCESS;
}
#[ConsoleCommand(name: 'docker:stats', description: 'Show container statistics')]
public function stats(ConsoleInput $input): int
{
$containerId = $input->getArgument('container');
if ($containerId === null) {
echo "❌ Please provide a container ID or name.\n";
echo "Usage: php console.php docker:stats <container>\n";
return ExitCode::FAILURE;
}
echo "Loading stats for container: {$containerId}...\n\n";
$stats = $this->dockerService->getContainerStats($containerId);
if ($stats === null) {
echo "❌ Could not retrieve stats for container: {$containerId}\n";
return ExitCode::FAILURE;
}
echo "📊 Container Statistics:\n\n";
echo " Container: {$stats->containerName}\n";
echo " ID: {$stats->containerId}\n\n";
echo " CPU Usage: {$stats->cpuPercent->format(2)}\n";
echo " Memory: {$stats->memoryUsage->toHumanReadable()} / {$stats->memoryLimit->toHumanReadable()} ({$stats->memoryPercent->format(2)})\n";
echo " Network I/O: ↓ {$stats->networkIn->toHumanReadable()} / ↑ {$stats->networkOut->toHumanReadable()}\n";
echo " Block I/O: ↓ {$stats->blockIn->toHumanReadable()} / ↑ {$stats->blockOut->toHumanReadable()}\n";
return ExitCode::SUCCESS;
}
#[ConsoleCommand(name: 'docker:logs', description: 'Show container logs')]
public function logs(ConsoleInput $input): int
{
$containerId = $input->getArgument('container');
if ($containerId === null) {
echo "❌ Please provide a container ID or name.\n";
echo "Usage: php console.php docker:logs <container> [--lines=N]\n";
return ExitCode::FAILURE;
}
$lines = (int) ($input->getOption('lines') ?? 100);
echo "Loading logs for container: {$containerId} (last {$lines} lines)...\n\n";
$logs = $this->dockerService->getContainerLogs($containerId, $lines);
if ($logs === null) {
echo "❌ Could not retrieve logs for container: {$containerId}\n";
return ExitCode::FAILURE;
}
echo "--- Container Logs ---\n\n";
echo $logs;
return ExitCode::SUCCESS;
}
#[ConsoleCommand(name: 'docker:inspect', description: 'Inspect container details')]
public function inspect(ConsoleInput $input): int
{
$containerId = $input->getArgument('container');
if ($containerId === null) {
echo "❌ Please provide a container ID or name.\n";
echo "Usage: php console.php docker:inspect <container>\n";
return ExitCode::FAILURE;
}
echo "Inspecting container: {$containerId}...\n\n";
$data = $this->dockerService->inspectContainer($containerId);
if ($data === null) {
echo "❌ Could not inspect container: {$containerId}\n";
return ExitCode::FAILURE;
}
echo "--- Container Inspection ---\n\n";
echo json_encode($data, JSON_PRETTY_PRINT);
echo "\n";
return ExitCode::SUCCESS;
}
#[ConsoleCommand(name: 'docker:start', description: 'Start a container')]
public function start(ConsoleInput $input): int
{
$containerId = $input->getArgument('container');
if ($containerId === null) {
echo "❌ Please provide a container ID or name.\n";
echo "Usage: php console.php docker:start <container>\n";
return ExitCode::FAILURE;
}
echo "Starting container: {$containerId}...\n";
$success = $this->dockerService->startContainer($containerId);
if ($success) {
echo "✅ Container started successfully!\n";
return ExitCode::SUCCESS;
}
echo "❌ Failed to start container.\n";
return ExitCode::FAILURE;
}
#[ConsoleCommand(name: 'docker:stop', description: 'Stop a container')]
public function stop(ConsoleInput $input): int
{
$containerId = $input->getArgument('container');
if ($containerId === null) {
echo "❌ Please provide a container ID or name.\n";
echo "Usage: php console.php docker:stop <container>\n";
return ExitCode::FAILURE;
}
echo "Stopping container: {$containerId}...\n";
$success = $this->dockerService->stopContainer($containerId);
if ($success) {
echo "✅ Container stopped successfully!\n";
return ExitCode::SUCCESS;
}
echo "❌ Failed to stop container.\n";
return ExitCode::FAILURE;
}
#[ConsoleCommand(name: 'docker:restart', description: 'Restart a container')]
public function restart(ConsoleInput $input): int
{
$containerId = $input->getArgument('container');
if ($containerId === null) {
echo "❌ Please provide a container ID or name.\n";
echo "Usage: php console.php docker:restart <container>\n";
return ExitCode::FAILURE;
}
echo "Restarting container: {$containerId}...\n";
$success = $this->dockerService->restartContainer($containerId);
if ($success) {
echo "✅ Container restarted successfully!\n";
return ExitCode::SUCCESS;
}
echo "❌ Failed to restart container.\n";
return ExitCode::FAILURE;
}
#[ConsoleCommand(name: 'docker:version', description: 'Show Docker version')]
public function version(ConsoleInput $input): int
{
$version = $this->dockerService->getVersion();
if ($version === null) {
echo "❌ Could not retrieve Docker version.\n";
return ExitCode::FAILURE;
}
echo "Docker version: {$version}\n";
return ExitCode::SUCCESS;
}
#[ConsoleCommand(name: 'docker:status', description: 'Check if Docker is running')]
public function status(ConsoleInput $input): int
{
echo "Checking Docker status...\n\n";
$isRunning = $this->dockerService->isRunning();
if ($isRunning) {
echo "✅ Docker is running!\n";
$version = $this->dockerService->getVersion();
if ($version !== null) {
echo " Version: {$version}\n";
}
return ExitCode::SUCCESS;
}
echo "❌ Docker is not running or not accessible.\n";
return ExitCode::FAILURE;
}
}

View File

@@ -0,0 +1,275 @@
<?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\NpmService;
/**
* NPM Console Commands.
*/
final readonly class NpmCommands
{
public function __construct(
private NpmService $npmService
) {
}
#[ConsoleCommand(name: 'npm:audit', description: 'Run NPM security audit')]
public function audit(ConsoleInput $input): int
{
echo "Running NPM security audit...\n\n";
$result = $this->npmService->audit();
echo "🔒 Security Audit Results:\n\n";
echo " Total Vulnerabilities: {$result->total}\n\n";
if ($result->hasVulnerabilities()) {
echo " 📊 Breakdown by Severity:\n";
echo " Info: {$result->info}\n";
echo " Low: {$result->low}\n";
echo " Moderate: {$result->moderate}\n";
echo " High: {$result->high}\n";
echo " Critical: {$result->critical}\n\n";
if (! empty($result->vulnerabilities)) {
echo " 📦 Affected Packages:\n";
foreach ($result->vulnerabilities as $package) {
echo " - {$package}\n";
}
echo "\n";
}
if ($result->hasCriticalIssues()) {
echo " ⚠️ CRITICAL: High or critical vulnerabilities found!\n";
echo " Run 'npm audit fix' to attempt automatic fixes.\n";
return ExitCode::FAILURE;
}
echo " Run 'npm audit fix' to attempt automatic fixes.\n";
return ExitCode::WARNING;
}
echo " ✅ No vulnerabilities found!\n";
return ExitCode::SUCCESS;
}
#[ConsoleCommand(name: 'npm:audit-fix', description: 'Fix NPM security vulnerabilities')]
public function auditFix(ConsoleInput $input): int
{
$force = $input->hasOption('force');
echo "Attempting to fix security vulnerabilities" . ($force ? ' (--force)' : '') . "...\n\n";
$success = $this->npmService->auditFix($force);
if ($success) {
echo "✅ Audit fix completed!\n";
echo " Run 'npm audit' to verify remaining vulnerabilities.\n";
return ExitCode::SUCCESS;
}
echo "❌ Audit fix failed.\n";
return ExitCode::FAILURE;
}
#[ConsoleCommand(name: 'npm:outdated', description: 'List outdated NPM packages')]
public function outdated(ConsoleInput $input): int
{
echo "Checking for outdated packages...\n\n";
$packages = $this->npmService->getOutdatedPackages();
if (empty($packages)) {
echo "✅ All packages are up to date!\n";
return ExitCode::SUCCESS;
}
echo "📦 Found " . count($packages) . " outdated package(s):\n\n";
foreach ($packages as $package) {
echo " 📌 {$package->name}";
if ($package->type !== null) {
echo " ({$package->type})";
}
echo "\n";
echo " Current: {$package->currentVersion}\n";
if ($package->wantedVersion !== null) {
echo " Wanted: {$package->wantedVersion}\n";
}
if ($package->latestVersion !== null) {
echo " Latest: {$package->latestVersion}\n";
}
echo "\n";
}
echo " Run 'npm update' to update packages.\n";
return ExitCode::SUCCESS;
}
#[ConsoleCommand(name: 'npm:installed', description: 'List installed NPM packages')]
public function installed(ConsoleInput $input): int
{
echo "Loading installed packages...\n\n";
$packages = $this->npmService->getInstalledPackages();
if (empty($packages)) {
echo "No packages installed.\n";
return ExitCode::SUCCESS;
}
$dependencies = array_filter($packages, fn ($p) => $p->type === 'dependencies');
$devDependencies = array_filter($packages, fn ($p) => $p->type === 'devDependencies');
if (! empty($dependencies)) {
echo "📦 Dependencies (" . count($dependencies) . "):\n";
foreach ($dependencies as $package) {
echo " {$package->name}@{$package->currentVersion}\n";
}
echo "\n";
}
if (! empty($devDependencies)) {
echo "🔧 DevDependencies (" . count($devDependencies) . "):\n";
foreach ($devDependencies as $package) {
echo " {$package->name}@{$package->currentVersion}\n";
}
echo "\n";
}
return ExitCode::SUCCESS;
}
#[ConsoleCommand(name: 'npm:validate', description: 'Validate package.json')]
public function validate(ConsoleInput $input): int
{
echo "Validating package.json...\n\n";
$isValid = $this->npmService->validate();
if ($isValid) {
echo "✅ package.json is valid!\n";
return ExitCode::SUCCESS;
}
echo "❌ package.json validation failed or not found!\n";
return ExitCode::FAILURE;
}
#[ConsoleCommand(name: 'npm:version', description: 'Show NPM version')]
public function version(ConsoleInput $input): int
{
$version = $this->npmService->getVersion();
echo "NPM version: {$version}\n";
return ExitCode::SUCCESS;
}
#[ConsoleCommand(name: 'npm:install', description: 'Install NPM dependencies')]
public function install(ConsoleInput $input): int
{
$production = $input->hasOption('production') || $input->hasOption('prod');
echo "Installing NPM dependencies" . ($production ? ' (production only)' : '') . "...\n\n";
$success = $this->npmService->install($production);
if ($success) {
echo "✅ Dependencies installed successfully!\n";
return ExitCode::SUCCESS;
}
echo "❌ Installation failed.\n";
return ExitCode::FAILURE;
}
#[ConsoleCommand(name: 'npm:update', description: 'Update NPM dependencies')]
public function update(ConsoleInput $input): int
{
$production = $input->hasOption('production') || $input->hasOption('prod');
echo "Updating NPM dependencies" . ($production ? ' (production only)' : '') . "...\n\n";
$success = $this->npmService->update($production);
if ($success) {
echo "✅ Dependencies updated successfully!\n";
return ExitCode::SUCCESS;
}
echo "❌ Update failed.\n";
return ExitCode::FAILURE;
}
#[ConsoleCommand(name: 'npm:clean', description: 'Clean node_modules and package-lock.json')]
public function clean(ConsoleInput $input): int
{
echo "Cleaning node_modules and package-lock.json...\n\n";
$success = $this->npmService->clean();
if ($success) {
echo "✅ Cleaned successfully!\n";
echo " Run 'npm install' to reinstall dependencies.\n";
return ExitCode::SUCCESS;
}
echo "❌ Clean failed.\n";
return ExitCode::FAILURE;
}
#[ConsoleCommand(name: 'npm:run', description: 'Run NPM script')]
public function run(ConsoleInput $input): int
{
$script = $input->getArgument('script');
if ($script === null) {
echo "❌ Please provide a script name.\n";
echo "Usage: php console.php npm:run <script>\n";
return ExitCode::FAILURE;
}
echo "Running NPM script: {$script}...\n\n";
$success = $this->npmService->runScript($script);
if ($success) {
echo "\n✅ Script completed successfully!\n";
return ExitCode::SUCCESS;
}
echo "\n❌ Script failed.\n";
return ExitCode::FAILURE;
}
}

View File

@@ -0,0 +1,83 @@
<?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;
}
}

View File

@@ -0,0 +1,94 @@
<?php
declare(strict_types=1);
namespace App\Framework\Process\Exceptions;
use App\Framework\Exception\Core\ConsoleErrorCode;
use App\Framework\Exception\Core\FileSystemErrorCode;
use App\Framework\Exception\Core\SystemErrorCode;
use App\Framework\Exception\FrameworkException;
use App\Framework\Process\ProcessResult;
use App\Framework\Process\ValueObjects\Command;
/**
* Exception für Prozess-bezogene Fehler.
*/
final class ProcessException extends FrameworkException
{
/**
* Erstellt eine Exception, wenn ein Prozess nicht gestartet werden kann.
*/
public static function failedToStart(Command $command, ?string $reason = null): self
{
$message = "Failed to start process: {$command->toString()}";
if ($reason !== null) {
$message .= " - {$reason}";
}
return self::create(
SystemErrorCode::RESOURCE_EXHAUSTED,
$message
)->withData([
'command' => $command->toString(),
'reason' => $reason,
]);
}
/**
* Erstellt eine Exception, wenn ein Prozess fehlschlägt.
*/
public static function executionFailed(ProcessResult $result): self
{
return self::create(
SystemErrorCode::INITIALIZATION_FAILED,
"Process execution failed: {$result->command->toString()}"
)->withData([
'command' => $result->command->toString(),
'exit_code' => $result->exitCode->value,
'exit_description' => $result->exitCode->getDescription(),
'stderr' => $result->stderr,
'runtime_ms' => $result->runtime->toMilliseconds(),
]);
}
/**
* Erstellt eine Exception, wenn ein Kommando nicht gefunden wird.
*/
public static function commandNotFound(string $commandName): self
{
return self::create(
ConsoleErrorCode::COMMAND_NOT_FOUND,
"Command not found: {$commandName}"
)->withData([
'command' => $commandName,
]);
}
/**
* Erstellt eine Exception, wenn ein Signal nicht gesendet werden kann.
*/
public static function failedToSendSignal(int $pid, int $signal): self
{
return self::create(
SystemErrorCode::RESOURCE_EXHAUSTED,
"Failed to send signal {$signal} to process {$pid}"
)->withData([
'pid' => $pid,
'signal' => $signal,
]);
}
/**
* Erstellt eine Exception, wenn das Arbeitsverzeichnis ungültig ist.
*/
public static function invalidWorkingDirectory(string $path): self
{
return self::create(
FileSystemErrorCode::FILE_NOT_FOUND,
"Invalid working directory: {$path}"
)->withData([
'path' => $path,
]);
}
}

View File

@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace App\Framework\Process\Exceptions;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Exception\ErrorCode;
use App\Framework\Exception\FrameworkException;
use App\Framework\Process\ValueObjects\Command;
/**
* Exception für Prozess-Timeout-Fehler.
*/
final class ProcessTimeoutException extends FrameworkException
{
/**
* Erstellt eine Exception, wenn ein Prozess-Timeout überschritten wird.
*/
public static function exceeded(Command $command, Duration $timeout): self
{
return self::create(
ErrorCode::TEMP_FAIL,
"Process timeout exceeded: {$command->toString()} (timeout: {$timeout->toHumanReadable()})"
)->withData([
'command' => $command->toString(),
'timeout_seconds' => $timeout->toSeconds(),
'timeout_human' => $timeout->toHumanReadable(),
])->withRetryAfter((int) ceil($timeout->toSeconds()));
}
}

View File

@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
namespace App\Framework\Process;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Filesystem\ValueObjects\FilePath;
use App\Framework\Process\Exceptions\ProcessException;
use App\Framework\Process\Exceptions\ProcessTimeoutException;
use App\Framework\Process\ValueObjects\Command;
use App\Framework\Process\ValueObjects\EnvironmentVariables;
/**
* Interface für Prozess-Ausführung.
*
* Ermöglicht das Ausführen von System-Prozessen mit konfigurierbaren
* Optionen wie Umgebungsvariablen, Arbeitsverzeichnis und Timeout.
*/
interface Process
{
/**
* Führt einen Prozess synchron aus und wartet bis zum Ende.
*
* @param Command $command Das auszuführende Kommando
* @param EnvironmentVariables|null $env Zusätzliche Umgebungsvariablen
* @param FilePath|null $workingDirectory Arbeitsverzeichnis
* @param Duration|null $timeout Timeout für die Ausführung
* @return ProcessResult Das Ergebnis der Prozess-Ausführung
*
* @throws ProcessException wenn der Prozess nicht gestartet werden kann
* @throws ProcessTimeoutException wenn das Timeout überschritten wird
*/
public function run(
Command $command,
?EnvironmentVariables $env = null,
?FilePath $workingDirectory = null,
?Duration $timeout = null
): ProcessResult;
/**
* Startet einen Prozess asynchron (non-blocking).
*
* @param Command $command Das auszuführende Kommando
* @param EnvironmentVariables|null $env Zusätzliche Umgebungsvariablen
* @param FilePath|null $workingDirectory Arbeitsverzeichnis
* @return RunningProcess Der laufende Prozess
*
* @throws ProcessException wenn der Prozess nicht gestartet werden kann
*/
public function start(
Command $command,
?EnvironmentVariables $env = null,
?FilePath $workingDirectory = null
): RunningProcess;
/**
* Prüft, ob ein Kommando existiert und ausführbar ist.
*
* @param string $commandName Name des Kommandos (z.B. 'ffmpeg')
* @return bool True wenn das Kommando verfügbar ist
*/
public function commandExists(string $commandName): bool;
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace App\Framework\Process;
use App\Framework\DI\Initializer;
use App\Framework\Logging\Logger;
/**
* Initialisiert das Process System im DI Container.
*/
final readonly class ProcessInitializer
{
public function __construct(
private Logger $logger
) {
}
#[Initializer]
public function __invoke(): Process
{
return new SystemProcess(
logger: $this->logger
);
}
}

View File

@@ -0,0 +1,106 @@
<?php
declare(strict_types=1);
namespace App\Framework\Process;
use App\Framework\Console\ExitCode;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Process\ValueObjects\Command;
/**
* Repräsentiert das Ergebnis einer Prozess-Ausführung.
*/
final readonly class ProcessResult
{
public function __construct(
public Command $command,
public string $stdout,
public string $stderr,
public ExitCode $exitCode,
public Duration $runtime
) {
}
/**
* Prüft, ob der Prozess erfolgreich war (Exit Code 0).
*/
public function isSuccess(): bool
{
return $this->exitCode->isSuccess();
}
/**
* Prüft, ob der Prozess fehlgeschlagen ist.
*/
public function isFailed(): bool
{
return $this->exitCode->isError();
}
/**
* Gibt die Standard-Ausgabe zurück.
*/
public function getOutput(): string
{
return $this->stdout;
}
/**
* Gibt die Fehler-Ausgabe zurück.
*/
public function getErrorOutput(): string
{
return $this->stderr;
}
/**
* Prüft, ob Standard-Ausgabe vorhanden ist.
*/
public function hasOutput(): bool
{
return ! empty($this->stdout);
}
/**
* Prüft, ob Fehler-Ausgabe vorhanden ist.
*/
public function hasErrors(): bool
{
return ! empty($this->stderr);
}
/**
* Gibt die kombinierte Ausgabe (stdout + stderr) zurück.
*/
public function getCombinedOutput(): string
{
return trim($this->stdout . "\n" . $this->stderr);
}
/**
* Gibt das Ergebnis als Array zurück.
*
* @return array{
* command: string,
* stdout: string,
* stderr: string,
* exit_code: int,
* exit_description: string,
* runtime_ms: float,
* success: bool
* }
*/
public function toArray(): array
{
return [
'command' => $this->command->toString(),
'stdout' => $this->stdout,
'stderr' => $this->stderr,
'exit_code' => $this->exitCode->value,
'exit_description' => $this->exitCode->getDescription(),
'runtime_ms' => $this->runtime->toMilliseconds(),
'success' => $this->isSuccess(),
];
}
}

View File

@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
namespace App\Framework\Process;
use App\Framework\Core\ValueObjects\Duration;
/**
* Interface für einen laufenden asynchronen Prozess.
*
* Ermöglicht die Verwaltung und Kontrolle eines Prozesses,
* der im Hintergrund läuft.
*/
interface RunningProcess
{
/**
* Prüft, ob der Prozess noch läuft.
*
* @return bool True wenn der Prozess noch läuft
*/
public function isRunning(): bool;
/**
* Gibt die Prozess-ID zurück.
*
* @return int Die Prozess-ID
*/
public function getPid(): int;
/**
* Wartet bis der Prozess beendet ist und gibt das Ergebnis zurück.
*
* @param Duration|null $timeout Optionales Timeout
* @return ProcessResult Das Ergebnis der Prozess-Ausführung
*
* @throws ProcessTimeoutException wenn das Timeout überschritten wird
*/
public function wait(?Duration $timeout = null): ProcessResult;
/**
* Sendet ein Signal an den Prozess.
*
* @param int $signal Signal-Nummer (default: SIGTERM = 15)
* @return void
*
* @throws ProcessException wenn das Signal nicht gesendet werden kann
*/
public function signal(int $signal = 15): void;
/**
* Beendet den Prozess (SIGTERM).
*
* @return void
*/
public function terminate(): void;
/**
* Beendet den Prozess forciert (SIGKILL).
*
* @return void
*/
public function kill(): void;
}

View File

@@ -0,0 +1,132 @@
<?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\Backup\BackupFile;
use App\Framework\Process\ValueObjects\Backup\BackupVerificationResult;
use App\Framework\Process\ValueObjects\Command;
/**
* Backup Verification Service.
*
* Verifiziert Backup-Dateien in einem Verzeichnis.
*/
final readonly class BackupVerificationService
{
public function __construct(
private Process $process
) {
}
/**
* Verifiziert Backups in einem Verzeichnis.
*/
public function verify(FilePath $backupDirectory, string $pattern = '*.sql'): BackupVerificationResult
{
if (! $backupDirectory->isDirectory()) {
return BackupVerificationResult::fromBackups([]);
}
$backups = $this->findBackups($backupDirectory, $pattern);
return BackupVerificationResult::fromBackups($backups);
}
/**
* Findet Backup-Dateien in einem Verzeichnis.
*
* @return BackupFile[]
*/
private function findBackups(FilePath $directory, string $pattern): array
{
// Find files matching pattern
$result = $this->process->run(
Command::fromString("find {$directory->toString()} -name '{$pattern}' -type f")
);
if (! $result->isSuccess() || empty($result->stdout)) {
return [];
}
$filePaths = explode("\n", trim($result->stdout));
$backups = [];
foreach ($filePaths as $filePath) {
if (empty($filePath)) {
continue;
}
$backupFile = $this->createBackupFile(FilePath::create($filePath));
if ($backupFile !== null) {
$backups[] = $backupFile;
}
}
// Sort by creation date (newest first)
usort($backups, fn (BackupFile $a, BackupFile $b) => $b->createdAt <=> $a->createdAt);
return $backups;
}
/**
* Erstellt ein BackupFile aus einem FilePath.
*/
private function createBackupFile(FilePath $path): ?BackupFile
{
if (! $path->exists() || ! $path->isFile()) {
return null;
}
$size = $path->getSize();
$modifiedTime = $path->getModifiedTime();
return new BackupFile(
path: $path,
size: $size,
createdAt: new \DateTimeImmutable('@' . $modifiedTime),
name: $path->getFilename()
);
}
/**
* Prüft die Integrität einer Backup-Datei (falls komprimiert).
*/
public function checkIntegrity(BackupFile $backup): bool
{
$extension = $backup->path->getExtension();
return match ($extension) {
'gz', 'gzip' => $this->testGzipIntegrity($backup->path),
'zip' => $this->testZipIntegrity($backup->path),
default => true, // Unkomprimierte Dateien als OK annehmen
};
}
/**
* Testet Gzip-Integrität.
*/
private function testGzipIntegrity(FilePath $file): bool
{
$result = $this->process->run(
Command::fromArray(['gzip', '-t', $file->toString()])
);
return $result->isSuccess();
}
/**
* Testet ZIP-Integrität.
*/
private function testZipIntegrity(FilePath $file): bool
{
$result = $this->process->run(
Command::fromArray(['unzip', '-t', $file->toString()])
);
return $result->isSuccess();
}
}

View File

@@ -0,0 +1,227 @@
<?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;
use App\Framework\Process\ValueObjects\Composer\ComposerDiagnosticResult;
use App\Framework\Process\ValueObjects\Composer\ComposerPackage;
/**
* Composer Service.
*
* Verwaltet Composer-Operationen wie Diagnose, Updates und Package-Informationen.
*/
final readonly class ComposerService
{
public function __construct(
private Process $process,
private ?FilePath $projectRoot = null
) {
}
/**
* Führt Composer Diagnose durch.
*/
public function diagnose(): ComposerDiagnosticResult
{
$result = $this->process->run(
command: Command::fromArray(['composer', 'diagnose']),
workingDirectory: $this->projectRoot,
timeout: Duration::fromSeconds(30)
);
$warnings = [];
$errors = [];
// Parse Output
$lines = explode("\n", $result->stdout);
foreach ($lines as $line) {
$line = trim($line);
if (str_contains($line, '[warning]')) {
$warnings[] = $line;
} elseif (str_contains($line, '[error]')) {
$errors[] = $line;
}
}
// Composer Version abrufen
$version = $this->getVersion();
if (empty($errors)) {
return ComposerDiagnosticResult::healthy($version);
}
return ComposerDiagnosticResult::withIssues($warnings, $errors, $version);
}
/**
* Gibt die Composer-Version zurück.
*/
public function getVersion(): string
{
$result = $this->process->run(
Command::fromArray(['composer', '--version'])
);
if (! $result->isSuccess()) {
return 'unknown';
}
// Parse "Composer version 2.x.x"
if (preg_match('/Composer version ([^\s]+)/', $result->stdout, $matches)) {
return $matches[1];
}
return trim($result->stdout);
}
/**
* Validiert composer.json.
*/
public function validate(): bool
{
$result = $this->process->run(
command: Command::fromArray(['composer', 'validate', '--no-check-all']),
workingDirectory: $this->projectRoot
);
return $result->isSuccess();
}
/**
* Listet veraltete Packages auf.
*
* @return ComposerPackage[]
*/
public function getOutdatedPackages(): array
{
$result = $this->process->run(
command: Command::fromArray(['composer', 'outdated', '--format=json']),
workingDirectory: $this->projectRoot,
timeout: Duration::fromSeconds(60)
);
if (! $result->isSuccess()) {
return [];
}
$data = json_decode($result->stdout, true);
if (! isset($data['installed'])) {
return [];
}
$packages = [];
foreach ($data['installed'] as $package) {
$packages[] = ComposerPackage::fromComposerOutput(
name: $package['name'] ?? 'unknown',
currentVersion: $package['version'] ?? 'unknown',
latestVersion: $package['latest'] ?? null
);
}
return $packages;
}
/**
* Installiert Dependencies.
*/
public function install(bool $noDev = false): bool
{
$args = ['composer', 'install'];
if ($noDev) {
$args[] = '--no-dev';
}
$result = $this->process->run(
command: Command::fromArray($args),
workingDirectory: $this->projectRoot,
timeout: Duration::fromMinutes(5)
);
return $result->isSuccess();
}
/**
* Aktualisiert Dependencies.
*/
public function update(bool $noDev = false): bool
{
$args = ['composer', 'update'];
if ($noDev) {
$args[] = '--no-dev';
}
$result = $this->process->run(
command: Command::fromArray($args),
workingDirectory: $this->projectRoot,
timeout: Duration::fromMinutes(10)
);
return $result->isSuccess();
}
/**
* Zeigt installierte Packages.
*
* @return ComposerPackage[]
*/
public function getInstalledPackages(): array
{
$result = $this->process->run(
command: Command::fromArray(['composer', 'show', '--format=json']),
workingDirectory: $this->projectRoot
);
if (! $result->isSuccess()) {
return [];
}
$data = json_decode($result->stdout, true);
if (! isset($data['installed'])) {
return [];
}
$packages = [];
foreach ($data['installed'] as $package) {
$packages[] = new ComposerPackage(
name: $package['name'] ?? 'unknown',
currentVersion: $package['version'] ?? 'unknown',
description: $package['description'] ?? null
);
}
return $packages;
}
/**
* Dumpt Autoloader.
*/
public function dumpAutoload(bool $optimize = false): bool
{
$args = ['composer', 'dump-autoload'];
if ($optimize) {
$args[] = '--optimize';
}
$result = $this->process->run(
command: Command::fromArray($args),
workingDirectory: $this->projectRoot
);
return $result->isSuccess();
}
}

View File

@@ -0,0 +1,362 @@
<?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\Docker\DockerContainer;
use App\Framework\Process\ValueObjects\Docker\DockerContainerStats;
/**
* Docker Service.
*
* Verwaltet Docker-Container-Operationen wie Listing, Stats, Logs und Container-Management.
*/
final readonly class DockerService
{
public function __construct(
private Process $process
) {
}
/**
* Listet alle Container auf (laufend und gestoppt).
*
* @return DockerContainer[]
*/
public function listContainers(bool $all = false): array
{
$args = ['docker', 'ps', '--format', '{{.ID}}|{{.Names}}|{{.Image}}|{{.Status}}'];
if ($all) {
$args[] = '--all';
}
$result = $this->process->run(
command: Command::fromArray($args),
timeout: Duration::fromSeconds(10)
);
if (! $result->isSuccess()) {
return [];
}
$containers = [];
$lines = explode("\n", trim($result->stdout));
foreach ($lines as $line) {
if (empty($line)) {
continue;
}
$parts = explode('|', $line);
if (count($parts) < 4) {
continue;
}
$containers[] = DockerContainer::fromDockerPs(
id: $parts[0],
name: $parts[1],
image: $parts[2],
status: $parts[3]
);
}
return $containers;
}
/**
* Ruft Container-Statistiken ab.
*/
public function getContainerStats(string $containerId): ?DockerContainerStats
{
$result = $this->process->run(
command: Command::fromArray([
'docker',
'stats',
$containerId,
'--no-stream',
'--format',
'{{.CPUPerc}}|{{.MemUsage}}|{{.NetIO}}|{{.BlockIO}}',
]),
timeout: Duration::fromSeconds(5)
);
if (! $result->isSuccess()) {
return null;
}
$line = trim($result->stdout);
$parts = explode('|', $line);
if (count($parts) < 4) {
return null;
}
// Parse CPU percentage (z.B. "0.50%")
$cpuPercent = (float) rtrim($parts[0], '%');
// Parse Memory Usage (z.B. "256MiB / 2GiB")
$memoryParts = explode(' / ', $parts[1]);
$memoryUsage = $this->parseBytes($memoryParts[0] ?? '0B');
$memoryLimit = $this->parseBytes($memoryParts[1] ?? '0B');
// Parse Network I/O (z.B. "1.2kB / 3.4kB")
$networkParts = explode(' / ', $parts[2]);
$networkIn = $this->parseBytes($networkParts[0] ?? '0B');
$networkOut = $this->parseBytes($networkParts[1] ?? '0B');
// Parse Block I/O (z.B. "5MB / 10MB")
$blockParts = explode(' / ', $parts[3]);
$blockIn = $this->parseBytes($blockParts[0] ?? '0B');
$blockOut = $this->parseBytes($blockParts[1] ?? '0B');
// Get container name
$container = $this->getContainer($containerId);
$containerName = $container?->name ?? $containerId;
return DockerContainerStats::fromDockerStats(
containerId: $containerId,
containerName: $containerName,
cpuPercent: $cpuPercent,
memoryUsageBytes: $memoryUsage,
memoryLimitBytes: $memoryLimit,
networkInBytes: $networkIn,
networkOutBytes: $networkOut,
blockInBytes: $blockIn,
blockOutBytes: $blockOut
);
}
/**
* Ruft detaillierte Container-Informationen ab.
*/
public function inspectContainer(string $containerId): ?array
{
$result = $this->process->run(
command: Command::fromArray(['docker', 'inspect', $containerId]),
timeout: Duration::fromSeconds(5)
);
if (! $result->isSuccess()) {
return null;
}
$data = json_decode($result->stdout, true);
return $data[0] ?? null;
}
/**
* Ruft Container-Logs ab.
*/
public function getContainerLogs(string $containerId, int $lines = 100): ?string
{
$result = $this->process->run(
command: Command::fromArray([
'docker',
'logs',
'--tail',
(string) $lines,
$containerId,
]),
timeout: Duration::fromSeconds(10)
);
if (! $result->isSuccess()) {
return null;
}
return $result->stdout . $result->stderr;
}
/**
* Startet einen Container.
*/
public function startContainer(string $containerId): bool
{
$result = $this->process->run(
command: Command::fromArray(['docker', 'start', $containerId]),
timeout: Duration::fromSeconds(30)
);
return $result->isSuccess();
}
/**
* Stoppt einen Container.
*/
public function stopContainer(string $containerId, int $timeoutSeconds = 10): bool
{
$result = $this->process->run(
command: Command::fromArray([
'docker',
'stop',
'--time',
(string) $timeoutSeconds,
$containerId,
]),
timeout: Duration::fromSeconds($timeoutSeconds + 5)
);
return $result->isSuccess();
}
/**
* Startet Container neu.
*/
public function restartContainer(string $containerId, int $timeoutSeconds = 10): bool
{
$result = $this->process->run(
command: Command::fromArray([
'docker',
'restart',
'--time',
(string) $timeoutSeconds,
$containerId,
]),
timeout: Duration::fromSeconds($timeoutSeconds + 5)
);
return $result->isSuccess();
}
/**
* Entfernt einen Container.
*/
public function removeContainer(string $containerId, bool $force = false): bool
{
$args = ['docker', 'rm'];
if ($force) {
$args[] = '--force';
}
$args[] = $containerId;
$result = $this->process->run(
command: Command::fromArray($args),
timeout: Duration::fromSeconds(30)
);
return $result->isSuccess();
}
/**
* Führt einen Befehl in einem Container aus.
*/
public function execInContainer(string $containerId, array $command, bool $interactive = false): ?string
{
$args = ['docker', 'exec'];
if ($interactive) {
$args[] = '-i';
}
$args[] = $containerId;
$args = [...$args, ...$command];
$result = $this->process->run(
command: Command::fromArray($args),
timeout: Duration::fromSeconds(60)
);
if (! $result->isSuccess()) {
return null;
}
return $result->stdout;
}
/**
* Ruft Docker-Version ab.
*/
public function getVersion(): ?string
{
$result = $this->process->run(
command: Command::fromArray(['docker', '--version']),
timeout: Duration::fromSeconds(5)
);
if (! $result->isSuccess()) {
return null;
}
// Parse "Docker version 20.10.17, build 100c701"
if (preg_match('/Docker version ([^,]+)/', $result->stdout, $matches)) {
return $matches[1];
}
return trim($result->stdout);
}
/**
* Prüft ob Docker läuft.
*/
public function isRunning(): bool
{
$result = $this->process->run(
command: Command::fromArray(['docker', 'ps']),
timeout: Duration::fromSeconds(5)
);
return $result->isSuccess();
}
/**
* Holt einen einzelnen Container.
*/
private function getContainer(string $containerId): ?DockerContainer
{
$containers = $this->listContainers(all: true);
foreach ($containers as $container) {
if ($container->id === $containerId || $container->name === $containerId) {
return $container;
}
}
return null;
}
/**
* Parst Byte-Strings (z.B. "256MiB", "2GiB", "1.2kB").
*/
private function parseBytes(string $value): int
{
$value = trim($value);
if (empty($value) || $value === '0B') {
return 0;
}
// Match number and unit
if (! preg_match('/^([\d.]+)\s*([KMGT]i?B)$/i', $value, $matches)) {
return 0;
}
$number = (float) $matches[1];
$unit = strtoupper($matches[2]);
// Binary units (KiB, MiB, GiB, TiB)
$multipliers = [
'B' => 1,
'KB' => 1000,
'KIB' => 1024,
'MB' => 1000 * 1000,
'MIB' => 1024 * 1024,
'GB' => 1000 * 1000 * 1000,
'GIB' => 1024 * 1024 * 1024,
'TB' => 1000 * 1000 * 1000 * 1000,
'TIB' => 1024 * 1024 * 1024 * 1024,
];
$multiplier = $multipliers[$unit] ?? 1;
return (int) round($number * $multiplier);
}
}

View File

@@ -0,0 +1,129 @@
<?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;
use App\Framework\Process\ValueObjects\Logs\LogAnalysisResult;
use App\Framework\Process\ValueObjects\Logs\LogEntry;
/**
* Log Analysis Service.
*
* Analysiert Log-Dateien und gibt strukturierte Ergebnisse zurück.
*/
final readonly class LogAnalysisService
{
public function __construct(
private Process $process
) {
}
/**
* Analysiert eine Log-Datei.
*/
public function analyze(
FilePath $logFile,
int $lines = 1000,
?string $levelFilter = null
): LogAnalysisResult {
$command = $this->buildCommand($logFile, $lines, $levelFilter);
$result = $this->process->run($command);
if (! $result->isSuccess()) {
return LogAnalysisResult::fromEntries([], 0);
}
$logLines = explode("\n", trim($result->stdout));
$entries = array_map(
fn (string $line) => LogEntry::fromLine($line),
array_filter($logLines)
);
return LogAnalysisResult::fromEntries($entries, count($logLines));
}
/**
* Findet Errors in einer Log-Datei.
*/
public function findErrors(FilePath $logFile, int $lines = 1000): LogAnalysisResult
{
return $this->analyze($logFile, $lines, 'ERROR');
}
/**
* Findet Warnings in einer Log-Datei.
*/
public function findWarnings(FilePath $logFile, int $lines = 1000): LogAnalysisResult
{
return $this->analyze($logFile, $lines, 'WARNING');
}
/**
* Gibt die letzten N Zeilen einer Log-Datei zurück.
*/
public function tail(FilePath $logFile, int $lines = 100): string
{
$result = $this->process->run(
Command::fromArray(['tail', '-n', (string) $lines, $logFile->toString()])
);
return $result->stdout;
}
/**
* Sucht nach einem Pattern in einer Log-Datei.
*/
public function search(FilePath $logFile, string $pattern, int $lines = 1000): LogAnalysisResult
{
$command = Command::fromString(
"tail -n {$lines} {$logFile->toString()} | grep -i '{$pattern}'"
);
$result = $this->process->run($command);
$logLines = explode("\n", trim($result->stdout));
$entries = array_map(
fn (string $line) => LogEntry::fromLine($line),
array_filter($logLines)
);
return LogAnalysisResult::fromEntries($entries, count($logLines));
}
/**
* Gibt Log-Statistiken zurück.
*
* @return array<string, mixed>
*/
public function getStatistics(FilePath $logFile, int $lines = 1000): array
{
$analysis = $this->analyze($logFile, $lines);
return [
'total_lines' => $analysis->totalLines,
'error_count' => $analysis->getErrorCount(),
'warning_count' => $analysis->getWarningCount(),
'level_distribution' => $analysis->levelCounts,
'top_errors' => $analysis->getTopErrors(5),
];
}
/**
* Baut das Kommando zum Lesen der Log-Datei.
*/
private function buildCommand(FilePath $logFile, int $lines, ?string $levelFilter): Command
{
$cmd = "tail -n {$lines} {$logFile->toString()}";
if ($levelFilter !== null) {
$cmd .= " | grep -i '{$levelFilter}'";
}
return Command::fromString($cmd);
}
}

View File

@@ -0,0 +1,207 @@
<?php
declare(strict_types=1);
namespace App\Framework\Process\Services;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Http\IpAddress;
use App\Framework\Process\Process;
use App\Framework\Process\ValueObjects\Command;
use App\Framework\Process\ValueObjects\Network\DnsResult;
use App\Framework\Process\ValueObjects\Network\PingResult;
use App\Framework\Process\ValueObjects\Network\PortStatus;
/**
* Network Diagnostics Service.
*
* Führt Netzwerk-Diagnosen durch: Ping, DNS, Port-Checks.
*/
final readonly class NetworkDiagnosticsService
{
public function __construct(
private Process $process
) {
}
/**
* Pingt einen Host an.
*/
public function ping(string $host, int $count = 4): PingResult
{
$result = $this->process->run(
command: Command::fromArray(['ping', '-c', (string) $count, '-W', '2', $host]),
timeout: Duration::fromSeconds(10)
);
if (! $result->isSuccess()) {
return PingResult::failed($host);
}
// Parse ping output für Latenz
$latency = $this->parsePingLatency($result->stdout);
$packetsReceived = $this->parsePingPacketsReceived($result->stdout);
return PingResult::success(
host: $host,
latency: $latency ?? Duration::fromMilliseconds(0),
packetsSent: $count,
packetsReceived: $packetsReceived ?? 0
);
}
/**
* Führt DNS-Lookup durch.
*/
public function dnsLookup(string $hostname): DnsResult
{
$result = $this->process->run(
Command::fromArray(['host', $hostname])
);
if (! $result->isSuccess()) {
return DnsResult::failed($hostname);
}
$addresses = $this->parseDnsAddresses($result->stdout);
return DnsResult::success($hostname, $addresses);
}
/**
* Prüft, ob ein Port offen ist.
*/
public function checkPort(string $host, int $port): PortStatus
{
// Use nc (netcat) for port checking
$result = $this->process->run(
command: Command::fromArray(['nc', '-zv', '-w', '2', $host, (string) $port]),
timeout: Duration::fromSeconds(5)
);
if ($result->isSuccess()) {
$service = $this->getServiceName($port);
return PortStatus::open($port, $service);
}
return PortStatus::closed($port);
}
/**
* Scannt mehrere Ports.
*
* @param int[] $ports
* @return PortStatus[]
*/
public function scanPorts(string $host, array $ports): array
{
$results = [];
foreach ($ports as $port) {
$results[] = $this->checkPort($host, $port);
}
return $results;
}
/**
* Scannt Standard-Ports (80, 443, 22, 21, 25, 3306, 5432).
*
* @return PortStatus[]
*/
public function scanCommonPorts(string $host): array
{
return $this->scanPorts($host, [80, 443, 22, 21, 25, 3306, 5432]);
}
/**
* Prüft Netzwerk-Konnektivität zu mehreren wichtigen Hosts.
*
* @return PingResult[]
*/
public function checkConnectivity(): array
{
$hosts = [
'8.8.8.8', // Google DNS
'1.1.1.1', // Cloudflare DNS
'google.com', // Google
];
$results = [];
foreach ($hosts as $host) {
$results[$host] = $this->ping($host, 2);
}
return $results;
}
/**
* Parst Ping-Latenz aus Output.
*/
private function parsePingLatency(string $output): ?Duration
{
// Suche nach: "time=X.XX ms" oder "time=XXX ms"
if (preg_match('/time=([\d.]+)\s*ms/', $output, $matches)) {
return Duration::fromMilliseconds((float) $matches[1]);
}
return null;
}
/**
* Parst empfangene Pakete aus Ping-Output.
*/
private function parsePingPacketsReceived(string $output): ?int
{
// Suche nach: "4 received"
if (preg_match('/(\d+)\s+received/', $output, $matches)) {
return (int) $matches[1];
}
return null;
}
/**
* Parst IP-Adressen aus DNS-Output.
*
* @return IpAddress[]
*/
private function parseDnsAddresses(string $output): array
{
$addresses = [];
$lines = explode("\n", $output);
foreach ($lines as $line) {
// Suche nach "has address X.X.X.X" oder "has IPv6 address X:X:X::X"
if (preg_match('/has (?:IPv6 )?address (.+)$/', $line, $matches)) {
$ip = trim($matches[1]);
if (IpAddress::isValid($ip)) {
$addresses[] = IpAddress::from($ip);
}
}
}
return $addresses;
}
/**
* Gibt den Service-Namen für einen Port zurück.
*/
private function getServiceName(int $port): string
{
return match ($port) {
80 => 'HTTP',
443 => 'HTTPS',
22 => 'SSH',
21 => 'FTP',
25 => 'SMTP',
3306 => 'MySQL',
5432 => 'PostgreSQL',
6379 => 'Redis',
27017 => 'MongoDB',
default => '',
};
}
}

View File

@@ -0,0 +1,362 @@
<?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;
use App\Framework\Process\ValueObjects\Npm\NpmAuditResult;
use App\Framework\Process\ValueObjects\Npm\NpmPackage;
/**
* NPM Service.
*
* Verwaltet NPM-Operationen wie Installations, Updates, Security Audits und Package-Informationen.
*/
final readonly class NpmService
{
public function __construct(
private Process $process,
private ?FilePath $projectRoot = null
) {
}
/**
* Gibt die NPM-Version zurück.
*/
public function getVersion(): string
{
$result = $this->process->run(
Command::fromArray(['npm', '--version'])
);
if (! $result->isSuccess()) {
return 'unknown';
}
return trim($result->stdout);
}
/**
* Installiert Dependencies.
*/
public function install(bool $production = false): bool
{
$args = ['npm', 'install'];
if ($production) {
$args[] = '--production';
}
$result = $this->process->run(
command: Command::fromArray($args),
workingDirectory: $this->projectRoot,
timeout: Duration::fromMinutes(5)
);
return $result->isSuccess();
}
/**
* Aktualisiert Dependencies.
*/
public function update(bool $production = false): bool
{
$args = ['npm', 'update'];
if ($production) {
$args[] = '--production';
}
$result = $this->process->run(
command: Command::fromArray($args),
workingDirectory: $this->projectRoot,
timeout: Duration::fromMinutes(10)
);
return $result->isSuccess();
}
/**
* Führt Security Audit durch.
*/
public function audit(): NpmAuditResult
{
$result = $this->process->run(
command: Command::fromArray(['npm', 'audit', '--json']),
workingDirectory: $this->projectRoot,
timeout: Duration::fromSeconds(60)
);
if (! $result->isSuccess()) {
// Auch bei Vulnerabilities ist Exit-Code != 0
// Versuche trotzdem JSON zu parsen
}
$data = json_decode($result->stdout, true);
if (! is_array($data)) {
return NpmAuditResult::safe();
}
return NpmAuditResult::fromAuditJson($data);
}
/**
* Versucht automatisches Fixing von Vulnerabilities.
*/
public function auditFix(bool $force = false): bool
{
$args = ['npm', 'audit', 'fix'];
if ($force) {
$args[] = '--force';
}
$result = $this->process->run(
command: Command::fromArray($args),
workingDirectory: $this->projectRoot,
timeout: Duration::fromMinutes(5)
);
return $result->isSuccess();
}
/**
* Listet veraltete Packages auf.
*
* @return NpmPackage[]
*/
public function getOutdatedPackages(): array
{
$result = $this->process->run(
command: Command::fromArray(['npm', 'outdated', '--json']),
workingDirectory: $this->projectRoot,
timeout: Duration::fromSeconds(60)
);
// npm outdated gibt Exit-Code != 0 wenn es outdated packages gibt
$data = json_decode($result->stdout, true);
if (! is_array($data)) {
return [];
}
$packages = [];
foreach ($data as $name => $info) {
$packages[] = NpmPackage::fromNpmOutput(
name: $name,
currentVersion: $info['current'] ?? 'unknown',
wantedVersion: $info['wanted'] ?? null,
latestVersion: $info['latest'] ?? null,
type: $info['type'] ?? null
);
}
return $packages;
}
/**
* Listet installierte Packages auf.
*
* @return NpmPackage[]
*/
public function getInstalledPackages(): array
{
$result = $this->process->run(
command: Command::fromArray(['npm', 'list', '--json', '--depth=0']),
workingDirectory: $this->projectRoot,
timeout: Duration::fromSeconds(30)
);
if (! $result->isSuccess()) {
return [];
}
$data = json_decode($result->stdout, true);
if (! is_array($data)) {
return [];
}
$packages = [];
// Dependencies
foreach ($data['dependencies'] ?? [] as $name => $info) {
$packages[] = new NpmPackage(
name: $name,
currentVersion: $info['version'] ?? 'unknown',
type: 'dependencies'
);
}
// DevDependencies (wenn --depth=0 nicht nur dependencies zurückgibt)
foreach ($data['devDependencies'] ?? [] as $name => $info) {
$packages[] = new NpmPackage(
name: $name,
currentVersion: $info['version'] ?? 'unknown',
type: 'devDependencies'
);
}
return $packages;
}
/**
* Führt NPM Script aus.
*/
public function runScript(string $script): bool
{
$result = $this->process->run(
command: Command::fromArray(['npm', 'run', $script]),
workingDirectory: $this->projectRoot,
timeout: Duration::fromMinutes(10)
);
return $result->isSuccess();
}
/**
* Cleaned node_modules und package-lock.json.
*/
public function clean(): bool
{
if ($this->projectRoot === null) {
return false;
}
$nodeModules = $this->projectRoot->toString() . '/node_modules';
$packageLock = $this->projectRoot->toString() . '/package-lock.json';
// Remove node_modules
if (is_dir($nodeModules)) {
$result = $this->process->run(
command: Command::fromArray(['rm', '-rf', $nodeModules]),
timeout: Duration::fromMinutes(2)
);
if (! $result->isSuccess()) {
return false;
}
}
// Remove package-lock.json
if (file_exists($packageLock)) {
$result = $this->process->run(
command: Command::fromArray(['rm', $packageLock]),
timeout: Duration::fromSeconds(5)
);
if (! $result->isSuccess()) {
return false;
}
}
return true;
}
/**
* Prüft package.json Validität.
*/
public function validate(): bool
{
if ($this->projectRoot === null) {
return false;
}
$packageJson = $this->projectRoot->toString() . '/package.json';
if (! file_exists($packageJson)) {
return false;
}
$content = file_get_contents($packageJson);
$data = json_decode($content, true);
// Grundlegende Validierung
return is_array($data)
&& isset($data['name'])
&& isset($data['version']);
}
/**
* Initialisiert neues NPM-Projekt.
*/
public function init(bool $yes = false): bool
{
$args = ['npm', 'init'];
if ($yes) {
$args[] = '--yes';
}
$result = $this->process->run(
command: Command::fromArray($args),
workingDirectory: $this->projectRoot,
timeout: Duration::fromSeconds(30)
);
return $result->isSuccess();
}
/**
* Installiert spezifisches Package.
*/
public function installPackage(string $package, bool $dev = false, bool $exact = false): bool
{
$args = ['npm', 'install', $package];
if ($dev) {
$args[] = '--save-dev';
}
if ($exact) {
$args[] = '--save-exact';
}
$result = $this->process->run(
command: Command::fromArray($args),
workingDirectory: $this->projectRoot,
timeout: Duration::fromMinutes(3)
);
return $result->isSuccess();
}
/**
* Deinstalliert spezifisches Package.
*/
public function uninstallPackage(string $package): bool
{
$result = $this->process->run(
command: Command::fromArray(['npm', 'uninstall', $package]),
workingDirectory: $this->projectRoot,
timeout: Duration::fromMinutes(2)
);
return $result->isSuccess();
}
/**
* Zeigt Package-Informationen.
*/
public function showPackageInfo(string $package): ?array
{
$result = $this->process->run(
command: Command::fromArray(['npm', 'show', $package, '--json']),
timeout: Duration::fromSeconds(30)
);
if (! $result->isSuccess()) {
return null;
}
$data = json_decode($result->stdout, true);
return is_array($data) ? $data : null;
}
}

View File

@@ -0,0 +1,193 @@
<?php
declare(strict_types=1);
namespace App\Framework\Process\Services;
use App\Framework\Process\ValueObjects\Health\HealthCheck;
use App\Framework\Process\ValueObjects\Health\HealthReport;
/**
* System Health Check Service.
*
* Prüft verschiedene System-Metriken und gibt einen Health Report zurück.
*/
final readonly class SystemHealthCheckService
{
// Thresholds
private const float MEMORY_WARNING_THRESHOLD = 80.0; // 80%
private const float MEMORY_CRITICAL_THRESHOLD = 90.0; // 90%
private const float DISK_WARNING_THRESHOLD = 80.0; // 80%
private const float DISK_CRITICAL_THRESHOLD = 90.0; // 90%
private const float LOAD_WARNING_MULTIPLIER = 0.8; // 80% of cores
private const float LOAD_CRITICAL_MULTIPLIER = 1.2; // 120% of cores
public function __construct(
private SystemInfoService $systemInfo
) {
}
/**
* Führt alle Health Checks durch und gibt einen Report zurück.
*/
public function __invoke(): HealthReport
{
$info = ($this->systemInfo)();
$checks = [
$this->checkMemory($info),
$this->checkDisk($info),
$this->checkLoad($info),
$this->checkUptime($info),
];
return HealthReport::fromChecks($checks);
}
/**
* Prüft Memory-Nutzung.
*/
private function checkMemory(SystemInfo $info): HealthCheck
{
$usage = $info->memory->getUsagePercentage();
if ($usage >= self::MEMORY_CRITICAL_THRESHOLD) {
return HealthCheck::unhealthy(
name: 'Memory Usage',
message: 'Critical memory usage',
value: $usage,
threshold: self::MEMORY_CRITICAL_THRESHOLD,
unit: '%'
);
}
if ($usage >= self::MEMORY_WARNING_THRESHOLD) {
return HealthCheck::degraded(
name: 'Memory Usage',
message: 'High memory usage',
value: $usage,
threshold: self::MEMORY_WARNING_THRESHOLD,
unit: '%'
);
}
return HealthCheck::healthy(
name: 'Memory Usage',
message: 'Memory usage is normal',
value: $usage,
threshold: self::MEMORY_WARNING_THRESHOLD,
unit: '%'
);
}
/**
* Prüft Disk-Nutzung.
*/
private function checkDisk(SystemInfo $info): HealthCheck
{
$usage = $info->disk->getUsagePercentage();
if ($usage >= self::DISK_CRITICAL_THRESHOLD) {
return HealthCheck::unhealthy(
name: 'Disk Usage',
message: 'Critical disk space',
value: $usage,
threshold: self::DISK_CRITICAL_THRESHOLD,
unit: '%'
);
}
if ($usage >= self::DISK_WARNING_THRESHOLD) {
return HealthCheck::degraded(
name: 'Disk Usage',
message: 'Low disk space',
value: $usage,
threshold: self::DISK_WARNING_THRESHOLD,
unit: '%'
);
}
return HealthCheck::healthy(
name: 'Disk Usage',
message: 'Disk space is sufficient',
value: $usage,
threshold: self::DISK_WARNING_THRESHOLD,
unit: '%'
);
}
/**
* Prüft System Load.
*/
private function checkLoad(SystemInfo $info): HealthCheck
{
$utilizationPercent = $info->load->getUtilization($info->cpu->cores) * 100;
$warningThreshold = self::LOAD_WARNING_MULTIPLIER * 100;
$criticalThreshold = self::LOAD_CRITICAL_MULTIPLIER * 100;
if ($utilizationPercent >= $criticalThreshold) {
return HealthCheck::unhealthy(
name: 'System Load',
message: 'Critical system load',
value: $utilizationPercent,
threshold: $criticalThreshold,
unit: '%'
);
}
if ($utilizationPercent >= $warningThreshold) {
return HealthCheck::degraded(
name: 'System Load',
message: 'High system load',
value: $utilizationPercent,
threshold: $warningThreshold,
unit: '%'
);
}
return HealthCheck::healthy(
name: 'System Load',
message: 'System load is normal',
value: $utilizationPercent,
threshold: $warningThreshold,
unit: '%'
);
}
/**
* Prüft Uptime (degraded wenn < 1 Stunde, könnte auf Reboot hinweisen).
*/
private function checkUptime(SystemInfo $info): HealthCheck
{
$uptimeDays = $info->uptime->getUptimeDays();
$uptimeHours = $info->uptime->uptime->toHours();
if ($uptimeHours < 1) {
return HealthCheck::degraded(
name: 'System Uptime',
message: 'System recently rebooted',
value: $uptimeHours,
threshold: 1.0,
unit: 'hours'
);
}
return HealthCheck::healthy(
name: 'System Uptime',
message: "System running for {$uptimeDays} days",
value: $uptimeDays,
threshold: 0.0,
unit: 'days'
);
}
/**
* Gibt nur kritische Probleme zurück.
*/
public function getCriticalIssues(): HealthReport
{
$report = $this();
return HealthReport::fromChecks($report->getUnhealthyChecks());
}
}

View File

@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace App\Framework\Process\Services;
use App\Framework\Process\ValueObjects\SystemInfo\CpuInfo;
use App\Framework\Process\ValueObjects\SystemInfo\DiskInfo;
use App\Framework\Process\ValueObjects\SystemInfo\LoadAverage;
use App\Framework\Process\ValueObjects\SystemInfo\MemoryInfo;
use App\Framework\Process\ValueObjects\SystemInfo\ProcessInfo;
use App\Framework\Process\ValueObjects\SystemInfo\SystemUptime;
/**
* Aggregierte System-Informationen.
*/
final readonly class SystemInfo
{
public function __construct(
public SystemUptime $uptime,
public LoadAverage $load,
public MemoryInfo $memory,
public DiskInfo $disk,
public CpuInfo $cpu,
public ProcessInfo $processes
) {
}
/**
* Konvertiert zu Array für API/JSON Ausgabe.
*
* @return array<string, mixed>
*/
public function toArray(): array
{
return [
'uptime' => $this->uptime->toArray(),
'load' => $this->load->toArray(),
'memory' => $this->memory->toArray(),
'disk' => $this->disk->toArray(),
'cpu' => $this->cpu->toArray(),
'processes' => $this->processes->toArray(),
];
}
}

View File

@@ -0,0 +1,251 @@
<?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\SystemInfo\CpuInfo;
use App\Framework\Process\ValueObjects\SystemInfo\DiskInfo;
use App\Framework\Process\ValueObjects\SystemInfo\LoadAverage;
use App\Framework\Process\ValueObjects\SystemInfo\MemoryInfo;
use App\Framework\Process\ValueObjects\SystemInfo\ProcessInfo;
use App\Framework\Process\ValueObjects\SystemInfo\SystemUptime;
/**
* Service für System-Informationen.
*
* Invokable Klasse die System-Metriken sammelt und als Value Objects zurückgibt.
* Kann in Commands, API Endpoints, LiveComponents etc. verwendet werden.
*/
final readonly class SystemInfoService
{
public function __construct(
private Process $process
) {
}
/**
* Sammelt alle System-Informationen.
*/
public function __invoke(): SystemInfo
{
return new SystemInfo(
uptime: $this->getUptime(),
load: $this->getLoadAverage(),
memory: $this->getMemoryInfo(),
disk: $this->getDiskInfo(),
cpu: $this->getCpuInfo(),
processes: $this->getProcessInfo()
);
}
/**
* Gibt die System-Uptime zurück.
*/
public function getUptime(): SystemUptime
{
// Read uptime directly from /proc/uptime
$uptimeSecondsResult = $this->process->run(
Command::fromString('cat /proc/uptime | cut -d " " -f1')
);
$uptimeSeconds = (float) trim($uptimeSecondsResult->stdout);
// Calculate boot time from uptime using timestamp subtraction
$now = new \DateTimeImmutable();
$bootTime = $now->setTimestamp((int) ($now->getTimestamp() - $uptimeSeconds));
return new SystemUptime(
bootTime: $bootTime,
uptime: Duration::fromSeconds($uptimeSeconds)
);
}
/**
* Gibt Load Average zurück.
*/
public function getLoadAverage(): LoadAverage
{
$result = $this->process->run(Command::fromString('cat /proc/loadavg'));
if (! $result->isSuccess()) {
return LoadAverage::empty();
}
$parts = explode(' ', trim($result->stdout));
return new LoadAverage(
oneMinute: (float) ($parts[0] ?? 0),
fiveMinutes: (float) ($parts[1] ?? 0),
fifteenMinutes: (float) ($parts[2] ?? 0)
);
}
/**
* Gibt Memory-Informationen zurück.
*/
public function getMemoryInfo(): MemoryInfo
{
// Try free command first
$result = $this->process->run(
Command::fromString('free -b | grep "^Mem:"')
);
if ($result->isSuccess()) {
$parts = preg_split('/\s+/', trim($result->stdout));
return new MemoryInfo(
totalBytes: (int) ($parts[1] ?? 0),
usedBytes: (int) ($parts[2] ?? 0),
freeBytes: (int) ($parts[3] ?? 0),
availableBytes: (int) ($parts[6] ?? 0)
);
}
// Fallback: Read /proc/meminfo directly
$meminfoResult = $this->process->run(
Command::fromString('cat /proc/meminfo')
);
if (! $meminfoResult->isSuccess()) {
return MemoryInfo::empty();
}
// Parse /proc/meminfo
$lines = explode("\n", $meminfoResult->stdout);
$meminfo = [];
foreach ($lines as $line) {
if (preg_match('/^(\w+):\s+(\d+)/', $line, $matches)) {
$meminfo[$matches[1]] = (int) $matches[2] * 1024; // Convert kB to bytes
}
}
$total = $meminfo['MemTotal'] ?? 0;
$free = $meminfo['MemFree'] ?? 0;
$available = $meminfo['MemAvailable'] ?? 0;
$used = $total - $free;
return new MemoryInfo(
totalBytes: $total,
usedBytes: $used,
freeBytes: $free,
availableBytes: $available
);
}
/**
* Gibt Disk-Informationen zurück.
*/
public function getDiskInfo(): DiskInfo
{
$result = $this->process->run(
Command::fromString('df -B1 / | tail -1')
);
if (! $result->isSuccess()) {
return DiskInfo::empty();
}
$parts = preg_split('/\s+/', trim($result->stdout));
return new DiskInfo(
totalBytes: (int) ($parts[1] ?? 0),
usedBytes: (int) ($parts[2] ?? 0),
availableBytes: (int) ($parts[3] ?? 0),
mountPoint: $parts[5] ?? '/'
);
}
/**
* Gibt CPU-Informationen zurück.
*/
public function getCpuInfo(): CpuInfo
{
$coresResult = $this->process->run(
Command::fromString('grep -c ^processor /proc/cpuinfo')
);
$modelResult = $this->process->run(
Command::fromString('grep "^model name" /proc/cpuinfo | head -1 | cut -d ":" -f2')
);
return new CpuInfo(
cores: (int) trim($coresResult->stdout),
model: trim($modelResult->stdout)
);
}
/**
* Gibt Prozess-Informationen zurück.
*/
public function getProcessInfo(): ProcessInfo
{
// Try ps command first
$totalResult = $this->process->run(
Command::fromString('ps aux | wc -l')
);
// Check if ps actually worked (stdout > 0) not just exit code
// because shell pipes return exit code of last command
$total = (int) trim($totalResult->stdout);
if ($totalResult->isSuccess() && $total > 0) {
$runningResult = $this->process->run(
Command::fromString('ps aux | grep " R " | wc -l')
);
$sleepingResult = $this->process->run(
Command::fromString('ps aux | grep " S " | wc -l')
);
return new ProcessInfo(
total: max(0, $total - 1), // Minus header
running: (int) trim($runningResult->stdout),
sleeping: (int) trim($sleepingResult->stdout)
);
}
// Fallback: Read /proc directly with PHP (more reliable than shell commands in containers)
$total = 0;
$running = 0;
$sleeping = 0;
if (is_dir('/proc')) {
// Count process directories
$procEntries = scandir('/proc');
foreach ($procEntries as $entry) {
// Check if entry is a numeric directory (PID)
if (is_numeric($entry) && is_dir("/proc/{$entry}")) {
$total++;
// Try to read process state from /proc/[pid]/stat
$statFile = "/proc/{$entry}/stat";
if (is_readable($statFile)) {
$statContent = @file_get_contents($statFile);
if ($statContent !== false) {
// State is the 3rd field in /proc/[pid]/stat
// Format: "pid (comm) state ..."
if (preg_match('/\)\s+([A-Z])/', $statContent, $matches)) {
$state = $matches[1];
if ($state === 'R') {
$running++;
} elseif ($state === 'S') {
$sleeping++;
}
}
}
}
}
}
}
return new ProcessInfo(
total: $total,
running: $running,
sleeping: $sleeping
);
}
}

View File

@@ -0,0 +1,114 @@
<?php
declare(strict_types=1);
namespace App\Framework\Process\Services;
/**
* TCP Port Check Service
*
* Simple service to check if a TCP port is open without external dependencies.
* Uses PHP's native fsockopen() with proper error suppression.
*/
final readonly class TcpPortCheckService
{
private const DEFAULT_TIMEOUT = 2;
/**
* Check if a TCP port is open on a given host
*
* @param string $host Hostname or IP address
* @param int $port Port number
* @param int $timeout Timeout in seconds
*/
public function isPortOpen(string $host, int $port, int $timeout = self::DEFAULT_TIMEOUT): bool
{
// Temporarily disable error handler to prevent warnings
set_error_handler(function () {}, E_ALL);
try {
$errno = 0;
$errstr = '';
$socket = @fsockopen(
$host,
$port,
$errno,
$errstr,
$timeout
);
if ($socket === false) {
return false;
}
fclose($socket);
return true;
} finally {
restore_error_handler();
}
}
/**
* Check if an SSL/TLS port is open
*
* @param string $host Hostname or IP address
* @param int $port Port number (typically 443)
* @param int $timeout Timeout in seconds
*/
public function isSslPortOpen(string $host, int $port, int $timeout = self::DEFAULT_TIMEOUT): bool
{
// Temporarily disable error handler to prevent warnings
set_error_handler(function () {}, E_ALL);
try {
$errno = 0;
$errstr = '';
$context = stream_context_create([
'ssl' => [
'verify_peer' => false,
'verify_peer_name' => false,
],
]);
$socket = @stream_socket_client(
"ssl://{$host}:{$port}",
$errno,
$errstr,
$timeout,
STREAM_CLIENT_CONNECT,
$context
);
if ($socket === false) {
return false;
}
fclose($socket);
return true;
} finally {
restore_error_handler();
}
}
/**
* Check multiple ports on a host
*
* @param string $host Hostname or IP address
* @param array<int> $ports Array of port numbers
* @return array<int, bool> Port => isOpen mapping
*/
public function checkPorts(string $host, array $ports): array
{
$results = [];
foreach ($ports as $port) {
$results[$port] = $this->isPortOpen($host, $port);
}
return $results;
}
}

View File

@@ -0,0 +1,224 @@
<?php
declare(strict_types=1);
namespace App\Framework\Process\Services;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Http\Headers;
use App\Framework\Http\Status;
use App\Framework\Http\Url\Url;
use App\Framework\Process\Process;
use App\Framework\Process\ValueObjects\Command;
use App\Framework\Process\ValueObjects\Http\UrlHealthCheck;
/**
* URL Health Check Service.
*
* Prüft HTTP-Endpunkte auf Erreichbarkeit, Response-Zeit und Status.
*/
final readonly class UrlHealthCheckService
{
public function __construct(
private Process $process
) {
}
/**
* Prüft URL Health.
*/
public function checkUrl(Url $url, Duration $timeout): UrlHealthCheck
{
$startTime = microtime(true);
// curl verwenden für HTTP-Request
$result = $this->process->run(
command: Command::fromArray([
'curl',
'-s', // Silent mode
'-w',
'%{http_code}|%{redirect_url}', // Output format
'-o',
'/dev/null', // Discard body
'-L', // Follow redirects
'--max-time',
(string) $timeout->toSeconds(),
'-I', // HEAD request nur
$url->toString(),
]),
timeout: $timeout->add(Duration::fromSeconds(1)) // Extra Zeit für Process
);
$responseTime = Duration::fromMilliseconds((int) ((microtime(true) - $startTime) * 1000));
if (! $result->isSuccess()) {
return UrlHealthCheck::failed($url, 'Failed to connect: ' . trim($result->stderr));
}
// Parse curl output
$output = trim($result->stdout);
if (empty($output)) {
return UrlHealthCheck::failed($url, 'Empty response from curl');
}
// Format: "200|" oder "301|https://redirect.url"
$parts = explode('|', $output, 2);
$statusCode = (int) $parts[0];
$redirectUrl = ! empty($parts[1]) ? $parts[1] : null;
// Status-Code zu Status Enum mappen
$status = $this->mapHttpStatusCode($statusCode);
$healthCheck = UrlHealthCheck::success(
url: $url,
status: $status,
responseTime: $responseTime
);
// Redirect-URL hinzufügen falls vorhanden
if ($redirectUrl !== null && $redirectUrl !== '') {
try {
$healthCheck = $healthCheck->withRedirect(
Url::parse($redirectUrl)
);
} catch (\InvalidArgumentException $e) {
// Ungültige Redirect-URL ignorieren
}
}
return $healthCheck;
}
/**
* Prüft URL mit detaillierten Headers.
*/
public function checkUrlWithHeaders(Url $url, Duration $timeout): UrlHealthCheck
{
$startTime = microtime(true);
// curl mit Header-Output
$result = $this->process->run(
command: Command::fromArray([
'curl',
'-s',
'-i', // Include headers in output
'-L', // Follow redirects
'--max-time',
(string) $timeout->toSeconds(),
'-X',
'HEAD',
$url->toString(),
]),
timeout: $timeout->add(Duration::fromSeconds(1))
);
$responseTime = Duration::fromMilliseconds((int) ((microtime(true) - $startTime) * 1000));
if (! $result->isSuccess()) {
return UrlHealthCheck::failed($url, 'Failed to connect: ' . trim($result->stderr));
}
// Parse HTTP Response
$response = $this->parseHttpResponse($result->stdout);
if ($response === null) {
return UrlHealthCheck::failed($url, 'Failed to parse HTTP response');
}
$status = $this->mapHttpStatusCode($response['status_code']);
return UrlHealthCheck::success(
url: $url,
status: $status,
responseTime: $responseTime,
headers: new Headers($response['headers'])
);
}
/**
* Prüft mehrere URLs.
*
* @param Url[] $urls
* @return UrlHealthCheck[]
*/
public function checkMultipleUrls(array $urls, Duration $timeout): array
{
$results = [];
foreach ($urls as $url) {
$results[] = $this->checkUrl($url, $timeout);
}
return $results;
}
/**
* Prüft Standard-Service-Endpunkte.
*
* @return UrlHealthCheck[]
*/
public function checkCommonServices(): array
{
$services = [
Url::parse('https://www.google.com'),
Url::parse('https://www.cloudflare.com'),
Url::parse('https://www.github.com'),
];
return $this->checkMultipleUrls($services, Duration::fromSeconds(5));
}
/**
* Mappt HTTP Status-Code zu Status Enum.
*/
private function mapHttpStatusCode(int $code): Status
{
return Status::tryFrom($code) ?? Status::INTERNAL_SERVER_ERROR;
}
/**
* Parst HTTP Response aus curl Output.
*
* @return array{status_code: int, headers: array<string, string>}|null
*/
private function parseHttpResponse(string $output): ?array
{
$lines = explode("\n", $output);
if (empty($lines)) {
return null;
}
// Erste Zeile ist Status-Line
$statusLine = array_shift($lines);
// Format: "HTTP/1.1 200 OK"
if (! preg_match('/HTTP\/\d\.\d\s+(\d+)/', $statusLine, $matches)) {
return null;
}
$statusCode = (int) $matches[1];
// Headers parsen
$headers = [];
foreach ($lines as $line) {
$line = trim($line);
if (empty($line)) {
break; // Ende der Headers
}
// Format: "Header-Name: Value"
if (strpos($line, ':') !== false) {
[$name, $value] = explode(':', $line, 2);
$headers[trim($name)] = trim($value);
}
}
return [
'status_code' => $statusCode,
'headers' => $headers,
];
}
}

View File

@@ -0,0 +1,244 @@
<?php
declare(strict_types=1);
namespace App\Framework\Process;
use App\Framework\Console\ExitCode;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\DI\Attributes\DefaultImplementation;
use App\Framework\Filesystem\ValueObjects\FilePath;
use App\Framework\Logging\Logger;
use App\Framework\Logging\ValueObjects\LogContext;
use App\Framework\Process\Exceptions\ProcessException;
use App\Framework\Process\ValueObjects\Command;
use App\Framework\Process\ValueObjects\EnvironmentVariables;
/**
* System-basierte Prozess-Implementation.
*
* Verwendet proc_open() für Prozess-Ausführung mit voller Kontrolle
* über stdin, stdout, stderr, Umgebungsvariablen und Timeout.
*/
#[DefaultImplementation]
final readonly class SystemProcess implements Process
{
public function __construct(
private Logger $logger
) {
}
public function run(
Command $command,
?EnvironmentVariables $env = null,
?FilePath $workingDirectory = null,
?Duration $timeout = null
): ProcessResult {
$startTime = microtime(true);
// Validate working directory if provided
if ($workingDirectory !== null && ! $workingDirectory->isDirectory()) {
throw ProcessException::invalidWorkingDirectory($workingDirectory->toString());
}
$descriptors = [
0 => ['pipe', 'r'], // stdin
1 => ['pipe', 'w'], // stdout
2 => ['pipe', 'w'], // stderr
];
$pipes = [];
$process = proc_open(
command: $command->getValue(),
descriptor_spec: $descriptors,
pipes: $pipes,
cwd: $workingDirectory?->toString(),
env_vars: $env?->toArray(),
options: ['bypass_shell' => $command->isArray()]
);
if (! is_resource($process)) {
throw ProcessException::failedToStart($command, 'proc_open failed');
}
// Close stdin - we're not sending input
fclose($pipes[0]);
// Apply timeout if specified
if ($timeout !== null) {
$this->applyTimeout($pipes, $timeout);
}
// Read outputs with timeout handling
$stdout = $this->readStream($pipes[1], $timeout);
$stderr = $this->readStream($pipes[2], $timeout);
fclose($pipes[1]);
fclose($pipes[2]);
// Get exit code
$exitCodeValue = proc_close($process);
// Calculate runtime
$runtimeSeconds = microtime(true) - $startTime;
$runtime = Duration::fromSeconds($runtimeSeconds);
// Map exit code to ExitCode enum
$exitCode = $this->mapExitCode($exitCodeValue);
$result = new ProcessResult(
command: $command,
stdout: $stdout,
stderr: $stderr,
exitCode: $exitCode,
runtime: $runtime
);
$this->logger->debug(
'Process executed',
LogContext::withData([
'command' => $command->toString(),
'exit_code' => $exitCodeValue,
'runtime_ms' => $runtime->toMilliseconds(),
'success' => $result->isSuccess(),
])
);
return $result;
}
public function start(
Command $command,
?EnvironmentVariables $env = null,
?FilePath $workingDirectory = null
): RunningProcess {
// Validate working directory if provided
if ($workingDirectory !== null && ! $workingDirectory->isDirectory()) {
throw ProcessException::invalidWorkingDirectory($workingDirectory->toString());
}
$descriptors = [
0 => ['pipe', 'r'],
1 => ['pipe', 'w'],
2 => ['pipe', 'w'],
];
$pipes = [];
$process = proc_open(
command: $command->getValue(),
descriptor_spec: $descriptors,
pipes: $pipes,
cwd: $workingDirectory?->toString(),
env_vars: $env?->toArray(),
options: ['bypass_shell' => $command->isArray()]
);
if (! is_resource($process)) {
throw ProcessException::failedToStart($command, 'proc_open failed for async start');
}
// Get process status to extract PID
$status = proc_get_status($process);
$pid = $status['pid'] ?? 0;
$this->logger->debug(
'Process started asynchronously',
LogContext::withData([
'command' => $command->toString(),
'pid' => $pid,
])
);
return new SystemRunningProcess(
process: $process,
pipes: $pipes,
command: $command,
pid: $pid,
logger: $this->logger
);
}
public function commandExists(string $commandName): bool
{
$result = $this->run(
command: Command::fromArray(['which', $commandName])
);
return $result->isSuccess();
}
/**
* Wendet ein Timeout auf die Pipes an.
*
* @param array<int, resource> $pipes
*/
private function applyTimeout(array $pipes, Duration $timeout): void
{
$timeoutSeconds = $timeout->toTimeoutSeconds();
foreach ($pipes as $pipe) {
if (is_resource($pipe)) {
stream_set_timeout($pipe, $timeoutSeconds);
}
}
}
/**
* Liest einen Stream mit optionalem Timeout.
*
* @param resource $stream
*/
private function readStream($stream, ?Duration $timeout): string
{
if (! is_resource($stream)) {
return '';
}
$content = stream_get_contents($stream);
if ($content === false) {
return '';
}
// Check for timeout
if ($timeout !== null) {
$metadata = stream_get_meta_data($stream);
if ($metadata['timed_out'] ?? false) {
return $content; // Return partial content on timeout
}
}
return $content;
}
/**
* Mappt einen numerischen Exit-Code zum ExitCode Enum.
*/
private function mapExitCode(int $exitCodeValue): ExitCode
{
return match ($exitCodeValue) {
0 => ExitCode::SUCCESS,
1 => ExitCode::GENERAL_ERROR,
2 => ExitCode::USAGE_ERROR,
64 => ExitCode::COMMAND_NOT_FOUND,
65 => ExitCode::INVALID_INPUT,
66 => ExitCode::NO_INPUT,
69 => ExitCode::UNAVAILABLE,
70 => ExitCode::SOFTWARE_ERROR,
71 => ExitCode::OS_ERROR,
72 => ExitCode::OS_FILE_ERROR,
73 => ExitCode::CANT_CREATE,
74 => ExitCode::IO_ERROR,
75 => ExitCode::TEMP_FAIL,
76 => ExitCode::PROTOCOL_ERROR,
77 => ExitCode::NO_PERMISSION,
78 => ExitCode::CONFIG_ERROR,
79 => ExitCode::DATABASE_ERROR,
80 => ExitCode::RATE_LIMITED,
126 => ExitCode::PERMISSION_DENIED,
130 => ExitCode::INTERRUPTED,
default => ExitCode::GENERAL_ERROR,
};
}
}

View File

@@ -0,0 +1,182 @@
<?php
declare(strict_types=1);
namespace App\Framework\Process;
use App\Framework\Console\ExitCode;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Logging\Logger;
use App\Framework\Logging\ValueObjects\LogContext;
use App\Framework\Process\Exceptions\ProcessException;
use App\Framework\Process\Exceptions\ProcessTimeoutException;
use App\Framework\Process\ValueObjects\Command;
/**
* Repräsentiert einen laufenden asynchronen Prozess.
*
* @internal
*/
final class SystemRunningProcess implements RunningProcess
{
/**
* @param resource $process
* @param array<int, resource> $pipes
*/
public function __construct(
private $process,
private readonly array $pipes,
private readonly Command $command,
private readonly int $pid,
private readonly Logger $logger
) {
}
public function isRunning(): bool
{
if (! is_resource($this->process)) {
return false;
}
$status = proc_get_status($this->process);
return $status['running'] ?? false;
}
public function getPid(): int
{
return $this->pid;
}
public function wait(?Duration $timeout = null): ProcessResult
{
$startTime = microtime(true);
// Apply timeout if specified
if ($timeout !== null) {
$this->applyTimeout($timeout);
}
// Wait for process to finish by reading outputs
$stdout = stream_get_contents($this->pipes[1]);
$stderr = stream_get_contents($this->pipes[2]);
// Close all pipes
fclose($this->pipes[0]);
fclose($this->pipes[1]);
fclose($this->pipes[2]);
// Get exit code
$exitCodeValue = proc_close($this->process);
// Check for timeout
if ($timeout !== null) {
$elapsed = microtime(true) - $startTime;
if ($elapsed >= $timeout->toSeconds()) {
throw ProcessTimeoutException::exceeded($this->command, $timeout);
}
}
// Calculate runtime
$runtimeSeconds = microtime(true) - $startTime;
$runtime = Duration::fromSeconds($runtimeSeconds);
// Map exit code
$exitCode = $this->mapExitCode($exitCodeValue);
$result = new ProcessResult(
command: $this->command,
stdout: $stdout ?: '',
stderr: $stderr ?: '',
exitCode: $exitCode,
runtime: $runtime
);
$this->logger->debug(
'Async process completed',
LogContext::withData([
'command' => $this->command->toString(),
'pid' => $this->pid,
'exit_code' => $exitCodeValue,
'runtime_ms' => $runtime->toMilliseconds(),
])
);
return $result;
}
public function signal(int $signal = 15): void
{
if (! $this->isRunning()) {
return;
}
$result = proc_terminate($this->process, $signal);
if (! $result) {
throw ProcessException::failedToSendSignal($this->pid, $signal);
}
$this->logger->debug(
'Signal sent to process',
LogContext::withData([
'pid' => $this->pid,
'signal' => $signal,
])
);
}
public function terminate(): void
{
$this->signal(15); // SIGTERM
}
public function kill(): void
{
$this->signal(9); // SIGKILL
}
/**
* Wendet ein Timeout auf die Pipes an.
*/
private function applyTimeout(Duration $timeout): void
{
$timeoutSeconds = $timeout->toTimeoutSeconds();
foreach ($this->pipes as $pipe) {
if (is_resource($pipe)) {
stream_set_timeout($pipe, $timeoutSeconds);
}
}
}
/**
* Mappt einen numerischen Exit-Code zum ExitCode Enum.
*/
private function mapExitCode(int $exitCodeValue): ExitCode
{
return match ($exitCodeValue) {
0 => ExitCode::SUCCESS,
1 => ExitCode::GENERAL_ERROR,
2 => ExitCode::USAGE_ERROR,
64 => ExitCode::COMMAND_NOT_FOUND,
65 => ExitCode::INVALID_INPUT,
66 => ExitCode::NO_INPUT,
69 => ExitCode::UNAVAILABLE,
70 => ExitCode::SOFTWARE_ERROR,
71 => ExitCode::OS_ERROR,
72 => ExitCode::OS_FILE_ERROR,
73 => ExitCode::CANT_CREATE,
74 => ExitCode::IO_ERROR,
75 => ExitCode::TEMP_FAIL,
76 => ExitCode::PROTOCOL_ERROR,
77 => ExitCode::NO_PERMISSION,
78 => ExitCode::CONFIG_ERROR,
79 => ExitCode::DATABASE_ERROR,
80 => ExitCode::RATE_LIMITED,
126 => ExitCode::PERMISSION_DENIED,
130 => ExitCode::INTERRUPTED,
default => ExitCode::GENERAL_ERROR,
};
}
}

View File

@@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
namespace App\Framework\Process\ValueObjects\Backup;
use App\Framework\Core\ValueObjects\Byte;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Filesystem\ValueObjects\FilePath;
/**
* Backup-Datei Information.
*/
final readonly class BackupFile
{
public function __construct(
public FilePath $path,
public Byte $size,
public \DateTimeImmutable $createdAt,
public string $name
) {
}
/**
* Gibt das Alter des Backups zurück.
*/
public function getAge(): Duration
{
$now = new \DateTimeImmutable();
$diff = $now->getTimestamp() - $this->createdAt->getTimestamp();
return Duration::fromSeconds($diff);
}
/**
* Prüft, ob das Backup älter als X Tage ist.
*/
public function isOlderThan(Duration $duration): bool
{
return $this->getAge()->greaterThan($duration);
}
/**
* Prüft, ob das Backup frisch ist (< 24 Stunden).
*/
public function isFresh(): bool
{
return ! $this->isOlderThan(Duration::fromDays(1));
}
/**
* Konvertiert zu Array.
*
* @return array<string, mixed>
*/
public function toArray(): array
{
return [
'name' => $this->name,
'path' => $this->path->toString(),
'size_bytes' => $this->size->toBytes(),
'size_human' => $this->size->toHumanReadable(),
'created_at' => $this->createdAt->format('Y-m-d H:i:s'),
'age_human' => $this->getAge()->toHumanReadable(),
'is_fresh' => $this->isFresh(),
];
}
}

View File

@@ -0,0 +1,131 @@
<?php
declare(strict_types=1);
namespace App\Framework\Process\ValueObjects\Backup;
use App\Framework\Core\ValueObjects\Duration;
/**
* Ergebnis einer Backup-Verifikation.
*/
final readonly class BackupVerificationResult
{
/**
* @param BackupFile[] $backups
*/
public function __construct(
public array $backups,
public int $totalCount,
public ?\DateTimeImmutable $latestBackupDate,
public \DateTimeImmutable $verifiedAt
) {
}
/**
* Erstellt Ergebnis aus Backup-Dateien.
*
* @param BackupFile[] $backups
*/
public static function fromBackups(array $backups): self
{
$latestDate = null;
foreach ($backups as $backup) {
if ($latestDate === null || $backup->createdAt > $latestDate) {
$latestDate = $backup->createdAt;
}
}
return new self(
backups: $backups,
totalCount: count($backups),
latestBackupDate: $latestDate,
verifiedAt: new \DateTimeImmutable()
);
}
/**
* Prüft, ob es frische Backups gibt (< 24h).
*/
public function hasFreshBackup(): bool
{
return $this->latestBackupDate !== null
&& ! $this->isLatestBackupOlderThan(Duration::fromDays(1));
}
/**
* Prüft, ob das letzte Backup älter als X ist.
*/
public function isLatestBackupOlderThan(Duration $duration): bool
{
if ($this->latestBackupDate === null) {
return true;
}
$now = new \DateTimeImmutable();
$diff = $now->getTimestamp() - $this->latestBackupDate->getTimestamp();
return $diff > $duration->toSeconds();
}
/**
* Gibt die Anzahl frischer Backups zurück.
*/
public function getFreshBackupCount(): int
{
return count(array_filter(
$this->backups,
fn (BackupFile $backup) => $backup->isFresh()
));
}
/**
* Gibt die Anzahl alter Backups zurück (> 7 Tage).
*/
public function getOldBackupCount(): int
{
return count(array_filter(
$this->backups,
fn (BackupFile $backup) => $backup->isOlderThan(Duration::fromDays(7))
));
}
/**
* Gibt das neueste Backup zurück.
*/
public function getLatestBackup(): ?BackupFile
{
if (empty($this->backups)) {
return null;
}
$latest = null;
foreach ($this->backups as $backup) {
if ($latest === null || $backup->createdAt > $latest->createdAt) {
$latest = $backup;
}
}
return $latest;
}
/**
* Konvertiert zu Array.
*
* @return array<string, mixed>
*/
public function toArray(): array
{
return [
'total_count' => $this->totalCount,
'fresh_count' => $this->getFreshBackupCount(),
'old_count' => $this->getOldBackupCount(),
'latest_backup_date' => $this->latestBackupDate?->format('Y-m-d H:i:s'),
'has_fresh_backup' => $this->hasFreshBackup(),
'verified_at' => $this->verifiedAt->format('Y-m-d H:i:s'),
'backups' => array_map(fn (BackupFile $b) => $b->toArray(), $this->backups),
];
}
}

View File

@@ -0,0 +1,101 @@
<?php
declare(strict_types=1);
namespace App\Framework\Process\ValueObjects;
/**
* Repräsentiert ein Kommando zur Prozess-Ausführung.
*
* Verwendung:
* - new Command('docker', 'ps', '-a') // Variadic constructor (bypass shell)
* - Command::fromString('cat /proc/uptime') // Shell-Kommando String (uses shell)
*/
final readonly class Command
{
/** @var array<int, string> */
public array $parts;
public function __construct(string ...$values)
{
if (empty($values)) {
throw new \InvalidArgumentException('Command cannot be empty');
}
// Validate all parts are non-empty strings
foreach ($values as $value) {
if (trim($value) === '') {
throw new \InvalidArgumentException('Command parts cannot contain empty strings');
}
}
$this->parts = array_values($values);
}
/**
* Konvertiert das Kommando zu einem String mit escaped arguments.
*/
public function toString(): string
{
return implode(' ', array_map('escapeshellarg', $this->parts));
}
/**
* Gibt die rohen Kommando-Teile zurück.
*
* Für proc_open:
* - String wenn fromString (bypass_shell=false)
* - Array wenn variadic/fromArray (bypass_shell=true)
*
* @return string|array<int, string>
*/
public function getValue(): string|array
{
// If only one part (fromString), return as string for shell execution
if (count($this->parts) === 1) {
return $this->parts[0];
}
// Multiple parts, return as array for bypass_shell
return $this->parts;
}
/**
* Prüft, ob das Kommando als Array erstellt wurde (mehr als 1 Element).
*
* Ein Element = fromString (Shell verwenden)
* Mehrere Elemente = fromArray oder variadic constructor (Shell umgehen)
*/
public function isArray(): bool
{
return count($this->parts) > 1;
}
/**
* Erstellt ein Kommando aus einem Shell-String.
* Nutze dies nur für einfache Shell-Kommandos.
*/
public static function fromString(string $command): self
{
if (trim($command) === '') {
throw new \InvalidArgumentException('Command string cannot be empty');
}
// Store as single part - will be executed as shell command
return new self($command);
}
/**
* Erstellt ein Kommando aus einem Array.
*
* @param array<int, string> $parts
*/
public static function fromArray(array $parts): self
{
if (empty($parts)) {
throw new \InvalidArgumentException('Command array cannot be empty');
}
return new self(...$parts);
}
}

View File

@@ -0,0 +1,83 @@
<?php
declare(strict_types=1);
namespace App\Framework\Process\ValueObjects\Composer;
/**
* Composer Diagnostic Result.
*/
final readonly class ComposerDiagnosticResult
{
/**
* @param string[] $warnings
* @param string[] $errors
*/
public function __construct(
public bool $isHealthy,
public array $warnings = [],
public array $errors = [],
public ?string $composerVersion = null
) {
}
/**
* Erstellt ein gesundes Ergebnis.
*/
public static function healthy(string $composerVersion): self
{
return new self(
isHealthy: true,
composerVersion: $composerVersion
);
}
/**
* Erstellt ein Ergebnis mit Problemen.
*
* @param string[] $warnings
* @param string[] $errors
*/
public static function withIssues(array $warnings, array $errors, ?string $composerVersion = null): self
{
return new self(
isHealthy: empty($errors),
warnings: $warnings,
errors: $errors,
composerVersion: $composerVersion
);
}
/**
* Prüft, ob es Warnungen gibt.
*/
public function hasWarnings(): bool
{
return count($this->warnings) > 0;
}
/**
* Prüft, ob es Fehler gibt.
*/
public function hasErrors(): bool
{
return count($this->errors) > 0;
}
/**
* Konvertiert zu Array.
*
* @return array<string, mixed>
*/
public function toArray(): array
{
return [
'is_healthy' => $this->isHealthy,
'composer_version' => $this->composerVersion,
'warnings' => $this->warnings,
'errors' => $this->errors,
'has_warnings' => $this->hasWarnings(),
'has_errors' => $this->hasErrors(),
];
}
}

View File

@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace App\Framework\Process\ValueObjects\Composer;
/**
* Composer Package Information.
*/
final readonly class ComposerPackage
{
public function __construct(
public string $name,
public string $currentVersion,
public ?string $latestVersion = null,
public ?string $description = null,
public bool $isOutdated = false
) {
}
/**
* Erstellt Package aus Composer-Output.
*/
public static function fromComposerOutput(string $name, string $currentVersion, ?string $latestVersion = null): self
{
return new self(
name: $name,
currentVersion: $currentVersion,
latestVersion: $latestVersion,
isOutdated: $latestVersion !== null && $latestVersion !== $currentVersion
);
}
/**
* Konvertiert zu Array.
*
* @return array<string, mixed>
*/
public function toArray(): array
{
return [
'name' => $this->name,
'current_version' => $this->currentVersion,
'latest_version' => $this->latestVersion,
'description' => $this->description,
'is_outdated' => $this->isOutdated,
];
}
}

View File

@@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace App\Framework\Process\ValueObjects\Docker;
/**
* Docker Container Information.
*/
final readonly class DockerContainer
{
public function __construct(
public string $id,
public string $name,
public string $image,
public string $status,
public bool $isRunning,
public ?string $created = null,
public array $ports = []
) {
}
/**
* Erstellt Container aus Docker-Output.
*/
public static function fromDockerPs(string $id, string $name, string $image, string $status): self
{
$isRunning = str_starts_with(strtolower($status), 'up');
return new self(
id: $id,
name: $name,
image: $image,
status: $status,
isRunning: $isRunning
);
}
/**
* Konvertiert zu Array.
*
* @return array<string, mixed>
*/
public function toArray(): array
{
return [
'id' => $this->id,
'name' => $this->name,
'image' => $this->image,
'status' => $this->status,
'is_running' => $this->isRunning,
'created' => $this->created,
'ports' => $this->ports,
];
}
}

View File

@@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
namespace App\Framework\Process\ValueObjects\Docker;
use App\Framework\Core\ValueObjects\Byte;
use App\Framework\Core\ValueObjects\Percentage;
/**
* Docker Container Statistics.
*/
final readonly class DockerContainerStats
{
public function __construct(
public string $containerId,
public string $containerName,
public Percentage $cpuPercent,
public Byte $memoryUsage,
public Byte $memoryLimit,
public Percentage $memoryPercent,
public Byte $networkIn,
public Byte $networkOut,
public Byte $blockIn,
public Byte $blockOut
) {
}
/**
* Erstellt Stats aus Docker-Output.
*/
public static function fromDockerStats(
string $containerId,
string $containerName,
float $cpuPercent,
int $memoryUsageBytes,
int $memoryLimitBytes,
int $networkInBytes,
int $networkOutBytes,
int $blockInBytes,
int $blockOutBytes
): self {
$memoryPercent = $memoryLimitBytes > 0
? ($memoryUsageBytes / $memoryLimitBytes) * 100
: 0.0;
return new self(
containerId: $containerId,
containerName: $containerName,
cpuPercent: Percentage::from($cpuPercent),
memoryUsage: Byte::fromBytes($memoryUsageBytes),
memoryLimit: Byte::fromBytes($memoryLimitBytes),
memoryPercent: Percentage::from($memoryPercent),
networkIn: Byte::fromBytes($networkInBytes),
networkOut: Byte::fromBytes($networkOutBytes),
blockIn: Byte::fromBytes($blockInBytes),
blockOut: Byte::fromBytes($blockOutBytes)
);
}
/**
* Konvertiert zu Array.
*
* @return array<string, mixed>
*/
public function toArray(): array
{
return [
'container_id' => $this->containerId,
'container_name' => $this->containerName,
'cpu_percent' => $this->cpuPercent->format(2),
'memory_usage' => $this->memoryUsage->toHumanReadable(),
'memory_limit' => $this->memoryLimit->toHumanReadable(),
'memory_percent' => $this->memoryPercent->format(2),
'network_in' => $this->networkIn->toHumanReadable(),
'network_out' => $this->networkOut->toHumanReadable(),
'block_in' => $this->blockIn->toHumanReadable(),
'block_out' => $this->blockOut->toHumanReadable(),
];
}
}

View File

@@ -0,0 +1,100 @@
<?php
declare(strict_types=1);
namespace App\Framework\Process\ValueObjects;
/**
* Repräsentiert Umgebungsvariablen für Prozess-Ausführung.
*/
final readonly class EnvironmentVariables
{
/**
* @param array<string, string> $variables
*/
public function __construct(
public array $variables = []
) {
}
/**
* Erstellt eine leere EnvironmentVariables Instanz.
*/
public static function empty(): self
{
return new self([]);
}
/**
* Erstellt EnvironmentVariables aus einem Array.
*
* @param array<string, string> $env
*/
public static function fromArray(array $env): self
{
return new self($env);
}
/**
* Fügt zusätzliche Variablen hinzu und gibt eine neue Instanz zurück.
*
* @param array<string, string> $additional
*/
public function merge(array $additional): self
{
return new self(array_merge($this->variables, $additional));
}
/**
* Fügt eine einzelne Variable hinzu.
*/
public function with(string $key, string $value): self
{
return new self(array_merge($this->variables, [$key => $value]));
}
/**
* Entfernt eine Variable.
*/
public function without(string $key): self
{
$variables = $this->variables;
unset($variables[$key]);
return new self($variables);
}
/**
* Prüft, ob eine Variable existiert.
*/
public function has(string $key): bool
{
return isset($this->variables[$key]);
}
/**
* Gibt eine Variable zurück.
*/
public function get(string $key, ?string $default = null): ?string
{
return $this->variables[$key] ?? $default;
}
/**
* Gibt alle Variablen als Array zurück.
*
* @return array<string, string>
*/
public function toArray(): array
{
return $this->variables;
}
/**
* Prüft, ob keine Variablen gesetzt sind.
*/
public function isEmpty(): bool
{
return empty($this->variables);
}
}

View File

@@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
namespace App\Framework\Process\ValueObjects\Health;
/**
* Einzelner Health Check.
*/
final readonly class HealthCheck
{
public function __construct(
public string $name,
public HealthStatus $status,
public string $message,
public float $value,
public float $threshold,
public string $unit = ''
) {
}
/**
* Erstellt einen erfolgreichen Health Check.
*/
public static function healthy(
string $name,
string $message,
float $value = 0.0,
float $threshold = 0.0,
string $unit = ''
): self {
return new self($name, HealthStatus::HEALTHY, $message, $value, $threshold, $unit);
}
/**
* Erstellt einen degraded Health Check.
*/
public static function degraded(
string $name,
string $message,
float $value = 0.0,
float $threshold = 0.0,
string $unit = ''
): self {
return new self($name, HealthStatus::DEGRADED, $message, $value, $threshold, $unit);
}
/**
* Erstellt einen unhealthy Health Check.
*/
public static function unhealthy(
string $name,
string $message,
float $value = 0.0,
float $threshold = 0.0,
string $unit = ''
): self {
return new self($name, HealthStatus::UNHEALTHY, $message, $value, $threshold, $unit);
}
/**
* Konvertiert zu Array.
*
* @return array<string, mixed>
*/
public function toArray(): array
{
return [
'name' => $this->name,
'status' => $this->status->value,
'message' => $this->message,
'value' => $this->value,
'threshold' => $this->threshold,
'unit' => $this->unit,
];
}
}

View File

@@ -0,0 +1,128 @@
<?php
declare(strict_types=1);
namespace App\Framework\Process\ValueObjects\Health;
/**
* Aggregierter Health Report.
*/
final readonly class HealthReport
{
/**
* @param HealthCheck[] $checks
*/
public function __construct(
public HealthStatus $overallStatus,
public array $checks,
public \DateTimeImmutable $timestamp
) {
}
/**
* Erstellt einen Health Report aus Checks.
*
* @param HealthCheck[] $checks
*/
public static function fromChecks(array $checks): self
{
$overallStatus = self::determineOverallStatus($checks);
return new self(
overallStatus: $overallStatus,
checks: $checks,
timestamp: new \DateTimeImmutable()
);
}
/**
* Bestimmt den Overall Status basierend auf den Checks.
*
* @param HealthCheck[] $checks
*/
private static function determineOverallStatus(array $checks): HealthStatus
{
$hasUnhealthy = false;
$hasDegraded = false;
foreach ($checks as $check) {
if ($check->status === HealthStatus::UNHEALTHY) {
$hasUnhealthy = true;
} elseif ($check->status === HealthStatus::DEGRADED) {
$hasDegraded = true;
}
}
if ($hasUnhealthy) {
return HealthStatus::UNHEALTHY;
}
if ($hasDegraded) {
return HealthStatus::DEGRADED;
}
return HealthStatus::HEALTHY;
}
/**
* Gibt die Anzahl der Checks pro Status zurück.
*
* @return array{healthy: int, degraded: int, unhealthy: int}
*/
public function getStatusCounts(): array
{
$counts = [
'healthy' => 0,
'degraded' => 0,
'unhealthy' => 0,
];
foreach ($this->checks as $check) {
$counts[$check->status->value]++;
}
return $counts;
}
/**
* Gibt nur unhealthy Checks zurück.
*
* @return HealthCheck[]
*/
public function getUnhealthyChecks(): array
{
return array_filter(
$this->checks,
fn (HealthCheck $check) => $check->status === HealthStatus::UNHEALTHY
);
}
/**
* Gibt nur degraded Checks zurück.
*
* @return HealthCheck[]
*/
public function getDegradedChecks(): array
{
return array_filter(
$this->checks,
fn (HealthCheck $check) => $check->status === HealthStatus::DEGRADED
);
}
/**
* Konvertiert zu Array.
*
* @return array<string, mixed>
*/
public function toArray(): array
{
return [
'overall_status' => $this->overallStatus->value,
'overall_description' => $this->overallStatus->getDescription(),
'timestamp' => $this->timestamp->format('Y-m-d H:i:s'),
'checks' => array_map(fn (HealthCheck $c) => $c->toArray(), $this->checks),
'status_counts' => $this->getStatusCounts(),
];
}
}

View File

@@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
namespace App\Framework\Process\ValueObjects\Health;
/**
* System Health Status.
*/
enum HealthStatus: string
{
case HEALTHY = 'healthy';
case DEGRADED = 'degraded';
case UNHEALTHY = 'unhealthy';
/**
* Gibt eine menschenlesbare Beschreibung zurück.
*/
public function getDescription(): string
{
return match ($this) {
self::HEALTHY => 'System is operating normally',
self::DEGRADED => 'System is experiencing issues but operational',
self::UNHEALTHY => 'System has critical issues',
};
}
/**
* Gibt ein Icon für den Status zurück.
*/
public function getIcon(): string
{
return match ($this) {
self::HEALTHY => '✅',
self::DEGRADED => '⚠️',
self::UNHEALTHY => '❌',
};
}
/**
* Prüft, ob der Status gesund ist.
*/
public function isHealthy(): bool
{
return $this === self::HEALTHY;
}
/**
* Prüft, ob der Status degraded ist.
*/
public function isDegraded(): bool
{
return $this === self::DEGRADED;
}
/**
* Prüft, ob der Status ungesund ist.
*/
public function isUnhealthy(): bool
{
return $this === self::UNHEALTHY;
}
}

View File

@@ -0,0 +1,121 @@
<?php
declare(strict_types=1);
namespace App\Framework\Process\ValueObjects\Http;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Http\Headers;
use App\Framework\Http\Status;
use App\Framework\Http\Url\Url;
/**
* URL Health Check Result.
*/
final readonly class UrlHealthCheck
{
public function __construct(
public Url $url,
public bool $isAccessible,
public Status $status,
public Duration $responseTime,
public ?string $error = null,
public ?Url $redirectUrl = null,
public ?Headers $headers = null
) {
}
/**
* Erstellt ein erfolgreiches Health-Check-Ergebnis.
*/
public static function success(
Url $url,
Status $status,
Duration $responseTime,
?Headers $headers = null
): self {
return new self(
url: $url,
isAccessible: true,
status: $status,
responseTime: $responseTime,
headers: $headers
);
}
/**
* Erstellt ein fehlgeschlagenes Health-Check-Ergebnis.
*/
public static function failed(Url $url, string $error): self
{
return new self(
url: $url,
isAccessible: false,
status: Status::INTERNAL_SERVER_ERROR,
responseTime: Duration::fromMilliseconds(0),
error: $error
);
}
/**
* Fügt Redirect-Informationen hinzu.
*/
public function withRedirect(Url $redirectUrl): self
{
return new self(
url: $this->url,
isAccessible: $this->isAccessible,
status: $this->status,
responseTime: $this->responseTime,
error: $this->error,
redirectUrl: $redirectUrl,
headers: $this->headers
);
}
/**
* Prüft, ob Status-Code erfolgreich ist (2xx).
*/
public function isSuccessful(): bool
{
return $this->status->isSuccess();
}
/**
* Prüft, ob ein Redirect vorliegt (3xx).
*/
public function isRedirect(): bool
{
return $this->status->isRedirection();
}
/**
* Prüft, ob Response-Zeit akzeptabel ist.
*/
public function isResponseTimeFast(Duration $threshold): bool
{
return $this->responseTime->toMilliseconds() < $threshold->toMilliseconds();
}
/**
* Konvertiert zu Array.
*
* @return array<string, mixed>
*/
public function toArray(): array
{
return [
'url' => $this->url->toString(),
'is_accessible' => $this->isAccessible,
'is_successful' => $this->isSuccessful(),
'status_code' => $this->status->value,
'status_description' => $this->status->getDescription(),
'response_time_ms' => $this->responseTime->toMilliseconds(),
'response_time_human' => $this->responseTime->toHumanReadable(),
'error' => $this->error,
'redirect_url' => $this->redirectUrl?->toString(),
'is_redirect' => $this->isRedirect(),
'headers' => $this->headers?->all(),
];
}
}

View File

@@ -0,0 +1,126 @@
<?php
declare(strict_types=1);
namespace App\Framework\Process\ValueObjects\Logs;
/**
* Ergebnis einer Log-Analyse.
*/
final readonly class LogAnalysisResult
{
/**
* @param LogEntry[] $entries
* @param array<string, int> $levelCounts
*/
public function __construct(
public array $entries,
public int $totalLines,
public array $levelCounts,
public \DateTimeImmutable $analyzedAt
) {
}
/**
* Erstellt Ergebnis aus Log-Einträgen.
*
* @param LogEntry[] $entries
*/
public static function fromEntries(array $entries, int $totalLines): self
{
$levelCounts = [];
foreach ($entries as $entry) {
$level = $entry->level;
$levelCounts[$level] = ($levelCounts[$level] ?? 0) + 1;
}
return new self(
entries: $entries,
totalLines: $totalLines,
levelCounts: $levelCounts,
analyzedAt: new \DateTimeImmutable()
);
}
/**
* Gibt die Anzahl der Errors zurück.
*/
public function getErrorCount(): int
{
return array_sum(array_map(
fn ($level, $count) => in_array($level, ['ERROR', 'CRITICAL', 'ALERT', 'EMERGENCY'], true) ? $count : 0,
array_keys($this->levelCounts),
$this->levelCounts
));
}
/**
* Gibt die Anzahl der Warnings zurück.
*/
public function getWarningCount(): int
{
return $this->levelCounts['WARNING'] ?? 0;
}
/**
* Gibt nur Error-Einträge zurück.
*
* @return LogEntry[]
*/
public function getErrors(): array
{
return array_filter(
$this->entries,
fn (LogEntry $entry) => $entry->isError()
);
}
/**
* Gibt nur Warning-Einträge zurück.
*
* @return LogEntry[]
*/
public function getWarnings(): array
{
return array_filter(
$this->entries,
fn (LogEntry $entry) => $entry->isWarning()
);
}
/**
* Gibt die häufigsten Fehler-Messages zurück.
*
* @return array<string, int>
*/
public function getTopErrors(int $limit = 10): array
{
$messageCounts = [];
foreach ($this->getErrors() as $entry) {
$messageCounts[$entry->message] = ($messageCounts[$entry->message] ?? 0) + 1;
}
arsort($messageCounts);
return array_slice($messageCounts, 0, $limit, true);
}
/**
* Konvertiert zu Array.
*
* @return array<string, mixed>
*/
public function toArray(): array
{
return [
'total_lines' => $this->totalLines,
'analyzed_entries' => count($this->entries),
'level_counts' => $this->levelCounts,
'error_count' => $this->getErrorCount(),
'warning_count' => $this->getWarningCount(),
'analyzed_at' => $this->analyzedAt->format('Y-m-d H:i:s'),
];
}
}

View File

@@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
namespace App\Framework\Process\ValueObjects\Logs;
/**
* Einzelner Log-Eintrag.
*/
final readonly class LogEntry
{
public function __construct(
public string $level,
public string $message,
public ?\DateTimeImmutable $timestamp = null,
public ?string $context = null
) {
}
/**
* Erstellt einen Log Entry aus einer Log-Zeile.
*/
public static function fromLine(string $line): self
{
// Versuche Standard-Format zu parsen: [timestamp] level: message
if (preg_match('/^\[([^\]]+)\]\s+(\w+):\s+(.+)$/', $line, $matches)) {
return new self(
level: strtoupper($matches[2]),
message: $matches[3],
timestamp: new \DateTimeImmutable($matches[1])
);
}
// Fallback: Ganzer Line als Message
return new self(
level: 'UNKNOWN',
message: $line
);
}
/**
* Prüft, ob es ein Error-Level ist.
*/
public function isError(): bool
{
return in_array($this->level, ['ERROR', 'CRITICAL', 'ALERT', 'EMERGENCY'], true);
}
/**
* Prüft, ob es ein Warning-Level ist.
*/
public function isWarning(): bool
{
return $this->level === 'WARNING';
}
/**
* Konvertiert zu Array.
*
* @return array<string, mixed>
*/
public function toArray(): array
{
return [
'level' => $this->level,
'message' => $this->message,
'timestamp' => $this->timestamp?->format('Y-m-d H:i:s'),
'context' => $this->context,
];
}
}

View File

@@ -0,0 +1,100 @@
<?php
declare(strict_types=1);
namespace App\Framework\Process\ValueObjects\Network;
use App\Framework\Http\IpAddress;
/**
* DNS Lookup Result.
*/
final readonly class DnsResult
{
/**
* @param IpAddress[] $addresses
*/
public function __construct(
public string $hostname,
public bool $resolved,
public array $addresses = []
) {
}
/**
* Erstellt ein erfolgreiches DNS-Ergebnis.
*
* @param IpAddress[] $addresses
*/
public static function success(string $hostname, array $addresses): self
{
return new self(
hostname: $hostname,
resolved: true,
addresses: $addresses
);
}
/**
* Erstellt ein fehlgeschlagenes DNS-Ergebnis.
*/
public static function failed(string $hostname): self
{
return new self(
hostname: $hostname,
resolved: false,
addresses: []
);
}
/**
* Gibt die erste IP-Adresse zurück.
*/
public function getFirstAddress(): ?IpAddress
{
return $this->addresses[0] ?? null;
}
/**
* Gibt nur IPv4-Adressen zurück.
*
* @return IpAddress[]
*/
public function getIpv4Addresses(): array
{
return array_filter(
$this->addresses,
fn (IpAddress $ip) => $ip->isV4()
);
}
/**
* Gibt nur IPv6-Adressen zurück.
*
* @return IpAddress[]
*/
public function getIpv6Addresses(): array
{
return array_filter(
$this->addresses,
fn (IpAddress $ip) => $ip->isV6()
);
}
/**
* Konvertiert zu Array.
*
* @return array<string, mixed>
*/
public function toArray(): array
{
return [
'hostname' => $this->hostname,
'resolved' => $this->resolved,
'addresses' => array_map(fn (IpAddress $ip) => $ip->value, $this->addresses),
'address_count' => count($this->addresses),
'ipv4_count' => count($this->getIpv4Addresses()),
'ipv6_count' => count($this->getIpv6Addresses()),
];
}
}

View File

@@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
namespace App\Framework\Process\ValueObjects\Network;
use App\Framework\Core\ValueObjects\Duration;
/**
* Ping Test Result.
*/
final readonly class PingResult
{
public function __construct(
public string $host,
public bool $isReachable,
public ?Duration $latency = null,
public ?int $packetsSent = null,
public ?int $packetsReceived = null,
public ?float $packetLoss = null
) {
}
/**
* Erstellt ein erfolgreiches Ping-Ergebnis.
*/
public static function success(
string $host,
Duration $latency,
int $packetsSent = 4,
int $packetsReceived = 4
): self {
$packetLoss = $packetsSent > 0
? (($packetsSent - $packetsReceived) / $packetsSent) * 100
: 0.0;
return new self(
host: $host,
isReachable: true,
latency: $latency,
packetsSent: $packetsSent,
packetsReceived: $packetsReceived,
packetLoss: $packetLoss
);
}
/**
* Erstellt ein fehlgeschlagenes Ping-Ergebnis.
*/
public static function failed(string $host): self
{
return new self(
host: $host,
isReachable: false
);
}
/**
* Konvertiert zu Array.
*
* @return array<string, mixed>
*/
public function toArray(): array
{
return [
'host' => $this->host,
'is_reachable' => $this->isReachable,
'latency_ms' => $this->latency?->toMilliseconds(),
'latency_human' => $this->latency?->toHumanReadable(),
'packets_sent' => $this->packetsSent,
'packets_received' => $this->packetsReceived,
'packet_loss_percent' => $this->packetLoss,
];
}
}

View File

@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace App\Framework\Process\ValueObjects\Network;
/**
* Port Status Result.
*/
final readonly class PortStatus
{
public function __construct(
public int $port,
public bool $isOpen,
public string $service = ''
) {
}
/**
* Erstellt einen offenen Port.
*/
public static function open(int $port, string $service = ''): self
{
return new self($port, true, $service);
}
/**
* Erstellt einen geschlossenen Port.
*/
public static function closed(int $port): self
{
return new self($port, false);
}
/**
* Konvertiert zu Array.
*
* @return array<string, mixed>
*/
public function toArray(): array
{
return [
'port' => $this->port,
'is_open' => $this->isOpen,
'service' => $this->service,
];
}
}

View File

@@ -0,0 +1,116 @@
<?php
declare(strict_types=1);
namespace App\Framework\Process\ValueObjects\Npm;
/**
* NPM Audit Result.
*
* Ergebnis eines NPM Security Audits mit Severity-Breakdown.
*/
final readonly class NpmAuditResult
{
/**
* @param array<string> $vulnerabilities
*/
public function __construct(
public int $total,
public int $info,
public int $low,
public int $moderate,
public int $high,
public int $critical,
public array $vulnerabilities = []
) {
}
/**
* Erstellt Ergebnis aus NPM Audit JSON.
*/
public static function fromAuditJson(array $data): self
{
$metadata = $data['metadata'] ?? [];
$vulnerabilities = $metadata['vulnerabilities'] ?? [];
return new self(
total: (int) ($metadata['total'] ?? 0),
info: (int) ($vulnerabilities['info'] ?? 0),
low: (int) ($vulnerabilities['low'] ?? 0),
moderate: (int) ($vulnerabilities['moderate'] ?? 0),
high: (int) ($vulnerabilities['high'] ?? 0),
critical: (int) ($vulnerabilities['critical'] ?? 0),
vulnerabilities: self::extractVulnerabilityNames($data)
);
}
/**
* Erstellt sicheres Ergebnis (keine Vulnerabilities).
*/
public static function safe(): self
{
return new self(
total: 0,
info: 0,
low: 0,
moderate: 0,
high: 0,
critical: 0
);
}
/**
* Prüft ob es kritische oder hohe Vulnerabilities gibt.
*/
public function hasCriticalIssues(): bool
{
return $this->critical > 0 || $this->high > 0;
}
/**
* Prüft ob es irgendwelche Vulnerabilities gibt.
*/
public function hasVulnerabilities(): bool
{
return $this->total > 0;
}
/**
* Konvertiert zu Array.
*
* @return array<string, mixed>
*/
public function toArray(): array
{
return [
'total' => $this->total,
'info' => $this->info,
'low' => $this->low,
'moderate' => $this->moderate,
'high' => $this->high,
'critical' => $this->critical,
'has_critical_issues' => $this->hasCriticalIssues(),
'has_vulnerabilities' => $this->hasVulnerabilities(),
'vulnerabilities' => $this->vulnerabilities,
];
}
/**
* Extrahiert Vulnerability-Namen aus Audit-Daten.
*
* @return string[]
*/
private static function extractVulnerabilityNames(array $data): array
{
$names = [];
$advisories = $data['advisories'] ?? [];
foreach ($advisories as $advisory) {
if (isset($advisory['module_name'])) {
$names[] = $advisory['module_name'];
}
}
return array_unique($names);
}
}

View File

@@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace App\Framework\Process\ValueObjects\Npm;
/**
* NPM Package Information.
*/
final readonly class NpmPackage
{
public function __construct(
public string $name,
public string $currentVersion,
public ?string $wantedVersion = null,
public ?string $latestVersion = null,
public ?string $type = null, // 'dependencies' or 'devDependencies'
public bool $isOutdated = false
) {
}
/**
* Erstellt Package aus NPM-Output.
*/
public static function fromNpmOutput(
string $name,
string $currentVersion,
?string $wantedVersion = null,
?string $latestVersion = null,
?string $type = null
): self {
return new self(
name: $name,
currentVersion: $currentVersion,
wantedVersion: $wantedVersion,
latestVersion: $latestVersion,
type: $type,
isOutdated: $latestVersion !== null && $latestVersion !== $currentVersion
);
}
/**
* Konvertiert zu Array.
*
* @return array<string, mixed>
*/
public function toArray(): array
{
return [
'name' => $this->name,
'current_version' => $this->currentVersion,
'wanted_version' => $this->wantedVersion,
'latest_version' => $this->latestVersion,
'type' => $this->type,
'is_outdated' => $this->isOutdated,
];
}
}

View File

@@ -0,0 +1,85 @@
<?php
declare(strict_types=1);
namespace App\Framework\Process\ValueObjects\Ssl;
/**
* SSL Certificate Information.
*/
final readonly class CertificateInfo
{
public function __construct(
public string $subject,
public string $issuer,
public \DateTimeImmutable $validFrom,
public \DateTimeImmutable $validTo,
public array $subjectAltNames,
public bool $isSelfSigned,
public ?string $serialNumber = null,
public ?string $signatureAlgorithm = null
) {
}
/**
* Prüft, ob das Zertifikat gültig ist.
*/
public function isValid(): bool
{
$now = new \DateTimeImmutable();
return $now >= $this->validFrom && $now <= $this->validTo;
}
/**
* Prüft, ob das Zertifikat bald abläuft.
*/
public function isExpiringSoon(int $daysThreshold = 30): bool
{
$now = new \DateTimeImmutable();
$threshold = $now->modify("+{$daysThreshold} days");
return $this->validTo <= $threshold;
}
/**
* Gibt die verbleibenden Tage bis zum Ablauf zurück.
*/
public function getDaysUntilExpiry(): int
{
$now = new \DateTimeImmutable();
$diff = $now->diff($this->validTo);
return $diff->days * ($diff->invert ? -1 : 1);
}
/**
* Prüft, ob das Zertifikat abgelaufen ist.
*/
public function isExpired(): bool
{
return new \DateTimeImmutable() > $this->validTo;
}
/**
* Konvertiert zu Array.
*
* @return array<string, mixed>
*/
public function toArray(): array
{
return [
'subject' => $this->subject,
'issuer' => $this->issuer,
'valid_from' => $this->validFrom->format('Y-m-d H:i:s'),
'valid_to' => $this->validTo->format('Y-m-d H:i:s'),
'subject_alt_names' => $this->subjectAltNames,
'is_self_signed' => $this->isSelfSigned,
'is_valid' => $this->isValid(),
'is_expired' => $this->isExpired(),
'days_until_expiry' => $this->getDaysUntilExpiry(),
'serial_number' => $this->serialNumber,
'signature_algorithm' => $this->signatureAlgorithm,
];
}
}

View File

@@ -0,0 +1,106 @@
<?php
declare(strict_types=1);
namespace App\Framework\Process\ValueObjects\Ssl;
/**
* SSL Certificate Validation Result.
*/
final readonly class CertificateValidationResult
{
public function __construct(
public string $hostname,
public bool $isValid,
public ?CertificateInfo $certificateInfo = null,
public array $errors = [],
public array $warnings = []
) {
}
/**
* Erstellt ein erfolgreiches Validierungsergebnis.
*/
public static function success(string $hostname, CertificateInfo $info): self
{
return new self(
hostname: $hostname,
isValid: true,
certificateInfo: $info
);
}
/**
* Erstellt ein fehlgeschlagenes Validierungsergebnis.
*
* @param string[] $errors
*/
public static function failed(string $hostname, array $errors): self
{
return new self(
hostname: $hostname,
isValid: false,
errors: $errors
);
}
/**
* Erstellt ein Ergebnis mit Warnungen.
*
* @param string[] $warnings
*/
public function withWarnings(array $warnings): self
{
return new self(
hostname: $this->hostname,
isValid: $this->isValid,
certificateInfo: $this->certificateInfo,
errors: $this->errors,
warnings: $warnings
);
}
/**
* Gibt alle Probleme (Errors + Warnings) zurück.
*
* @return string[]
*/
public function getAllIssues(): array
{
return array_merge($this->errors, $this->warnings);
}
/**
* Prüft, ob es Warnungen gibt.
*/
public function hasWarnings(): bool
{
return count($this->warnings) > 0;
}
/**
* Prüft, ob es Fehler gibt.
*/
public function hasErrors(): bool
{
return count($this->errors) > 0;
}
/**
* Konvertiert zu Array.
*
* @return array<string, mixed>
*/
public function toArray(): array
{
return [
'hostname' => $this->hostname,
'is_valid' => $this->isValid,
'certificate' => $this->certificateInfo?->toArray(),
'errors' => $this->errors,
'warnings' => $this->warnings,
'has_warnings' => $this->hasWarnings(),
'has_errors' => $this->hasErrors(),
];
}
}

View File

@@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace App\Framework\Process\ValueObjects\SystemInfo;
/**
* CPU Information.
*/
final readonly class CpuInfo
{
public function __construct(
public int $cores,
public string $model = 'Unknown'
) {
}
/**
* Erstellt eine leere CpuInfo Instanz.
*/
public static function empty(): self
{
return new self(0, 'Unknown');
}
/**
* Gibt einen gekürzten Model-Namen zurück.
*/
public function getShortModel(): string
{
// Entfernt "(R)", "(TM)", "CPU" etc. für lesbareren Output
$model = $this->model;
$model = str_replace(['(R)', '(TM)', '(tm)'], '', $model);
$model = preg_replace('/\s+CPU\s+/', ' ', $model);
$model = preg_replace('/\s+/', ' ', $model);
return trim($model);
}
/**
* Konvertiert zu Array.
*
* @return array<string, mixed>
*/
public function toArray(): array
{
return [
'cores' => $this->cores,
'model' => $this->model,
'model_short' => $this->getShortModel(),
];
}
}

View File

@@ -0,0 +1,93 @@
<?php
declare(strict_types=1);
namespace App\Framework\Process\ValueObjects\SystemInfo;
use App\Framework\Core\ValueObjects\Byte;
/**
* Disk Information.
*/
final readonly class DiskInfo
{
public function __construct(
public int $totalBytes,
public int $usedBytes,
public int $availableBytes,
public string $mountPoint = '/'
) {
}
/**
* Erstellt eine leere DiskInfo Instanz.
*/
public static function empty(): self
{
return new self(0, 0, 0, '/');
}
/**
* Gibt die Auslastung als Prozent zurück (0-100).
*/
public function getUsagePercentage(): float
{
if ($this->totalBytes === 0) {
return 0.0;
}
return ($this->usedBytes / $this->totalBytes) * 100;
}
/**
* Prüft, ob die Disk fast voll ist (>90%).
*/
public function isAlmostFull(): bool
{
return $this->getUsagePercentage() > 90;
}
/**
* Gibt Total als Byte Value Object zurück.
*/
public function getTotal(): Byte
{
return Byte::fromBytes($this->totalBytes);
}
/**
* Gibt Used als Byte Value Object zurück.
*/
public function getUsed(): Byte
{
return Byte::fromBytes($this->usedBytes);
}
/**
* Gibt Available als Byte Value Object zurück.
*/
public function getAvailable(): Byte
{
return Byte::fromBytes($this->availableBytes);
}
/**
* Konvertiert zu Array.
*
* @return array<string, mixed>
*/
public function toArray(): array
{
return [
'mount_point' => $this->mountPoint,
'total_bytes' => $this->totalBytes,
'total_human' => $this->getTotal()->toHumanReadable(),
'used_bytes' => $this->usedBytes,
'used_human' => $this->getUsed()->toHumanReadable(),
'available_bytes' => $this->availableBytes,
'available_human' => $this->getAvailable()->toHumanReadable(),
'usage_percentage' => round($this->getUsagePercentage(), 2),
'is_almost_full' => $this->isAlmostFull(),
];
}
}

View File

@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
namespace App\Framework\Process\ValueObjects\SystemInfo;
/**
* System Load Average.
*/
final readonly class LoadAverage
{
public function __construct(
public float $oneMinute,
public float $fiveMinutes,
public float $fifteenMinutes
) {
}
/**
* Erstellt eine leere LoadAverage Instanz.
*/
public static function empty(): self
{
return new self(0.0, 0.0, 0.0);
}
/**
* Prüft, ob das System überlastet ist (basierend auf 1min Load).
* Überlastet wenn Load > CPU Cores.
*/
public function isOverloaded(int $cpuCores): bool
{
return $this->oneMinute > $cpuCores;
}
/**
* Gibt den Auslastungsgrad zurück (0.0 - 1.0+).
*/
public function getUtilization(int $cpuCores): float
{
if ($cpuCores === 0) {
return 0.0;
}
return $this->oneMinute / $cpuCores;
}
/**
* Konvertiert zu Array.
*
* @return array<string, mixed>
*/
public function toArray(): array
{
return [
'one_minute' => $this->oneMinute,
'five_minutes' => $this->fiveMinutes,
'fifteen_minutes' => $this->fifteenMinutes,
];
}
}

View File

@@ -0,0 +1,93 @@
<?php
declare(strict_types=1);
namespace App\Framework\Process\ValueObjects\SystemInfo;
use App\Framework\Core\ValueObjects\Byte;
/**
* Memory Information.
*/
final readonly class MemoryInfo
{
public function __construct(
public int $totalBytes,
public int $usedBytes,
public int $freeBytes,
public int $availableBytes
) {
}
/**
* Erstellt eine leere MemoryInfo Instanz.
*/
public static function empty(): self
{
return new self(0, 0, 0, 0);
}
/**
* Gibt die Auslastung als Prozent zurück (0-100).
*/
public function getUsagePercentage(): float
{
if ($this->totalBytes === 0) {
return 0.0;
}
return ($this->usedBytes / $this->totalBytes) * 100;
}
/**
* Gibt Total als Byte Value Object zurück.
*/
public function getTotal(): Byte
{
return Byte::fromBytes($this->totalBytes);
}
/**
* Gibt Used als Byte Value Object zurück.
*/
public function getUsed(): Byte
{
return Byte::fromBytes($this->usedBytes);
}
/**
* Gibt Free als Byte Value Object zurück.
*/
public function getFree(): Byte
{
return Byte::fromBytes($this->freeBytes);
}
/**
* Gibt Available als Byte Value Object zurück.
*/
public function getAvailable(): Byte
{
return Byte::fromBytes($this->availableBytes);
}
/**
* Konvertiert zu Array.
*
* @return array<string, mixed>
*/
public function toArray(): array
{
return [
'total_bytes' => $this->totalBytes,
'total_human' => $this->getTotal()->toHumanReadable(),
'used_bytes' => $this->usedBytes,
'used_human' => $this->getUsed()->toHumanReadable(),
'free_bytes' => $this->freeBytes,
'free_human' => $this->getFree()->toHumanReadable(),
'available_bytes' => $this->availableBytes,
'available_human' => $this->getAvailable()->toHumanReadable(),
'usage_percentage' => round($this->getUsagePercentage(), 2),
];
}
}

View File

@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace App\Framework\Process\ValueObjects\SystemInfo;
/**
* Process Information.
*/
final readonly class ProcessInfo
{
public function __construct(
public int $total,
public int $running,
public int $sleeping
) {
}
/**
* Erstellt eine leere ProcessInfo Instanz.
*/
public static function empty(): self
{
return new self(0, 0, 0);
}
/**
* Gibt die Anzahl der anderen Prozesse zurück.
*/
public function getOther(): int
{
return max(0, $this->total - $this->running - $this->sleeping);
}
/**
* Konvertiert zu Array.
*
* @return array<string, mixed>
*/
public function toArray(): array
{
return [
'total' => $this->total,
'running' => $this->running,
'sleeping' => $this->sleeping,
'other' => $this->getOther(),
];
}
}

View File

@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace App\Framework\Process\ValueObjects\SystemInfo;
use App\Framework\Core\ValueObjects\Duration;
use DateTimeImmutable;
/**
* System Uptime Information.
*/
final readonly class SystemUptime
{
public function __construct(
public DateTimeImmutable $bootTime,
public Duration $uptime
) {
}
/**
* Gibt das Boot-Datum formatiert zurück.
*/
public function getBootTimeFormatted(): string
{
return $this->bootTime->format('Y-m-d H:i:s');
}
/**
* Gibt die Uptime in Tagen zurück.
*/
public function getUptimeDays(): int
{
return (int) floor($this->uptime->toSeconds() / 86400);
}
/**
* Konvertiert zu Array.
*
* @return array<string, mixed>
*/
public function toArray(): array
{
return [
'boot_time' => $this->bootTime->format('Y-m-d H:i:s'),
'uptime_seconds' => $this->uptime->toSeconds(),
'uptime_human' => $this->uptime->toHumanReadable(),
'uptime_days' => $this->getUptimeDays(),
];
}
}