Explorar o código

Standardize error output handling

michelphp hai 20 horas
pai
achega
37dd3a5173
Modificáronse 5 ficheiros con 650 adicións e 639 borrados
  1. 70 70
      src/Argument/CommandArgument.php
  2. 89 89
      src/Option/CommandOption.php
  3. 61 51
      src/Output.php
  4. 418 418
      src/Output/ConsoleOutput.php
  5. 12 11
      src/OutputInterface.php

+ 70 - 70
src/Argument/CommandArgument.php

@@ -1,70 +1,70 @@
-<?php
-
-namespace Michel\Console\Argument;
-
-final class CommandArgument
-{
-    private string $name;
-    private bool $isRequired;
-    private $defaultValue;
-    private ?string $description;
-
-    public  function __construct(string $name, bool $isRequired = false, $defaultValue = null, ?string $description = null)
-    {
-        if ($name === '') {
-            throw new \InvalidArgumentException("Option name cannot be empty.");
-        }
-        if (!ctype_alpha($name)) {
-            throw new \InvalidArgumentException("Option name must contain only letters. '$name' is invalid.");
-        }
-
-        if ($isRequired && $defaultValue !== null) {
-            throw new \LogicException("Argument '$name' cannot be required and have a default value.");
-        }
-
-        $this->name = strtolower($name);
-        $this->isRequired = $isRequired;
-        $this->defaultValue = $defaultValue;
-        $this->description = $description;
-    }
-
-    public function validate($value): void
-    {
-        if ($this->isRequired && empty($value)) {
-            throw new \InvalidArgumentException(sprintf('The required argument "%s" was not provided.', $this->name));
-        }
-    }
-
-    public function getName(): string
-    {
-        return $this->name;
-    }
-
-    public function isRequired(): bool
-    {
-        return $this->isRequired;
-    }
-
-    /**
-     * @return mixed|null
-     */
-    public function getDefaultValue()
-    {
-        return $this->defaultValue;
-    }
-
-    public function getDescription(): ?string
-    {
-        return $this->description;
-    }
-
-    public static function required(string $name, ?string $description = null): self
-    {
-        return new self($name, true, null, $description);
-    }
-
-    public static function optional(string $name, $default = null, ?string $description = null): self
-    {
-        return new self($name, false, $default, $description);
-    }
-}
+<?php
+
+namespace Michel\Console\Argument;
+
+final class CommandArgument
+{
+    private string $name;
+    private bool $isRequired;
+    private $defaultValue;
+    private ?string $description;
+
+    public  function __construct(string $name, bool $isRequired = false, $defaultValue = null, ?string $description = null)
+    {
+        if ($name === '') {
+            throw new \InvalidArgumentException("Option name cannot be empty.");
+        }
+        if (!ctype_alpha($name)) {
+            throw new \InvalidArgumentException("Option name must contain only letters. '$name' is invalid.");
+        }
+
+        if ($isRequired && $defaultValue !== null) {
+            throw new \LogicException("Argument '$name' cannot be required and have a default value.");
+        }
+
+        $this->name = strtolower($name);
+        $this->isRequired = $isRequired;
+        $this->defaultValue = $defaultValue;
+        $this->description = $description;
+    }
+
+    public function validate($value): void
+    {
+        if ($this->isRequired && empty($value)) {
+            throw new \InvalidArgumentException(sprintf('The required argument "%s" was not provided.', $this->name));
+        }
+    }
+
+    public function getName(): string
+    {
+        return $this->name;
+    }
+
+    public function isRequired(): bool
+    {
+        return $this->isRequired;
+    }
+
+    /**
+     * @return mixed|null
+     */
+    public function getDefaultValue()
+    {
+        return $this->defaultValue;
+    }
+
+    public function getDescription(): ?string
+    {
+        return $this->description;
+    }
+
+    public static function required(string $name, ?string $description = null): self
+    {
+        return new self($name, true, null, $description);
+    }
+
+    public static function optional(string $name, $default = null, ?string $description = null): self
+    {
+        return new self($name, false, $default, $description);
+    }
+}

+ 89 - 89
src/Option/CommandOption.php

