chore: complete update

This commit is contained in:
2025-07-17 16:24:20 +02:00
parent 899227b0a4
commit 64a7051137
1300 changed files with 85570 additions and 2756 deletions

View File

@@ -0,0 +1,257 @@
<?php
declare(strict_types=1);
namespace App\Framework\Console\Components;
use App\Framework\Console\ConsoleColor;
use App\Framework\Console\ConsoleOutput;
/**
* Klasse für interaktive Menüs in der Konsole.
* Unterstützt sowohl einfache nummerierte Menüs als auch
* Menüs mit Pfeiltasten-Navigation.
*/
final class InteractiveMenu
{
private ConsoleOutput $output;
private array $menuItems = [];
private string $title = '';
private int $selectedIndex = 0;
private bool $showNumbers = true;
public function __construct(ConsoleOutput $output)
{
$this->output = $output;
}
/**
* Setzt den Titel des Menüs.
*/
public function setTitle(string $title): self
{
$this->title = $title;
return $this;
}
/**
* Fügt einen Menüpunkt hinzu.
*
* @param string $label Die Beschriftung des Menüpunkts
* @param callable|null $action Eine optionale Aktion, die bei Auswahl ausgeführt wird
* @param mixed $value Ein optionaler Wert, der zurückgegeben wird
*/
public function addItem(string $label, ?callable $action = null, mixed $value = null): self
{
$this->menuItems[] = [
'label' => $label,
'action' => $action,
'value' => $value ?? $label
];
return $this;
}
/**
* Fügt einen Separator zum Menü hinzu.
*/
public function addSeparator(): self
{
$this->menuItems[] = [
'label' => '---',
'action' => null,
'value' => null,
'separator' => true
];
return $this;
}
/**
* Steuert, ob Nummern angezeigt werden sollen.
*/
public function showNumbers(bool $show = true): self
{
$this->showNumbers = $show;
return $this;
}
/**
* Zeigt ein einfaches Menü mit Nummern-Auswahl.
*/
public function showSimple(): mixed
{
// Verwende den ScreenManager anstelle des direkten clearScreen()
$this->output->screen()->newMenu();
if ($this->title) {
$this->output->writeLine($this->title, ConsoleColor::BRIGHT_CYAN);
$this->output->writeLine(str_repeat('=', strlen($this->title)));
$this->output->writeLine('');
}
foreach ($this->menuItems as $index => $item) {
if (isset($item['separator'])) {
$this->output->writeLine($item['label'], ConsoleColor::GRAY);
continue;
}
$number = $this->showNumbers ? ($index + 1) . '. ' : '';
$this->output->writeLine($number . $item['label']);
}
$this->output->writeLine('');
$this->output->write('Ihre Wahl: ', ConsoleColor::BRIGHT_CYAN);
$input = trim(fgets(STDIN));
if (is_numeric($input)) {
$selectedIndex = (int)$input - 1;
if (isset($this->menuItems[$selectedIndex]) && !isset($this->menuItems[$selectedIndex]['separator'])) {
$item = $this->menuItems[$selectedIndex];
if ($item['action']) {
return $item['action']();
}
return $item['value'];
}
}
$this->output->writeError('Ungültige Auswahl!');
return $this->showSimple();
}
/**
* Zeigt ein interaktives Menü mit Pfeiltasten-Navigation.
*/
public function showInteractive(): mixed
{
$this->output->screen()->setInteractiveMode();
// Terminal in Raw-Modus setzen für Tastatur-Input
$this->setRawMode(true);
try {
while (true) {
$this->renderMenu();
$key = $this->readKey();
switch ($key) {
case "\033[A": // Pfeil hoch
$this->moveUp();
break;
case "\033[B": // Pfeil runter
$this->moveDown();
break;
case "\n": // Enter
case "\r":
return $this->selectCurrentItem();
case "\033": // ESC
return null;
case 'q':
case 'Q':
return null;
}
}
} finally {
$this->setRawMode(false);
}
}
/**
* Rendert das Menü auf dem Bildschirm.
*/
private function renderMenu(): void
{
// Verwende den ScreenManager anstelle des direkten clearScreen()
$this->output->screen()->newMenu();
if ($this->title) {
$this->output->writeLine($this->title, ConsoleColor::BRIGHT_CYAN);
$this->output->writeLine(str_repeat('=', strlen($this->title)));
$this->output->writeLine('');
}
foreach ($this->menuItems as $index => $item) {
if (isset($item['separator'])) {
$this->output->writeLine($item['label'], ConsoleColor::GRAY);
continue;
}
$isSelected = ($index === $this->selectedIndex);
$prefix = $isSelected ? '► ' : ' ';
$color = $isSelected ? ConsoleColor::BRIGHT_YELLOW : null;
$number = $this->showNumbers ? ($index + 1) . '. ' : '';
$this->output->writeLine($prefix . $number . $item['label'], $color);
}
$this->output->writeLine('');
$this->output->writeLine('Verwenden Sie ↑/↓ zum Navigieren, Enter zum Auswählen, ESC/Q zum Beenden', ConsoleColor::GRAY);
}
/**
* Bewegt die Auswahl nach oben.
*/
private function moveUp(): void
{
do {
$this->selectedIndex = ($this->selectedIndex - 1 + count($this->menuItems)) % count($this->menuItems);
} while (isset($this->menuItems[$this->selectedIndex]['separator']) && count($this->menuItems) > 1);
}
/**
* Bewegt die Auswahl nach unten.
*/
private function moveDown(): void
{
do {
$this->selectedIndex = ($this->selectedIndex + 1) % count($this->menuItems);
} while (isset($this->menuItems[$this->selectedIndex]['separator']) && count($this->menuItems) > 1);
}
/**
* Wählt den aktuellen Menüpunkt aus.
*/
private function selectCurrentItem(): mixed
{
$item = $this->menuItems[$this->selectedIndex];
if ($item['action']) {
return $item['action']();
}
return $item['value'];
}
/**
* Liest einen Tastendruck.
*/
private function readKey(): string
{
$key = fgetc(STDIN);
// Behandle Escape-Sequenzen
if ($key === "\033") {
$key .= fgetc(STDIN);
if ($key === "\033[") {
$key .= fgetc(STDIN);
}
}
return $key;
}
/**
* Setzt den Terminal-Modus.
*/
private function setRawMode(bool $enable): void
{
if ($enable) {
system('stty -echo -icanon min 1 time 0');
} else {
system('stty echo icanon');
}
}
// Die clearScreen-Methode wurde entfernt, da sie durch den ScreenManager ersetzt wurde.
// Verwende stattdessen $this->output->screen()->newScreen() oder $this->output->display()->clear().
}

View File

@@ -0,0 +1,205 @@
<?php
declare(strict_types=1);
namespace App\Framework\Console\Components;
use App\Framework\Console\ConsoleStyle;
use App\Framework\Console\ConsoleColor;
use App\Framework\Console\ConsoleFormat;
/**
* Rendert eine Tabelle in der Konsole.
*/
final class Table
{
private array $headers = [];
private array $rows = [];
private array $columnWidths = [];
private int $padding = 1;
public function __construct(
private ?ConsoleStyle $headerStyle = null,
private ?ConsoleStyle $rowStyle = null,
private ?ConsoleStyle $borderStyle = null,
private bool $showBorders = true
) {
$this->headerStyle ??= ConsoleStyle::create(color: ConsoleColor::BRIGHT_WHITE, format: ConsoleFormat::BOLD);
$this->rowStyle ??= ConsoleStyle::create();
$this->borderStyle ??= ConsoleStyle::create(color: ConsoleColor::GRAY);
}
/**
* Setzt die Spaltenüberschriften der Tabelle.
*/
public function setHeaders(array $headers): self
{
$this->headers = $headers;
$this->calculateColumnWidths();
return $this;
}
/**
* Fügt eine Zeile zur Tabelle hinzu.
*/
public function addRow(array $row): self
{
$this->rows[] = $row;
$this->calculateColumnWidths();
return $this;
}
/**
* Setzt alle Zeilen der Tabelle.
*/
public function setRows(array $rows): self
{
$this->rows = $rows;
$this->calculateColumnWidths();
return $this;
}
/**
* Setzt den Innenabstand der Zellen.
*/
public function setPadding(int $padding): self
{
$this->padding = max(0, $padding);
return $this;
}
/**
* Rendert die Tabelle und gibt den Text zurück.
*/
public function render(): string
{
if (empty($this->headers) && empty($this->rows)) {
return '';
}
$output = '';
if ($this->showBorders) {
$output .= $this->renderBorder('top') . "\n";
}
// Header
if (!empty($this->headers)) {
$output .= $this->renderRow($this->headers, $this->headerStyle) . "\n";
if ($this->showBorders) {
$output .= $this->renderBorder('middle') . "\n";
}
}
// Zeilen
foreach ($this->rows as $index => $row) {
$output .= $this->renderRow($row, $this->rowStyle) . "\n";
}
if ($this->showBorders) {
$output .= $this->renderBorder('bottom') . "\n";
}
return $output;
}
/**
* Rendert eine Zeile der Tabelle.
*/
private function renderRow(array $cells, ConsoleStyle $style): string
{
$output = '';
if ($this->showBorders) {
$output .= $this->borderStyle->apply('│');
}
foreach ($cells as $index => $cell) {
$width = $this->columnWidths[$index] ?? 10;
$cellText = (string)$cell;
// Linksbündige Ausrichtung mit exakter Breite
$paddedCell = str_pad($cellText, $width, ' ', STR_PAD_RIGHT);
// Padding hinzufügen
$padding = str_repeat(' ', $this->padding);
$finalCell = $padding . $paddedCell . $padding;
$output .= $style->apply($finalCell);
if ($this->showBorders) {
$output .= $this->borderStyle->apply('│');
}
}
return $output;
}
/**
* Rendert eine Trennlinie der Tabelle.
*/
private function renderBorder(string $type): string
{
$left = match($type) {
'top' => '┌',
'middle' => '├',
'row' => '├',
'bottom' => '└',
default => '│'
};
$right = match($type) {
'top' => '┐',
'middle' => '┤',
'row' => '┤',
'bottom' => '┘',
default => '│'
};
$horizontal = '─';
$junction = match($type) {
'top' => '┬',
'middle' => '┼',
'row' => '┼',
'bottom' => '┴',
default => '│'
};
$border = $left;
foreach ($this->columnWidths as $index => $width) {
$cellWidth = $width + ($this->padding * 2);
$border .= str_repeat($horizontal, $cellWidth);
if ($index < count($this->columnWidths) - 1) {
$border .= $junction;
}
}
$border .= $right;
return $this->borderStyle->apply($border);
}
/**
* Berechnet die Breite jeder Spalte basierend auf dem Inhalt.
*/
private function calculateColumnWidths(): void
{
$this->columnWidths = [];
// Header-Breiten
foreach ($this->headers as $index => $header) {
$this->columnWidths[$index] = mb_strlen((string)$header);
}
// Zeilen-Breiten
foreach ($this->rows as $row) {
foreach ($row as $index => $cell) {
$length = mb_strlen((string)$cell);
$this->columnWidths[$index] = max($this->columnWidths[$index] ?? 0, $length);
}
}
}
}

View File

