refactor(console): extract responsibilities from ConsoleApplication

- Extract terminal compatibility checking to TerminalCompatibilityChecker
- Extract input validation to ConsoleInputValidator
- Extract command categorization to CommandCategorizer
- Extract result processing to CommandResultProcessor
- Extract help rendering to ConsoleHelpRenderer
- Extract TUI/Dialog initialization to TuiFactory/DialogFactory
- Extract signal handling to ConsoleSignalHandler (uses Pcntl module)
- Remove legacy PCNTL fallback code
- Reduce ConsoleApplication from 757 to ~340 lines (55% reduction)

All changes maintain backward compatibility - no breaking changes.
This commit is contained in:
2025-11-09 13:37:17 +01:00
parent 4586f0e9ee
commit 85c369e846
9 changed files with 896 additions and 447 deletions

View File

@@ -0,0 +1,86 @@
<?php
declare(strict_types=1);
namespace App\Framework\Console;
/**
* Kategorisiert Commands basierend auf ihrem Präfix (vor dem Doppelpunkt).
*
* Bietet auch Metadaten für Kategorien (Beschreibungen).
*/
final readonly class CommandCategorizer
{
/**
* Kategorie-Informationen mit Beschreibungen.
*
* @var array<string, string>
*/
private const CATEGORY_INFO = [
'db' => 'Database operations (migrations, health checks)',
'errors' => 'Error management and analytics',
'backup' => 'Backup and restore operations',
'secrets' => 'Secret management and encryption',
'cache' => 'Cache management operations',
'demo' => 'Demo and example commands',
'logs' => 'Log management and rotation',
'alerts' => 'Alert system management',
'mcp' => 'Model Context Protocol server',
'make' => 'Code generation commands',
'docs' => 'Documentation generation',
'openapi' => 'OpenAPI specification generation',
'static' => 'Static file generation',
'redis' => 'Redis cache operations',
'routes' => 'Route management',
'discovery' => 'Framework discovery system',
];
/**
* Kategorisiert Commands basierend auf ihrem Präfix.
*
* @param CommandList $commandList Liste der Commands
* @return array<string, array<ConsoleCommand>> Kategorien mit ihren Commands
*/
public function categorize(CommandList $commandList): array
{
$categories = [];
foreach ($commandList as $command) {
$parts = explode(':', $command->name);
$category = $parts[0];
if (!isset($categories[$category])) {
$categories[$category] = [];
}
$categories[$category][] = $command;
}
// Sortiere Kategorien
ksort($categories);
return $categories;
}
/**
* Gibt die Beschreibung für eine Kategorie zurück.
*
* @param string $category Kategorie-Name
* @return string Beschreibung oder 'Various commands' als Fallback
*/
public function getCategoryDescription(string $category): string
{
return self::CATEGORY_INFO[$category] ?? 'Various commands';
}
/**
* Gibt alle Kategorie-Informationen zurück.
*
* @return array<string, string> Kategorie-Name => Beschreibung
*/
public function getCategoryInfo(): array
{
return self::CATEGORY_INFO;
}
}

View File

@@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
namespace App\Framework\Console;
use App\Framework\Console\Result\ConsoleResult;
use App\Framework\DI\Container;
use App\Framework\Logging\Logger;
use App\Framework\Logging\ValueObjects\LogContext;
/**
* Verarbeitet Command-Results und konvertiert sie zu ExitCode.
*
* Unterstützt:
* - ConsoleResult (neue Pattern) - rendert und gibt exitCode zurück
* - ExitCode (Enum) - gibt direkt zurück
* - int (Legacy) - konvertiert zu ExitCode
* - Invalid - loggt Warnung und gibt GENERAL_ERROR zurück
*/
final readonly class CommandResultProcessor
{
public function __construct(
private Container $container,
private ConsoleOutputInterface $output
) {
}
/**
* Verarbeitet ein Command-Result und gibt den ExitCode zurück.
*
* @param mixed $result Command-Result (ConsoleResult, ExitCode, int, oder ungültig)
* @return ExitCode Exit-Code für den Prozess
*/
public function process(mixed $result): ExitCode
{
// New ConsoleResult pattern
if ($result instanceof ConsoleResult) {
// Render result to output
$result->render($this->output);
// Return exit code from result
return $result->exitCode;
}
// Legacy ExitCode pattern
if ($result instanceof ExitCode) {
return $result;
}
// Legacy int pattern (for backwards compatibility)
if (is_int($result)) {
return ExitCode::from($result);
}
// Invalid return type - log warning and return error
if ($this->container->has(Logger::class)) {
$logger = $this->container->get(Logger::class);
$logger->warning('Command returned invalid result type', LogContext::withData([
'result_type' => get_debug_type($result),
'component' => 'CommandResultProcessor',
]));
}
return ExitCode::GENERAL_ERROR;
}
}

View File