@@ -1,89 +1,89 @@
-<?php
-
-namespace Michel\Console\Option;
-
-final class CommandOption
-{
-    private string $name;
-    private ?string $shortcut;
-    private ?string $description;
-    private bool $isFlag;
-
-    /**
-     * @var bool|float|int|string
-     */
-    private $default = null;
-
-    public function __construct(
-        string  $name,
-        ?string $shortcut = null,
-        ?string $description = null,
-        bool    $isFlag = false,
-        $default = null
-    )
-    {
-
-        if ($name === '') {
-            throw new \InvalidArgumentException("Option name cannot be empty.");
-        }
-
-        foreach (explode('-', $name) as $part) {
-            if ($part === '' || !ctype_alpha($part)) {
-                throw new \InvalidArgumentException("Option name must contain only letters and dashes. '$name' is invalid.");
-            }
-        }
-
-        if ($shortcut !== null && (strlen($shortcut) !== 1 || !ctype_alpha($shortcut))) {
-            throw new \InvalidArgumentException('Shortcut must be a single character and contain only letters. "' . $shortcut . '" is invalid.');
-        }
-
-        if (!is_null($default) && !is_scalar($default)) {
-            throw new \InvalidArgumentException(
-                sprintf(
-                    'Invalid default value: expected a scalar (int, float, string, or bool), got "%s".',
-                    gettype($default)
-                )
-            );
-        }
-
-        $this->name = strtolower($name);
-        $this->shortcut = $shortcut !== null ? strtolower($shortcut) : null;
-        $this->description = $description;
-        $this->isFlag = $isFlag;
-        $this->default = $default;
-    }
-
-    public function getName(): string
-    {
-        return $this->name;
-    }
-
-    public function getShortcut(): ?string
-    {
-        return $this->shortcut;
-    }
-
-    public function getDescription(): ?string
-    {
-        return $this->description;
-    }
-
-    public function isFlag(): bool
-    {
-        return $this->isFlag;
-    }
-
-    public function getDefaultValue()
-    {
-        return $this->default;
-    }
-
-    public static function flag(string $name, ?string $shortcut = null, ?string $description = null): self {
-        return new self($name, $shortcut, $description, true, false);
-    }
-    
-    public static function withValue(string $name, ?string $shortcut = null, ?string $description = null, $default = null): self {
-        return new self($name, $shortcut, $description, false, $default);
-    }
-
-}
+<?php
+
+namespace Michel\Console\Option;
+
+final class CommandOption
+{
+    private string $name;
+    private ?string $shortcut;
+    private ?string $description;
+    private bool $isFlag;
+
+    /**
+     * @var bool|float|int|string
+     */
+    private $default = null;
+
+    public function __construct(
+        string  $name,
+        ?string $shortcut = null,
+        ?string $description = null,
+        bool    $isFlag = false,
+        $default = null
+    )
+    {
+
+        if ($name === '') {
+            throw new \InvalidArgumentException("Option name cannot be empty.");
+        }
+
+        foreach (explode('-', $name) as $part) {
+            if ($part === '' || !ctype_alpha($part)) {
+                throw new \InvalidArgumentException("Option name must contain only letters and dashes. '$name' is invalid.");
+            }
+        }
+
+        if ($shortcut !== null && (strlen($shortcut) !== 1 || !ctype_alpha($shortcut))) {
+            throw new \InvalidArgumentException('Shortcut must be a single character and contain only letters. "' . $shortcut . '" is invalid.');
+        }
+
+        if (!is_null($default) && !is_scalar($default)) {
+            throw new \InvalidArgumentException(
+                sprintf(
+                    'Invalid default value: expected a scalar (int, float, string, or bool), got "%s".',
+                    gettype($default)
+                )
+            );
+        }
+
+        $this->name = strtolower($name);
+        $this->shortcut = $shortcut !== null ? strtolower($shortcut) : null;
+        $this->description = $description;
+        $this->isFlag = $isFlag;
+        $this->default = $default;
+    }
+
+    public function getName(): string
+    {
+        return $this->name;
+    }
+
+    public function getShortcut(): ?string
+    {
+        return $this->shortcut;
+    }
+
+    public function getDescription(): ?string
+    {
+        return $this->description;
+    }
+
+    public function isFlag(): bool
+    {
+        return $this->isFlag;
+    }
+
+    public function getDefaultValue()
+    {
+        return $this->default;
+    }
+
+    public static function flag(string $name, ?string $shortcut = null, ?string $description = null): self {
+        return new self($name, $shortcut, $description, true, false);
+    }
+    
+    public static function withValue(string $name, ?string $shortcut = null, ?string $description = null, $default = null): self {
+        return new self($name, $shortcut, $description, false, $default);
+    }
+
+}

+ 61 - 51
src/Output.php