@@ -0,0 +1,124 @@
<?php
declare(strict_types=1);
namespace App\Framework\Console\Components;
use App\Framework\Console\ConsoleStyle;
use App\Framework\Console\ConsoleColor;
use App\Framework\Console\ConsoleFormat;
/**
* Rendert eine Textbox in der Konsole.
*/
final readonly class TextBox
{
public function __construct(
private string $content,
private int $width = 80,
private int $padding = 1,
private ?ConsoleStyle $borderStyle = null,
private ?ConsoleStyle $contentStyle = null,
private ?string $title = null
) {}
public function render(): string
{
$borderStyle = $this->borderStyle ?? ConsoleStyle::create(color: ConsoleColor::GRAY);
$contentStyle = $this->contentStyle ?? ConsoleStyle::create();
$contentWidth = $this->width - ($this->padding * 2) - 2;
$lines = $this->wrapText($this->content, $contentWidth);
$output = '';
// Obere Linie
if ($this->title) {
$titleLength = mb_strlen($this->title);
$availableSpace = $this->width - $titleLength - 6; // Abzug für '[ ]' und Ränder
$leftPadding = max(0, (int)floor($availableSpace / 2));
$rightPadding = max(0, $availableSpace - $leftPadding);
$output .= $borderStyle->apply('┌' . str_repeat('─', $leftPadding) . '[ ' . $this->title . ' ]' . str_repeat('─', $rightPadding) . '┐') . "\n";
} else {
$output .= $borderStyle->apply('┌' . str_repeat('─', $this->width - 2) . '┐') . "\n";
}
// Padding oben
for ($i = 0; $i < $this->padding; $i++) {
$output .= $borderStyle->apply('│') . str_repeat(' ', $this->width - 2) . $borderStyle->apply('│') . "\n";
}
// Inhalt
foreach ($lines as $line) {
$lineLength = mb_strlen($line);
$spaces = $contentWidth - $lineLength;
$paddedLine = $line . str_repeat(' ', $spaces);
$output .= $borderStyle->apply('│') .
str_repeat(' ', $this->padding) .
$contentStyle->apply($paddedLine) .
str_repeat(' ', $this->padding) .
$borderStyle->apply('│') . "\n";
}
// Padding unten
for ($i = 0; $i < $this->padding; $i++) {
$output .= $borderStyle->apply('│') . str_repeat(' ', $this->width - 2) . $borderStyle->apply('│') . "\n";
}
// Untere Linie
$output .= $borderStyle->apply('└' . str_repeat('─', $this->width - 2) . '┘') . "\n";
return $output;
}
private function wrapText(string $text, int $width): array
{
$lines = explode("\n", $text);
$wrapped = [];
foreach ($lines as $line) {
if (mb_strlen($line) <= $width) {
$wrapped[] = $line;
} else {
$wrapped = array_merge($wrapped, $this->splitTextIntoLines($line, $width));
}
}
// Leere Zeile hinzufügen, falls keine Inhalte vorhanden
if (empty($wrapped)) {
$wrapped[] = '';
}
return $wrapped;
}
private function splitTextIntoLines(string $text, int $width): array
{
$lines = [];
$words = explode(' ', $text);
$currentLine = '';
foreach ($words as $word) {
$testLine = empty($currentLine) ? $word : $currentLine . ' ' . $word;
if (mb_strlen($testLine) <= $width) {
$currentLine = $testLine;
} else {
if (!empty($currentLine)) {
$lines[] = $currentLine;
$currentLine = $word;
} else {
// Wort ist länger als die Zeile - hart umbrechen
$lines[] = mb_substr($word, 0, $width);
$currentLine = mb_substr($word, $width);
}
}
}
if (!empty($currentLine)) {
$lines[] = $currentLine;
}
return $lines ?: [''];
}
}

View File

@@ -0,0 +1,215 @@
<?php
declare(strict_types=1);
namespace App\Framework\Console\Components;
use App\Framework\Console\ConsoleStyle;
use App\Framework\Console\ConsoleColor;
use App\Framework\Console\ConsoleFormat;
use App\Framework\Console\ConsoleOutput;
/**
* TreeHelper zum Anzeigen hierarchischer Baumstrukturen in der Konsole.
* Ähnlich dem Symfony TreeHelper, aber angepasst an unser Styling-System.
*/
final class TreeHelper
{
private string $prefix = '';
private bool $isLastElement = true;
private ?ConsoleStyle $nodeStyle = null;
private ?ConsoleStyle $leafStyle = null;
private ?ConsoleStyle $lineStyle = null;
/**
* @var array<array{title: string, node: ?self, isLeaf: bool}>
*/
private array $nodes = [];
public function __construct(
private string $title = '',
private ConsoleOutput $output = new ConsoleOutput(),
) {
$this->nodeStyle = ConsoleStyle::create(color: ConsoleColor::BRIGHT_YELLOW, format: ConsoleFormat::BOLD);
$this->leafStyle = ConsoleStyle::create(color: ConsoleColor::WHITE);
$this->lineStyle = ConsoleStyle::create(color: ConsoleColor::GRAY);
}
/**
* Setzt den Stil für Knotentitel (Verzeichnisse/Kategorien).
*/
public function setNodeStyle(?ConsoleStyle $style): self
{
$this->nodeStyle = $style;
return $this;
}
/**
* Setzt den Stil für Blätter/Endpunkte.
*/
public function setLeafStyle(?ConsoleStyle $style): self
{
$this->leafStyle = $style;
return $this;
}
/**
* Setzt den Stil für Baumlinien.
*/
public function setLineStyle(?ConsoleStyle $style): self
{
$this->lineStyle = $style;
return $this;
}
/**
* Setzt den Haupttitel des Baums.
*/
public function setTitle(string $title): self
{
$this->title = $title;
return $this;
}
/**
* Fügt einen Unterknoten (z.B. Unterverzeichnis) hinzu.
*/
public function addNode(string $title): self
{
$node = new self($title);
$node->nodeStyle = $this->nodeStyle;
$node->leafStyle = $this->leafStyle;
$node->lineStyle = $this->lineStyle;
$this->nodes[] = [
'title' => $title,
'node' => $node,
'isLeaf' => false
];
return $node;
}
/**
* Fügt einen Endpunkt (z.B. Datei) hinzu.
*/
public function addLeaf(string $title): self
{
$this->nodes[] = [
'title' => $title,
'node' => null,
'isLeaf' => true
];
return $this;
}
/**
* Zeigt die vollständige Baumstruktur an.
*/
public function display(): void
{
if (!empty($this->title)) {
$this->output->writeLine($this->title, $this->nodeStyle);
}
$this->displayTree();
}
/**
* Rendert die Baumstruktur und gibt den Text zurück.
*/
public function render(): string
{
$output = '';
if (!empty($this->title)) {
$output .= $this->nodeStyle->apply($this->title) . "\n";
}
$output .= $this->renderTree();
return $output;
}
/**
* Setzt den Präfix für die aktuelle Ebene.
* (Interne Methode für rekursives Rendern)
*/
private function setPrefix(string $prefix, bool $isLastElement): self
{
$this->prefix = $prefix;
$this->isLastElement = $isLastElement;
return $this;
}
/**
* Zeigt die Baumstruktur mit dem aktuellen Präfix an.
* (Interne Methode für rekursives Rendern)
*/
private function displayTree(): void
{
$count = count($this->nodes);
foreach ($this->nodes as $index => $item) {
$isLast = ($index === $count - 1);
$nodePrefix = $this->prefix . ($this->isLastElement ? ' ' : '│ ');
// Baumlinien und Verbindungen
$connector = $isLast ? '└── ' : '├── ';
$linePrefix = $this->prefix . $connector;
// Titel anzeigen
$style = $item['isLeaf'] ? $this->leafStyle : $this->nodeStyle;
$title = $linePrefix . $item['title'];
$this->output->writeLine(
$this->lineStyle->apply($linePrefix) .
$style->apply($item['title'])
);
// Unterelemente rekursiv anzeigen
if (!$item['isLeaf'] && $item['node'] !== null) {
$item['node']
->setPrefix($nodePrefix, $isLast)
->displayTree();
}
}
}
/**
* Rendert die Baumstruktur mit dem aktuellen Präfix und gibt den Text zurück.
* (Interne Methode für rekursives Rendern)
*/
private function renderTree(): string
{
$output = '';
$count = count($this->nodes);
foreach ($this->nodes as $index => $item) {
$isLast = ($index === $count - 1);
$nodePrefix = $this->prefix . ($this->isLastElement ? ' ' : '│ ');
// Baumlinien und Verbindungen
$connector = $isLast ? '└── ' : '├── ';
$linePrefix = $this->prefix . $connector;
// Titel formatieren
$style = $item['isLeaf'] ? $this->leafStyle : $this->nodeStyle;
$title = $item['title'];
$output .= $this->lineStyle->apply($linePrefix) .
$style->apply($title) . "\n";
// Unterelemente rekursiv rendern
if (!$item['isLeaf'] && $item['node'] !== null) {
$childOutput = $item['node']
->setPrefix($nodePrefix, $isLast)
->renderTree();
$output .= $childOutput;
}
}
return $output;
}
}

View File