@@ -0,0 +1,120 @@
<?php
declare(strict_types=1);
namespace App\Framework\Console\Components;
use App\Framework\Config\AppConfig;
use App\Framework\Console\CommandGroupRegistry;
use App\Framework\Console\CommandHistory;
use App\Framework\Console\CommandRegistry;
use App\Framework\Console\ConsoleColor;
use App\Framework\Console\ConsoleOutputInterface;
use App\Framework\Console\ExitCode;
use App\Framework\Console\Help\ConsoleHelpRenderer;
use App\Framework\DI\Container;
use App\Framework\Discovery\Results\DiscoveryRegistry;
use Throwable;
/**
* Factory für Dialog-Modus Initialisierung.
*
* Kapselt die Dependency-Injection und Fehlerbehandlung für Dialog-Modus.
*/
final readonly class DialogFactory
{
public function __construct(
private Container $container,
private ConsoleOutputInterface $output,
private CommandRegistry $commandRegistry,
private string $scriptName
) {
}
/**
* Erstellt und startet eine Dialog-Instanz.
*
* @return int Exit-Code
*/
public function createAndRun(): int
{
try {
// Get DiscoveryRegistry for dialog components
$discoveryRegistry = $this->container->get(DiscoveryRegistry::class);
// Create CommandHistory
$commandHistory = new CommandHistory();
// Create new services
$groupRegistry = new CommandGroupRegistry($discoveryRegistry);
$commandList = $this->commandRegistry->getCommandList();
// Create dialog components
$commandExecutor = new DialogCommandExecutor(
$this->output,
$this->commandRegistry,
$commandHistory,
$this->scriptName
);
// Create dialog instance
$dialog = new ConsoleDialog(
$this->output,
$discoveryRegistry,
$commandHistory,
$groupRegistry,
$commandExecutor,
$commandList,
$this->container,
$this->scriptName . '> '
);
// Start dialog
return $dialog->run()->value;
} catch (Throwable $e) {
$this->output->writeError("Failed to launch dialog mode: " . $e->getMessage());
$config = $this->container->get(AppConfig::class);
if ($config->isDevelopment()) {
$this->output->writeLine("Stack trace:", ConsoleColor::RED);
$this->output->writeLine($e->getTraceAsString());
}
// Fallback to help
$this->output->newLine();
$this->output->writeLine("Falling back to command-line help:", ConsoleColor::YELLOW);
$this->showHelpFallback();
return ExitCode::SOFTWARE_ERROR->value;
}
}
/**
* Zeigt Help als Fallback an.
*/
private function showHelpFallback(): void
{
$commandList = $this->commandRegistry->getCommandList();
// Create minimal error handler for help rendering
$suggestionEngine = new \App\Framework\Console\ErrorRecovery\CommandSuggestionEngine($commandList);
$recoveryService = new \App\Framework\Console\ErrorRecovery\ErrorRecoveryService(
$suggestionEngine,
$commandList,
$this->commandRegistry
);
$logger = $this->container->has(\App\Framework\Logging\Logger::class) ? $this->container->get(\App\Framework\Logging\Logger::class) : null;
$errorHandler = new \App\Framework\Console\ErrorRecovery\ConsoleErrorHandler($recoveryService, $logger);
$helpRenderer = new ConsoleHelpRenderer(
$this->output,
$this->commandRegistry,
$this->container,
$errorHandler,
$this->scriptName
);
$helpRenderer->showHelp($commandList);
}
}

View File

@@ -0,0 +1,154 @@
<?php
declare(strict_types=1);
namespace App\Framework\Console\Components;
use App\Framework\Config\AppConfig;
use App\Framework\Console\Animation\AnimationManager;
use App\Framework\Console\CommandGroupRegistry;
use App\Framework\Console\CommandHelpGenerator;
use App\Framework\Console\CommandHistory;
use App\Framework\Console\CommandRegistry;
use App\Framework\Console\CommandValidator;
use App\Framework\Console\ConsoleColor;
use App\Framework\Console\ConsoleOutputInterface;
use App\Framework\Console\ExitCode;
use App\Framework\Console\Help\ConsoleHelpRenderer;
use App\Framework\Console\ParameterInspector;
use App\Framework\Console\SimpleWorkflowExecutor;
use App\Framework\Console\Terminal\TerminalCompatibilityChecker;
use App\Framework\DI\Container;
use App\Framework\Discovery\Results\DiscoveryRegistry;
use Throwable;
/**
* Factory für TUI (Text User Interface) Initialisierung.
*
* Kapselt die Dependency-Injection und Fehlerbehandlung für TUI.
*/
final readonly class TuiFactory
{
public function __construct(
private Container $container,
private ConsoleOutputInterface $output,
private CommandRegistry $commandRegistry,
private string $scriptName
) {
}
/**
* Erstellt und startet eine TUI-Instanz.
*
* @return int Exit-Code
*/
public function createAndRun(): int
{
try {
// Prüfe ob Terminal kompatibel ist
$checker = TerminalCompatibilityChecker::create();
if (!$checker->isTuiCompatible()) {
$this->output->writeError("Interactive TUI requires a compatible terminal.");
$this->output->writeLine("Use 'php {$this->scriptName} help' for command-line help.");
error_log("TUI: Terminal compatibility check failed - TERM=" . ($_SERVER['TERM'] ?? 'not set') . ", isatty(STDOUT)=" . (function_exists('posix_isatty') && posix_isatty(STDOUT) ? 'true' : 'false'));
return ExitCode::SOFTWARE_ERROR->value;
}
error_log("TUI: Starting interactive TUI initialization...");
// Get DiscoveryRegistry for TUI components
$discoveryRegistry = $this->container->get(DiscoveryRegistry::class);
// Create CommandHistory
$commandHistory = new CommandHistory();
// Create new services
$groupRegistry = new CommandGroupRegistry($discoveryRegistry);
$workflowExecutor = new SimpleWorkflowExecutor($this->commandRegistry, $groupRegistry, $this->output);
// Create TUI components
$state = new TuiState();
// Create animation manager first, then pass it to renderer
$animationManager = new AnimationManager();
$renderer = new TuiRenderer($this->output, $animationManager);
$commandExecutor = new TuiCommandExecutor(
$this->output,
$this->commandRegistry,
$this->container,
$discoveryRegistry,
$commandHistory,
new CommandValidator(),
new CommandHelpGenerator(new ParameterInspector()),
$this->scriptName
);
// Erstelle TUI Instanz
$tui = new ConsoleTUI(
$this->output,
$this->container,
$discoveryRegistry,
$state,
$renderer,
$commandExecutor,
$commandHistory,
$groupRegistry,
$workflowExecutor
);
// Starte TUI
return $tui->run()->value;
} catch (Throwable $e) {
$this->output->writeError("Failed to launch interactive TUI: " . $e->getMessage());
// Always log the full error for debugging
error_log("TUI Launch Error: " . $e->getMessage());
error_log("TUI Launch Error Class: " . $e::class);
error_log("TUI Launch Error File: " . $e->getFile() . ":" . $e->getLine());
error_log("TUI Launch Error Trace: " . $e->getTraceAsString());
$config = $this->container->get(AppConfig::class);
if ($config->isDevelopment()) {
$this->output->writeLine("Stack trace:", ConsoleColor::RED);
$this->output->writeLine($e->getTraceAsString());
}
// Fallback to help
$this->output->newLine();
$this->output->writeLine("Falling back to command-line help:", ConsoleColor::YELLOW);
$this->showHelpFallback();
return ExitCode::SOFTWARE_ERROR->value;
}
}
/**
* Zeigt Help als Fallback an.
*/
private function showHelpFallback(): void
{
$discoveryRegistry = $this->container->get(DiscoveryRegistry::class);
$commandList = $this->commandRegistry->getCommandList();
// Create minimal error handler for help rendering
$suggestionEngine = new \App\Framework\Console\ErrorRecovery\CommandSuggestionEngine($commandList);
$recoveryService = new \App\Framework\Console\ErrorRecovery\ErrorRecoveryService(
$suggestionEngine,
$commandList,
$this->commandRegistry
);
$logger = $this->container->has(\App\Framework\Logging\Logger::class) ? $this->container->get(\App\Framework\Logging\Logger::class) : null;
$errorHandler = new \App\Framework\Console\ErrorRecovery\ConsoleErrorHandler($recoveryService, $logger);
$helpRenderer = new ConsoleHelpRenderer(
$this->output,
$this->commandRegistry,
$this->container,
$errorHandler,
$this->scriptName
);
$helpRenderer->showHelp($commandList);
}
}