@@ -1,51 +1,61 @@
-<?php
-
-namespace Michel\Console;
-
-use RuntimeException;
-use const PHP_EOL;
-
-final class Output implements OutputInterface
-{
-    /**
-     * @var callable
-     */
-    private $output;
-
-    public function __construct(callable $output = null)
-    {
-        if ($output === null) {
-            $output = function ($message) {
-                fwrite(STDOUT, $message);
-            };
-        }
-        $this->output = $output;
-    }
-
-    /**
-     * @var bool
-     */
-    private bool $verbose = false;
-
-    public function write(string $message): void
-    {
-        $output = $this->output;
-        $output($message);
-    }
-
-    public function writeln(string $message): void
-    {
-        $this->write($message);
-        $this->write(PHP_EOL);
-    }
-
-    public function setVerbose(bool $verbose): void
-    {
-        $this->verbose = $verbose;
-    }
-
-    public function isVerbose(): bool
-    {
-        return $this->verbose;
-    }
-}
+<?php
+
+namespace Michel\Console;
+
+use RuntimeException;
+use const PHP_EOL;
+
+final class Output implements OutputInterface
+{
+    /**
+     * @var callable
+     */
+    private $output;
+
+    public function __construct(callable $output = null)
+    {
+        if ($output === null) {
+            $output = function ($message, $error = false) {
+                if ($error) {
+                    fwrite(STDERR, $message);
+                    return;
+                }
+                fwrite(STDOUT, $message);
+            };
+        }
+        $this->output = $output;
+    }
+
+    /**
+     * @var bool
+     */
+    private bool $verbose = false;
+
+    public function error(string $message): void
+    {
+        $output = $this->output;
+        $output($message, true);
+    }
+
+    public function write(string $message): void
+    {
+        $output = $this->output;
+        $output($message);
+    }
+
+    public function writeln(string $message): void
+    {
+        $this->write($message);
+        $this->write(PHP_EOL);
+    }
+
+    public function setVerbose(bool $verbose): void
+    {
+        $this->verbose = $verbose;
+    }
+
+    public function isVerbose(): bool
+    {
+        return $this->verbose;
+    }
+}

+ 418 - 418
src/Output/ConsoleOutput.php

