chore: complete update
This commit is contained in:
257
src/Framework/Console/Components/InteractiveMenu.php
Normal file
257
src/Framework/Console/Components/InteractiveMenu.php
Normal 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().
|
||||
}
|
||||
205
src/Framework/Console/Components/Table.php
Normal file
205
src/Framework/Console/Components/Table.php
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
124
src/Framework/Console/Components/TextBox.php
Normal file
124
src/Framework/Console/Components/TextBox.php
Normal 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 ?: [''];
|
||||
}
|
||||
}
|
||||
215
src/Framework/Console/Components/TreeHelper.php
Normal file
215
src/Framework/Console/Components/TreeHelper.php
Normal 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;
|
||||
}
|
||||
}
|
||||
168
src/Framework/Console/ConsoleApplication.php
Normal file
168
src/Framework/Console/ConsoleApplication.php
Normal 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]");
|
||||
}
|
||||
}
|
||||
52
src/Framework/Console/ConsoleColor.php
Normal file
52
src/Framework/Console/ConsoleColor.php
Normal 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";
|
||||
}
|
||||
|
||||
}
|
||||
15
src/Framework/Console/ConsoleCommand.php
Normal file
15
src/Framework/Console/ConsoleCommand.php
Normal 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 = ''
|
||||
) {}
|
||||
}
|
||||
24
src/Framework/Console/ConsoleCommandMapper.php
Normal file
24
src/Framework/Console/ConsoleCommandMapper.php
Normal 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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
20
src/Framework/Console/ConsoleFormat.php
Normal file
20
src/Framework/Console/ConsoleFormat.php
Normal 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";
|
||||
}
|
||||
}
|
||||
160
src/Framework/Console/ConsoleInput.php
Normal file
160
src/Framework/Console/ConsoleInput.php
Normal 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;
|
||||
}
|
||||
}
|
||||
61
src/Framework/Console/ConsoleInputInterface.php
Normal file
61
src/Framework/Console/ConsoleInputInterface.php
Normal 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;
|
||||
}
|
||||
166
src/Framework/Console/ConsoleOutput.php
Normal file
166
src/Framework/Console/ConsoleOutput.php
Normal 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);
|
||||
}
|
||||
}
|
||||
61
src/Framework/Console/ConsoleOutputInterface.php
Normal file
61
src/Framework/Console/ConsoleOutputInterface.php
Normal 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;
|
||||
}
|
||||
89
src/Framework/Console/ConsoleStyle.php
Normal file
89
src/Framework/Console/ConsoleStyle.php
Normal 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);
|
||||
}
|
||||
}
|
||||
193
src/Framework/Console/DemoCommand.php
Normal file
193
src/Framework/Console/DemoCommand.php
Normal 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';
|
||||
}
|
||||
}
|
||||
173
src/Framework/Console/DemoCommand/ScreenDemoCommand.php
Normal file
173
src/Framework/Console/DemoCommand/ScreenDemoCommand.php
Normal 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);
|
||||
}
|
||||
}
|
||||
50
src/Framework/Console/Examples/ProgressBarExample.php
Normal file
50
src/Framework/Console/Examples/ProgressBarExample.php
Normal 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;
|
||||
}
|
||||
}
|
||||
72
src/Framework/Console/Examples/SpinnerExample.php
Normal file
72
src/Framework/Console/Examples/SpinnerExample.php
Normal 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;
|
||||
}
|
||||
}
|
||||
60
src/Framework/Console/Examples/TableExample.php
Normal file
60
src/Framework/Console/Examples/TableExample.php
Normal 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;
|
||||
}
|
||||
}
|
||||
64
src/Framework/Console/Examples/TextBoxExample.php
Normal file
64
src/Framework/Console/Examples/TextBoxExample.php
Normal 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;
|
||||
}
|
||||
}
|
||||
89
src/Framework/Console/Examples/TreeExample.php
Normal file
89
src/Framework/Console/Examples/TreeExample.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
7
src/Framework/Console/Exceptions/ConsoleException.php
Normal file
7
src/Framework/Console/Exceptions/ConsoleException.php
Normal file
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\Console\Exceptions;
|
||||
|
||||
class ConsoleException extends \Exception
|
||||
{
|
||||
}
|
||||
74
src/Framework/Console/ExitCode.php
Normal file
74
src/Framework/Console/ExitCode.php
Normal 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();
|
||||
}
|
||||
}
|
||||
185
src/Framework/Console/ProgressBar.php
Normal file
185
src/Framework/Console/ProgressBar.php
Normal 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);
|
||||
}
|
||||
}
|
||||
106
src/Framework/Console/README.md
Normal file
106
src/Framework/Console/README.md
Normal 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.
|
||||
15
src/Framework/Console/Screen/ClearStrategy.php
Normal file
15
src/Framework/Console/Screen/ClearStrategy.php
Normal 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
|
||||
}
|
||||
127
src/Framework/Console/Screen/Cursor.php
Normal file
127
src/Framework/Console/Screen/Cursor.php
Normal 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;
|
||||
}
|
||||
}
|
||||
48
src/Framework/Console/Screen/CursorControlCode.php
Normal file
48
src/Framework/Console/Screen/CursorControlCode.php
Normal 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}";
|
||||
}
|
||||
}
|
||||
107
src/Framework/Console/Screen/Display.php
Normal file
107
src/Framework/Console/Screen/Display.php
Normal 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;
|
||||
}
|
||||
}
|
||||
38
src/Framework/Console/Screen/ScreenControlCode.php
Normal file
38
src/Framework/Console/Screen/ScreenControlCode.php
Normal 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}";
|
||||
}
|
||||
}
|
||||
145
src/Framework/Console/Screen/ScreenManager.php
Normal file
145
src/Framework/Console/Screen/ScreenManager.php
Normal 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;
|
||||
}
|
||||
}
|
||||
17
src/Framework/Console/Screen/ScreenType.php
Normal file
17
src/Framework/Console/Screen/ScreenType.php
Normal 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
|
||||
}
|
||||
119
src/Framework/Console/Spinner.php
Normal file
119
src/Framework/Console/Spinner.php
Normal 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();
|
||||
}
|
||||
}
|
||||
24
src/Framework/Console/SpinnerStyle.php
Normal file
24
src/Framework/Console/SpinnerStyle.php
Normal 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 => ['←', '↖', '↑', '↗', '→', '↘', '↓', '↙']
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user