View File

@@ -4,24 +4,24 @@ declare(strict_types=1);
namespace App\Framework\Console; namespace App\Framework\Console;
use App\Framework\Config\AppConfig; use App\Framework\Cache\Cache;
use App\Framework\Console\Components\ConsoleDialog;
use App\Framework\Console\Components\ConsoleTUI;
use App\Framework\Console\Components\DialogCommandExecutor;
use App\Framework\Console\Components\TuiCommandExecutor;
use App\Framework\Console\Components\TuiInputHandler;
use App\Framework\Console\Components\TuiRenderer;
use App\Framework\Console\Components\TuiState;
use App\Framework\Console\ErrorRecovery\CommandSuggestionEngine; use App\Framework\Console\ErrorRecovery\CommandSuggestionEngine;
use App\Framework\Console\ErrorRecovery\ConsoleErrorHandler; use App\Framework\Console\ErrorRecovery\ConsoleErrorHandler;
use App\Framework\Console\ErrorRecovery\ErrorRecoveryService; use App\Framework\Console\ErrorRecovery\ErrorRecoveryService;
use App\Framework\Console\Exceptions\ConsoleException;
use App\Framework\Console\Exceptions\ConsoleInitializationException;
use App\Framework\Console\Exceptions\CommandNotFoundException; use App\Framework\Console\Exceptions\CommandNotFoundException;
use App\Framework\Console\Help\ConsoleHelpRenderer;
use App\Framework\Console\Terminal\TerminalCompatibilityChecker;
use App\Framework\Console\Validation\ConsoleInputValidator;
use App\Framework\Core\PathProvider;
use App\Framework\DateTime\Clock;
use App\Framework\DI\Container; use App\Framework\DI\Container;
use App\Framework\Discovery\DiscoveryServiceBootstrapper;
use App\Framework\Discovery\Results\DiscoveryRegistry; use App\Framework\Discovery\Results\DiscoveryRegistry;
use App\Framework\Exception\ErrorCode;
use App\Framework\Exception\FrameworkException;
use App\Framework\Logging\Logger; use App\Framework\Logging\Logger;
use App\Framework\Logging\ValueObjects\LogContext; use App\Framework\Logging\ValueObjects\LogContext;
use App\Framework\Pcntl\ValueObjects\Signal;
use Throwable; use Throwable;
final class ConsoleApplication final class ConsoleApplication
@@ -34,6 +34,8 @@ final class ConsoleApplication
private ConsoleErrorHandler $errorHandler; private ConsoleErrorHandler $errorHandler;
private ConsoleSignalHandler $signalHandler;
public function __construct( public function __construct(
private readonly Container $container, private readonly Container $container,
private readonly string $scriptName = 'console', private readonly string $scriptName = 'console',
@@ -43,7 +45,13 @@ final class ConsoleApplication
$this->output = $output ?? new ConsoleOutput(); $this->output = $output ?? new ConsoleOutput();
// Setup signal handlers für graceful shutdown // Setup signal handlers für graceful shutdown
$this->setupSignalHandlers(); $this->signalHandler = new ConsoleSignalHandler(
$this->container,
function (Signal $signal) {
$this->handleShutdown($signal);
}
);
$this->signalHandler->setupShutdownHandlers();
// Setze den Fenstertitel // Setze den Fenstertitel
$this->output->writeWindowTitle($this->title); $this->output->writeWindowTitle($this->title);
@@ -56,9 +64,9 @@ final class ConsoleApplication
error_log("Console initialization failed: " . $e->getMessage()); error_log("Console initialization failed: " . $e->getMessage());
error_log("Stack trace: " . $e->getTraceAsString()); error_log("Stack trace: " . $e->getTraceAsString());
throw FrameworkException::create( throw new ConsoleInitializationException(
ErrorCode::SYS_INITIALIZATION_FAILED, 'Initialisierung der Console-Anwendung ist fehlgeschlagen.',
'Failed to initialize console application: ' . $e->getMessage() $e
); );
} }
} }
@@ -79,25 +87,17 @@ final class ConsoleApplication
$this->errorHandler = new ConsoleErrorHandler($recoveryService, $logger); $this->errorHandler = new ConsoleErrorHandler($recoveryService, $logger);
} }
private function setupSignalHandlers(): void public function handleShutdown(Signal $signal): void
{
if (function_exists('pcntl_signal')) {
pcntl_signal(SIGTERM, [$this, 'handleShutdown']);
pcntl_signal(SIGINT, [$this, 'handleShutdown']);
pcntl_signal(SIGHUP, [$this, 'handleShutdown']);
}
}
public function handleShutdown(int $signal): void
{ {
$this->shutdownRequested = true; $this->shutdownRequested = true;
$this->output->writeLine("Shutdown signal received ({$signal}). Cleaning up...", ConsoleColor::YELLOW); $this->output->writeLine("Shutdown signal received ({$signal->getName()}). Cleaning up...", ConsoleColor::YELLOW);
// Cleanup resources // Cleanup resources
$this->cleanup(); $this->cleanup();
exit(ExitCode::SUCCESS->value); exit(ExitCode::SUCCESS->value);
} }
private function cleanup(): void private function cleanup(): void
{ {
// Reset window title // Reset window title
@@ -121,14 +121,15 @@ final class ConsoleApplication
} }
// Force fresh discovery // Force fresh discovery
$bootstrapper = new \App\Framework\Discovery\DiscoveryServiceBootstrapper( $bootstrapper = new DiscoveryServiceBootstrapper(
$this->container, $this->container,
$this->container->get(\App\Framework\DateTime\Clock::class) $this->container->get(Clock::class),
$this->container->get(Logger::class),
); );
$freshRegistry = $bootstrapper->performBootstrap( $freshRegistry = $bootstrapper->performBootstrap(
$this->container->get(\App\Framework\Core\PathProvider::class), $this->container->get(PathProvider::class),
$this->container->get(\App\Framework\Cache\Cache::class), $this->container->get(Cache::class),
null null
); );
@@ -160,9 +161,7 @@ final class ConsoleApplication
$argv = $this->validateAndSanitizeInput($argv); $argv = $this->validateAndSanitizeInput($argv);
// Check for shutdown signal // Check for shutdown signal
if (function_exists('pcntl_signal_dispatch')) { $this->signalHandler->dispatchSignals();
pcntl_signal_dispatch();
}
if ($this->shutdownRequested) { if ($this->shutdownRequested) {
return ExitCode::INTERRUPTED->value; return ExitCode::INTERRUPTED->value;
@@ -188,11 +187,20 @@ final class ConsoleApplication
// Handle built-in commands // Handle built-in commands
if (in_array($commandName, ['help', '--help', '-h'])) { if (in_array($commandName, ['help', '--help', '-h'])) {
$helpRenderer = new ConsoleHelpRenderer(
$this->output,
$this->commandRegistry,
$this->container,
$this->errorHandler,
$this->scriptName
);
$commandList = $this->commandRegistry->getCommandList();
// Spezifische Command-Hilfe // Spezifische Command-Hilfe
if (! empty($arguments) && isset($arguments[0])) { if (!empty($arguments) && isset($arguments[0])) {
$this->showCommandHelp($arguments[0]); $helpRenderer->showCommandHelp($arguments[0], $commandList);
} else { } else {
$this->showHelp(); $helpRenderer->showHelp($commandList);
} }
return ExitCode::SUCCESS->value; return ExitCode::SUCCESS->value;
@@ -206,9 +214,17 @@ final class ConsoleApplication
} }
// Prüfe ob es eine Kategorie ist // Prüfe ob es eine Kategorie ist
$categories = $this->categorizeCommands($commandList); $categorizer = new CommandCategorizer();
$categories = $categorizer->categorize($commandList);
if (isset($categories[$commandName])) { if (isset($categories[$commandName])) {
$this->showCategoryCommands($commandName, $categories[$commandName]); $helpRenderer = new ConsoleHelpRenderer(
$this->output,
$this->commandRegistry,
$this->container,
$this->errorHandler,
$this->scriptName
);
$helpRenderer->showCategoryCommands($commandName, $categories[$commandName]);
return ExitCode::SUCCESS->value; return ExitCode::SUCCESS->value;
} }
@@ -230,31 +246,8 @@ final class ConsoleApplication
*/ */
private function validateAndSanitizeInput(array $argv): array private function validateAndSanitizeInput(array $argv): array
{ {
if (empty($argv)) { $validator = new ConsoleInputValidator();
throw new \InvalidArgumentException('No arguments provided'); return $validator->validateAndSanitize($argv);
}
// Validate argv array structure
if (! is_array($argv) || ! isset($argv[0])) {
throw new \InvalidArgumentException('Invalid argv structure');
}
// Sanitize each argument
return array_map(function ($arg) {
if (! is_string($arg)) {
throw new \InvalidArgumentException('All arguments must be strings');
}
// Remove null bytes and control characters
$sanitized = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', '', $arg);
// Limit argument length to prevent memory issues
if (strlen($sanitized) > 4096) {
throw new \InvalidArgumentException('Argument too long (max 4096 characters)');
}
return $sanitized;
}, $argv);
} }
/** /**
@@ -264,9 +257,7 @@ final class ConsoleApplication
{ {
try { try {
// Check for shutdown signal before execution // Check for shutdown signal before execution
if (function_exists('pcntl_signal_dispatch')) { $this->signalHandler->dispatchSignals();
pcntl_signal_dispatch();
}
if ($this->shutdownRequested) { if ($this->shutdownRequested) {
return ExitCode::INTERRUPTED; return ExitCode::INTERRUPTED;
@@ -279,12 +270,13 @@ final class ConsoleApplication
$result = $this->commandRegistry->executeCommand($commandName, $arguments, $this->output); $result = $this->commandRegistry->executeCommand($commandName, $arguments, $this->output);
// Handle ConsoleResult (new) or ExitCode (legacy) // Handle ConsoleResult (new) or ExitCode (legacy)
return $this->processCommandResult($result); $processor = new CommandResultProcessor($this->container, $this->output);
return $processor->process($result);
} catch (CommandNotFoundException $e) { } catch (CommandNotFoundException $e) {
return $this->errorHandler->handleCommandNotFound($commandName, $this->output); return $this->errorHandler->handleCommandNotFound($commandName, $this->output);
} catch (FrameworkException $e) { } catch (ConsoleException $e) {
return $this->errorHandler->handleCommandExecutionError($commandName, $e, $this->output); return $this->errorHandler->handleCommandExecutionError($commandName, $e, $this->output);
} catch (\InvalidArgumentException $e) { } catch (\InvalidArgumentException $e) {
@@ -301,327 +293,20 @@ final class ConsoleApplication
} }
} }
/**
* Process command result - supports both ConsoleResult and ExitCode
*/
private function processCommandResult(mixed $result): ExitCode
{
// New ConsoleResult pattern
if ($result instanceof \App\Framework\Console\Result\ConsoleResult) {
// Render result to output
$result->render($this->output);
// Return exit code from result
return $result->exitCode;
}
// Legacy ExitCode pattern
if ($result instanceof ExitCode) {
return $result;
}
// Legacy int pattern (for backwards compatibility)
if (is_int($result)) {
return ExitCode::from($result);
}
// Invalid return type - log warning and return error
if ($this->container->has(Logger::class)) {
$logger = $this->container->get(Logger::class);
$logger->warning('Command returned invalid result type', LogContext::withData([
'result_type' => get_debug_type($result),
'component' => 'ConsoleApplication',
]));
}
return ExitCode::GENERAL_ERROR;
}
private function showCommandUsage(string $commandName): void
{
try {
$commandList = $this->commandRegistry->getCommandList();
if (! $commandList->has($commandName)) {
return;
}
$command = $commandList->get($commandName);
$this->output->writeLine("Usage:", ConsoleColor::BRIGHT_YELLOW);
$this->output->writeLine(" php {$this->scriptName} {$commandName} [arguments]");
if (! empty($command->description)) {
$this->output->newLine();
$this->output->writeLine("Description:", ConsoleColor::BRIGHT_YELLOW);
$this->output->writeLine(" " . $command->description);
}
} catch (Throwable $e) {
// Ignore errors in usage display
}
}
private function showHelp(): void
{
$this->output->writeLine("Console Commands", ConsoleColor::BRIGHT_CYAN);
$this->output->newLine();
$commandList = $this->commandRegistry->getCommandList();
if ($commandList->isEmpty()) {
$this->output->writeLine(" Keine Kommandos verfügbar.", ConsoleColor::YELLOW);
return;
}
// Kategorisiere Commands
$categories = $this->categorizeCommands($commandList);
// Zeige Kategorien-Übersicht
$this->showCategoryOverview($categories);
$this->output->newLine();
$this->output->writeLine("Verwendung:", ConsoleColor::BRIGHT_YELLOW);
$this->output->writeLine(" php {$this->scriptName} # Interaktive TUI starten");
$this->output->writeLine(" php {$this->scriptName} --interactive # Interaktive TUI explizit starten");
$this->output->writeLine(" php {$this->scriptName} --dialog # Dialog-Modus starten (AI-Assistent-ähnlich)");
$this->output->writeLine(" php {$this->scriptName} --chat # Dialog-Modus starten (Alias)");
$this->output->writeLine(" php {$this->scriptName} <kategorie> # Commands einer Kategorie anzeigen");
$this->output->writeLine(" php {$this->scriptName} <kommando> [argumente] # Kommando direkt ausführen");
$this->output->writeLine(" php {$this->scriptName} help <kommando> # Hilfe für spezifisches Kommando");
$this->output->newLine();
$this->output->writeLine("Hinweis:", ConsoleColor::CYAN);
$this->output->writeLine(" Ohne Argumente wird automatisch die interaktive TUI gestartet.");
$this->output->writeLine(" Die TUI bietet eine grafische Navigation durch alle verfügbaren Commands.");
$this->output->writeLine(" Der Dialog-Modus bietet eine einfache Text-Eingabe mit Tab-Completion und History.");
}
/**
* Kategorisiert Commands basierend auf ihrem Präfix
*/
private function categorizeCommands(CommandList $commandList): array
{
$categories = [];
foreach ($commandList as $command) {
$parts = explode(':', $command->name);
$category = $parts[0];
if (! isset($categories[$category])) {
$categories[$category] = [];
}
$categories[$category][] = $command;
}
// Sortiere Kategorien
ksort($categories);
return $categories;
}
/**
* Zeigt eine übersichtliche Kategorien-Übersicht
*/
private function showCategoryOverview(array $categories): void
{
$this->output->writeLine("Verfügbare Kategorien:", ConsoleColor::BRIGHT_YELLOW);
$this->output->newLine();
$categoryInfo = [
'db' => 'Database operations (migrations, health checks)',
'errors' => 'Error management and analytics',
'backup' => 'Backup and restore operations',
'secrets' => 'Secret management and encryption',
'cache' => 'Cache management operations',
'demo' => 'Demo and example commands',
'logs' => 'Log management and rotation',
'alerts' => 'Alert system management',
'mcp' => 'Model Context Protocol server',
'make' => 'Code generation commands',
'docs' => 'Documentation generation',
'openapi' => 'OpenAPI specification generation',
'static' => 'Static file generation',
'redis' => 'Redis cache operations',
'routes' => 'Route management',
'discovery' => 'Framework discovery system',
];
foreach ($categories as $category => $commands) {
$count = count($commands);
$description = $categoryInfo[$category] ?? 'Various commands';
$categoryName = str_pad($category, 12);
$commandCount = str_pad("({$count} commands)", 15);
$this->output->writeLine(
" {$categoryName} {$commandCount} {$description}",
ConsoleColor::WHITE
);
}
$this->output->newLine();
$this->output->writeLine("Für Commands einer Kategorie: php {$this->scriptName} <kategorie>", ConsoleColor::CYAN);
}
/**
* Zeigt Commands einer spezifischen Kategorie
*/
private function showCategoryCommands(string $category, array $commands): void
{
$this->output->writeLine("Commands der Kategorie '{$category}':", ConsoleColor::BRIGHT_CYAN);
$this->output->newLine();
foreach ($commands as $command) {
$description = $command->description ?: 'Keine Beschreibung verfügbar';
$commandName = str_pad($command->name, 25);
$this->output->writeLine(" {$commandName} {$description}", ConsoleColor::WHITE);
}
$this->output->newLine();
$this->output->writeLine("Verwendung:", ConsoleColor::BRIGHT_YELLOW);
$this->output->writeLine(" php {$this->scriptName} <kommando> [argumente]");
$this->output->writeLine(" php {$this->scriptName} help <kommando> # Für detaillierte Hilfe");
}
/**
* Zeigt detaillierte Hilfe für ein spezifisches Kommando
*/
private function showCommandHelp(string $commandName): void
{
$commandList = $this->commandRegistry->getCommandList();
if (! $commandList->has($commandName)) {
$this->errorHandler->handleCommandNotFound($commandName, $this->output);
return;
}
$command = $commandList->get($commandName);
$this->output->writeLine("Kommando: {$command->name}", ConsoleColor::BRIGHT_CYAN);
$this->output->newLine();
if ($command->description) {
$this->output->writeLine("Beschreibung:", ConsoleColor::BRIGHT_YELLOW);
$this->output->writeLine(" {$command->description}");
$this->output->newLine();
}
// Versuche Parameter-Informationen anzuzeigen
try {
$this->showCommandParameters($command);
} catch (Throwable $e) {
// Fallback zu Standard-Verwendung
$this->output->writeLine("Verwendung:", ConsoleColor::BRIGHT_YELLOW);
$this->output->writeLine(" php {$this->scriptName} {$command->name} [argumente]");
}
}
/**
* Zeigt Parameter-Informationen für ein Kommando
*/
private function showCommandParameters(ConsoleCommand $command): void
{
try {
// Hole die DiscoveredAttribute für das Command
$discoveredAttribute = $this->commandRegistry->getDiscoveredAttribute($command->name);
// Hole Reflection Information
$reflection = new \ReflectionMethod($discoveredAttribute->className->toString(), $discoveredAttribute->methodName->toString());
// Prüfe ob es moderne Parameter-Parsing verwendet
if ($this->commandRegistry->usesReflectionParameters($reflection)) {
// Nutze den CommandParameterResolver für moderne Parameter-Hilfe
$resolver = $this->container->resolve(CommandParameterResolver::class);
$help = $resolver->generateMethodHelp($reflection, $command->name);
$this->output->writeLine($help);
} else {
// Fallback für Legacy-Commands
$this->output->writeLine("Verwendung:", ConsoleColor::BRIGHT_YELLOW);
$this->output->writeLine(" php {$this->scriptName} {$command->name} [argumente]");
}
} catch (\Throwable $e) {
// Fallback bei Fehlern
$this->output->writeLine("Verwendung:", ConsoleColor::BRIGHT_YELLOW);
$this->output->writeLine(" php {$this->scriptName} {$command->name} [argumente]");
}
}
/** /**
* Startet die interaktive TUI * Startet die interaktive TUI
*/ */
private function launchInteractiveTUI(): int private function launchInteractiveTUI(): int
{ {
try { $factory = new Components\TuiFactory(
// Prüfe ob Terminal kompatibel ist $this->container,
if (! $this->isTerminalCompatible()) { $this->output,
$this->output->writeError("Interactive TUI requires a compatible terminal."); $this->commandRegistry,
$this->output->writeLine("Use 'php {$this->scriptName} help' for command-line help."); $this->scriptName
);
return ExitCode::SOFTWARE_ERROR->value; return $factory->createAndRun();
}
// Get DiscoveryRegistry for TUI components
$discoveryRegistry = $this->container->get(DiscoveryRegistry::class);
// Create CommandHistory
$commandHistory = new CommandHistory();
// Create new services
$groupRegistry = new CommandGroupRegistry($discoveryRegistry);
$workflowExecutor = new SimpleWorkflowExecutor($this->commandRegistry, $groupRegistry, $this->output);
// Create TUI components
$state = new TuiState();
$renderer = new TuiRenderer($this->output);
$menuBar = $renderer->getMenuBar();
$commandExecutor = new TuiCommandExecutor(
$this->output,
$this->commandRegistry,
$this->container,
$discoveryRegistry,
$commandHistory,
new CommandValidator(),
new CommandHelpGenerator(new ParameterInspector()),
$this->scriptName
);
$inputHandler = new TuiInputHandler($commandExecutor, $menuBar);
// Erstelle TUI Instanz
$tui = new ConsoleTUI(
$this->output,
$this->container,
$discoveryRegistry,
$state,
$renderer,
$inputHandler,
$commandExecutor,
$commandHistory,
$groupRegistry,
$workflowExecutor
);
// Starte TUI
return $tui->run()->value;
} catch (Throwable $e) {
$this->output->writeError("Failed to launch interactive TUI: " . $e->getMessage());
$config = $this->container->get(AppConfig::class);
if ($config->isDevelopment()) {
$this->output->writeLine("Stack trace:", ConsoleColor::RED);
$this->output->writeLine($e->getTraceAsString());
}
// Fallback to help
$this->output->newLine();
$this->output->writeLine("Falling back to command-line help:", ConsoleColor::YELLOW);
$this->showHelp();
return ExitCode::SOFTWARE_ERROR->value;
}
} }
/** /**
@@ -629,56 +314,14 @@ final class ConsoleApplication
*/ */
private function launchDialogMode(): int private function launchDialogMode(): int
{ {
try { $factory = new Components\DialogFactory(
// Get DiscoveryRegistry for dialog components $this->container,
$discoveryRegistry = $this->container->get(DiscoveryRegistry::class); $this->output,
$this->commandRegistry,
$this->scriptName
);
// Create CommandHistory return $factory->createAndRun();
$commandHistory = new CommandHistory();
// Create new services
$groupRegistry = new CommandGroupRegistry($discoveryRegistry);
$commandList = $this->commandRegistry->getCommandList();
// Create dialog components
$commandExecutor = new DialogCommandExecutor(
$this->output,
$this->commandRegistry,
$commandHistory,
$this->scriptName
);
// Create dialog instance
$dialog = new ConsoleDialog(
$this->output,
$discoveryRegistry,
$commandHistory,
$groupRegistry,
$commandExecutor,
$commandList,
$this->container,
$this->scriptName . '> '
);
// Start dialog
return $dialog->run()->value;
} catch (Throwable $e) {
$this->output->writeError("Failed to launch dialog mode: " . $e->getMessage());
$config = $this->container->get(AppConfig::class);
if ($config->isDevelopment()) {
$this->output->writeLine("Stack trace:", ConsoleColor::RED);
$this->output->writeLine($e->getTraceAsString());
}
// Fallback to help
$this->output->newLine();
$this->output->writeLine("Falling back to command-line help:", ConsoleColor::YELLOW);
$this->showHelp();
return ExitCode::SOFTWARE_ERROR->value;
}
} }
/** /**
@@ -686,22 +329,7 @@ final class ConsoleApplication
*/ */
private function isTerminalCompatible(): bool private function isTerminalCompatible(): bool
{ {
// Prüfe ob wir in einem Terminal sind $checker = TerminalCompatibilityChecker::create();
if (! function_exists('posix_isatty') || ! posix_isatty(STDOUT)) { return $checker->isTuiCompatible();
return false;
}
// Prüfe TERM environment variable
$term = $_SERVER['TERM'] ?? '';
if (empty($term) || $term === 'dumb') {
return false;
}
// Prüfe ob das Terminal interaktiv ist
if (! stream_isatty(STDIN) || ! stream_isatty(STDOUT)) {
return false;
}
return true;
} }
} }

