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:
86
src/Framework/Console/CommandCategorizer.php
Normal file
86
src/Framework/Console/CommandCategorizer.php
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
68
src/Framework/Console/CommandResultProcessor.php
Normal file
68
src/Framework/Console/CommandResultProcessor.php
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
120
src/Framework/Console/Components/DialogFactory.php
Normal file
120
src/Framework/Console/Components/DialogFactory.php
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
154
src/Framework/Console/Components/TuiFactory.php
Normal file
154
src/Framework/Console/Components/TuiFactory.php
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,24 +4,24 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Console;
|
||||
|
||||
use App\Framework\Config\AppConfig;
|
||||
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\Cache\Cache;
|
||||
use App\Framework\Console\ErrorRecovery\CommandSuggestionEngine;
|
||||
use App\Framework\Console\ErrorRecovery\ConsoleErrorHandler;
|
||||
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\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\Discovery\DiscoveryServiceBootstrapper;
|
||||
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\ValueObjects\LogContext;
|
||||
use App\Framework\Pcntl\ValueObjects\Signal;
|
||||
use Throwable;
|
||||
|
||||
final class ConsoleApplication
|
||||
@@ -34,6 +34,8 @@ final class ConsoleApplication
|
||||
|
||||
private ConsoleErrorHandler $errorHandler;
|
||||
|
||||
private ConsoleSignalHandler $signalHandler;
|
||||
|
||||
public function __construct(
|
||||
private readonly Container $container,
|
||||
private readonly string $scriptName = 'console',
|
||||
@@ -43,7 +45,13 @@ final class ConsoleApplication
|
||||
$this->output = $output ?? new ConsoleOutput();
|
||||
|
||||
// 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
|
||||
$this->output->writeWindowTitle($this->title);
|
||||
@@ -56,9 +64,9 @@ final class ConsoleApplication
|
||||
error_log("Console initialization failed: " . $e->getMessage());
|
||||
error_log("Stack trace: " . $e->getTraceAsString());
|
||||
|
||||
throw FrameworkException::create(
|
||||
ErrorCode::SYS_INITIALIZATION_FAILED,
|
||||
'Failed to initialize console application: ' . $e->getMessage()
|
||||
throw new ConsoleInitializationException(
|
||||
'Initialisierung der Console-Anwendung ist fehlgeschlagen.',
|
||||
$e
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -79,25 +87,17 @@ final class ConsoleApplication
|
||||
$this->errorHandler = new ConsoleErrorHandler($recoveryService, $logger);
|
||||
}
|
||||
|
||||
private function setupSignalHandlers(): 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
|
||||
public function handleShutdown(Signal $signal): void
|
||||
{
|
||||
$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
|
||||
$this->cleanup();
|
||||
exit(ExitCode::SUCCESS->value);
|
||||
}
|
||||
|
||||
|
||||
private function cleanup(): void
|
||||
{
|
||||
// Reset window title
|
||||
@@ -121,14 +121,15 @@ final class ConsoleApplication
|
||||
}
|
||||
|
||||
// Force fresh discovery
|
||||
$bootstrapper = new \App\Framework\Discovery\DiscoveryServiceBootstrapper(
|
||||
$bootstrapper = new DiscoveryServiceBootstrapper(
|
||||
$this->container,
|
||||
$this->container->get(\App\Framework\DateTime\Clock::class)
|
||||
$this->container->get(Clock::class),
|
||||
$this->container->get(Logger::class),
|
||||
);
|
||||
|
||||
$freshRegistry = $bootstrapper->performBootstrap(
|
||||
$this->container->get(\App\Framework\Core\PathProvider::class),
|
||||
$this->container->get(\App\Framework\Cache\Cache::class),
|
||||
$this->container->get(PathProvider::class),
|
||||
$this->container->get(Cache::class),
|
||||
null
|
||||
);
|
||||
|
||||
@@ -160,9 +161,7 @@ final class ConsoleApplication
|
||||
$argv = $this->validateAndSanitizeInput($argv);
|
||||
|
||||
// Check for shutdown signal
|
||||
if (function_exists('pcntl_signal_dispatch')) {
|
||||
pcntl_signal_dispatch();
|
||||
}
|
||||
$this->signalHandler->dispatchSignals();
|
||||
|
||||
if ($this->shutdownRequested) {
|
||||
return ExitCode::INTERRUPTED->value;
|
||||
@@ -188,11 +187,20 @@ final class ConsoleApplication
|
||||
|
||||
// Handle built-in commands
|
||||
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
|
||||
if (! empty($arguments) && isset($arguments[0])) {
|
||||
$this->showCommandHelp($arguments[0]);
|
||||
if (!empty($arguments) && isset($arguments[0])) {
|
||||
$helpRenderer->showCommandHelp($arguments[0], $commandList);
|
||||
} else {
|
||||
$this->showHelp();
|
||||
$helpRenderer->showHelp($commandList);
|
||||
}
|
||||
|
||||
return ExitCode::SUCCESS->value;
|
||||
@@ -206,9 +214,17 @@ final class ConsoleApplication
|
||||
}
|
||||
|
||||
// Prüfe ob es eine Kategorie ist
|
||||
$categories = $this->categorizeCommands($commandList);
|
||||
$categorizer = new CommandCategorizer();
|
||||
$categories = $categorizer->categorize($commandList);
|
||||
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;
|
||||
}
|
||||
@@ -230,31 +246,8 @@ final class ConsoleApplication
|
||||
*/
|
||||
private function validateAndSanitizeInput(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);
|
||||
$validator = new ConsoleInputValidator();
|
||||
return $validator->validateAndSanitize($argv);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -264,9 +257,7 @@ final class ConsoleApplication
|
||||
{
|
||||
try {
|
||||
// Check for shutdown signal before execution
|
||||
if (function_exists('pcntl_signal_dispatch')) {
|
||||
pcntl_signal_dispatch();
|
||||
}
|
||||
$this->signalHandler->dispatchSignals();
|
||||
|
||||
if ($this->shutdownRequested) {
|
||||
return ExitCode::INTERRUPTED;
|
||||
@@ -279,12 +270,13 @@ final class ConsoleApplication
|
||||
$result = $this->commandRegistry->executeCommand($commandName, $arguments, $this->output);
|
||||
|
||||
// Handle ConsoleResult (new) or ExitCode (legacy)
|
||||
return $this->processCommandResult($result);
|
||||
$processor = new CommandResultProcessor($this->container, $this->output);
|
||||
return $processor->process($result);
|
||||
|
||||
} catch (CommandNotFoundException $e) {
|
||||
return $this->errorHandler->handleCommandNotFound($commandName, $this->output);
|
||||
|
||||
} catch (FrameworkException $e) {
|
||||
} catch (ConsoleException $e) {
|
||||
return $this->errorHandler->handleCommandExecutionError($commandName, $e, $this->output);
|
||||
|
||||
} 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
|
||||
*/
|
||||
private function launchInteractiveTUI(): int
|
||||
{
|
||||
try {
|
||||
// Prüfe ob Terminal kompatibel ist
|
||||
if (! $this->isTerminalCompatible()) {
|
||||
$this->output->writeError("Interactive TUI requires a compatible terminal.");
|
||||
$this->output->writeLine("Use 'php {$this->scriptName} help' for command-line help.");
|
||||
|
||||
return ExitCode::SOFTWARE_ERROR->value;
|
||||
}
|
||||
|
||||
// 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(
|
||||
$factory = new Components\TuiFactory(
|
||||
$this->container,
|
||||
$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;
|
||||
}
|
||||
return $factory->createAndRun();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -629,56 +314,14 @@ final class ConsoleApplication
|
||||
*/
|
||||
private function launchDialogMode(): 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(
|
||||
$factory = new Components\DialogFactory(
|
||||
$this->container,
|
||||
$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;
|
||||
}
|
||||
return $factory->createAndRun();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -686,22 +329,7 @@ final class ConsoleApplication
|
||||
*/
|
||||
private function isTerminalCompatible(): bool
|
||||
{
|
||||
// Prüfe ob wir in einem Terminal sind
|
||||
if (! function_exists('posix_isatty') || ! posix_isatty(STDOUT)) {
|
||||
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;
|
||||
$checker = TerminalCompatibilityChecker::create();
|
||||
return $checker->isTuiCompatible();
|
||||
}
|
||||
}
|
||||
|
||||
74
src/Framework/Console/ConsoleSignalHandler.php
Normal file
74
src/Framework/Console/ConsoleSignalHandler.php
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
207
src/Framework/Console/Help/ConsoleHelpRenderer.php
Normal file
207
src/Framework/Console/Help/ConsoleHelpRenderer.php
Normal 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]");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
52
src/Framework/Console/Validation/ConsoleInputValidator.php
Normal file
52
src/Framework/Console/Validation/ConsoleInputValidator.php
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user