@@ -0,0 +1,168 @@
<?php
declare(strict_types=1);
namespace App\Framework\Console;
use App\Framework\Console\Components\InteractiveMenu;
use App\Framework\Console\Exceptions\CommandNotFoundException;
use App\Framework\DI\Container;
use App\Framework\Discovery\Results\DiscoveryResults;
use ReflectionClass;
use ReflectionMethod;
use Throwable;
final class ConsoleApplication
{
private array $commands = [];
private ConsoleOutputInterface $output;
public function __construct(
private readonly Container $container,
private readonly string $scriptName = 'console',
private readonly string $title = 'Console Application',
?ConsoleOutputInterface $output = null,
) {
$this->output = $output ?? new ConsoleOutput();
// Setze den Fenstertitel
$this->output->writeWindowTitle($this->title);
$results = $this->container->get(DiscoveryResults::class);
foreach($results->get(ConsoleCommand::class) as $command) {
$this->commands[$command['attribute_data']['name']] = [
'instance' => $this->container->get($command['class']),
'method' => $command['method'],
'description' => $command['attribute_data']['description'] ?? ['Keine Beschreibung verfügbar'],
'reflection' => new ReflectionMethod($command['class'], $command['method'])
];
}
}
/**
* Registriert alle Kommandos aus einer Klasse
*/
public function registerCommands(object $commandClass): void
{
$reflection = new ReflectionClass($commandClass);
foreach ($reflection->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
$attributes = $method->getAttributes(ConsoleCommand::class);
foreach ($attributes as $attribute) {
/** @var ConsoleCommand $command */
$command = $attribute->newInstance();
$this->commands[$command->name] = [
'instance' => $commandClass,
'method' => $method->getName(),
'description' => $command->description,
'reflection' => $method
];
}
}
}
/**
* Führt ein Kommando aus
*/
public function run(array $argv): int
{
if (count($argv) < 2) {
$this->showHelp();
return ExitCode::SUCCESS->value;
}
$commandName = $argv[1];
$arguments = array_slice($argv, 2);
if (in_array($commandName, ['help', '--help', '-h'])) {
$this->showHelp();
return ExitCode::SUCCESS->value;
}
if (!isset($this->commands[$commandName])) {
$this->output->writeError("Kommando '{$commandName}' nicht gefunden.");
$this->showHelp();
return ExitCode::COMMAND_NOT_FOUND->value;
}
return $this->executeCommand($commandName, $arguments)->value;
}
private function executeCommand(string $commandName, array $arguments): ExitCode
{
$command = $this->commands[$commandName];
$instance = $command['instance'];
$method = $command['method'];
try {
// Setze den Fenstertitel für das aktuelle Kommando
$this->output->writeWindowTitle("{$this->scriptName} - {$commandName}");
// Erstelle ConsoleInput
$input = new ConsoleInput($arguments, $this->output);
// Führe das Kommando aus
$result = $instance->$method($input, $this->output);
// Behandle verschiedene Rückgabetypen
if ($result instanceof ExitCode) {
return $result;
}
if (is_int($result)) {
return ExitCode::from($result);
}
// Standardmäßig Erfolg, wenn nichts anderes zurückgegeben wird
return ExitCode::SUCCESS;
} catch (CommandNotFoundException $e) {
$this->output->writeError("Kommando nicht gefunden: " . $e->getMessage());
return ExitCode::COMMAND_NOT_FOUND;
} catch (Throwable $e) {
$this->output->writeError("Fehler beim Ausführen des Kommandos: " . $e->getMessage());
// Erweiterte Fehlerbehandlung basierend auf Exception-Typ
if ($e instanceof \InvalidArgumentException) {
return ExitCode::INVALID_INPUT;
}
if ($e instanceof \RuntimeException) {
return ExitCode::SOFTWARE_ERROR;
}
return ExitCode::GENERAL_ERROR;
}
}
private function showHelp(): void
{
$this->output->writeLine("Verfügbare Kommandos:", ConsoleColor::BRIGHT_CYAN);
$this->output->newLine();
$menu = new InteractiveMenu($this->output);
$menu->setTitle("Kommandos");
foreach ($this->commands as $name => $command) {
$description = $command['description'] ?: 'Keine Beschreibung verfügbar';
$menu->addItem($name, function () use ($name) {
return $this->executeCommand($name, [])->value;
}, $description);
#$this->output->writeLine(sprintf(" %-20s %s", $name, $description));
}
$this->output->writeLine(" " . $menu->showInteractive());
$this->output->newLine();
$this->output->writeLine("Verwendung:", ConsoleColor::BRIGHT_YELLOW);
$this->output->writeLine(" php {$this->scriptName} <kommando> [argumente]");
}
}

View File

@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace App\Framework\Console;
/**
* Enum für Konsolen-Farben mit ANSI-Escape-Codes.
*/
enum ConsoleColor: string
{
case RESET = "0";
// Einfache Textfarben
case BLACK = "30";
case RED = "31";
case GREEN = "32";
case YELLOW = "33";
case BLUE = "34";
case MAGENTA = "35";
case CYAN = "36";
case WHITE = "37";
case GRAY = "90";
// Helle Textfarben
case BRIGHT_RED = "91";
case BRIGHT_GREEN = "92";
case BRIGHT_YELLOW = "93";
case BRIGHT_BLUE = "94";
case BRIGHT_MAGENTA = "95";
case BRIGHT_CYAN = "96";
case BRIGHT_WHITE = "97";
// Hintergrundfarben
case BG_BLACK = "40";
case BG_RED = "41";
case BG_GREEN = "42";
case BG_YELLOW = "43";
case BG_BLUE = "44";
case BG_MAGENTA = "45";
case BG_CYAN = "46";
case BG_WHITE = "47";
// Kombinierte Farben
case WHITE_ON_RED = "97;41";
case BLACK_ON_YELLOW = "30;43";
public function toAnsi(): string
{
return "\033[{$this->value}m";
}
}

View File