View File

@@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
namespace App\Framework\Console;
use App\Framework\DI\Container;
use App\Framework\Pcntl\PcntlService;
use App\Framework\Pcntl\ValueObjects\Signal;
/**
* Kapselt Signal-Handler-Setup für ConsoleApplication.
*
* Nutzt ausschließlich PcntlService aus dem Pcntl-Modul.
* Keine direkten PCNTL-Funktionsaufrufe mehr.
*/
final readonly class ConsoleSignalHandler
{
private ?PcntlService $pcntlService = null;
public function __construct(
private Container $container,
private callable $shutdownCallback
) {
}
/**
* Setup shutdown handlers für SIGTERM, SIGINT, SIGHUP.
*
* Nutzt PcntlService wenn verfügbar, sonst wird kein Signal-Handling eingerichtet.
*/
public function setupShutdownHandlers(): void
{
try {
$this->pcntlService = $this->container->get(PcntlService::class);
$this->pcntlService->registerSignal(Signal::SIGTERM, function (Signal $signal) {
($this->shutdownCallback)($signal);
});
$this->pcntlService->registerSignal(Signal::SIGINT, function (Signal $signal) {
($this->shutdownCallback)($signal);
});
$this->pcntlService->registerSignal(Signal::SIGHUP, function (Signal $signal) {
($this->shutdownCallback)($signal);
});
} catch (\Throwable $e) {
// PCNTL not available or not registered, ignore
// Kein Fallback mehr - Signal-Handling ist optional
}
}
/**
* Dispatch pending signals.
*
* Sollte regelmäßig in lang laufenden Prozessen aufgerufen werden.
*/
public function dispatchSignals(): void
{
if ($this->pcntlService !== null) {
$this->pcntlService->dispatchSignals();
}
}
/**
* Prüft ob Signal-Handling verfügbar ist.
*/
public function isAvailable(): bool
{
return $this->pcntlService !== null;
}
}