@@ -1,418 +1,418 @@
-<?php
-
-namespace Michel\Console\Output;
-
-
-use InvalidArgumentException;
-use Michel\Console\OutputInterface;
-use RuntimeException;
-
-final class ConsoleOutput implements OutputInterface
-{
-    const FOREGROUND_COLORS = [
-        'black' => '0;30',
-        'dark_gray' => '1;30',
-        'green' => '0;32',
-        'light_green' => '1;32',
-        'red' => '0;31',
-        'light_red' => '1;31',
-        'yellow' => '0;33',
-        'light_yellow' => '1;33',
-        'blue' => '0;34',
-        'dark_blue' => '0;34',
-        'light_blue' => '1;34',
-        'purple' => '0;35',
-        'light_purple' => '1;35',
-        'cyan' => '0;36',
-        'light_cyan' => '1;36',
-        'light_gray' => '0;37',
-        'white' => '1;37',
-    ];
-
-    const BG_COLORS = [
-        'black' => '40',
-        'red' => '41',
-        'green' => '42',
-        'yellow' => '43',
-        'blue' => '44',
-        'magenta' => '45',
-        'cyan' => '46',
-        'light_gray' => '47',
-    ];
-
-    private OutputInterface $output;
-
-    public static function create(OutputInterface $output): ConsoleOutput
-    {
-        return new self($output);
-    }
-
-    public function __construct(OutputInterface $output)
-    {
-        $this->output = $output;
-    }
-
-    public function success(string $message): void
-    {
-        [$formattedMessage, $lineLength, $color] = $this->formatMessage('OK', $message, 'green');
-        $this->outputMessage($formattedMessage, $lineLength, $color);
-    }
-
-    public function error(string $message): void
-    {
-        [$formattedMessage, $lineLength, $color] = $this->formatMessage('ERROR', $message, 'red');
-        $this->outputMessage($formattedMessage, $lineLength, $color, true);
-    }
-
-    public function warning(string $message): void
-    {
-        [$formattedMessage, $lineLength, $color] = $this->formatMessage('WARNING', $message, 'yellow');
-        $this->outputMessage($formattedMessage, $lineLength, $color);
-    }
-
-    public function info(string $message): void
-    {
-        [$formattedMessage, $lineLength, $color] = $this->formatMessage('INFO', $message, 'blue');
-        $this->outputMessage($formattedMessage, $lineLength, $color);
-    }
-
-    public function debug(string $message): void
-    {
-        if (!$this->output->isVerbose()) {
-            return;
-        }
-        [$formattedMessage, $lineLength, $color] = $this->formatMessage('DEBUG', $message, 'cyan');
-        $this->outputMessage($formattedMessage, $lineLength, $color);
-    }
-
-
-    public function title(string $message): void
-    {
-        $consoleWidth = $this->geTerminalWidth();
-        $titleLength = mb_strlen($message);
-        $underline = str_repeat('=', min($consoleWidth, $titleLength));
-
-        $this->write(PHP_EOL);
-        $this->write($message);
-        $this->write(PHP_EOL);
-        $this->write($underline);
-        $this->write(PHP_EOL);
-    }
-
-    public function list(array $items): void
-    {
-        foreach ($items as $item) {
-            $item = $this->variableToString($item);
-            $this->write('- ' . $item);
-            $this->write(PHP_EOL);
-        }
-        $this->write(PHP_EOL);
-    }
-
-    public function listKeyValues(array $items, bool $inlined = false): void
-    {
-        $maxKeyLength = 0;
-        if ($inlined) {
-            foreach ($items as $key => $value) {
-                $keyLength = mb_strlen($key);
-                if ($keyLength > $maxKeyLength) {
-                    $maxKeyLength = $keyLength;
-                }
-            }
-        }
-
-        foreach ($items as $key => $value) {
-            $value = $this->variableToString($value);
-            $value = implode(' ', explode(PHP_EOL, $value));
-            $key = str_pad($key, $maxKeyLength, ' ', STR_PAD_RIGHT);
-            $this->writeColor($key, 'green');
-            $this->write(' : ');
-            $this->writeColor($value, 'white');
-            $this->write(PHP_EOL);
-        }
-        $this->write(PHP_EOL);
-    }
-
-    public function indentedList(array $items, int $indentLevel = 1): void
-    {
-        if ($indentLevel == 1) {
-            $this->write(PHP_EOL);
-        }
-
-        foreach ($items as $item) {
-            if (is_array($item)) {
-                $this->indentedList($item, $indentLevel + 1);
-            } else {
-                $indentation = '';
-                if ($indentLevel > 1) {
-                    $indentation = str_repeat('  ', $indentLevel); // Indent with spaces
-                }
-                $this->writeColor($indentation . '- ', 'red');
-                $this->writeColor($item, 'white');
-                $this->write(PHP_EOL);
-            }
-        }
-
-        if ($indentLevel == 1) {
-            $this->write(PHP_EOL);
-        }
-    }
-
-    public function numberedList(array $items)
-    {
-        foreach ($items as $index => $item) {
-            $this->writeColor(($index + 1) . '. ', 'white');
-            $this->writeColor($item, 'green');
-            $this->write(PHP_EOL);
-        }
-        $this->write(PHP_EOL);
-    }
-
-    public function table(array $headers, array $rows): void
-    {
-        $maxWidth = $this->geTerminalWidth();
-        $maxWidthPerColumn = $maxWidth / count($headers);
-        $columnWidths = array_map(function ($header) {
-            return mb_strlen($header);
-        }, $headers);
-
-        $processedRows = [];
-        foreach ($rows as $row) {
-            $row = array_values($row);
-            $processedRow = [];
-            foreach ($row as $index => $column) {
-                $column = $this->variableToString($column);
-                $lines = explode(PHP_EOL, trim($column));
-                foreach ($lines as $i => $line) {
-                    $maxWidth = max($columnWidths[$index], mb_strlen($line));
-                    if ($maxWidth > $maxWidthPerColumn) {
-                        $maxWidth = $maxWidthPerColumn;
-                    }
-                    $columnWidths[$index] = $maxWidth;
-                    if (mb_strlen($line) > $maxWidth) {
-                        $lines[$i] = mb_substr($line, 0, $maxWidth) . '...';
-                    }
-                }
-                $processedRow[$index] = $lines;
-            }
-            $processedRows[] = $processedRow;
-        }
-
-        foreach ($headers as $index => $header) {
-            $this->write(str_pad($header, $columnWidths[$index] + 2));
-        }
-        $this->write(PHP_EOL);
-        $this->write(str_repeat('-', array_sum($columnWidths) + count($columnWidths) * 2));
-        $this->write(PHP_EOL);
-
-        foreach ($processedRows as $row) {
-            $maxLines = max(array_map('count', $row));
-            for ($lineIndex = 0; $lineIndex < $maxLines; $lineIndex++) {
-                foreach ($row as $index => $lines) {
-                    $line = $lines[$lineIndex] ?? ''; // Récupère la ligne actuelle ou une chaîne vide
-                    $this->write(str_pad($line, $columnWidths[$index] + 2));
-                }
-                $this->write(PHP_EOL);
-            }
-        }
-    }
-
-    public function progressBar(int $total, int $current): void
-    {
-        $barWidth = 50;
-        $progress = ($current / $total) * $barWidth;
-        $bar = str_repeat('#', (int)$progress) . str_repeat(' ', $barWidth - (int)$progress);
-        $this->write(sprintf("\r[%s] %d%%", $bar, ($current / $total) * 100));
-        if ($current === $total) {
-            $this->write(PHP_EOL);
-        }
-    }
-
-    public function confirm(string $message): bool
-    {
-        $this->writeColor($message . ' [y/n]: ', 'yellow');
-
-        $handle = fopen('php://stdin', 'r');
-        $input = trim(fgets($handle));
-        fclose($handle);
-
-        return in_array($input, ['y', 'yes', 'Y', 'YES'], true);
-    }
-
-    public function ask(string $question, bool $hidden = false, bool $required = false): string
-    {
-        $this->writeColor($question . ': ', 'cyan');
-
-        if ($hidden) {
-            if (strncasecmp(PHP_OS, 'WIN', 3) == 0) {
-                throw new RuntimeException('Windows platform is not supported for hidden input');
-            } else {
-                system('stty -echo');
-                $input = trim(fgets(STDIN));
-                system('stty echo');
-                $this->write(PHP_EOL);
-            }
-        } else {
-            $handle = fopen('php://stdin', 'r');
-            $input = trim(fgets($handle));
-            fclose($handle);
-        }
-
-        if ($required && empty($input)) {
-            throw new InvalidArgumentException('Response cannot be empty');
-        }
-
-        return $input;
-    }
-
-    public function spinner(int $duration = 3): void
-    {
-        $spinnerChars = ['|', '/', '-', '\\'];
-        $time = microtime(true);
-        while ((microtime(true) - $time) < $duration) {
-            foreach ($spinnerChars as $char) {
-                $this->write("\r$char");
-                usleep(100000);
-            }
-        }
-        $this->write("\r");
-    }
-
-    public function json(array $data): void
-    {
-        $jsonOutput = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
-
-        if (json_last_error() !== JSON_ERROR_NONE) {
-            $this->writeColor("Error encoding JSON: " . json_last_error_msg() . PHP_EOL, 'red');
-            return;
-        }
-        $this->write($jsonOutput . PHP_EOL);
-    }
-
-    public function boxed(string $message, string $borderChar = '*', int $padding = 1): void
-    {
-        $this->write(PHP_EOL);
-        $lineLength = mb_strlen($message);
-        $boxWidth = $this->geTerminalWidth();
-        if ($lineLength > $boxWidth) {
-            $lineLength = $boxWidth - ($padding * 2) - 2;
-        }
-        $lines = explode('|', wordwrap($message, $lineLength, '|', true));
-        $border = str_repeat($borderChar, $lineLength + ($padding * 2) + 2);
-
-        $this->write($border . PHP_EOL);
-        foreach ($lines as $line) {
-            $strPad = str_repeat(' ', $padding);
-            $this->write($borderChar . $strPad . str_pad($line, $lineLength) . $strPad . $borderChar . PHP_EOL);
-        }
-        $this->write($border . PHP_EOL);
-        $this->write(PHP_EOL);
-    }
-
-    public function writeColor(string $message, ?string $color = null, ?string $background = null, bool $isError = false): void
-    {
-
-        $formattedMessage = '';
-
-        if ($color) {
-            $formattedMessage .= "\033[" . self::FOREGROUND_COLORS[$color] . 'm';
-        }
-        if ($background) {
-            $formattedMessage .= "\033[" . self::BG_COLORS[$background] . 'm';
-        }
-
-        $formattedMessage .= $message . "\033[0m";
-
-        $this->write($formattedMessage, $isError);
-    }
-
-    public function write(string $message, bool $isError = false): void
-    {
-        if ($isError) {
-            fwrite(STDERR, $message);
-            return;
-        }
-        $this->output->write($message);
-    }
-
-    public function writeln(string $message): void
-    {
-        $this->output->writeln($message);
-    }
-
-    private function outputMessage($formattedMessage, int $lineLength, string $color, bool $isError = false): void
-    {
-        $this->write(PHP_EOL, $isError);
-        $this->writeColor(str_repeat(' ', $lineLength), 'white', $color, $isError);
-        $this->write(PHP_EOL, $isError);
-
-        if (is_string($formattedMessage)) {
-            $formattedMessage = [$formattedMessage];
-        }
-
-        foreach ($formattedMessage as $line) {
-            $line = str_pad($line, $lineLength).PHP_EOL;
-            $this->writeColor($line, 'white', $color, $isError);
-        }
-
-        $this->writeColor(str_repeat(' ', $lineLength), 'white', $color, $isError);
-        $this->write(PHP_EOL, $isError);
-        $this->write(PHP_EOL, $isError);
-    }
-
-    private function formatMessage(string $prefix, string $message, string $color): array
-    {
-        $formattedMessage = sprintf('[%s] %s', $prefix, trim($message));
-        $lineLength = mb_strlen($formattedMessage);
-        $consoleWidth = $this->geTerminalWidth();
-
-        if ($lineLength > $consoleWidth) {
-            $lineLength = $consoleWidth;
-            $lines = explode('|', wordwrap($formattedMessage, $lineLength, '|', true));
-            $formattedMessage = array_map(function ($line) use ($lineLength) {
-                return str_pad($line, $lineLength);
-            }, $lines);
-        }
-        return [$formattedMessage, $lineLength, $color];
-    }
-
-    private function geTerminalWidth(): int
-    {
-        $width = 85;
-        if (getenv('TERM')) {
-            $width = ((int) @exec('tput cols') ?: $width);
-        }
-        return $width - 5;
-    }
-
-    private function variableToString($variable): string
-    {
-        if (is_object($variable)) {
-            return 'Object: ' . get_class($variable);
-        } elseif (is_array($variable)) {
-            $variables = [];
-            foreach ($variable as $item) {
-                $variables[] = $this->variableToString($item);
-            }
-            return var_export($variables, true);
-        } elseif (is_resource($variable)) {
-            return (string)$variable;
-        } elseif (is_null($variable)) {
-            return 'NULL';
-        } elseif (is_string($variable)) {
-            return $variable;
-        }
-
-        return var_export($variable, true);
-    }
-
-    public function setVerbose(bool $verbose): void
-    {
-        $this->output->setVerbose($verbose);
-    }
-
-    public function isVerbose(): bool
-    {
-        return  $this->output->isVerbose();
-    }
-}
+<?php
+
+namespace Michel\Console\Output;
+
+
+use InvalidArgumentException;
+use Michel\Console\OutputInterface;
+use RuntimeException;
+
+final class ConsoleOutput implements OutputInterface
+{
+    const FOREGROUND_COLORS = [
+        'black' => '0;30',
+        'dark_gray' => '1;30',
+        'green' => '0;32',
+        'light_green' => '1;32',
+        'red' => '0;31',
+        'light_red' => '1;31',
+        'yellow' => '0;33',
+        'light_yellow' => '1;33',
+        'blue' => '0;34',
+        'dark_blue' => '0;34',
+        'light_blue' => '1;34',
+        'purple' => '0;35',
+        'light_purple' => '1;35',
+        'cyan' => '0;36',
+        'light_cyan' => '1;36',
+        'light_gray' => '0;37',
+        'white' => '1;37',
+    ];
+
+    const BG_COLORS = [
+        'black' => '40',
+        'red' => '41',
+        'green' => '42',
+        'yellow' => '43',
+        'blue' => '44',
+        'magenta' => '45',
+        'cyan' => '46',
+        'light_gray' => '47',
+    ];
+
+    private OutputInterface $output;
+
+    public static function create(OutputInterface $output): ConsoleOutput
+    {
+        return new self($output);
+    }
+
+    public function __construct(OutputInterface $output)
+    {
+        $this->output = $output;
+    }
+
+    public function success(string $message): void
+    {
+        [$formattedMessage, $lineLength, $color] = $this->formatMessage('OK', $message, 'green');
+        $this->outputMessage($formattedMessage, $lineLength, $color);
+    }
+
+    public function error(string $message): void
+    {
+        [$formattedMessage, $lineLength, $color] = $this->formatMessage('ERROR', $message, 'red');
+        $this->outputMessage($formattedMessage, $lineLength, $color, true);
+    }
+
+    public function warning(string $message): void
+    {
+        [$formattedMessage, $lineLength, $color] = $this->formatMessage('WARNING', $message, 'yellow');
+        $this->outputMessage($formattedMessage, $lineLength, $color);
+    }
+
+    public function info(string $message): void
+    {
+        [$formattedMessage, $lineLength, $color] = $this->formatMessage('INFO', $message, 'blue');
+        $this->outputMessage($formattedMessage, $lineLength, $color);
+    }
+
+    public function debug(string $message): void
+    {
+        if (!$this->output->isVerbose()) {
+            return;
+        }
+        [$formattedMessage, $lineLength, $color] = $this->formatMessage('DEBUG', $message, 'cyan');
+        $this->outputMessage($formattedMessage, $lineLength, $color);
+    }
+
+
+    public function title(string $message): void
+    {
+        $consoleWidth = $this->geTerminalWidth();
+        $titleLength = mb_strlen($message);
+        $underline = str_repeat('=', min($consoleWidth, $titleLength));
+
+        $this->write(PHP_EOL);
+        $this->write($message);
+        $this->write(PHP_EOL);
+        $this->write($underline);
+        $this->write(PHP_EOL);
+    }
+
+    public function list(array $items): void
+    {
+        foreach ($items as $item) {
+            $item = $this->variableToString($item);
+            $this->write('- ' . $item);
+            $this->write(PHP_EOL);
+        }
+        $this->write(PHP_EOL);
+    }
+
+    public function listKeyValues(array $items, bool $inlined = false): void
+    {
+        $maxKeyLength = 0;
+        if ($inlined) {
+            foreach ($items as $key => $value) {
+                $keyLength = mb_strlen($key);
+                if ($keyLength > $maxKeyLength) {
+                    $maxKeyLength = $keyLength;
+                }
+            }
+        }
+
+        foreach ($items as $key => $value) {
+            $value = $this->variableToString($value);
+            $value = implode(' ', explode(PHP_EOL, $value));
+            $key = str_pad($key, $maxKeyLength, ' ', STR_PAD_RIGHT);
+            $this->writeColor($key, 'green');
+            $this->write(' : ');
+            $this->writeColor($value, 'white');
+            $this->write(PHP_EOL);
+        }
+        $this->write(PHP_EOL);
+    }
+
+    public function indentedList(array $items, int $indentLevel = 1): void
+    {
+        if ($indentLevel == 1) {
+            $this->write(PHP_EOL);
+        }
+
+        foreach ($items as $item) {
+            if (is_array($item)) {
+                $this->indentedList($item, $indentLevel + 1);
+            } else {
+                $indentation = '';
+                if ($indentLevel > 1) {
+                    $indentation = str_repeat('  ', $indentLevel); // Indent with spaces
+                }
+                $this->writeColor($indentation . '- ', 'red');
+                $this->writeColor($item, 'white');
+                $this->write(PHP_EOL);
+            }
+        }
+
+        if ($indentLevel == 1) {
+            $this->write(PHP_EOL);
+        }
+    }
+
+    public function numberedList(array $items)
+    {
+        foreach ($items as $index => $item) {
+            $this->writeColor(($index + 1) . '. ', 'white');
+            $this->writeColor($item, 'green');
+            $this->write(PHP_EOL);
+        }
+        $this->write(PHP_EOL);
+    }
+
+    public function table(array $headers, array $rows): void
+    {
+        $maxWidth = $this->geTerminalWidth();
+        $maxWidthPerColumn = $maxWidth / count($headers);
+        $columnWidths = array_map(function ($header) {
+            return mb_strlen($header);
+        }, $headers);
+
+        $processedRows = [];
+        foreach ($rows as $row) {
+            $row = array_values($row);
+            $processedRow = [];
+            foreach ($row as $index => $column) {
+                $column = $this->variableToString($column);
+                $lines = explode(PHP_EOL, trim($column));
+                foreach ($lines as $i => $line) {
+                    $maxWidth = max($columnWidths[$index], mb_strlen($line));
+                    if ($maxWidth > $maxWidthPerColumn) {
+                        $maxWidth = $maxWidthPerColumn;
+                    }
+                    $columnWidths[$index] = $maxWidth;
+                    if (mb_strlen($line) > $maxWidth) {
+                        $lines[$i] = mb_substr($line, 0, $maxWidth) . '...';
+                    }
+                }
+                $processedRow[$index] = $lines;
+            }
+            $processedRows[] = $processedRow;
+        }
+
+        foreach ($headers as $index => $header) {
+            $this->write(str_pad($header, $columnWidths[$index] + 2));
+        }
+        $this->write(PHP_EOL);
+        $this->write(str_repeat('-', array_sum($columnWidths) + count($columnWidths) * 2));
+        $this->write(PHP_EOL);
+
+        foreach ($processedRows as $row) {
+            $maxLines = max(array_map('count', $row));
+            for ($lineIndex = 0; $lineIndex < $maxLines; $lineIndex++) {
+                foreach ($row as $index => $lines) {
+                    $line = $lines[$lineIndex] ?? ''; // Récupère la ligne actuelle ou une chaîne vide
+                    $this->write(str_pad($line, $columnWidths[$index] + 2));
+                }
+                $this->write(PHP_EOL);
+            }
+        }
+    }
+
+    public function progressBar(int $total, int $current): void
+    {
+        $barWidth = 50;
+        $progress = ($current / $total) * $barWidth;
+        $bar = str_repeat('#', (int)$progress) . str_repeat(' ', $barWidth - (int)$progress);
+        $this->write(sprintf("\r[%s] %d%%", $bar, ($current / $total) * 100));
+        if ($current === $total) {
+            $this->write(PHP_EOL);
+        }
+    }
+
+    public function confirm(string $message): bool
+    {
+        $this->writeColor($message . ' [y/n]: ', 'yellow');
+
+        $handle = fopen('php://stdin', 'r');
+        $input = trim(fgets($handle));
+        fclose($handle);
+
+        return in_array($input, ['y', 'yes', 'Y', 'YES'], true);
+    }
+
+    public function ask(string $question, bool $hidden = false, bool $required = false): string
+    {
+        $this->writeColor($question . ': ', 'cyan');
+
+        if ($hidden) {
+            if (strncasecmp(PHP_OS, 'WIN', 3) == 0) {
+                throw new RuntimeException('Windows platform is not supported for hidden input');
+            } else {
+                system('stty -echo');
+                $input = trim(fgets(STDIN));
+                system('stty echo');
+                $this->write(PHP_EOL);
+            }
+        } else {
+            $handle = fopen('php://stdin', 'r');
+            $input = trim(fgets($handle));
+            fclose($handle);
+        }
+
+        if ($required && empty($input)) {
+            throw new InvalidArgumentException('Response cannot be empty');
+        }
+
+        return $input;
+    }
+
+    public function spinner(int $duration = 3): void
+    {
+        $spinnerChars = ['|', '/', '-', '\\'];
+        $time = microtime(true);
+        while ((microtime(true) - $time) < $duration) {
+            foreach ($spinnerChars as $char) {
+                $this->write("\r$char");
+                usleep(100000);
+            }
+        }
+        $this->write("\r");
+    }
+
+    public function json(array $data): void
+    {
+        $jsonOutput = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
+
+        if (json_last_error() !== JSON_ERROR_NONE) {
+            $this->writeColor("Error encoding JSON: " . json_last_error_msg() . PHP_EOL, 'red');
+            return;
+        }
+        $this->write($jsonOutput . PHP_EOL);
+    }
+
+    public function boxed(string $message, string $borderChar = '*', int $padding = 1): void
+    {
+        $this->write(PHP_EOL);
+        $lineLength = mb_strlen($message);
+        $boxWidth = $this->geTerminalWidth();
+        if ($lineLength > $boxWidth) {
+            $lineLength = $boxWidth - ($padding * 2) - 2;
+        }
+        $lines = explode('|', wordwrap($message, $lineLength, '|', true));
+        $border = str_repeat($borderChar, $lineLength + ($padding * 2) + 2);
+
+        $this->write($border . PHP_EOL);
+        foreach ($lines as $line) {
+            $strPad = str_repeat(' ', $padding);
+            $this->write($borderChar . $strPad . str_pad($line, $lineLength) . $strPad . $borderChar . PHP_EOL);
+        }
+        $this->write($border . PHP_EOL);
+        $this->write(PHP_EOL);
+    }
+
+    public function writeColor(string $message, ?string $color = null, ?string $background = null, bool $isError = false): void
+    {
+
+        $formattedMessage = '';
+
+        if ($color) {
+            $formattedMessage .= "\033[" . self::FOREGROUND_COLORS[$color] . 'm';
+        }
+        if ($background) {
+            $formattedMessage .= "\033[" . self::BG_COLORS[$background] . 'm';
+        }
+
+        $formattedMessage .= $message . "\033[0m";
+
+        $this->write($formattedMessage, $isError);
+    }
+
+    public function write(string $message, bool $isError = false): void
+    {
+        if ($isError) {
+            $this->output->error($message);
+            return;
+        }
+        $this->output->write($message);
+    }
+
+    public function writeln(string $message): void
+    {
+        $this->output->writeln($message);
+    }
+
+    private function outputMessage($formattedMessage, int $lineLength, string $color, bool $isError = false): void
+    {
+        $this->write(PHP_EOL, $isError);
+        $this->writeColor(str_repeat(' ', $lineLength), 'white', $color, $isError);
+        $this->write(PHP_EOL, $isError);
+
+        if (is_string($formattedMessage)) {
+            $formattedMessage = [$formattedMessage];
+        }
+
+        foreach ($formattedMessage as $line) {
+            $line = str_pad($line, $lineLength).PHP_EOL;
+            $this->writeColor($line, 'white', $color, $isError);
+        }
+
+        $this->writeColor(str_repeat(' ', $lineLength), 'white', $color, $isError);
+        $this->write(PHP_EOL, $isError);
+        $this->write(PHP_EOL, $isError);
+    }
+
+    private function formatMessage(string $prefix, string $message, string $color): array
+    {
+        $formattedMessage = sprintf('[%s] %s', $prefix, trim($message));
+        $lineLength = mb_strlen($formattedMessage);
+        $consoleWidth = $this->geTerminalWidth();
+
+        if ($lineLength > $consoleWidth) {
+            $lineLength = $consoleWidth;
+            $lines = explode('|', wordwrap($formattedMessage, $lineLength, '|', true));
+            $formattedMessage = array_map(function ($line) use ($lineLength) {
+                return str_pad($line, $lineLength);
+            }, $lines);
+        }
+        return [$formattedMessage, $lineLength, $color];
+    }
+
+    private function geTerminalWidth(): int
+    {
+        $width = 85;
+        if (getenv('TERM')) {
+            $width = ((int) @exec('tput cols') ?: $width);
+        }
+        return $width - 5;
+    }
+
+    private function variableToString($variable): string
+    {
+        if (is_object($variable)) {
+            return 'Object: ' . get_class($variable);
+        } elseif (is_array($variable)) {
+            $variables = [];
+            foreach ($variable as $item) {
+                $variables[] = $this->variableToString($item);
+            }
+            return var_export($variables, true);
+        } elseif (is_resource($variable)) {
+            return (string)$variable;
+        } elseif (is_null($variable)) {
+            return 'NULL';
+        } elseif (is_string($variable)) {
+            return $variable;
+        }
+
+        return var_export($variable, true);
+    }
+
+    public function setVerbose(bool $verbose): void
+    {
+        $this->output->setVerbose($verbose);
+    }
+
+    public function isVerbose(): bool
+    {
+        return  $this->output->isVerbose();
+    }
+}

+ 12 - 11
src/OutputInterface.php

@@ -1,11 +1,12 @@
-<?php
-
-namespace Michel\Console;
-
-interface OutputInterface
-{
-    public function write(string $message): void;
-    public function writeln(string $message): void;
-    public function setVerbose(bool $verbose): void;
-    public function isVerbose(): bool;
-}
+<?php
+
+namespace Michel\Console;
+
+interface OutputInterface
+{
+    public function error(string $message): void;
+    public function write(string $message): void;
+    public function writeln(string $message): void;
+    public function setVerbose(bool $verbose): void;
+    public function isVerbose(): bool;
+}