@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace App\Framework\Console;
use Attribute;
#[Attribute(\Attribute::TARGET_METHOD)]
final readonly class ConsoleCommand
{
public function __construct(
public string $name,
public string $description = ''
) {}
}

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace App\Framework\Console;
use App\Framework\Core\AttributeMapper;
final readonly class ConsoleCommandMapper implements AttributeMapper
{
public function getAttributeClass(): string
{
return ConsoleCommand::class;
}
public function map(object $reflectionTarget, object $attributeInstance): ?array
{
return [
'name' => $attributeInstance->name,
'description' => $attributeInstance->description,
'class' => $reflectionTarget->getDeclaringClass()->getName(),
'method' => $reflectionTarget->getName(),
];
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace App\Framework\Console;
enum ConsoleFormat: string
{
case RESET = "0";
case BOLD = "1";
case DIM = "2";
case ITALIC = "3";
case UNDERLINE = "4";
case BLINK = "5";
case REVERSE = "7";
case STRIKETHROUGH = "9";
public function toAnsi(): string
{
return "\033[{$this->value}m";
}
}

View File

@@ -0,0 +1,160 @@
<?php
declare(strict_types=1);
namespace App\Framework\Console;
namespace App\Framework\Console;
use App\Framework\Console\Components\InteractiveMenu;
final class ConsoleInput implements ConsoleInputInterface
{
private array $arguments;
private array $options = [];
private ?ConsoleOutputInterface $output = null;
public function __construct(array $arguments, ?ConsoleOutputInterface $output = null)
{
$this->parseArguments($arguments);
$this->output = $output ?? new ConsoleOutput();
}
private function parseArguments(array $arguments): void
{
$this->arguments = [];
foreach ($arguments as $arg) {
if (str_starts_with($arg, '--')) {
// Long option: --key=value oder --key
$parts = explode('=', substr($arg, 2), 2);
$this->options[$parts[0]] = $parts[1] ?? true;
} elseif (str_starts_with($arg, '-')) {
// Short option: -k value oder -k
$key = substr($arg, 1);
$this->options[$key] = true;
} else {
// Argument
$this->arguments[] = $arg;
}
}
}
public function getArgument(int $index, ?string $default = null): ?string
{
return $this->arguments[$index] ?? $default;
}
public function getArguments(): array
{
return $this->arguments;
}
public function getOption(string $name, mixed $default = null): mixed
{
return $this->options[$name] ?? $default;
}
public function hasOption(string $name): bool
{
return isset($this->options[$name]);
}
public function getOptions(): array
{
return $this->options;
}
/**
* Fragt nach einer Benutzereingabe.
*/
public function ask(string $question, string $default = ''): string
{
return $this->output->askQuestion($question, $default);
}
/**
* Fragt nach einem Passwort (versteckte Eingabe).
*/
public function askPassword(string $question): string
{
echo $question . ": ";
// Verstecke Eingabe
system('stty -echo');
$password = trim(fgets(STDIN));
system('stty echo');
echo PHP_EOL;
return $password;
}
/**
* Fragt nach einer Bestätigung (Ja/Nein).
*/
public function confirm(string $question, bool $default = false): bool
{
return $this->output->confirm($question, $default);
}
/**
* Zeigt ein einfaches Auswahlmenü.
*/
public function choice(string $question, array $choices, mixed $default = null): mixed
{
$menu = new InteractiveMenu($this->output);
$menu->setTitle($question);
foreach ($choices as $key => $choice) {
$menu->addItem($choice, null, $key);
}
return $menu->showSimple();
}
/**
* Zeigt ein interaktives Menü mit Pfeiltasten-Navigation.
*/
public function menu(string $title, array $items): mixed
{
$menu = new InteractiveMenu($this->output);
$menu->setTitle($title);
foreach ($items as $key => $item) {
if ($item === '---') {
$menu->addSeparator();
} else {
$menu->addItem($item, null, $key);
}
}
return $menu->showInteractive();
}
/**
* Ermöglicht die Mehrfachauswahl aus einer Liste von Optionen.
*/
public function multiSelect(string $question, array $options): array
{
$this->output->writeLine($question, ConsoleColor::BRIGHT_CYAN);
$this->output->writeLine("Mehrfachauswahl mit Komma getrennt (z.B. 1,3,5):");
foreach ($options as $key => $option) {
$this->output->writeLine(" " . ($key + 1) . ": {$option}");
}
$this->output->write("Ihre Auswahl: ", ConsoleColor::BRIGHT_CYAN);
$input = trim(fgets(STDIN));
$selected = [];
$indices = explode(',', $input);
foreach ($indices as $index) {
$index = (int)trim($index) - 1;
if (isset($options[$index])) {
$selected[] = $options[$index];
}
}
return $selected;
}
}

View File

@@ -0,0 +1,61 @@
<?php
namespace App\Framework\Console;
interface ConsoleInputInterface
{
/**
* Gibt ein Argument an der angegebenen Position zurück
*/
public function getArgument(int $index, ?string $default = null): ?string;
/**
* Gibt alle Argumente zurück
*/
public function getArguments(): array;
/**
* Gibt eine Option zurück
*/
public function getOption(string $name, mixed $default = null): mixed;
/**
* Prüft, ob eine Option vorhanden ist
*/
public function hasOption(string $name): bool;
/**
* Gibt alle Optionen zurück
*/
public function getOptions(): array;
/**
* Fragt nach einer Benutzereingabe
*/
public function ask(string $question, string $default = ''): string;
/**
* Fragt nach einem Passwort (versteckte Eingabe)
*/
public function askPassword(string $question): string;
/**
* Fragt nach einer Bestätigung (Ja/Nein)
*/
public function confirm(string $question, bool $default = false): bool;
/**
* Zeigt ein einfaches Auswahlmenü
*/
public function choice(string $question, array $choices, mixed $default = null): mixed;
/**
* Zeigt ein interaktives Menü mit Pfeiltasten-Navigation
*/
public function menu(string $title, array $items): mixed;
/**
* Ermöglicht die Mehrfachauswahl aus einer Liste von Optionen
*/
public function multiSelect(string $question, array $options): array;
}

View File

@@ -0,0 +1,166 @@
<?php
declare(strict_types=1);
namespace App\Framework\Console;
use App\Framework\Console\Screen\Cursor;
use App\Framework\Console\Screen\Display;
use App\Framework\Console\Screen\ScreenManager;
final readonly class ConsoleOutput implements ConsoleOutputInterface
{
public Cursor $cursor;
public Display $display;
public ScreenManager $screen;
public function __construct()
{
$this->cursor = new Cursor($this);
$this->display = new Display($this);
$this->screen = new ScreenManager($this);
}
/**
* Gibt den Cursor-Controller zurück.
*/
public function cursor(): Cursor
{
return $this->cursor;
}
/**
* Gibt den Display-Controller zurück.
*/
public function display(): Display
{
return $this->display;
}
/**
* Gibt den ScreenManager zurück.
*/
public function screen(): ScreenManager
{
return $this->screen;
}
/**
* Schreibt Text mit optionalem Stil.
*/
public function write(string $message, ConsoleStyle|ConsoleColor|null $style = null): void
{
if ($style instanceof ConsoleColor) {
// Abwärtskompatibilität
echo $style->toAnsi() . $message . ConsoleColor::RESET->toAnsi();
} elseif ($style instanceof ConsoleStyle) {
echo $style->apply($message);
} else {
echo $message;
}
}
/**
* Schreibt rohe ANSI-Sequenzen ohne Verarbeitung.
*/
public function writeRaw(string $raw): void
{
echo $raw;
}
/**
* Setzt den Fenstertitel.
*/
public function writeWindowTitle(string $title, int $mode = 0): void
{
$this->writeRaw("\033]$mode;{$title}\007");
}
/**
* Schreibt eine Zeile mit optionalem Stil.
*/
public function writeLine(string $message = '', ConsoleStyle|ConsoleColor|null $color = null): void
{
$this->write($message . PHP_EOL, $color);
}
/**
* Schreibt eine Erfolgsmeldung.
*/
public function writeSuccess(string $message): void
{
$this->writeLine('✓ ' . $message, ConsoleStyle::success());
}
/**
* Schreibt eine Fehlermeldung.
*/
public function writeError(string $message): void
{
$this->writeLine('✗ ' . $message, ConsoleStyle::error());
}
/**
* Schreibt eine Warnmeldung.
*/
public function writeWarning(string $message): void
{
$this->writeLine('⚠ ' . $message, ConsoleStyle::warning());
}
/**
* Schreibt eine Infomeldung.
*/
public function writeInfo(string $message): void
{
$this->writeLine(' ' . $message, ConsoleStyle::info());
}
/**
* Fügt eine oder mehrere Leerzeilen ein.
*/
public function newLine(int $count = 1): void
{
echo str_repeat(PHP_EOL, $count);
}
/**
* Stellt eine Frage und gibt die Antwort zurück.
*/
public function askQuestion(string $question, ?string $default = null): string
{
$prompt = $question;
if ($default !== null) {
$prompt .= " [{$default}]";
}
$prompt .= ': ';
$this->write($prompt, ConsoleColor::BRIGHT_CYAN);
$answer = trim(fgets(STDIN));
return $answer === '' && $default !== null ? $default : $answer;
}
/**
* Stellt eine Ja/Nein-Frage.
*/
public function confirm(string $question, bool $default = false): bool
{
$defaultText = $default ? 'Y/n' : 'y/N';
$answer = $this->askQuestion("{$question} [{$defaultText}]");
if ($answer === '') {
return $default;
}
return in_array(strtolower($answer), ['y', 'yes', 'ja', '1', 'true']);
}
/**
* Prüft, ob der Output zu einem Terminal geht.
*/
public function isTerminal(): bool
{
return function_exists('posix_isatty') && posix_isatty(STDOUT);
}
}

View File

@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
namespace App\Framework\Console;
/**
* Interface für ConsoleOutput-Klassen.
*/
interface ConsoleOutputInterface
{
/**
* Schreibt Text in die Konsole
*/
public function write(string $message, ?ConsoleColor $color = null): void;
/**
* Schreibt Text mit Zeilenumbruch in die Konsole
*/
public function writeLine(string $message = '', ?ConsoleColor $color = null): void;
/**
* Schreibt eine Erfolgsmeldung
*/
public function writeSuccess(string $message): void;
/**
* Schreibt eine Fehlermeldung
*/
public function writeError(string $message): void;
/**
* Schreibt eine Warnmeldung
*/
public function writeWarning(string $message): void;
/**
* Schreibt eine Informationsmeldung
*/
public function writeInfo(string $message): void;
/**
* Fügt leere Zeilen hinzu
*/
public function newLine(int $count = 1): void;
/**
* Stellt eine Frage und wartet auf Benutzereingabe
*/
public function askQuestion(string $question, ?string $default = null): string;
/**
* Fragt nach einer Bestätigung (ja/nein)
*/
public function confirm(string $question, bool $default = false): bool;
/**
* Setzt den Fenstertitel des Terminals
*/
public function writeWindowTitle(string $title, int $mode = 0): void;
}

View File

@@ -0,0 +1,89 @@
<?php
namespace App\Framework\Console;
final readonly class ConsoleStyle
{
public function __construct(
public ?ConsoleColor $color = null,
public ?ConsoleFormat $format = null,
public ?ConsoleColor $background = null,
){}
/**
* Erstellt einen Style mit den gewünschten Eigenschaften.
*/
public static function create(
?ConsoleColor $color = null,
?ConsoleFormat $format = null,
?ConsoleColor $backgroundColor = null
): self {
return new self($color, $format, $backgroundColor);
}
public function toAnsi(): string
{
$codes = [];
if ($this->format !== null) {
$codes[] = $this->format->value;
}
if ($this->color !== null) {
$codes[] = $this->color->value;
}
if ($this->background !== null) {
$codes[] = $this->background->value;
}
if (empty($codes)) {
return '';
}
return "\033[" . implode(';', $codes) . 'm';
}
/**
* Wendet den Style auf einen Text an.
*/
public function apply(string $text): string
{
$ansi = $this->toAnsi();
if ($ansi === '') {
return $text;
}
return $ansi . $text . ConsoleColor::RESET->toAnsi();
}
// Vordefinierte Styles für häufige Anwendungen
public static function success(): self
{
return new self(ConsoleColor::BRIGHT_GREEN, ConsoleFormat::BOLD);
}
public static function error(): self
{
return new self(ConsoleColor::BRIGHT_RED, ConsoleFormat::BOLD);
}
public static function warning(): self
{
return new self(ConsoleColor::BRIGHT_YELLOW, ConsoleFormat::BOLD);
}
public static function info(): self
{
return new self(ConsoleColor::BRIGHT_BLUE);
}
public static function highlight(): self
{
return new self(format: ConsoleFormat::BOLD);
}
public static function dim(): self
{
return new self(format: ConsoleFormat::DIM);
}
}

View File

@@ -0,0 +1,193 @@
<?php
declare(strict_types=1);
namespace App\Framework\Console;
use App\Framework\Console\Components\InteractiveMenu;
final readonly class DemoCommand
{
#[ConsoleCommand('demo:hello', 'Zeigt eine einfache Hallo-Welt-Nachricht')]
public function hello(ConsoleInput $input, ConsoleOutput $output): int
{
$output->writeWindowTitle('Help Title', 2);
$name = $input->getArgument(0, 'Welt');
$output->writeLine("Hallo, {$name}!", ConsoleColor::BRIGHT_GREEN);
if ($input->hasOption('time')) {
$output->writeInfo('Aktuelle Zeit: ' . date('Y-m-d H:i:s'));
}
return 0;
}
#[ConsoleCommand('demo:colors', 'Zeigt alle verfügbaren Farben')]
public function colors(ConsoleInput $input, ConsoleOutput $output): int
{
$output->writeLine('Verfügbare Farben:', ConsoleColor::BRIGHT_WHITE);
$output->newLine();
$colors = [
'BLACK' => ConsoleColor::BLACK,
'RED' => ConsoleColor::RED,
'GREEN' => ConsoleColor::GREEN,
'YELLOW' => ConsoleColor::YELLOW,
'BLUE' => ConsoleColor::BLUE,
'MAGENTA' => ConsoleColor::MAGENTA,
'CYAN' => ConsoleColor::CYAN,
'WHITE' => ConsoleColor::WHITE,
'GRAY' => ConsoleColor::GRAY,
];
foreach ($colors as $name => $color) {
$output->writeLine(" {$name}: Dies ist ein Test", $color);
}
$output->newLine();
$output->writeLine('Spezielle Ausgaben:');
$output->writeSuccess('Erfolg-Nachricht');
$output->writeError('Fehler-Nachricht');
$output->writeWarning('Warnung-Nachricht');
$output->writeInfo('Info-Nachricht');
return 0;
}
#[ConsoleCommand('demo:interactive', 'Interaktive Demo mit Benutzereingaben')]
public function interactive(ConsoleInput $input, ConsoleOutput $output): int
{
$output->writeLine('Interaktive Demo', ConsoleColor::BRIGHT_CYAN);
$output->newLine();
$name = $output->askQuestion('Wie ist Ihr Name?', 'Unbekannt');
$age = $output->askQuestion('Wie alt sind Sie?');
$confirmed = $output->confirm('Sind die Angaben korrekt?', true);
$output->newLine();
if ($confirmed) {
$output->writeSuccess("Hallo {$name}, Sie sind {$age} Jahre alt!");
} else {
$output->writeWarning('Vorgang abgebrochen.');
}
return 0;
}
#[ConsoleCommand('demo:menu', 'Zeigt ein interaktives Menü')]
public function menu(ConsoleInput $input, ConsoleOutput $output): int
{
$menu = new InteractiveMenu($output);
$result = $menu
->setTitle('Hauptmenü - Demo Application')
->addItem('Benutzer verwalten', function() use ($output) {
return $this->userMenu($output);
})
->addItem('Einstellungen', function() use ($output) {
$output->writeInfo('Einstellungen werden geöffnet...');
return 'settings';
})
->addSeparator()
->addItem('Hilfe anzeigen', function() use ($output) {
$output->writeInfo('Hilfe wird angezeigt...');
return 'help';
})
->addItem('Beenden', function() use ($output) {
$output->writeSuccess('Auf Wiedersehen!');
return 'exit';
})
->showInteractive();
if ($result) {
$output->writeInfo("Menü-Rückgabe: {$result}");
}
return 0;
}
#[ConsoleCommand('demo:simple-menu', 'Zeigt ein einfaches Nummern-Menü')]
public function simpleMenu(ConsoleInput $input, ConsoleOutput $output): int
{
$menu = new InteractiveMenu($output);
$result = $menu
->setTitle('Einfaches Menü')
->addItem('Option 1')
->addItem('Option 2')
->addItem('Option 3')
->showSimple();
$output->writeSuccess("Sie haben gewählt: {$result}");
return 0;
}
#[ConsoleCommand('demo:wizard', 'Zeigt einen Setup-Wizard')]
public function wizard(ConsoleInput $input, ConsoleOutput $output): int
{
$output->writeInfo('🧙 Setup-Wizard gestartet');
$output->newLine();
// Schritt 1: Grundeinstellungen
$output->writeLine('Schritt 1: Grundeinstellungen', ConsoleColor::BRIGHT_CYAN);
$appName = $input->ask('Name der Anwendung', 'Meine App');
$version = $input->ask('Version', '1.0.0');
// Schritt 2: Datenbank
$output->newLine();
$output->writeLine('Schritt 2: Datenbank-Konfiguration', ConsoleColor::BRIGHT_CYAN);
$dbHost = $input->ask('Datenbank-Host', 'localhost');
$dbName = $input->ask('Datenbank-Name', 'myapp');
$dbUser = $input->ask('Datenbank-Benutzer', 'root');
$dbPass = $input->askPassword('Datenbank-Passwort');
// Schritt 3: Features
$output->newLine();
$output->writeLine('Schritt 3: Features auswählen', ConsoleColor::BRIGHT_CYAN);
$features = $input->multiSelect('Welche Features möchten Sie aktivieren?', [
'Caching',
'Logging',
'Email-Versand',
'API-Schnittstelle',
'Admin-Panel'
]);
// Zusammenfassung
$output->newLine();
$output->writeLine('🎯 Konfiguration abgeschlossen!', ConsoleColor::BRIGHT_GREEN);
$output->newLine();
$output->writeLine('Ihre Einstellungen:');
$output->writeLine("- App-Name: {$appName}");
$output->writeLine("- Version: {$version}");
$output->writeLine("- DB-Host: {$dbHost}");
$output->writeLine("- DB-Name: {$dbName}");
$output->writeLine("- Features: " . implode(', ', $features));
if ($input->confirm('Konfiguration speichern?', true)) {
$output->writeSuccess('✅ Konfiguration wurde gespeichert!');
} else {
$output->writeWarning('❌ Konfiguration wurde nicht gespeichert.');
}
return 0;
}
/**
* Hilfsmethode für das Benutzer-Untermenü.
*/
private function userMenu(ConsoleOutput $output): string
{
$menu = new InteractiveMenu($output);
return $menu
->setTitle('Benutzer-Verwaltung')
->addItem('Benutzer auflisten', fn() => 'list_users')
->addItem('Neuen Benutzer erstellen', fn() => 'create_user')
->addItem('Benutzer bearbeiten', fn() => 'edit_user')
->addItem('Benutzer löschen', fn() => 'delete_user')
->addSeparator()
->addItem('← Zurück zum Hauptmenü', fn() => 'back')
->showInteractive() ?? 'back';
}
}

View File

@@ -0,0 +1,173 @@
<?php
declare(strict_types=1);
namespace App\Framework\Console\DemoCommand;
use App\Framework\Console\ConsoleCommand;
use App\Framework\Console\ConsoleInput;
use App\Framework\Console\ConsoleOutput;
use App\Framework\Console\ConsoleStyle;
use App\Framework\Console\Screen\ScreenType;
final class ScreenDemoCommand
{
#[ConsoleCommand('demo:screen', 'Zeigt die verschiedenen Screen-Management-Funktionen')]
public function __invoke(ConsoleInput $input, ConsoleOutput $output): int
{
// Aktiviere interaktiven Modus
$output->screen()->setInteractiveMode(true);
// Zeige ein Menü
$output->screen()->newMenu();
$output->writeLine('=== Screen-Management Demo ===', ConsoleStyle::success());
$output->writeLine('');
$output->writeLine('1. Cursor-Bewegungen');
$output->writeLine('2. Bildschirmlöschung');
$output->writeLine('3. Fortschrittsanzeige');
$output->writeLine('4. Typensatz-Beispiel');
$output->writeLine('');
$choice = $output->askQuestion('Wählen Sie eine Option (1-4)');
switch ($choice) {
case '1':
$this->demoCursor($input, $output);
break;
case '2':
$this->demoDisplay($input, $output);
break;
case '3':
$this->demoProgress($input, $output);
break;
case '4':
$this->demoTypeset($input, $output);
break;
default:
$output->writeError('Ungültige Auswahl!');
}
return 0;
}
private function demoCursor(ConsoleInput $input, ConsoleOutput $output): void
{
$output->screen()->newScreen(ScreenType::CONTENT);
$output->writeLine('=== Cursor-Bewegungs-Demo ===', ConsoleStyle::success());
$output->writeLine('');
// Cursor-Bewegungen
$output->writeLine('Der Cursor wird jetzt bewegt...');
sleep(1);
$output->cursor()->down(2)->right(5);
$output->write('Hallo!', ConsoleStyle::success());
sleep(1);
$output->cursor()->down(1)->left(5);
$output->write('Welt!', ConsoleStyle::info());
sleep(1);
$output->cursor()->moveTo(10, 20);
$output->write('Position 10,20', ConsoleStyle::warning());
sleep(1);
$output->cursor()->home();
$output->writeLine("\nZurück zum Anfang!", ConsoleStyle::error());
sleep(1);
$output->writeLine("\nDrücken Sie eine Taste, um fortzufahren...");
$output->screen()->waitForInput();
}
private function demoDisplay(ConsoleInput $input, ConsoleOutput $output): void
{
$output->screen()->newScreen(ScreenType::CONTENT);
$output->writeLine('=== Bildschirmlöschungs-Demo ===', ConsoleStyle::success());
$output->writeLine('');
// Bildschirm füllen
for ($i = 1; $i <= 10; $i++) {
$output->writeLine("Zeile $i: Dies ist ein Test");
}
sleep(1);
$output->writeLine("\nLösche in 3 Sekunden den Bildschirm...");
sleep(3);
// Bildschirm löschen
$output->display()->clear();
$output->writeLine('Bildschirm wurde gelöscht!');
sleep(1);
// Zeilen hinzufügen
for ($i = 1; $i <= 5; $i++) {
$output->writeLine("Neue Zeile $i");
}
sleep(1);
$output->writeLine("\nLösche nur die aktuelle Zeile in 2 Sekunden...");
sleep(2);
// Zeile löschen
$output->display()->clearLine();
$output->writeLine('Die Zeile wurde gelöscht und durch diese ersetzt!');
sleep(1);
$output->writeLine("\nDrücken Sie eine Taste, um fortzufahren...");
$output->screen()->waitForInput();
}
private function demoProgress(ConsoleInput $input, ConsoleOutput $output): void
{
$output->screen()->newScreen(ScreenType::PROGRESS);
$output->writeLine('=== Fortschrittsanzeige-Demo ===', ConsoleStyle::success());
$output->writeLine('');
$total = 20;
for ($i = 0; $i <= $total; $i++) {
$percent = floor(($i / $total) * 100);
$bar = str_repeat('█', $i) . str_repeat('░', $total - $i);
// Zeile löschen und neue Fortschrittsanzeige
$output->display()->clearLine();
$output->write("Fortschritt: [{$bar}] {$percent}%");
usleep(200000); // 200ms pause
}
$output->writeLine("\n\nFortschritt abgeschlossen!");
sleep(1);
$output->writeLine("\nDrücken Sie eine Taste, um fortzufahren...");
$output->screen()->waitForInput();
}
private function demoTypeset(ConsoleInput $input, ConsoleOutput $output): void
{
$output->screen()->newScreen(ScreenType::CONTENT);
$output->writeLine('=== Typensatz-Demo ===', ConsoleStyle::success());
$output->writeLine('');
$text = 'Dies ist eine Demonstration des Typensatz-Effekts. Der Text wird Zeichen für Zeichen angezeigt, ' .
'als ob er gerade getippt würde. Diese Technik kann für Intros, Tutorials oder ' .
'dramatische Effekte verwendet werden.';
// Typensatz-Effekt
foreach (str_split($text) as $char) {
$output->write($char);
usleep(rand(50000, 150000)); // 50-150ms zufällige Pause
}
$output->writeLine("\n\nTypensatz abgeschlossen!");
sleep(1);
$output->writeLine("\nDrücken Sie eine Taste, um zurückzukehren...");
$output->screen()->waitForInput();
// Zurück zum Hauptmenü
$output->screen()->newMenu();
$this->__invoke($input, $output);
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace App\Framework\Console\Examples;
use App\Framework\Console\ConsoleCommand;
use App\Framework\Console\ConsoleInput;
use App\Framework\Console\ConsoleOutput;
use App\Framework\Console\ProgressBar;
class ProgressBarExample
{
#[ConsoleCommand(name: 'demo:progressbar', description: 'Zeigt eine Demonstration der Fortschrittsanzeige')]
public function showProgressBarDemo(ConsoleInput $input, ConsoleOutput $output): int
{
$output->writeInfo('Demonstration der Fortschrittsanzeige');
$output->newLine();
// Einfache Fortschrittsanzeige
$output->writeLine('Einfache Fortschrittsanzeige:');
$progress = new ProgressBar($output, 10);
$progress->start();
for ($i = 0; $i < 10; $i++) {
// Simuliere Arbeit
usleep(200000);
$progress->advance();
}
$progress->finish();
// Fortschrittsanzeige mit angepasstem Format
$output->writeLine('Fortschrittsanzeige mit angepasstem Format:');
$progress = new ProgressBar($output, 5);
$progress->setFormat('%bar% %current%/%total% [%percent%%] - %elapsed%s vergangen, %remaining%s verbleibend');
$progress->setBarCharacters('█', '░', '█');
$progress->start();
for ($i = 0; $i < 5; $i++) {
// Simuliere Arbeit
usleep(500000);
$progress->advance();
}
$progress->finish();
$output->writeSuccess('Demonstration abgeschlossen!');
return 0;
}
}

View File

@@ -0,0 +1,72 @@
<?php
namespace App\Framework\Console\Examples;
use App\Framework\Console\ConsoleCommand;
use App\Framework\Console\ConsoleInput;
use App\Framework\Console\ConsoleOutput;
use App\Framework\Console\Spinner;
use App\Framework\Console\SpinnerStyle;
class SpinnerExample
{
#[ConsoleCommand(name: 'demo:spinner', description: 'Zeigt eine Demonstration der Spinner-Komponente')]
public function showSpinnerDemo(ConsoleInput $input, ConsoleOutput $output): int
{
$output->writeInfo('Demonstration der Spinner-Komponente');
$output->newLine();
// Einfacher Spinner
$spinner = new Spinner($output, 'Lade Daten...');
$spinner->start();
// Simuliere Arbeit
for ($i = 0; $i < 10; $i++) {
usleep(100000);
$spinner->update();
}
$spinner->success('Daten erfolgreich geladen!');
// Spinner mit verschiedenen Stilen
$styles = [
SpinnerStyle::DOTS,
SpinnerStyle::LINE,
SpinnerStyle::BOUNCE,
SpinnerStyle::ARROW
];
foreach ($styles as $style) {
$output->writeLine("Spinner-Stil: {$style->name}");
$spinner = new Spinner($output, 'Verarbeite...', $style);
$spinner->start();
for ($i = 0; $i < 15; $i++) {
usleep(100000);
if ($i === 5) {
$spinner->setMessage('Fast fertig...');
}
$spinner->update();
}
$spinner->success('Fertig!');
}
// Fehlerhafter Prozess
$spinner = new Spinner($output, 'Verbinde mit Server...');
$spinner->start();
for ($i = 0; $i < 8; $i++) {
usleep(150000);
$spinner->update();
}
$spinner->error('Verbindung fehlgeschlagen!');
$output->writeSuccess('Demonstration abgeschlossen!');
return 0;
}
}

View File

@@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace App\Framework\Console\Examples;
use App\Framework\Console\Components\Table;
use App\Framework\Console\ConsoleCommand;
use App\Framework\Console\ConsoleColor;
use App\Framework\Console\ConsoleFormat;
use App\Framework\Console\ConsoleInput;
use App\Framework\Console\ConsoleOutput;
use App\Framework\Console\ConsoleStyle;
final class TableExample
{
#[ConsoleCommand('demo:table', 'Zeigt eine Beispiel-Tabelle')]
public function showTable(ConsoleInput $input, ConsoleOutput $output): int
{
$output->writeLine('Beispiel für die Table-Komponente', ConsoleStyle::create(
color: ConsoleColor::BRIGHT_WHITE,
format: ConsoleFormat::BOLD
));
$output->newLine();
// Einfache Tabelle
$table = new Table(
headerStyle: ConsoleStyle::create(color: ConsoleColor::BRIGHT_YELLOW, format: ConsoleFormat::BOLD),
borderStyle: ConsoleStyle::create(color: ConsoleColor::CYAN)
);
$table->setHeaders(['Name', 'Alter', 'Stadt'])
->addRow(['Max Mustermann', '30', 'Berlin'])
->addRow(['Anna Schmidt', '25', 'München'])
->addRow(['Peter Weber', '35', 'Hamburg']);
$output->write($table->render());
$output->newLine();
// Komplexere Tabelle mit Produktdaten
$output->writeLine('Produktübersicht:', ConsoleStyle::create(color: ConsoleColor::BRIGHT_GREEN));
$productTable = new Table(
headerStyle: ConsoleStyle::create(color: ConsoleColor::BRIGHT_WHITE, format: ConsoleFormat::BOLD),
rowStyle: ConsoleStyle::create(color: ConsoleColor::WHITE),
borderStyle: ConsoleStyle::create(color: ConsoleColor::GRAY)
);
$productTable->setHeaders(['Artikel-Nr.', 'Bezeichnung', 'Preis', 'Lagerbestand', 'Status'])
->setPadding(2)
->addRow(['A-1001', 'Tastatur Deluxe', '89,99 €', '45', 'Verfügbar'])
->addRow(['A-1002', 'Gaming Maus Pro', '69,99 €', '12', 'Knapp'])
->addRow(['A-1003', '4K Monitor 27"', '349,99 €', '0', 'Ausverkauft'])
->addRow(['A-1004', 'USB-C Kabel 2m', '19,99 €', '156', 'Verfügbar'])
->addRow(['A-1005', 'Wireless Headset', '129,99 €', '8', 'Knapp']);
$output->write($productTable->render());
return 0;
}
}

View File

@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
namespace App\Framework\Console\Examples;
use App\Framework\Console\Components\TextBox;
use App\Framework\Console\ConsoleCommand;
use App\Framework\Console\ConsoleColor;
use App\Framework\Console\ConsoleFormat;
use App\Framework\Console\ConsoleInput;
use App\Framework\Console\ConsoleOutput;
use App\Framework\Console\ConsoleStyle;
final class TextBoxExample
{
#[ConsoleCommand('demo:textbox', 'Zeigt verschiedene TextBox-Beispiele')]
public function showTextBox(ConsoleInput $input, ConsoleOutput $output): int
{
$output->writeLine('Beispiele für die TextBox-Komponente', ConsoleStyle::create(
color: ConsoleColor::BRIGHT_WHITE,
format: ConsoleFormat::BOLD
));
$output->newLine();
// Einfache TextBox
$infoBox = new TextBox(
content: "Dies ist eine einfache Informationsbox.\nSie kann mehrere Zeilen enthalten und passt den Text automatisch an die Breite an.",
width: 60,
borderStyle: ConsoleStyle::create(color: ConsoleColor::BRIGHT_BLUE),
contentStyle: ConsoleStyle::create(color: ConsoleColor::WHITE),
title: "Information"
);
$output->write($infoBox->render());
$output->newLine();
// Warnung TextBox
$warningBox = new TextBox(
content: "Warnung! Diese Aktion kann nicht rückgängig gemacht werden. Stellen Sie sicher, dass Sie ein Backup Ihrer Daten haben, bevor Sie fortfahren.",
width: 70,
padding: 2,
borderStyle: ConsoleStyle::create(color: ConsoleColor::BRIGHT_YELLOW),
contentStyle: ConsoleStyle::create(color: ConsoleColor::BRIGHT_WHITE),
title: "⚠ Warnung"
);
$output->write($warningBox->render());
$output->newLine();
// Fehler TextBox
$errorBox = new TextBox(
content: "Fehler: Die Verbindung zur Datenbank konnte nicht hergestellt werden. Überprüfen Sie Ihre Zugangsdaten und stellen Sie sicher, dass der Datenbankserver läuft.",
width: 80,
padding: 1,
borderStyle: ConsoleStyle::create(color: ConsoleColor::BRIGHT_RED),
contentStyle: ConsoleStyle::create(color: ConsoleColor::BRIGHT_WHITE),
title: "✗ Fehler"
);
$output->write($errorBox->render());
return 0;
}
}

View File

@@ -0,0 +1,89 @@
<?php
declare(strict_types=1);
namespace App\Framework\Console\Examples;
use App\Framework\Console\Components\TreeHelper;
use App\Framework\Console\ConsoleCommand;
use App\Framework\Console\ConsoleColor;
use App\Framework\Console\ConsoleFormat;
use App\Framework\Console\ConsoleInput;
use App\Framework\Console\ConsoleOutput;
use App\Framework\Console\ConsoleStyle;
final class TreeExample
{
#[ConsoleCommand('demo:tree', 'Zeigt ein Beispiel für die TreeHelper-Komponente')]
public function showTreeExample(ConsoleInput $input, ConsoleOutput $output): int
{
$output->writeLine('Beispiel für den TreeHelper', ConsoleStyle::create(
color: ConsoleColor::BRIGHT_WHITE,
format: ConsoleFormat::BOLD
));
$output->newLine();
// Einfaches Verzeichnisbeispiel
$tree = new TreeHelper('Projektstruktur');
$tree->setNodeStyle(ConsoleStyle::create(color: ConsoleColor::BRIGHT_CYAN, format: ConsoleFormat::BOLD));
$tree->setLeafStyle(ConsoleStyle::create(color: ConsoleColor::WHITE));
$srcNode = $tree->addNode('src/');
$domainNode = $srcNode->addNode('Domain/');
$domainNode->addNode('User/')->addLeaf('User.php')->addLeaf('UserRepository.php');
$domainNode->addNode('Product/')->addLeaf('Product.php')->addLeaf('ProductRepository.php');
$frameworkNode = $srcNode->addNode('Framework/');
$consoleNode = $frameworkNode->addNode('Console/');
$consoleNode->addNode('Components/')
->addLeaf('Table.php')
->addLeaf('TextBox.php')
->addLeaf('TreeHelper.php');
$consoleNode->addLeaf('ConsoleColor.php');
$consoleNode->addLeaf('ConsoleFormat.php');
$consoleNode->addLeaf('ConsoleOutput.php');
$testNode = $tree->addNode('tests/');
$testNode->addLeaf('UserTest.php');
$testNode->addLeaf('ProductTest.php');
$tree->addLeaf('composer.json');
$tree->addLeaf('README.md');
$output->write($tree->render());
$output->newLine(2);
// Zweites Beispiel: Kategorien
$output->writeLine('Kategorie-Baum:', ConsoleStyle::create(color: ConsoleColor::BRIGHT_GREEN));
$categories = new TreeHelper();
$categories->setNodeStyle(ConsoleStyle::create(color: ConsoleColor::BRIGHT_YELLOW, format: ConsoleFormat::BOLD));
$categories->setLeafStyle(ConsoleStyle::create(color: ConsoleColor::BRIGHT_WHITE));
$categories->setLineStyle(ConsoleStyle::create(color: ConsoleColor::GRAY));
$electronics = $categories->addNode('Elektronik');
$computers = $electronics->addNode('Computer & Zubehör');
$computers->addLeaf('Laptops');
$computers->addLeaf('Desktop-PCs');
$peripherie = $computers->addNode('Peripheriegeräte');
$peripherie->addLeaf('Monitore');
$peripherie->addLeaf('Tastaturen');
$peripherie->addLeaf('Mäuse');
$smartphones = $electronics->addNode('Smartphones & Zubehör');
$smartphones->addLeaf('Handys');
$smartphones->addLeaf('Hüllen');
$smartphones->addLeaf('Ladegeräte');
$kleidung = $categories->addNode('Kleidung');
$kleidung->addLeaf('Herren');
$kleidung->addLeaf('Damen');
$kleidung->addLeaf('Kinder');
$output->write($categories->render());
return 0;
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace App\Framework\Console\Exceptions;
class CommandNotFoundException extends ConsoleException
{
public function __construct(string $commandName)
{
parent::__construct("Kommando '{$commandName}' nicht gefunden.");
}
}

View File

@@ -0,0 +1,7 @@
<?php
namespace App\Framework\Console\Exceptions;
class ConsoleException extends \Exception
{
}

View File

@@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
namespace App\Framework\Console;
/**
* Standard Exit-Codes für Console-Anwendungen
*
* Basiert auf den POSIX-Standards und bewährten Praktiken:
* - 0: Erfolg
* - 1: Allgemeiner Fehler
* - 2: Falsche Verwendung (ungültige Argumente)
* - 64-78: Spezifische Fehler-Codes (sysexits.h Standard)
*/
enum ExitCode: int
{
case SUCCESS = 0;
case GENERAL_ERROR = 1;
case USAGE_ERROR = 2;
case COMMAND_NOT_FOUND = 64;
case INVALID_INPUT = 65;
case NO_INPUT = 66;
case UNAVAILABLE = 69;
case SOFTWARE_ERROR = 70;
case OS_ERROR = 71;
case OS_FILE_ERROR = 72;
case CANT_CREATE = 73;
case IO_ERROR = 74;
case TEMP_FAIL = 75;
case PROTOCOL_ERROR = 76;
case NO_PERMISSION = 77;
case CONFIG_ERROR = 78;
/**
* Gibt eine menschenlesbare Beschreibung des Exit-Codes zurück
*/
public function getDescription(): string
{
return match($this) {
self::SUCCESS => 'Erfolgreich abgeschlossen',
self::GENERAL_ERROR => 'Allgemeiner Fehler',
self::USAGE_ERROR => 'Falsche Verwendung oder ungültige Argumente',
self::COMMAND_NOT_FOUND => 'Kommando nicht gefunden',
self::INVALID_INPUT => 'Ungültige Eingabedaten',
self::NO_INPUT => 'Keine Eingabe vorhanden',
self::UNAVAILABLE => 'Service nicht verfügbar',
self::SOFTWARE_ERROR => 'Interner Software-Fehler',
self::OS_ERROR => 'Betriebssystem-Fehler',
self::OS_FILE_ERROR => 'Datei-/Verzeichnis-Fehler',
self::CANT_CREATE => 'Kann Datei/Verzeichnis nicht erstellen',
self::IO_ERROR => 'Ein-/Ausgabe-Fehler',
self::TEMP_FAIL => 'Temporärer Fehler',
self::PROTOCOL_ERROR => 'Protokoll-Fehler',
self::NO_PERMISSION => 'Keine Berechtigung',
self::CONFIG_ERROR => 'Konfigurationsfehler',
};
}
/**
* Prüft, ob der Exit-Code einen Erfolg darstellt
*/
public function isSuccess(): bool
{
return $this === self::SUCCESS;
}
/**
* Prüft, ob der Exit-Code einen Fehler darstellt
*/
public function isError(): bool
{
return !$this->isSuccess();
}
}

View File

@@ -0,0 +1,185 @@
<?php
namespace App\Framework\Console;
class ProgressBar
{
private ConsoleOutputInterface $output;
private int $total;
private int $current = 0;
private int $width;
private float $startTime;
private string $format = '%bar% %percent%%';
private string $barChar = '=';
private string $emptyBarChar = '-';
private string $progressChar = '>';
private int $redrawFrequency = 1;
private int $writeCount = 0;
private bool $firstRun = true;
public function __construct(ConsoleOutputInterface $output, int $total = 100, int $width = 50)
{
$this->output = $output;
$this->total = max(1, $total);
$this->width = $width;
$this->startTime = microtime(true);
}
/**
* Setzt das Format der Fortschrittsanzeige
* Verfügbare Platzhalter:
* - %bar%: Die Fortschrittsanzeige
* - %current%: Aktueller Fortschritt
* - %total%: Gesamtwert
* - %percent%: Prozentsatz des Fortschritts
* - %elapsed%: Verstrichene Zeit in Sekunden
* - %remaining%: Geschätzte verbleibende Zeit in Sekunden
*/
public function setFormat(string $format): self
{
$this->format = $format;
return $this;
}
/**
* Setzt die Zeichen für die Fortschrittsanzeige
*/
public function setBarCharacters(string $barChar = '=', string $emptyBarChar = '-', string $progressChar = '>'): self
{
$this->barChar = $barChar;
$this->emptyBarChar = $emptyBarChar;
$this->progressChar = $progressChar;
return $this;
}
/**
* Setzt die Häufigkeit der Aktualisierung
*/
public function setRedrawFrequency(int $frequency): self
{
$this->redrawFrequency = max(1, $frequency);
return $this;
}
/**
* Erhöht den Fortschritt um den angegebenen Schritt
*/
public function advance(int $step = 1): self
{
$this->setCurrent($this->current + $step);
return $this;
}
/**
* Setzt den aktuellen Fortschritt
*/
public function setCurrent(int $current): self
{
$this->current = min($this->total, max(0, $current));
$this->writeCount++;
if ($this->writeCount >= $this->redrawFrequency || $this->firstRun || $this->current >= $this->total) {
$this->display();
$this->writeCount = 0;
}
return $this;
}
/**
* Startet die Fortschrittsanzeige
*/
public function start(): self
{
$this->startTime = microtime(true);
$this->current = 0;
$this->firstRun = true;
$this->display();
return $this;
}
/**
* Beendet die Fortschrittsanzeige
*/
public function finish(): self
{
if ($this->current < $this->total) {
$this->current = $this->total;
}
$this->display();
$this->output->newLine(2);
return $this;
}
/**
* Zeigt die Fortschrittsanzeige an
*/
private function display(): void
{
if (!$this->firstRun) {
// Bewege den Cursor eine Zeile nach oben
$this->output->write("\033[1A");
// Lösche die aktuelle Zeile
$this->output->write("\033[2K");
}
$this->firstRun = false;
$percent = $this->current / $this->total;
$bar = $this->getProgressBar($percent);
$replacements = [
'%bar%' => $bar,
'%current%' => (string) $this->current,
'%total%' => (string) $this->total,
'%percent%' => number_format($percent * 100, 0),
'%elapsed%' => $this->getElapsedTime(),
'%remaining%' => $this->getRemaining($percent),
];
$line = str_replace(array_keys($replacements), array_values($replacements), $this->format);
$this->output->writeLine($line);
}
/**
* Generiert die Fortschrittsanzeige
*/
private function getProgressBar(float $percent): string
{
$completedWidth = (int) floor($percent * $this->width);
$emptyWidth = $this->width - $completedWidth - ($completedWidth < $this->width ? 1 : 0);
$bar = str_repeat($this->barChar, $completedWidth);
if ($completedWidth < $this->width) {
$bar .= $this->progressChar . str_repeat($this->emptyBarChar, $emptyWidth);
}
return $bar;
}
/**
* Berechnet die verstrichene Zeit
*/
private function getElapsedTime(): string
{
return number_format(microtime(true) - $this->startTime, 1);
}
/**
* Berechnet die verbleibende Zeit
*/
private function getRemaining(float $percent): string
{
if ($percent === 0) {
return '--';
}
$elapsed = microtime(true) - $this->startTime;
$remaining = $elapsed / $percent - $elapsed;
return number_format($remaining, 1);
}
}

View File

@@ -0,0 +1,106 @@
# Console-Modul
Dieses Modul bietet eine flexible und benutzerfreundliche Konsolen-Schnittstelle für Ihre PHP-Anwendung. Es ermöglicht die Erstellung von CLI-Befehlen mit einfacher Eingabe- und Ausgabehandlung.
## Hauptkomponenten
### ConsoleApplication
Die zentrale Klasse zur Verwaltung und Ausführung von Konsolen-Befehlen.
```php
$app = new ConsoleApplication('app', 'Meine Anwendung');
$app->registerCommands(new MyCommands());
$app->run($argv);
```
### ConsoleCommand-Attribut
Verwenden Sie das `ConsoleCommand`-Attribut, um Methoden als Konsolenbefehle zu kennzeichnen:
```php
class MyCommands
{
#[ConsoleCommand(name: 'hello', description: 'Gibt eine Begrüßung aus')]
public function sayHello(ConsoleInput $input, ConsoleOutput $output): int
{
$name = $input->getArgument(0, 'Welt');
$output->writeLine("Hallo, {$name}!", ConsoleColor::BRIGHT_GREEN);
return 0;
}
}
```
### ConsoleInput und ConsoleOutput
Diese Klassen bieten Methoden für die Ein- und Ausgabe in der Konsole:
```php
// Eingabe
$name = $input->ask('Wie heißen Sie?');
$confirm = $input->confirm('Fortfahren?', true);
$option = $input->getOption('verbose');
// Ausgabe
$output->writeSuccess('Operation erfolgreich!');
$output->writeError('Fehler aufgetreten!');
$output->writeInfo('Wussten Sie schon...');
```
## Fortschrittsanzeigen
### ProgressBar
Zeigt eine Fortschrittsanzeige für Operationen mit bekannter Länge:
```php
$total = count($items);
$progress = new ProgressBar($output, $total);
$progress->start();
foreach ($items as $item) {
// Verarbeite $item
$progress->advance();
}
$progress->finish();
```
### Spinner
Zeigt einen animierten Spinner für Operationen unbekannter Länge:
```php
$spinner = new Spinner($output, 'Lade Daten...');
$spinner->start();
// Ausführung der Operation
do {
// Arbeit ausführen
$spinner->update();
} while (!$finished);
$spinner->success('Daten erfolgreich geladen!');
```
## Beispiele
Sehen Sie sich die Beispielklassen im `Examples`-Verzeichnis an, um mehr über die Verwendung der Komponenten zu erfahren:
- `ProgressBarExample`: Zeigt verschiedene Konfigurationen der Fortschrittsanzeige
- `SpinnerExample`: Demonstriert die Verwendung von Spinnern mit verschiedenen Stilen
## Anpassung
Sie können die Anzeige anpassen, indem Sie benutzerdefinierte Formatierungen und Farben verwenden:
```php
$progress->setFormat('%bar% %percent%%');
$progress->setBarCharacters('█', '░', '█');
$spinner = new Spinner($output, 'Lade...', SpinnerStyle::BOUNCE);
```
## Fehlerbehebung
Wenn Probleme mit der Anzeige auftreten, stellen Sie sicher, dass Ihr Terminal ANSI-Escape-Sequenzen unterstützt. Die meisten modernen Terminals tun dies, aber Windows-Terminals können Einschränkungen haben.

View File

@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace App\Framework\Console\Screen;
/**
* Strategien für das Löschen des Bildschirms.
*/
enum ClearStrategy
{
case NEVER; // Niemals löschen
case ALWAYS; // Immer löschen
case ON_NEW_SCREEN; // Nur bei neuen Bildschirmen
case SMART; // Intelligente Entscheidung
}

View File

@@ -0,0 +1,127 @@
<?php
declare(strict_types=1);
namespace App\Framework\Console\Screen;
use App\Framework\Console\ConsoleOutput;
/**
* Verantwortlich für Cursor-Positionierung.
*/
final class Cursor
{
public function __construct(
private ConsoleOutput $output
) {}
/**
* Bewegt den Cursor zu einer bestimmten Position.
*/
public function moveTo(int $row, int $col): self
{
if ($this->output->isTerminal()) {
$this->output->writeRaw(CursorControlCode::POSITION->format($row, $col));
}
return $this;
}
/**
* Bewegt den Cursor zum Anfang (Home-Position).
*/
public function home(): self
{
if ($this->output->isTerminal()) {
// Die Home-Position ist 1,1 (obere linke Ecke)
$this->output->writeRaw(CursorControlCode::POSITION->format(1, 1));
}
return $this;
}
/**
* Bewegt den Cursor um X Zeilen nach oben.
*/
public function up(int $lines = 1): self
{
if ($this->output->isTerminal() && $lines > 0) {
$this->output->writeRaw(CursorControlCode::UP->format($lines));
}
return $this;
}
/**
* Bewegt den Cursor um X Zeilen nach unten.
*/
public function down(int $lines = 1): self
{
if ($this->output->isTerminal() && $lines > 0) {
$this->output->writeRaw(CursorControlCode::DOWN->format($lines));
}
return $this;
}
/**
* Bewegt den Cursor um X Spalten nach links.
*/
public function left(int $columns = 1): self
{
if ($this->output->isTerminal() && $columns > 0) {
$this->output->writeRaw(CursorControlCode::LEFT->format($columns));
}
return $this;
}
/**
* Bewegt den Cursor um X Spalten nach rechts.
*/
public function right(int $columns = 1): self
{
if ($this->output->isTerminal() && $columns > 0) {
$this->output->writeRaw(CursorControlCode::RIGHT->format($columns));
}
return $this;
}
/**
* Versteckt den Cursor.
*/
public function hide(): self
{
if ($this->output->isTerminal()) {
$this->output->writeRaw(CursorControlCode::HIDE->format());
}
return $this;
}
/**
* Zeigt den Cursor wieder an.
*/
public function show(): self
{
if ($this->output->isTerminal()) {
$this->output->writeRaw(CursorControlCode::SHOW->format());
}
return $this;
}
/**
* Speichert die aktuelle Cursorposition.
*/
public function save(): self
{
if ($this->output->isTerminal()) {
$this->output->writeRaw(CursorControlCode::SAVE->format());
}
return $this;
}
/**
* Stellt die zuvor gespeicherte Cursorposition wieder her.
*/
public function restore(): self
{
if ($this->output->isTerminal()) {
$this->output->writeRaw(CursorControlCode::RESTORE->format());
}
return $this;
}
}

View File

@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace App\Framework\Console\Screen;
/**
* Cursor-Steuerungscodes nach ANSI-Standard.
*/
enum CursorControlCode: string
{
// Bewegung
case UP = "A"; // n Zeilen nach oben
case DOWN = "B"; // n Zeilen nach unten
case RIGHT = "C"; // n Spalten nach rechts
case LEFT = "D"; // n Spalten nach links
case NEXT_LINE = "E"; // n Zeilen nach unten und an den Anfang
case PREV_LINE = "F"; // n Zeilen nach oben und an den Anfang
case COLUMN = "G"; // Zur absoluten Spalte
case POSITION = "H"; // Zur Position (Zeile;Spalte)
// Sichtbarkeit
case HIDE = "?25l"; // Cursor verstecken
case SHOW = "?25h"; // Cursor anzeigen
// Andere
case SAVE = "s"; // Cursorposition speichern
case RESTORE = "u"; // Cursorposition wiederherstellen
/**
* Formatiert den ANSI-Steuerungscode korrekt.
*/
public function format(int ...$params): string
{
// Wenn keine Parameter gegeben sind, aber wir einen speziellen Escape-Code haben
if (empty($params) && in_array($this, [self::HIDE, self::SHOW])) {
return "\033[{$this->value}";
}
// Wenn keine Parameter, dann ohne formatieren
if (empty($params)) {
return "\033[{$this->value}";
}
// Wenn Parameter vorhanden sind, formatieren
$paramStr = implode(';', $params);
return "\033[{$paramStr}{$this->value}";
}
}

View File

@@ -0,0 +1,107 @@
<?php
declare(strict_types=1);
namespace App\Framework\Console\Screen;
use App\Framework\Console\ConsoleOutput;
/**
* Verantwortlich für Bildschirm-Management (Löschen, etc.)
*/
final readonly class Display
{
public function __construct(
private ConsoleOutput $output
) {}
/**
* Löscht den gesamten Bildschirm und setzt den Cursor an den Anfang.
*/
public function clear(): self
{
if ($this->output->isTerminal()) {
// Bildschirm löschen und Cursor an den Anfang setzen
$this->output->writeRaw(ScreenControlCode::CLEAR_ALL->format());
$this->output->writeRaw(CursorControlCode::POSITION->format(1, 1));
}
return $this;
}
/**
* Löscht die aktuelle Zeile.
*/
public function clearLine(): self
{
if ($this->output->isTerminal()) {
$this->output->writeRaw(ScreenControlCode::CLEAR_LINE->format());
$this->output->writeRaw("\r"); // Cursor an den Zeilenanfang
}
return $this;
}
/**
* Löscht vom Cursor bis zum Ende des Bildschirms.
*/
public function clearToEnd(): self
{
if ($this->output->isTerminal()) {
$this->output->writeRaw(ScreenControlCode::CLEAR_BELOW->format());
}
return $this;
}
/**
* Löscht vom Cursor bis zum Anfang des Bildschirms.
*/
public function clearToBeginning(): self
{
if ($this->output->isTerminal()) {
$this->output->writeRaw(ScreenControlCode::CLEAR_ABOVE->format());
}
return $this;
}
/**
* Löscht vom Cursor bis zum Ende der Zeile.
*/
public function clearLineToEnd(): self
{
if ($this->output->isTerminal()) {
$this->output->writeRaw(ScreenControlCode::CLEAR_LINE_RIGHT->format());
}
return $this;
}
/**
* Löscht vom Cursor bis zum Anfang der Zeile.
*/
public function clearLineToBeginning(): self
{
if ($this->output->isTerminal()) {
$this->output->writeRaw(ScreenControlCode::CLEAR_LINE_LEFT->format());
}
return $this;
}
/**
* Aktiviert den alternativen Buffer (für Vollbild-Anwendungen).
*/
public function useAlternateBuffer(): self
{
if ($this->output->isTerminal()) {
$this->output->writeRaw(ScreenControlCode::ALTERNATE_BUFFER->format());
}
return $this;
}
/**
* Kehrt zum Hauptbuffer zurück.
*/
public function useMainBuffer(): self
{
if ($this->output->isTerminal()) {
$this->output->writeRaw(ScreenControlCode::MAIN_BUFFER->format());
}
return $this;
}
}

View File

@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace App\Framework\Console\Screen;
/**
* Bildschirm-Steuerungscodes nach ANSI-Standard.
*/
enum ScreenControlCode: string
{
case CLEAR_ALL = "2J"; // Gesamten Bildschirm löschen
case CLEAR_ABOVE = "1J"; // Bildschirm vom Cursor nach oben löschen
case CLEAR_BELOW = "0J"; // Bildschirm vom Cursor nach unten löschen
case CLEAR_LINE = "2K"; // Komplette Zeile löschen
case CLEAR_LINE_LEFT = "1K"; // Zeile vom Cursor nach links löschen
case CLEAR_LINE_RIGHT = "0K"; // Zeile vom Cursor nach rechts löschen
case SCROLL_UP = "S"; // Bildschirm nach oben scrollen
case SCROLL_DOWN = "T"; // Bildschirm nach unten scrollen
case SAVE_SCREEN = "?47h"; // Bildschirm speichern
case RESTORE_SCREEN = "?47l"; // Bildschirm wiederherstellen
case ALTERNATE_BUFFER = "?1049h"; // Alternativen Puffer aktivieren
case MAIN_BUFFER = "?1049l"; // Hauptpuffer wiederherstellen
/**
* Formatiert den ANSI-Steuerungscode korrekt.
*/
public function format(int ...$params): string
{
// Wenn keine Parameter, dann ohne formatieren
if (empty($params)) {
return "\033[{$this->value}";
}
// Wenn Parameter vorhanden sind, formatieren
$paramStr = implode(';', $params);
return "\033[{$paramStr}{$this->value}";
}
}

View File

@@ -0,0 +1,145 @@
<?php
declare(strict_types=1);
namespace App\Framework\Console\Screen;
use App\Framework\Console\ConsoleOutput;
/**
* Verwaltet intelligentes Bildschirm-Management.
*/
final class ScreenManager
{
private ClearStrategy $strategy = ClearStrategy::SMART;
private bool $interactiveMode = false;
private ?ScreenType $lastScreenType = null;
private int $screenCount = 0;
public function __construct(
private ConsoleOutput $output
) {}
/**
* Setzt die Löschstrategie.
*/
public function setStrategy(ClearStrategy $strategy): self
{
$this->strategy = $strategy;
return $this;
}
/**
* Aktiviert/deaktiviert den interaktiven Modus.
*/
public function setInteractiveMode(bool $interactive = true): self
{
$this->interactiveMode = $interactive;
return $this;
}
/**
* Markiert den Beginn eines neuen Bildschirms.
* Diese Methode rufst du VOR der Ausgabe auf.
*/
public function newScreen(ScreenType $type = ScreenType::CONTENT): self
{
if ($this->shouldClear($type)) {
$this->output->display()->clear();
}
$this->lastScreenType = $type;
$this->screenCount++;
return $this;
}
/**
* Convenience-Methoden für häufige Screen-Typen.
*/
public function newMenu(): self
{
return $this->newScreen(ScreenType::MENU);
}
public function newDialog(): self
{
return $this->newScreen(ScreenType::DIALOG);
}
public function newContent(): self
{
return $this->newScreen(ScreenType::CONTENT);
}
public function newLog(): self
{
return $this->newScreen(ScreenType::LOG);
}
public function newProgress(): self
{
return $this->newScreen(ScreenType::PROGRESS);
}
/**
* Zeigt eine temporäre Nachricht für eine bestimmte Zeit an.
*/
public function temporary(string $message, int $seconds = 2): self
{
$this->output->writeLine($message);
sleep($seconds);
$this->output->display()->clearLine();
return $this;
}
/**
* Wartet auf Benutzereingabe.
*/
public function waitForInput(): self
{
if ($this->output->isTerminal()) {
fread(STDIN, 1);
}
return $this;
}
/**
* Entscheidet, ob der Bildschirm gelöscht werden soll.
*/
private function shouldClear(ScreenType $type): bool
{
if (!$this->output->isTerminal()) {
return false;
}
return match($this->strategy) {
ClearStrategy::NEVER => false,
ClearStrategy::ALWAYS => true,
ClearStrategy::ON_NEW_SCREEN => $type === ScreenType::MENU || $type === ScreenType::DIALOG,
ClearStrategy::SMART => $this->shouldClearSmart($type),
};
}
/**
* Intelligente Entscheidung für das Löschen.
*/
private function shouldClearSmart(ScreenType $type): bool
{
// Nie löschen für Logs und Progress
if ($type === ScreenType::LOG || $type === ScreenType::PROGRESS) {
return false;
}
// Immer löschen für Menüs und Dialoge im interaktiven Modus
if ($this->interactiveMode && ($type === ScreenType::MENU || $type === ScreenType::DIALOG)) {
return true;
}
// Für Content: nur löschen wenn der letzte Screen ein anderer Typ war
if ($type === ScreenType::CONTENT) {
return $this->lastScreenType !== null && $this->lastScreenType !== ScreenType::CONTENT;
}
return false;
}
}

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace App\Framework\Console\Screen;
/**
* Typen von Bildschirminhalten.
*/
enum ScreenType
{
case MENU; // Menüs und Navigation
case DIALOG; // Dialoge und Formulare
case CONTENT; // Normale Inhaltsanzeige
case LOG; // Log-Ausgaben
case PROGRESS; // Fortschrittsanzeigen
case INFO; // Informationsmeldungen
}

View File

@@ -0,0 +1,119 @@
<?php
namespace App\Framework\Console;
class Spinner
{
private ConsoleOutputInterface $output;
private string $message;
private array $frames;
private int $currentFrame = 0;
private float $startTime;
private bool $active = false;
private float $interval;
private int $updateCount = 0;
public function __construct(
ConsoleOutputInterface $output,
string $message = 'Loading...',
SpinnerStyle $style = SpinnerStyle::DOTS,
float $interval = 0.1
) {
$this->output = $output;
$this->message = $message;
$this->frames = $style->getFrames();
$this->interval = $interval;
}
/**
* Startet den Spinner
*/
public function start(): self
{
$this->startTime = microtime(true);
$this->active = true;
$this->update();
return $this;
}
/**
* Stoppt den Spinner mit einer Erfolgs-, Fehler- oder neutralen Nachricht
*/
public function stop(?string $message = null, ?ConsoleColor $color = null): self
{
$this->active = false;
// Lösche die aktuelle Zeile
$this->output->write("\r\033[2K");
if ($message !== null) {
$this->output->writeLine($message, $color);
}
return $this;
}
/**
* Stoppt den Spinner mit einer Erfolgsmeldung
*/
public function success(string $message): self
{
return $this->stop("" . $message, ConsoleColor::BRIGHT_GREEN);
}
/**
* Stoppt den Spinner mit einer Fehlermeldung
*/
public function error(string $message): self
{
return $this->stop("" . $message, ConsoleColor::BRIGHT_RED);
}
/**
* Ändert die Nachricht des Spinners während er läuft
*/
public function setMessage(string $message): self
{
$this->message = $message;
if ($this->active) {
$this->update();
}
return $this;
}
/**
* Aktualisiert die Anzeige des Spinners
*/
public function update(): self
{
if (!$this->active) {
return $this;
}
$this->updateCount++;
// Aktualisiere nur in festgelegten Intervallen
$elapsed = microtime(true) - $this->startTime;
$expectedUpdates = floor($elapsed / $this->interval);
if ($this->updateCount < $expectedUpdates) {
$this->currentFrame = ($this->currentFrame + 1) % count($this->frames);
// Lösche die aktuelle Zeile und schreibe den aktualisierten Spinner
$frame = $this->frames[$this->currentFrame];
$this->output->write("\r\033[2K{$frame} {$this->message}");
$this->updateCount = $expectedUpdates;
}
return $this;
}
/**
* Beendet den Spinner ohne Ausgabe
*/
public function clear(): self
{
return $this->stop();
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace App\Framework\Console;
enum SpinnerStyle: string
{
case DOTS = 'dots';
case LINE = 'line';
case BOUNCE = 'bounce';
case ARROW = 'arrow';
/**
* Gibt die Frames für den gewählten Stil zurück
*/
public function getFrames(): array
{
return match($this) {
self::DOTS => ['. ', '.. ', '...', ' ..', ' .', ' '],
self::LINE => ['|', '/', '-', '\\'],
self::BOUNCE => ['⠈', '⠐', '⠠', '⢀', '⡀', '⠄', '⠂', '⠁'],
self::ARROW => ['←', '↖', '↑', '↗', '→', '↘', '↓', '↙']
};
}
}