View File

@@ -0,0 +1,207 @@
<?php
declare(strict_types=1);
namespace App\Framework\Console\Help;
use App\Framework\Console\CommandCategorizer;
use App\Framework\Console\CommandList;
use App\Framework\Console\CommandParameterResolver;
use App\Framework\Console\CommandRegistry;
use App\Framework\Console\ConsoleColor;
use App\Framework\Console\ConsoleCommand;
use App\Framework\Console\ConsoleOutputInterface;
use App\Framework\Console\ErrorRecovery\ConsoleErrorHandler;
use App\Framework\DI\Container;
use Throwable;
/**
* Rendert Help-Informationen für Console-Commands.
*
* Extrahiert alle Help-Generierungs-Logik aus ConsoleApplication.
*/
final readonly class ConsoleHelpRenderer
{
public function __construct(
private ConsoleOutputInterface $output,
private CommandRegistry $commandRegistry,
private Container $container,
private ConsoleErrorHandler $errorHandler,
private string $scriptName
) {
}
/**
* Zeigt allgemeine Help-Informationen.
*/
public function showHelp(CommandList $commandList): void
{
$this->output->writeLine("Console Commands", ConsoleColor::BRIGHT_CYAN);
$this->output->newLine();
if ($commandList->isEmpty()) {
$this->output->writeLine(" Keine Kommandos verfügbar.", ConsoleColor::YELLOW);
return;
}
// Kategorisiere Commands
$categorizer = new CommandCategorizer();
$categories = $categorizer->categorize($commandList);
// Zeige Kategorien-Übersicht
$this->showCategoryOverview($categories);
$this->output->newLine();
$this->output->writeLine("Verwendung:", ConsoleColor::BRIGHT_YELLOW);
$this->output->writeLine(" php {$this->scriptName} # Interaktive TUI starten");
$this->output->writeLine(" php {$this->scriptName} --interactive # Interaktive TUI explizit starten");
$this->output->writeLine(" php {$this->scriptName} --dialog # Dialog-Modus starten (AI-Assistent-ähnlich)");
$this->output->writeLine(" php {$this->scriptName} --chat # Dialog-Modus starten (Alias)");
$this->output->writeLine(" php {$this->scriptName} <kategorie> # Commands einer Kategorie anzeigen");
$this->output->writeLine(" php {$this->scriptName} <kommando> [argumente] # Kommando direkt ausführen");
$this->output->writeLine(" php {$this->scriptName} help <kommando> # Hilfe für spezifisches Kommando");
$this->output->newLine();
$this->output->writeLine("Hinweis:", ConsoleColor::CYAN);
$this->output->writeLine(" Ohne Argumente wird automatisch die interaktive TUI gestartet.");
$this->output->writeLine(" Die TUI bietet eine grafische Navigation durch alle verfügbaren Commands.");
$this->output->writeLine(" Der Dialog-Modus bietet eine einfache Text-Eingabe mit Tab-Completion und History.");
}
/**
* Zeigt detaillierte Hilfe für ein spezifisches Kommando.
*/
public function showCommandHelp(string $commandName, CommandList $commandList): void
{
if (!$commandList->has($commandName)) {
$this->errorHandler->handleCommandNotFound($commandName, $this->output);
return;
}
$command = $commandList->get($commandName);
$this->output->writeLine("Kommando: {$command->name}", ConsoleColor::BRIGHT_CYAN);
$this->output->newLine();
if ($command->description) {
$this->output->writeLine("Beschreibung:", ConsoleColor::BRIGHT_YELLOW);
$this->output->writeLine(" {$command->description}");
$this->output->newLine();
}
// Versuche Parameter-Informationen anzuzeigen
try {
$this->showCommandParameters($command);
} catch (Throwable $e) {
// Fallback zu Standard-Verwendung
$this->output->writeLine("Verwendung:", ConsoleColor::BRIGHT_YELLOW);
$this->output->writeLine(" php {$this->scriptName} {$command->name} [argumente]");
}
}
/**
* Zeigt eine übersichtliche Kategorien-Übersicht.
*/
public function showCategoryOverview(array $categories): void
{
$this->output->writeLine("Verfügbare Kategorien:", ConsoleColor::BRIGHT_YELLOW);
$this->output->newLine();
$categorizer = new CommandCategorizer();
foreach ($categories as $category => $commands) {
$count = count($commands);
$description = $categorizer->getCategoryDescription($category);
$categoryName = str_pad($category, 12);
$commandCount = str_pad("({$count} commands)", 15);
$this->output->writeLine(
" {$categoryName} {$commandCount} {$description}",
ConsoleColor::WHITE
);
}
$this->output->newLine();
$this->output->writeLine("Für Commands einer Kategorie: php {$this->scriptName} <kategorie>", ConsoleColor::CYAN);
}
/**
* Zeigt Commands einer spezifischen Kategorie.
*/
public function showCategoryCommands(string $category, array $commands): void
{
$this->output->writeLine("Commands der Kategorie '{$category}':", ConsoleColor::BRIGHT_CYAN);
$this->output->newLine();
foreach ($commands as $command) {
$description = $command->description ?: 'Keine Beschreibung verfügbar';
$commandName = str_pad($command->name, 25);
$this->output->writeLine(" {$commandName} {$description}", ConsoleColor::WHITE);
}
$this->output->newLine();
$this->output->writeLine("Verwendung:", ConsoleColor::BRIGHT_YELLOW);
$this->output->writeLine(" php {$this->scriptName} <kommando> [argumente]");
$this->output->writeLine(" php {$this->scriptName} help <kommando> # Für detaillierte Hilfe");
}
/**
* Zeigt Verwendung für ein Kommando.
*/
public function showCommandUsage(string $commandName, CommandList $commandList): void
{
try {
if (!$commandList->has($commandName)) {
return;
}
$command = $commandList->get($commandName);
$this->output->writeLine("Usage:", ConsoleColor::BRIGHT_YELLOW);
$this->output->writeLine(" php {$this->scriptName} {$commandName} [arguments]");
if (!empty($command->description)) {
$this->output->newLine();
$this->output->writeLine("Description:", ConsoleColor::BRIGHT_YELLOW);
$this->output->writeLine(" " . $command->description);
}
} catch (Throwable $e) {
// Ignore errors in usage display
}
}
/**
* Zeigt Parameter-Informationen für ein Kommando.
*/
private function showCommandParameters(ConsoleCommand $command): void
{
try {
// Hole die DiscoveredAttribute für das Command
$discoveredAttribute = $this->commandRegistry->getDiscoveredAttribute($command->name);
// Hole Reflection Information
$reflection = new \ReflectionMethod($discoveredAttribute->className->toString(), $discoveredAttribute->methodName->toString());
// Prüfe ob es moderne Parameter-Parsing verwendet
if ($this->commandRegistry->usesReflectionParameters($reflection)) {
// Nutze den CommandParameterResolver für moderne Parameter-Hilfe
$resolver = $this->container->resolve(CommandParameterResolver::class);
$help = $resolver->generateMethodHelp($reflection, $command->name);
$this->output->writeLine($help);
} else {
// Fallback für Legacy-Commands
$this->output->writeLine("Verwendung:", ConsoleColor::BRIGHT_YELLOW);
$this->output->writeLine(" php {$this->scriptName} {$command->name} [argumente]");
}
} catch (\Throwable $e) {
// Fallback bei Fehlern
$this->output->writeLine("Verwendung:", ConsoleColor::BRIGHT_YELLOW);
$this->output->writeLine(" php {$this->scriptName} {$command->name} [argumente]");
}
}
}

View File

@@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace App\Framework\Console\Terminal;
use App\Framework\Console\CliSapi;
use App\Framework\Console\TerminalDetector;
/**
* Prüft ob das Terminal für TUI (Text User Interface) kompatibel ist.
*
* Nutzt TerminalDetector und TerminalCapabilities statt direkter
* posix_isatty/stream_isatty Aufrufe.
*/
final readonly class TerminalCompatibilityChecker
{
public function __construct(
private CliSapi $cliSapi
) {
}
/**
* Prüft ob das Terminal für TUI kompatibel ist.
*
* Ein Terminal ist TUI-kompatibel wenn:
* - STDOUT ein Terminal ist
* - STDIN ein Terminal ist
* - TERM environment variable gesetzt und nicht "dumb" ist
*/
public function isTuiCompatible(): bool
{
// Prüfe ob STDOUT ein Terminal ist
if (!$this->cliSapi->isStdoutTerminal()) {
return false;
}
// Prüfe ob STDIN ein Terminal ist
if (!$this->cliSapi->isTerminal($this->cliSapi->stdin)) {
return false;
}
// Prüfe TERM environment variable
$term = getenv('TERM');
if ($term === false || $term === '' || strtolower($term) === 'dumb') {
return false;
}
return true;
}
/**
* Erstellt eine Instanz mit automatischer SAPI-Detection.
*/
public static function create(): self
{
return new self(CliSapi::detect());
}
}

View File

@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace App\Framework\Console\Validation;
use InvalidArgumentException;
/**
* Validiert und sanitized Console-Input (argv).
*
* Entfernt gefährliche Zeichen und prüft auf gültige Struktur.
*/
final readonly class ConsoleInputValidator
{
/**
* Validiert und sanitized argv Array.
*
* @param array<int, string> $argv Input arguments
* @return array<int, string> Sanitized arguments
* @throws InvalidArgumentException Bei ungültigen Eingaben
*/
public function validateAndSanitize(array $argv): array
{
if (empty($argv)) {
throw new InvalidArgumentException('No arguments provided');
}
// Validate argv array structure
if (!is_array($argv) || !isset($argv[0])) {
throw new InvalidArgumentException('Invalid argv structure');
}
// Sanitize each argument
return array_map(function ($arg) {
if (!is_string($arg)) {
throw new InvalidArgumentException('All arguments must be strings');
}
// Remove null bytes and control characters
$sanitized = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', '', $arg);
// Limit argument length to prevent memory issues
if (strlen($sanitized) > 4096) {
throw new InvalidArgumentException('Argument too long (max 4096 characters)');
}
return $sanitized;
}, $argv);
}
}