From 49635f4ce18f6809fe367be38fec4de5a23a8698 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Tue, 31 Mar 2026 23:39:14 +0200 Subject: [PATCH 1/9] Add initial PHPStan configuration --- lib/cli/Arguments.php | 8 +++++++- lib/cli/arguments/Argument.php | 4 ++++ lib/cli/table/Ascii.php | 1 + lib/cli/table/Tabular.php | 6 ++++-- phpstan.neon.dist | 9 +++++++++ 5 files changed, 25 insertions(+), 3 deletions(-) create mode 100644 phpstan.neon.dist diff --git a/lib/cli/Arguments.php b/lib/cli/Arguments.php index 298d1a0..18f8e3e 100644 --- a/lib/cli/Arguments.php +++ b/lib/cli/Arguments.php @@ -110,6 +110,8 @@ public function offsetGet($offset) { if (isset($this->_parsed[$offset])) { return $this->_parsed[$offset]; } + + return null; } /** @@ -302,6 +304,8 @@ public function getFlag($flag) { return $settings; } } + + return null; } public function getFlags() { @@ -362,6 +366,8 @@ public function getOption($option) { return $settings; } } + + return null; } public function getOptions() { @@ -388,7 +394,7 @@ public function isOption($argument) { * will use either the first long name given or the first name in the list * if a long name is not given. * - * @return array + * @return void * @throws arguments\InvalidArguments */ public function parse() { diff --git a/lib/cli/arguments/Argument.php b/lib/cli/arguments/Argument.php index 9bc01f9..dee9702 100644 --- a/lib/cli/arguments/Argument.php +++ b/lib/cli/arguments/Argument.php @@ -16,6 +16,10 @@ /** * Represents an Argument or a value and provides several helpers related to parsing an argument list. + * + * @property-read bool $isLong + * @property-read bool $isShort + * @property-read bool $isArgument */ class Argument extends Memoize { /** diff --git a/lib/cli/table/Ascii.php b/lib/cli/table/Ascii.php index fd20bc9..c6f2127 100644 --- a/lib/cli/table/Ascii.php +++ b/lib/cli/table/Ascii.php @@ -163,6 +163,7 @@ public function border() { public function row( array $row ) { $extra_row_count = 0; + $extra_rows = []; if ( count( $row ) > 0 ) { $extra_rows = array_fill( 0, count( $row ), array() ); diff --git a/lib/cli/table/Tabular.php b/lib/cli/table/Tabular.php index 0675b4c..553a04b 100644 --- a/lib/cli/table/Tabular.php +++ b/lib/cli/table/Tabular.php @@ -23,8 +23,10 @@ class Tabular extends Renderer { * @return string The formatted table row. */ public function row( array $row ) { - $rows = []; - $output = ''; + $rows = []; + $output = ''; + $split_lines = []; + $col = null; foreach ( $row as $col => $value ) { $value = isset( $value ) ? (string) $value : ''; diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 0000000..03eb7d1 --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,9 @@ +parameters: + level: 1 + paths: + - lib + scanDirectories: + - vendor/wp-cli/wp-cli/php + scanFiles: + - vendor/php-stubs/wordpress-stubs/wordpress-stubs.php + treatPhpDocTypesAsCertain: false From 8ae5b11d2b57ed0fcae194f2c63a2c7f779c7708 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Wed, 1 Apr 2026 10:45:00 +0200 Subject: [PATCH 2/9] Level 2 --- lib/cli/Arguments.php | 6 +++--- lib/cli/Streams.php | 18 +++++++++--------- lib/cli/Table.php | 2 +- lib/cli/arguments/Argument.php | 2 +- lib/cli/arguments/Lexer.php | 2 +- lib/cli/cli.php | 24 ++++++++++++------------ lib/cli/table/Ascii.php | 2 +- phpstan.neon.dist | 2 +- 8 files changed, 29 insertions(+), 29 deletions(-) diff --git a/lib/cli/Arguments.php b/lib/cli/Arguments.php index 18f8e3e..dad1556 100644 --- a/lib/cli/Arguments.php +++ b/lib/cli/Arguments.php @@ -287,7 +287,7 @@ public function getInvalidArguments() { public function getFlag($flag) { if ($flag instanceOf Argument) { $obj = $flag; - $flag = $flag->value; + $flag = $flag->value(); } if (isset($this->_flags[$flag])) { @@ -350,7 +350,7 @@ public function isStackable($flag) { public function getOption($option) { if ($option instanceOf Argument) { $obj = $option; - $option = $option->value; + $option = $option->value(); } if (isset($this->_options[$option])) { @@ -412,7 +412,7 @@ public function parse() { continue; } - array_push($this->_invalid, $argument->raw); + array_push($this->_invalid, $argument->raw()); } if ($this->_strict && !empty($this->_invalid)) { diff --git a/lib/cli/Streams.php b/lib/cli/Streams.php index 71b454f..2f84bb7 100755 --- a/lib/cli/Streams.php +++ b/lib/cli/Streams.php @@ -27,11 +27,11 @@ static public function isTty() { * then each key in the array will be the placeholder name. Placeholders are of the * format {:key}. * - * @param string $msg The message to render. - * @param mixed ... Either scalar arguments or a single array argument. + * @param string $msg The message to render. + * @param mixed ...$args Either scalar arguments or a single array argument. * @return string The rendered string. */ - public static function render( $msg ) { + public static function render( $msg, ...$args ) { $args = func_get_args(); // No string replacement is needed @@ -66,11 +66,11 @@ public static function render( $msg ) { * through `sprintf` before output. * * @param string $msg The message to output in `printf` format. - * @param mixed ... Either scalar arguments or a single array argument. + * @param mixed ...$args Either scalar arguments or a single array argument. * @return void * @see \cli\render() */ - public static function out( $msg ) { + public static function out( $msg, ...$args ) { fwrite( static::$out, self::_call( 'render', func_get_args() ) ); } @@ -78,11 +78,11 @@ public static function out( $msg ) { * Pads `$msg` to the width of the shell before passing to `cli\out`. * * @param string $msg The message to pad and pass on. - * @param mixed ... Either scalar arguments or a single array argument. + * @param mixed ...$args Either scalar arguments or a single array argument. * @return void * @see cli\out() */ - public static function out_padded( $msg ) { + public static function out_padded( $msg, ...$args ) { $msg = self::_call( 'render', func_get_args() ); self::out( str_pad( $msg, \cli\Shell::columns() ) ); } @@ -107,10 +107,10 @@ public static function line( $msg = '' ) { * * @param string $msg The message to output in `printf` format. With no string, * a newline is printed. - * @param mixed ... Either scalar arguments or a single array argument. + * @param mixed ...$args Either scalar arguments or a single array argument. * @return void */ - public static function err( $msg = '' ) { + public static function err( $msg = '', ...$args ) { // func_get_args is empty if no args are passed even with the default above. $args = array_merge( func_get_args(), array( '' ) ); $args[0] .= "\n"; diff --git a/lib/cli/Table.php b/lib/cli/Table.php index 8ae90aa..31f96da 100644 --- a/lib/cli/Table.php +++ b/lib/cli/Table.php @@ -311,7 +311,7 @@ public function countRows() { /** * Set whether items in an Ascii table are pre-colorized. * - * @param bool|array $precolorized A boolean to set all columns in the table as pre-colorized, or an array of booleans keyed by column index (number) to set individual columns as pre-colorized. + * @param bool|array $pre_colorized A boolean to set all columns in the table as pre-colorized, or an array of booleans keyed by column index (number) to set individual columns as pre-colorized. * @see cli\Ascii::setPreColorized() */ public function setAsciiPreColorized( $pre_colorized ) { diff --git a/lib/cli/arguments/Argument.php b/lib/cli/arguments/Argument.php index dee9702..ee91061 100644 --- a/lib/cli/arguments/Argument.php +++ b/lib/cli/arguments/Argument.php @@ -25,7 +25,7 @@ class Argument extends Memoize { /** * The canonical name of this argument, used for aliasing. * - * @param string + * @var string */ public $key; diff --git a/lib/cli/arguments/Lexer.php b/lib/cli/arguments/Lexer.php index 3fb054b..9a94b0f 100644 --- a/lib/cli/arguments/Lexer.php +++ b/lib/cli/arguments/Lexer.php @@ -32,7 +32,7 @@ public function __construct(array $items) { /** * The current token. * - * @return string + * @return Argument */ #[\ReturnTypeWillChange] public function current() { diff --git a/lib/cli/cli.php b/lib/cli/cli.php index ccc2b51..427675e 100755 --- a/lib/cli/cli.php +++ b/lib/cli/cli.php @@ -19,11 +19,11 @@ * then each key in the array will be the placeholder name. Placeholders are of the * format {:key}. * - * @param string $msg The message to render. - * @param mixed ... Either scalar arguments or a single array argument. + * @param string $msg The message to render. + * @param mixed ...$args Either scalar arguments or a single array argument. * @return string The rendered string. */ -function render( $msg ) { +function render( $msg, ...$args ) { return Streams::_call( 'render', func_get_args() ); } @@ -32,11 +32,11 @@ function render( $msg ) { * through `sprintf` before output. * * @param string $msg The message to output in `printf` format. - * @param mixed ... Either scalar arguments or a single array argument. + * @param mixed ...$args Either scalar arguments or a single array argument. * @return void * @see \cli\render() */ -function out( $msg ) { +function out( $msg, ...$args ) { Streams::_call( 'out', func_get_args() ); } @@ -44,11 +44,11 @@ function out( $msg ) { * Pads `$msg` to the width of the shell before passing to `cli\out`. * * @param string $msg The message to pad and pass on. - * @param mixed ... Either scalar arguments or a single array argument. + * @param mixed ...$args Either scalar arguments or a single array argument. * @return void * @see cli\out() */ -function out_padded( $msg ) { +function out_padded( $msg, ...$args ) { Streams::_call( 'out_padded', func_get_args() ); } @@ -68,10 +68,10 @@ function line( $msg = '' ) { * * @param string $msg The message to output in `printf` format. With no string, * a newline is printed. - * @param mixed ... Either scalar arguments or a single array argument. + * @param mixed ...$args Either scalar arguments or a single array argument. * @return void */ -function err( $msg = '' ) { +function err( $msg = '', ...$args ) { Streams::_call( 'err', func_get_args() ); } @@ -162,7 +162,7 @@ function menu( $items, $default = null, $title = 'Choose an item' ) { */ function safe_strlen( $str, $encoding = false ) { // Allow for selective testings - "1" bit set tests grapheme_strlen(), "2" preg_match_all( '/\X/u' ), "4" mb_strlen(), "other" strlen(). - $test_safe_strlen = getenv( 'PHP_CLI_TOOLS_TEST_SAFE_STRLEN' ); + $test_safe_strlen = (int) getenv( 'PHP_CLI_TOOLS_TEST_SAFE_STRLEN' ); // Assume UTF-8 if no encoding given - `grapheme_strlen()` will return null if given non-UTF-8 string. if ( ( ! $encoding || 'UTF-8' === $encoding ) && can_use_icu() && null !== ( $length = grapheme_strlen( $str ) ) ) { @@ -225,7 +225,7 @@ function safe_substr( $str, $start, $length = false, $is_width = false, $encodin } // Allow for selective testings - "1" bit set tests grapheme_substr(), "2" preg_split( '/\X/' ), "4" mb_substr(), "8" substr(). - $test_safe_substr = getenv( 'PHP_CLI_TOOLS_TEST_SAFE_SUBSTR' ); + $test_safe_substr = (int) getenv( 'PHP_CLI_TOOLS_TEST_SAFE_SUBSTR' ); // Assume UTF-8 if no encoding given - `grapheme_substr()` will return false (not null like `grapheme_strlen()`) if given non-UTF-8 string. if ( ( ! $encoding || 'UTF-8' === $encoding ) && can_use_icu() && false !== ( $try = grapheme_substr( $str, $start, $length ) ) ) { @@ -325,7 +325,7 @@ function strwidth( $string, $encoding = false ) { list( $eaw_regex, $m_regex ) = get_unicode_regexs(); // Allow for selective testings - "1" bit set tests grapheme_strlen(), "2" preg_match_all( '/\X/u' ), "4" mb_strwidth(), "other" safe_strlen(). - $test_strwidth = getenv( 'PHP_CLI_TOOLS_TEST_STRWIDTH' ); + $test_strwidth = (int) getenv( 'PHP_CLI_TOOLS_TEST_STRWIDTH' ); // Assume UTF-8 if no encoding given - `grapheme_strlen()` will return null if given non-UTF-8 string. if ( ( ! $encoding || 'UTF-8' === $encoding ) && can_use_icu() && null !== ( $width = grapheme_strlen( $string ) ) ) { diff --git a/lib/cli/table/Ascii.php b/lib/cli/table/Ascii.php index c6f2127..f1e8e27 100644 --- a/lib/cli/table/Ascii.php +++ b/lib/cli/table/Ascii.php @@ -246,7 +246,7 @@ private function padColumn($content, $column) { /** * Set whether items are pre-colorized. * - * @param bool|array $colorized A boolean to set all columns in the table as pre-colorized, or an array of booleans keyed by column index (number) to set individual columns as pre-colorized. + * @param bool|array $pre_colorized A boolean to set all columns in the table as pre-colorized, or an array of booleans keyed by column index (number) to set individual columns as pre-colorized. */ public function setPreColorized( $pre_colorized ) { $this->_pre_colorized = $pre_colorized; diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 03eb7d1..6f0e9fc 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,5 +1,5 @@ parameters: - level: 1 + level: 2 paths: - lib scanDirectories: From 4b91c1f7fbadab535365627c15da9924c937b3f4 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Wed, 1 Apr 2026 14:52:51 +0200 Subject: [PATCH 3/9] Level 3 --- lib/cli/Arguments.php | 4 ++-- lib/cli/Progress.php | 2 +- lib/cli/table/Renderer.php | 2 +- phpstan.neon.dist | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/cli/Arguments.php b/lib/cli/Arguments.php index dad1556..31d97ff 100644 --- a/lib/cli/Arguments.php +++ b/lib/cli/Arguments.php @@ -282,7 +282,7 @@ public function getInvalidArguments() { * * @param mixed $flag Either a string representing the flag or an * cli\arguments\Argument object. - * @return array + * @return array|null */ public function getFlag($flag) { if ($flag instanceOf Argument) { @@ -345,7 +345,7 @@ public function isStackable($flag) { * * @param mixed $option Either a string representing the option or an * cli\arguments\Argument object. - * @return array + * @return array|null */ public function getOption($option) { if ($option instanceOf Argument) { diff --git a/lib/cli/Progress.php b/lib/cli/Progress.php index a18c0a4..e1dd1bc 100644 --- a/lib/cli/Progress.php +++ b/lib/cli/Progress.php @@ -97,7 +97,7 @@ public function estimated() { } $estimated = round($this->_total / $speed); - return $estimated; + return (int)$estimated; } /** diff --git a/lib/cli/table/Renderer.php b/lib/cli/table/Renderer.php index 6bf6df7..7b52697 100644 --- a/lib/cli/table/Renderer.php +++ b/lib/cli/table/Renderer.php @@ -62,7 +62,7 @@ public function setWidths(array $widths, $fallback = false) { * Render a border for the top and bottom and separating the headers from the * table rows. * - * @return string The table border. + * @return string|null The table border. */ public function border() { return null; diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 6f0e9fc..04316f5 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,5 +1,5 @@ parameters: - level: 2 + level: 3 paths: - lib scanDirectories: From 97bd54be483a6f5abfdaea381b8afe5587410beb Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Wed, 1 Apr 2026 17:51:29 +0200 Subject: [PATCH 4/9] Level 4 --- lib/cli/Arguments.php | 9 ++++++--- phpstan.neon.dist | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/cli/Arguments.php b/lib/cli/Arguments.php index 31d97ff..a6ff1ef 100644 --- a/lib/cli/Arguments.php +++ b/lib/cli/Arguments.php @@ -484,12 +484,15 @@ private function _parseOption($option) { $values = array(); // Loop until we find a flag in peak-ahead - foreach ($this->_lexer as $value) { - array_push($values, $value->raw); + while ( $this->_lexer->valid() ) { + $value = $this->_lexer->current(); + array_push( $values, $value->raw ); - if (!$this->_lexer->end() && !$this->_lexer->peek->isValue) { + // @phpstan-ignore-next-line + if ( ! $this->_lexer->end() && ! $this->_lexer->peek->isValue ) { break; } + $this->_lexer->next(); } $this[$option->key] = join(' ', $values); diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 04316f5..4cefe9b 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,5 +1,5 @@ parameters: - level: 3 + level: 4 paths: - lib scanDirectories: From 7327bea89468d835c89f07838dc5500421f95aeb Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Thu, 2 Apr 2026 09:49:52 +0200 Subject: [PATCH 5/9] Level 5 --- lib/cli/Arguments.php | 2 ++ lib/cli/Notify.php | 2 +- lib/cli/progress/Bar.php | 4 ++-- phpstan.neon.dist | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/cli/Arguments.php b/lib/cli/Arguments.php index a6ff1ef..8c12613 100644 --- a/lib/cli/Arguments.php +++ b/lib/cli/Arguments.php @@ -483,6 +483,8 @@ private function _parseOption($option) { // Store as array and join to string after looping for values $values = array(); + $this->_lexer->next(); + // Loop until we find a flag in peak-ahead while ( $this->_lexer->valid() ) { $value = $this->_lexer->current(); diff --git a/lib/cli/Notify.php b/lib/cli/Notify.php index beaa0b3..cd93897 100644 --- a/lib/cli/Notify.php +++ b/lib/cli/Notify.php @@ -120,7 +120,7 @@ public function speed() { * @return string The formatted time span. */ public function formatTime($time) { - return floor($time / 60) . ':' . str_pad($time % 60, 2, 0, STR_PAD_LEFT); + return sprintf('%02d:%02d', (int)floor($time / 60), $time % 60); } /** diff --git a/lib/cli/progress/Bar.php b/lib/cli/progress/Bar.php index 9c58f76..1b4bd9a 100644 --- a/lib/cli/progress/Bar.php +++ b/lib/cli/progress/Bar.php @@ -71,7 +71,7 @@ public function __construct($msg, $total, $interval = 100, $formatMessage = null public function display($finish = false) { $_percent = $this->percent(); - $percent = str_pad(floor($_percent * 100), 3); + $percent = str_pad((string)(int)floor($_percent * 100), 3); $msg = $this->_message; $current = $this->current(); $total = $this->total(); @@ -91,7 +91,7 @@ public function display($finish = false) { $size = 0; } - $bar = str_repeat($this->_bars[0], floor($_percent * $size)) . $this->_bars[1]; + $bar = str_repeat($this->_bars[0], (int)floor($_percent * $size)) . $this->_bars[1]; // substr is needed to trim off the bar cap at 100% $bar = substr(str_pad($bar, $size, ' '), 0, $size); diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 4cefe9b..42c2699 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,5 +1,5 @@ parameters: - level: 4 + level: 5 paths: - lib scanDirectories: From 4fd98bfe6654d90758e871d30fd3563de8a1931f Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Thu, 2 Apr 2026 12:59:36 +0200 Subject: [PATCH 6/9] Level 6 --- lib/cli/Arguments.php | 73 ++++++++++++++++++++++---- lib/cli/Colors.php | 31 +++++++++-- lib/cli/Memoize.php | 13 +++++ lib/cli/Notify.php | 15 ++++++ lib/cli/Progress.php | 8 +++ lib/cli/Shell.php | 2 + lib/cli/Streams.php | 21 +++++++- lib/cli/Table.php | 59 +++++++++++++++------ lib/cli/Tree.php | 8 ++- lib/cli/arguments/Argument.php | 8 ++- lib/cli/arguments/HelpScreen.php | 40 ++++++++++++++ lib/cli/arguments/InvalidArguments.php | 8 ++- lib/cli/arguments/Lexer.php | 21 +++++++- lib/cli/cli.php | 10 ++-- lib/cli/notify/Dots.php | 6 ++- lib/cli/notify/Spinner.php | 4 ++ lib/cli/progress/Bar.php | 6 +++ lib/cli/table/Ascii.php | 49 +++++++++++++---- lib/cli/table/Renderer.php | 34 +++++++++--- lib/cli/table/Tabular.php | 7 +-- lib/cli/tree/Ascii.php | 2 +- lib/cli/tree/Markdown.php | 2 +- lib/cli/tree/Renderer.php | 2 +- phpstan.neon.dist | 2 +- 24 files changed, 365 insertions(+), 66 deletions(-) diff --git a/lib/cli/Arguments.php b/lib/cli/Arguments.php index 8c12613..fe3a0b5 100644 --- a/lib/cli/Arguments.php +++ b/lib/cli/Arguments.php @@ -19,14 +19,23 @@ /** * Parses command line arguments. + * + * @implements \ArrayAccess */ class Arguments implements \ArrayAccess { + /** @var array> */ protected $_flags = array(); + /** @var array> */ protected $_options = array(); + /** @var bool */ protected $_strict = false; + /** @var array */ protected $_input = array(); + /** @var array */ protected $_invalid = array(); + /** @var array|null */ protected $_parsed; + /** @var Lexer|null */ protected $_lexer; /** @@ -36,7 +45,7 @@ class Arguments implements \ArrayAccess { * * `'help'` is `true` by default, `'strict'` is false by default. * - * @param array $options An array of options for this parser. + * @param array $options An array of options for this parser. */ public function __construct($options = array()) { $options += array( @@ -58,7 +67,7 @@ public function __construct($options = array()) { /** * Get the list of arguments found by the defined definitions. * - * @return array + * @return array */ public function getArguments() { if (!isset($this->_parsed)) { @@ -67,6 +76,11 @@ public function getArguments() { return $this->_parsed; } + /** + * Get the help screen. + * + * @return HelpScreen + */ public function getHelpScreen() { return new HelpScreen($this); } @@ -147,7 +161,7 @@ public function offsetUnset($offset) { * Adds a flag (boolean argument) to the argument list. * * @param mixed $flag A string representing the flag, or an array of strings. - * @param array $settings An array of settings for this flag. + * @param array|string $settings An array of settings for this flag. * @setting string description A description to be shown in --help. * @setting bool default The default value for this flag. * @setting bool stackable Whether the flag is repeatable to increase the value. @@ -183,7 +197,7 @@ public function addFlag($flag, $settings = array()) { * primary flag character, and the values should be the settings array * used by {addFlag}. * - * @param array $flags An array of flags to add + * @param array|string> $flags An array of flags to add * @return $this */ public function addFlags($flags) { @@ -203,7 +217,7 @@ public function addFlags($flags) { * Adds an option (string argument) to the argument list. * * @param mixed $option A string representing the option, or an array of strings. - * @param array $settings An array of settings for this option. + * @param array|string $settings An array of settings for this option. * @setting string description A description to be shown in --help. * @setting bool default The default value for this option. * @setting array aliases Other ways to trigger this option. @@ -237,7 +251,7 @@ public function addOption($option, $settings = array()) { * primary option string, and the values should be the settings array * used by {addOption}. * - * @param array $options An array of options to add + * @param array|string> $options An array of options to add * @return $this */ public function addOptions($options) { @@ -271,7 +285,7 @@ public function setStrict($strict) { /** * Get the list of invalid arguments the parser found. * - * @return array + * @return array */ public function getInvalidArguments() { return $this->_invalid; @@ -282,7 +296,7 @@ public function getInvalidArguments() { * * @param mixed $flag Either a string representing the flag or an * cli\arguments\Argument object. - * @return array|null + * @return array|null */ public function getFlag($flag) { if ($flag instanceOf Argument) { @@ -308,10 +322,20 @@ public function getFlag($flag) { return null; } + /** + * Get all flags. + * + * @return array> + */ public function getFlags() { return $this->_flags; } + /** + * Check if there are any flags defined. + * + * @return bool + */ public function hasFlags() { return !empty($this->_flags); } @@ -345,7 +369,7 @@ public function isStackable($flag) { * * @param mixed $option Either a string representing the option or an * cli\arguments\Argument object. - * @return array|null + * @return array|null */ public function getOption($option) { if ($option instanceOf Argument) { @@ -370,10 +394,20 @@ public function getOption($option) { return null; } + /** + * Get all options. + * + * @return array> + */ public function getOptions() { return $this->_options; } + /** + * Check if there are any options defined. + * + * @return bool + */ public function hasOptions() { return !empty($this->_options); } @@ -424,6 +458,8 @@ public function parse() { * This applies the default values, if any, of all of the * flags and options, so that if there is a default value * it will be available. + * + * @return void */ private function _applyDefaults() { foreach($this->_flags as $flag => $settings) { @@ -438,10 +474,22 @@ private function _applyDefaults() { } } + /** + * Warn about something. + * + * @param string $message + * @return void + */ private function _warn($message) { trigger_error('[' . __CLASS__ .'] ' . $message, E_USER_WARNING); } + /** + * Parse a flag. + * + * @param Argument $argument + * @return bool + */ private function _parseFlag($argument) { if (!$this->isFlag($argument)) { return false; @@ -460,6 +508,12 @@ private function _parseFlag($argument) { return true; } + /** + * Parse an option. + * + * @param Argument $option + * @return bool + */ private function _parseOption($option) { if (!$this->isOption($option)) { return false; @@ -490,7 +544,6 @@ private function _parseOption($option) { $value = $this->_lexer->current(); array_push( $values, $value->raw ); - // @phpstan-ignore-next-line if ( ! $this->_lexer->end() && ! $this->_lexer->peek->isValue ) { break; } diff --git a/lib/cli/Colors.php b/lib/cli/Colors.php index bba9f40..574994a 100644 --- a/lib/cli/Colors.php +++ b/lib/cli/Colors.php @@ -18,6 +18,7 @@ * Reference: http://graphcomp.com/info/specs/ansi_col.html#colors */ class Colors { + /** @var array> */ static protected $_colors = array( 'color' => array( 'black' => 30, @@ -48,14 +49,28 @@ class Colors { 'white' => 47 ) ); + /** @var bool|null */ static protected $_enabled = null; + /** @var array> */ static protected $_string_cache = array(); + /** + * Enable colorized output. + * + * @param bool $force Force enable. + * @return void + */ static public function enable($force = true) { self::$_enabled = $force === true ? true : null; } + /** + * Disable colorized output. + * + * @param bool $force Force disable. + * @return void + */ static public function disable($force = true) { self::$_enabled = $force === true ? false : null; } @@ -64,6 +79,9 @@ static public function disable($force = true) { * Check if we should colorize output based on local flags and shell type. * * Only check the shell type if `Colors::$_enabled` is null and `$colored` is null. + * + * @param bool|null $colored Force enable or disable the colorized output. + * @return bool */ static public function shouldColorize($colored = null) { return self::$_enabled === true || @@ -75,8 +93,8 @@ static public function shouldColorize($colored = null) { /** * Set the color. * - * @param string $color The name of the color or style to set. - * @return string + * @param string|array $color The name of the color or style to set, or an array of options. + * @return string */ static public function color($color) { if (!is_array($color)) { @@ -171,6 +189,7 @@ static public function decolorize( $string, $keep = 0 ) { * @param string $passed The original string before colorization. * @param string $colorized The string after running through self::colorize. * @param string $deprecated Optional. Not used. Default null. + * @return void */ static public function cacheString( $passed, $colorized, $deprecated = null ) { self::$_string_cache[md5($passed)] = array( @@ -225,7 +244,7 @@ static public function pad( $string, $length, $pre_colorized = false, $encoding /** * Get the color mapping array. * - * @return array Array of color tokens mapped to colors and styles. + * @return array> Array of color tokens mapped to colors and styles. */ static public function getColors() { return array( @@ -268,7 +287,7 @@ static public function getColors() { /** * Get the cached string values. * - * @return array The cached string values. + * @return array> The cached string values. */ static public function getStringCache() { return self::$_string_cache; @@ -276,6 +295,8 @@ static public function getStringCache() { /** * Clear the string cache. + * + * @return void */ static public function clearStringCache() { self::$_string_cache = array(); @@ -305,7 +326,7 @@ static public function getResetCode() { * @param string $string The string to wrap (with ANSI codes). * @param int $width The maximum display width per line. * @param string|bool $encoding Optional. The encoding of the string. Default false. - * @return array Array of wrapped string segments. + * @return array Array of wrapped string segments. */ static public function wrapPreColorized( $string, $width, $encoding = false ) { $wrapped = array(); diff --git a/lib/cli/Memoize.php b/lib/cli/Memoize.php index cedbc19..4c6b478 100644 --- a/lib/cli/Memoize.php +++ b/lib/cli/Memoize.php @@ -13,8 +13,15 @@ namespace cli; abstract class Memoize { + /** @var array */ protected $_memoCache = array(); + /** + * Magic getter to retrieve memoized properties. + * + * @param string $name Property name. + * @return mixed + */ public function __get($name) { if (isset($this->_memoCache[$name])) { return $this->_memoCache[$name]; @@ -34,6 +41,12 @@ public function __get($name) { return $this->_memoCache[$name]; } + /** + * Unmemoize a property or all properties. + * + * @param string|bool $name Property name to unmemoize, or true to unmemoize all. + * @return void + */ protected function _unmemo($name) { if ($name === true) { $this->_memoCache = array(); diff --git a/lib/cli/Notify.php b/lib/cli/Notify.php index cd93897..14cdbe9 100644 --- a/lib/cli/Notify.php +++ b/lib/cli/Notify.php @@ -24,14 +24,23 @@ * of characters to indicate progress is being made. */ abstract class Notify { + /** @var int */ protected $_current = 0; + /** @var bool */ protected $_first = true; + /** @var int */ protected $_interval; + /** @var string */ protected $_message; + /** @var int|null */ protected $_start; + /** @var float|null */ protected $_timer; + /** @var float|int|null */ protected $_tick; + /** @var int */ protected $_iteration = 0; + /** @var float|int */ protected $_speed = 0; /** @@ -52,11 +61,14 @@ public function __construct($msg, $interval = 100) { * @abstract * @param boolean $finish * @see cli\Notify::tick() + * @return void */ abstract public function display($finish = false); /** * Reset the notifier state so the same instance can be used in multiple loops. + * + * @return void */ public function reset() { $this->_current = 0; @@ -128,6 +140,7 @@ public function formatTime($time) { * no longer needed. * * @see cli\Notify::display() + * @return void */ public function finish() { Streams::out("\r"); @@ -140,6 +153,7 @@ public function finish() { * the ticker is incremented by 1. * * @param int $increment The amount to increment by. + * @return void */ public function increment($increment = 1) { $this->_current += $increment; @@ -174,6 +188,7 @@ public function shouldUpdate() { * @see cli\Notify::increment() * @see cli\Notify::shouldUpdate() * @see cli\Notify::display() + * @return void */ public function tick($increment = 1) { $this->increment($increment); diff --git a/lib/cli/Progress.php b/lib/cli/Progress.php index e1dd1bc..cd6b4ba 100644 --- a/lib/cli/Progress.php +++ b/lib/cli/Progress.php @@ -20,6 +20,7 @@ * @see cli\Notify */ abstract class Progress extends \cli\Notify { + /** @var int */ protected $_total = 0; /** @@ -40,6 +41,7 @@ public function __construct($msg, $total, $interval = 100) { * * @param int $total The total number of times this indicator should be `tick`ed. * @throws \InvalidArgumentException Thrown if the `$total` is less than 0. + * @return void */ public function setTotal($total) { $this->_total = (int)$total; @@ -51,6 +53,9 @@ public function setTotal($total) { /** * Reset the progress state so the same instance can be used in multiple loops. + * + * @param int|null $total Optional new total. + * @return void */ public function reset($total = null) { parent::reset(); @@ -103,6 +108,8 @@ public function estimated() { /** * Forces the current tick count to the total ticks given at instantiation * time before passing on to `cli\Notify::finish()`. + * + * @return void */ public function finish() { $this->_current = $this->_total; @@ -114,6 +121,7 @@ public function finish() { * the ticker is incremented by 1. * * @param int $increment The amount to increment by. + * @return void */ public function increment($increment = 1) { $this->_current = min($this->_total, $this->_current + $increment); diff --git a/lib/cli/Shell.php b/lib/cli/Shell.php index 9afb257..3ab2122 100755 --- a/lib/cli/Shell.php +++ b/lib/cli/Shell.php @@ -99,7 +99,9 @@ static public function isPiped() { /** * Uses `stty` to hide input/output completely. + * * @param boolean $hidden Will hide/show the next data. Defaults to true. + * @return void */ static public function hide($hidden = true) { system( 'stty ' . ( $hidden? '-echo' : 'echo' ) ); diff --git a/lib/cli/Streams.php b/lib/cli/Streams.php index 2f84bb7..f6fa7ba 100755 --- a/lib/cli/Streams.php +++ b/lib/cli/Streams.php @@ -4,15 +4,30 @@ class Streams { + /** @var resource */ protected static $out = STDOUT; + /** @var resource */ protected static $in = STDIN; + /** @var resource */ protected static $err = STDERR; + /** + * Call a method on this class. + * + * @param string $func The method name. + * @param array $args The arguments. + * @return mixed + */ static function _call( $func, $args ) { $method = __CLASS__ . '::' . $func; return call_user_func_array( $method, $args ); } + /** + * Check if the stream is a TTY. + * + * @return bool + */ static public function isTty() { if ( function_exists('stream_isatty') ) { return stream_isatty(static::$out); @@ -91,6 +106,8 @@ public static function out_padded( $msg, ...$args ) { * Prints a message to `STDOUT` with a newline appended. See `\cli\out` for * more documentation. * + * @param string $msg The message to print. + * @return void * @see cli\out() */ public static function line( $msg = '' ) { @@ -215,8 +232,8 @@ public static function choose( $question, $choice = 'yn', $default = 'n' ) { * choose an option. The array must be a single dimension with either strings * or objects with a `__toString()` method. * - * @param array $items The list of items the user can choose from. - * @param string $default The index of the default item. + * @param array $items The list of items the user can choose from. + * @param string|null $default The index of the default item. * @param string $title The message displayed to the user when prompted. * @return string The index of the chosen item. * @see cli\line() diff --git a/lib/cli/Table.php b/lib/cli/Table.php index 31f96da..c166792 100644 --- a/lib/cli/Table.php +++ b/lib/cli/Table.php @@ -23,17 +23,23 @@ * The `Table` class is used to display data in a tabular format. */ class Table { + /** @var \cli\table\Renderer */ protected $_renderer; + /** @var array */ protected $_headers = array(); + /** @var array */ protected $_footers = array(); + /** @var array */ protected $_width = array(); + /** @var array> */ protected $_rows = array(); + /** @var array|array */ protected $_alignments = array(); /** * Cached map of valid alignment constants. * - * @var array|null + * @var array|null */ private static $_valid_alignments_map = null; @@ -49,10 +55,10 @@ class Table { * table are used as the header values. * 3. Pass nothing and use `setHeaders()` and `addRow()` or `setRows()`. * - * @param array $headers Headers used in this table. Optional. - * @param array $rows The rows of data for this table. Optional. - * @param array $footers Footers used in this table. Optional. - * @param array $alignments Column alignments. Optional. + * @param array $headers Headers used in this table. Optional. + * @param array $rows The rows of data for this table. Optional. + * @param array $footers Footers used in this table. Optional. + * @param array $alignments Column alignments. Optional. */ public function __construct(array $headers = array(), array $rows = array(), array $footers = array(), array $alignments = array()) { if (!empty($headers)) { @@ -87,6 +93,11 @@ public function __construct(array $headers = array(), array $rows = array(), arr } } + /** + * Reset the table state. + * + * @return $this + */ public function resetTable() { $this->_headers = array(); @@ -115,6 +126,7 @@ public function resetRows() * @see table\Renderer * @see table\Ascii * @see table\Tabular + * @return void */ public function setRenderer(Renderer $renderer) { $this->_renderer = $renderer; @@ -123,8 +135,8 @@ public function setRenderer(Renderer $renderer) { /** * Loops through the row and sets the maximum width for each column. * - * @param array $row The table row. - * @return array $row + * @param array $row The table row. + * @return array $row */ protected function checkRow(array $row) { foreach ($row as $column => $str) { @@ -146,6 +158,7 @@ protected function checkRow(array $row) { * @uses cli\Shell::isPiped() Determine what format to output * * @see cli\Table::renderRow() + * @return void */ public function display() { foreach( $this->getDisplayLines() as $line ) { @@ -159,7 +172,8 @@ public function display() { * This method is useful for adding rows incrementally to an already-rendered table. * It will display the row with side borders and a bottom border (if using Ascii renderer). * - * @param array $row The row data to display. + * @param array $row The row data to display. + * @return void */ public function displayRow(array $row) { // Update widths if this row has wider content @@ -186,7 +200,7 @@ public function displayRow(array $row) { * @see cli\Table::display() * @see cli\Table::renderRow() * - * @return array + * @return array */ public function getDisplayLines() { $this->_renderer->setWidths($this->_width, $fallback = true); @@ -227,6 +241,7 @@ public function getDisplayLines() { * Sort the table by a column. Must be called before `cli\Table::display()`. * * @param int $column The index of the column to sort by. + * @return void */ public function sort($column) { if (!isset($this->_headers[$column])) { @@ -242,7 +257,8 @@ public function sort($column) { /** * Set the headers of the table. * - * @param array $headers An array of strings containing column header names. + * @param array $headers An array of strings containing column header names. + * @return void */ public function setHeaders(array $headers) { $this->_headers = $this->checkRow($headers); @@ -251,7 +267,8 @@ public function setHeaders(array $headers) { /** * Set the footers of the table. * - * @param array $footers An array of strings containing column footers names. + * @param array $footers An array of strings containing column footers names. + * @return void */ public function setFooters(array $footers) { $this->_footers = $this->checkRow($footers); @@ -260,7 +277,8 @@ public function setFooters(array $footers) { /** * Set the alignments of the table. * - * @param array $alignments An array of alignment constants keyed by column name. + * @param array|array $alignments An array of alignment constants keyed by column name or index. + * @return void */ public function setAlignments(array $alignments) { // Initialize the cached valid alignments map on first use @@ -284,8 +302,9 @@ public function setAlignments(array $alignments) { /** * Add a row to the table. * - * @param array $row The row data. + * @param array $row The row data. * @see cli\Table::checkRow() + * @return void */ public function addRow(array $row) { $this->_rows[] = $this->checkRow($row); @@ -294,8 +313,9 @@ public function addRow(array $row) { /** * Clears all previous rows and adds the given rows. * - * @param array $rows A 2-dimensional array of row data. + * @param array> $rows A 2-dimensional array of row data. * @see cli\Table::addRow() + * @return void */ public function setRows(array $rows) { $this->_rows = array(); @@ -304,6 +324,11 @@ public function setRows(array $rows) { } } + /** + * Count the number of rows in the table. + * + * @return int + */ public function countRows() { return count($this->_rows); } @@ -311,8 +336,9 @@ public function countRows() { /** * Set whether items in an Ascii table are pre-colorized. * - * @param bool|array $pre_colorized A boolean to set all columns in the table as pre-colorized, or an array of booleans keyed by column index (number) to set individual columns as pre-colorized. + * @param bool|array $pre_colorized A boolean to set all columns in the table as pre-colorized, or an array of booleans keyed by column index (number) to set individual columns as pre-colorized. * @see cli\Ascii::setPreColorized() + * @return void */ public function setAsciiPreColorized( $pre_colorized ) { if ( $this->_renderer instanceof Ascii ) { @@ -324,8 +350,9 @@ public function setAsciiPreColorized( $pre_colorized ) { * Set the wrapping mode for table cells. * * @param string $mode One of: 'wrap' (default - wrap at character boundaries), - * 'word-wrap' (wrap at word boundaries), or 'truncate' (truncate with ellipsis). + * 'word-wrap' (word boundaries), or 'truncate' (truncate with ellipsis). * @see cli\Ascii::setWrappingMode() + * @return void */ public function setWrappingMode( $mode ) { if ( $this->_renderer instanceof Ascii ) { diff --git a/lib/cli/Tree.php b/lib/cli/Tree.php index 7570902..b1df849 100644 --- a/lib/cli/Tree.php +++ b/lib/cli/Tree.php @@ -17,7 +17,9 @@ */ class Tree { + /** @var \cli\tree\Renderer */ protected $_renderer; + /** @var array */ protected $_data = array(); /** @@ -27,6 +29,7 @@ class Tree { * @see tree\Renderer * @see tree\Ascii * @see tree\Markdown + * @return void */ public function setRenderer(tree\Renderer $renderer) { $this->_renderer = $renderer; @@ -41,7 +44,8 @@ public function setRenderer(tree\Renderer $renderer) { * ], * 'Thing', * ] - * @param array $data + * @param array $data + * @return void */ public function setData(array $data) { @@ -60,6 +64,8 @@ public function render() /** * Display the rendered tree + * + * @return void */ public function display() { diff --git a/lib/cli/arguments/Argument.php b/lib/cli/arguments/Argument.php index ee91061..c690f26 100644 --- a/lib/cli/arguments/Argument.php +++ b/lib/cli/arguments/Argument.php @@ -20,6 +20,10 @@ * @property-read bool $isLong * @property-read bool $isShort * @property-read bool $isArgument + * @property-read bool $canExplode + * @property-read array $exploded + * @property-read string $raw + * @property-read bool $isValue */ class Argument extends Memoize { /** @@ -29,7 +33,9 @@ class Argument extends Memoize { */ public $key; + /** @var string */ private $_argument; + /** @var string */ private $_raw; /** @@ -125,7 +131,7 @@ public function canExplode() { * Returns all but the first character of the argument, removing them from the * objects representation at the same time. * - * @return array + * @return array */ public function exploded() { $exploded = array(); diff --git a/lib/cli/arguments/HelpScreen.php b/lib/cli/arguments/HelpScreen.php index f800788..28913ce 100644 --- a/lib/cli/arguments/HelpScreen.php +++ b/lib/cli/arguments/HelpScreen.php @@ -18,24 +18,42 @@ * Arguments help screen renderer */ class HelpScreen { + /** @var array> */ protected $_flags = array(); + /** @var int */ protected $_flagMax = 0; + /** @var array> */ protected $_options = array(); + /** @var int */ protected $_optionMax = 0; + /** + * @param Arguments $arguments + */ public function __construct(Arguments $arguments) { $this->setArguments($arguments); } + /** + * @return string + */ public function __toString() { return $this->render(); } + /** + * @param Arguments $arguments + * @return void + */ public function setArguments(Arguments $arguments) { $this->consumeArgumentFlags($arguments); $this->consumeArgumentOptions($arguments); } + /** + * @param Arguments $arguments + * @return void + */ public function consumeArgumentFlags(Arguments $arguments) { $data = $this->_consume($arguments->getFlags()); @@ -43,6 +61,10 @@ public function consumeArgumentFlags(Arguments $arguments) { $this->_flagMax = $data[1]; } + /** + * @param Arguments $arguments + * @return void + */ public function consumeArgumentOptions(Arguments $arguments) { $data = $this->_consume($arguments->getOptions()); @@ -50,6 +72,9 @@ public function consumeArgumentOptions(Arguments $arguments) { $this->_optionMax = $data[1]; } + /** + * @return string + */ public function render() { $help = array(); @@ -59,6 +84,9 @@ public function render() { return join("\n\n", $help); } + /** + * @return string|null + */ private function _renderFlags() { if (empty($this->_flags)) { return null; @@ -67,6 +95,9 @@ private function _renderFlags() { return "Flags\n" . $this->_renderScreen($this->_flags, $this->_flagMax); } + /** + * @return string|null + */ private function _renderOptions() { if (empty($this->_options)) { return null; @@ -75,6 +106,11 @@ private function _renderOptions() { return "Options\n" . $this->_renderScreen($this->_options, $this->_optionMax); } + /** + * @param array> $options + * @param int $max + * @return string + */ private function _renderScreen($options, $max) { $help = array(); foreach ($options as $option => $settings) { @@ -100,6 +136,10 @@ private function _renderScreen($options, $max) { return join("\n", $help); } + /** + * @param array> $options + * @return array{0: array>, 1: int} + */ private function _consume($options) { $max = 0; $out = array(); diff --git a/lib/cli/arguments/InvalidArguments.php b/lib/cli/arguments/InvalidArguments.php index 633c8c6..5a0b315 100644 --- a/lib/cli/arguments/InvalidArguments.php +++ b/lib/cli/arguments/InvalidArguments.php @@ -16,10 +16,11 @@ * Thrown when undefined arguments are detected in strict mode. */ class InvalidArguments extends \InvalidArgumentException { + /** @var array */ protected $arguments; /** - * @param array $arguments A list of arguments that do not fit the profile. + * @param array $arguments A list of arguments that do not fit the profile. */ public function __construct(array $arguments) { $this->arguments = $arguments; @@ -29,12 +30,15 @@ public function __construct(array $arguments) { /** * Get the arguments that caused the exception. * - * @return array + * @return array */ public function getArguments() { return $this->arguments; } + /** + * @return string + */ private function _generateMessage() { return 'unknown argument' . (count($this->arguments) > 1 ? 's' : '') . diff --git a/lib/cli/arguments/Lexer.php b/lib/cli/arguments/Lexer.php index 9a94b0f..910110d 100644 --- a/lib/cli/arguments/Lexer.php +++ b/lib/cli/arguments/Lexer.php @@ -14,15 +14,25 @@ use cli\Memoize; +/** + * @property-read Argument $peek + * + * @implements \Iterator + */ class Lexer extends Memoize implements \Iterator { + /** @var Argument|null */ private $_item; + /** @var array */ private $_items = array(); + /** @var int */ private $_index = 0; + /** @var int */ private $_length = 0; + /** @var bool */ private $_first = true; /** - * @param array $items A list of strings to process as tokens. + * @param array $items A list of strings to process as tokens. */ public function __construct(array $items) { $this->_items = $items; @@ -95,6 +105,7 @@ public function valid() { * Push an element to the front of the stack. * * @param mixed $item The value to set + * @return void */ public function unshift($item) { array_unshift($this->_items, $item); @@ -110,6 +121,9 @@ public function end() { return ($this->_index + 1) == $this->_length; } + /** + * @return void + */ private function _shift() { $this->_item = new Argument(array_shift($this->_items)); $this->_index += 1; @@ -117,9 +131,12 @@ private function _shift() { $this->_unmemo('peek'); } + /** + * @return void + */ private function _explode() { if (!$this->_item->canExplode) { - return false; + return; } foreach ($this->_item->exploded as $piece) { diff --git a/lib/cli/cli.php b/lib/cli/cli.php index 427675e..0e473fa 100755 --- a/lib/cli/cli.php +++ b/lib/cli/cli.php @@ -56,6 +56,8 @@ function out_padded( $msg, ...$args ) { * Prints a message to `STDOUT` with a newline appended. See `\cli\out` for * more documentation. * + * @param string $msg Message to print. + * @return void * @see cli\out() */ function line( $msg = '' ) { @@ -140,7 +142,7 @@ function confirm( $question, $default = false ) { * choose an option. The array must be a single dimension with either strings * or objects with a `__toString()` method. * - * @param array $items The list of items the user can choose from. + * @param array $items The list of items the user can choose from. * @param string $default The index of the default item. * @param string $title The message displayed to the user when prompted. * @return string The index of the chosen item. @@ -262,6 +264,8 @@ function safe_substr( $str, $start, $length = false, $is_width = false, $encodin /** * Internal function used by `safe_substr()` to adjust for East Asian double-width chars. * + * @param string $str + * @param int $length * @return string */ function _safe_substr_eaw( $str, $length ) { @@ -393,8 +397,8 @@ function can_use_pcre_x() { /** * Get the regexs generated from Unicode data. * - * @param string $idx Optional. Return a specific regex only. Default null. - * @return array|string Returns keyed array if not given $idx or $idx doesn't exist, otherwise the specific regex string. + * @param string|null $idx Optional. Return a specific regex only. Default null. + * @return array|string Returns keyed array if not given $idx or $idx doesn't exist, otherwise the specific regex string. */ function get_unicode_regexs( $idx = null ) { static $eaw_regex; // East Asian Width regex. Characters that count as 2 characters as they're "wide" or "fullwidth". See http://www.unicode.org/reports/tr11/tr11-19.html diff --git a/lib/cli/notify/Dots.php b/lib/cli/notify/Dots.php index e3a5159..2d00de2 100644 --- a/lib/cli/notify/Dots.php +++ b/lib/cli/notify/Dots.php @@ -19,9 +19,12 @@ * A Notifier that displays a string of periods. */ class Dots extends Notify { + /** @var int */ protected $_dots; + /** @var string */ protected $_format = '{:msg}{:dots} ({:elapsed}, {:speed}/s)'; - protected $_iteration; + /** @var int */ + protected $_iteration = 0; /** * Instantiates a Notification object. @@ -46,6 +49,7 @@ public function __construct($msg, $dots = 3, $interval = 100) { * * @param boolean $finish `true` if this was called from * `cli\Notify::finish()`, `false` otherwise. + * @return void * @see cli\out_padded() * @see cli\Notify::formatTime() * @see cli\Notify::speed() diff --git a/lib/cli/notify/Spinner.php b/lib/cli/notify/Spinner.php index 8da7890..80f5faf 100644 --- a/lib/cli/notify/Spinner.php +++ b/lib/cli/notify/Spinner.php @@ -19,8 +19,11 @@ * The `Spinner` Notifier displays an ASCII spinner. */ class Spinner extends Notify { + /** @var string */ protected $_chars = '-\|/'; + /** @var string */ protected $_format = '{:msg} {:char} ({:elapsed}, {:speed}/s)'; + /** @var int */ protected $_iteration = 0; /** @@ -29,6 +32,7 @@ class Spinner extends Notify { * * @param boolean $finish `true` if this was called from * `cli\Notify::finish()`, `false` otherwise. + * @return void * @see cli\out_padded() * @see cli\Notify::formatTime() * @see cli\Notify::speed() diff --git a/lib/cli/progress/Bar.php b/lib/cli/progress/Bar.php index 1b4bd9a..9fcf397 100644 --- a/lib/cli/progress/Bar.php +++ b/lib/cli/progress/Bar.php @@ -26,9 +26,13 @@ * ^MSG PER% [======================= ] 00:00 / 00:00$ */ class Bar extends Progress { + /** @var string */ protected $_bars = '=>'; + /** @var string */ protected $_formatMessage = '{:msg} {:percent}% ['; + /** @var string */ protected $_formatTiming = '] {:elapsed} / {:estimated}'; + /** @var string */ protected $_format = '{:msg}{:bar}{:timing}'; /** @@ -61,6 +65,7 @@ public function __construct($msg, $total, $interval = 100, $formatMessage = null * * @param boolean $finish `true` if this was called from * `cli\Notify::finish()`, `false` otherwise. + * @return void * @see cli\out() * @see cli\Notify::formatTime() * @see cli\Notify::elapsed() @@ -104,6 +109,7 @@ public function display($finish = false) { * * @param int $increment The amount to increment by. * @param string $msg The text to display next to the Notifier. (optional) + * @return void * @see cli\Notify::tick() */ public function tick($increment = 1, $msg = null) { diff --git a/lib/cli/table/Ascii.php b/lib/cli/table/Ascii.php index f1e8e27..4e33461 100644 --- a/lib/cli/table/Ascii.php +++ b/lib/cli/table/Ascii.php @@ -34,22 +34,42 @@ class Ascii extends Renderer { */ private const ELLIPSIS_WIDTH = 3; + /** + * @var array + */ protected $_characters = array( 'corner' => '+', 'line' => '-', 'border' => '|', 'padding' => ' ', ); + + /** + * @var string|null + */ protected $_border = null; + + /** + * @var int|null + */ protected $_constraintWidth = null; + + /** + * @var bool|array + */ protected $_pre_colorized = false; + + /** + * @var string + */ protected $_wrapping_mode = 'wrap'; // 'wrap', 'word-wrap', or 'truncate' /** * Set the widths of each column in the table. * - * @param array $widths The widths of the columns. - * @param bool $fallback Whether to use these values as fallback only. + * @param array $widths The widths of the columns. + * @param bool $fallback Whether to use these values as fallback only. + * @return void */ public function setWidths(array $widths, $fallback = false) { if ($fallback) { @@ -107,6 +127,7 @@ public function setWidths(array $widths, $fallback = false) { * Set the constraint width for the table * * @param int $constraintWidth + * @return void */ public function setConstraintWidth( $constraintWidth ) { $this->_constraintWidth = $constraintWidth; @@ -117,6 +138,7 @@ public function setConstraintWidth( $constraintWidth ) { * * @param string $mode One of: 'wrap' (default - wrap at character boundaries), * 'word-wrap' (wrap at word boundaries), or 'truncate' (truncate with ellipsis). + * @return void */ public function setWrappingMode( $mode ) { if ( ! in_array( $mode, self::VALID_WRAPPING_MODES, true ) ) { @@ -130,7 +152,8 @@ public function setWrappingMode( $mode ) { * * The keys `corner`, `line` and `border` are used in rendering. * - * @param $characters array Characters used in rendering. + * @param array $characters Characters used in rendering. + * @return void */ public function setCharacters(array $characters) { $this->_characters = array_merge($this->_characters, $characters); @@ -157,8 +180,8 @@ public function border() { /** * Renders a row for output. * - * @param array $row The table row. - * @return string The formatted table row. + * @param array $row The table row. + * @return string The formatted table row. */ public function row( array $row ) { @@ -237,7 +260,14 @@ private function getColumnAlignment( $column ) { return Column::ALIGN_LEFT; } - private function padColumn($content, $column) { + /** + * Pad a column value. + * + * @param string $content The column content. + * @param int $column The column index. + * @return string The padded column. + */ + private function padColumn( $content, $column ) { $alignment = $this->getColumnAlignment( $column ); $content = str_replace( "\t", ' ', (string) $content ); return $this->_characters['padding'] . Colors::pad( $content, $this->_widths[ $column ], $this->isPreColorized( $column ), false, $alignment ) . $this->_characters['padding']; @@ -246,7 +276,8 @@ private function padColumn($content, $column) { /** * Set whether items are pre-colorized. * - * @param bool|array $pre_colorized A boolean to set all columns in the table as pre-colorized, or an array of booleans keyed by column index (number) to set individual columns as pre-colorized. + * @param bool|array $pre_colorized A boolean to set all columns in the table as pre-colorized, or an array of booleans keyed by column index (number) to set individual columns as pre-colorized. + * @return void */ public function setPreColorized( $pre_colorized ) { $this->_pre_colorized = $pre_colorized; @@ -259,7 +290,7 @@ public function setPreColorized( $pre_colorized ) { * @param int $width The maximum width. * @param string|bool $encoding The text encoding. * @param bool $is_precolorized Whether the text is pre-colorized. - * @return array Array of wrapped lines. + * @return array Array of wrapped lines. */ protected function wrapText( $text, $width, $encoding, $is_precolorized ) { if ( ! $width ) { @@ -319,7 +350,7 @@ protected function wrapText( $text, $width, $encoding, $is_precolorized ) { * @param int $width The maximum width. * @param string|bool $encoding The text encoding. * @param bool $is_precolorized Whether the text is pre-colorized. - * @return array Array of wrapped lines. + * @return array Array of wrapped lines. */ protected function wordWrap( $text, $width, $encoding, $is_precolorized ) { $wrapped_lines = array(); diff --git a/lib/cli/table/Renderer.php b/lib/cli/table/Renderer.php index 7b52697..10aa85a 100644 --- a/lib/cli/table/Renderer.php +++ b/lib/cli/table/Renderer.php @@ -16,10 +16,27 @@ * Table renderers are used to change how a table is displayed. */ abstract class Renderer { + /** + * @var array + */ protected $_widths = array(); + + /** + * @var array + */ protected $_alignments = array(); + + /** + * @var array + */ protected $_headers = array(); + /** + * Constructor. + * + * @param array $widths Column widths. + * @param array $alignments Column alignments. + */ public function __construct(array $widths = array(), array $alignments = array()) { $this->setWidths($widths); $this->setAlignments($alignments); @@ -28,7 +45,8 @@ public function __construct(array $widths = array(), array $alignments = array() /** * Set the alignments of each column in the table. * - * @param array $alignments The alignments of the columns. + * @param array $alignments The alignments of the columns. + * @return void */ public function setAlignments(array $alignments) { $this->_alignments = $alignments; @@ -37,7 +55,8 @@ public function setAlignments(array $alignments) { /** * Set the headers of the table. * - * @param array $headers The headers of the table. + * @param array $headers The headers of the table. + * @return void */ public function setHeaders(array $headers) { $this->_headers = $headers; @@ -46,8 +65,9 @@ public function setHeaders(array $headers) { /** * Set the widths of each column in the table. * - * @param array $widths The widths of the columns. - * @param bool $fallback Whether to use these values as fallback only. + * @param array $widths The widths of the columns. + * @param bool $fallback Whether to use these values as fallback only. + * @return void */ public function setWidths(array $widths, $fallback = false) { if ($fallback) { @@ -71,8 +91,8 @@ public function border() { /** * Renders a row for output. * - * @param array $row The table row. - * @return string The formatted table row. + * @param array $row The table row. + * @return string The formatted table row. */ - abstract public function row(array $row); + abstract public function row( array $row ); } diff --git a/lib/cli/table/Tabular.php b/lib/cli/table/Tabular.php index 553a04b..31170ed 100644 --- a/lib/cli/table/Tabular.php +++ b/lib/cli/table/Tabular.php @@ -19,10 +19,11 @@ class Tabular extends Renderer { /** * Renders a row for output. * - * @param array $row The table row. - * @return string The formatted table row. + * @param array $row The table row. + * @return string The formatted table row. */ public function row( array $row ) { + /** @var array> $rows */ $rows = []; $output = ''; $split_lines = []; @@ -46,7 +47,7 @@ public function row( array $row ) { } foreach ( $rows as $r ) { - $output .= implode( "\t", array_values( $r ) ) . PHP_EOL; + $output .= implode( "\t", $r ) . PHP_EOL; } return rtrim( $output, PHP_EOL ); } diff --git a/lib/cli/tree/Ascii.php b/lib/cli/tree/Ascii.php index 00edf38..a94a8ef 100644 --- a/lib/cli/tree/Ascii.php +++ b/lib/cli/tree/Ascii.php @@ -18,7 +18,7 @@ class Ascii extends Renderer { /** - * @param array $tree + * @param array $tree * @return string */ public function render(array $tree) diff --git a/lib/cli/tree/Markdown.php b/lib/cli/tree/Markdown.php index ba1fd05..7f718f7 100644 --- a/lib/cli/tree/Markdown.php +++ b/lib/cli/tree/Markdown.php @@ -37,7 +37,7 @@ function __construct($padding = null) /** * Renders the tree * - * @param array $tree + * @param array $tree * @param int $level Optional * @return string */ diff --git a/lib/cli/tree/Renderer.php b/lib/cli/tree/Renderer.php index ff352bc..8348ffe 100644 --- a/lib/cli/tree/Renderer.php +++ b/lib/cli/tree/Renderer.php @@ -18,7 +18,7 @@ abstract class Renderer { /** - * @param array $tree + * @param array $tree * @return string|null */ abstract public function render(array $tree); diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 42c2699..fb750b9 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,5 +1,5 @@ parameters: - level: 5 + level: 6 paths: - lib scanDirectories: From 2668466ce922d854a048a2d5cbd7f4999acf2f8b Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Tue, 7 Apr 2026 09:41:02 +0200 Subject: [PATCH 7/9] Level 7 --- lib/cli/Arguments.php | 6 +++++- lib/cli/Colors.php | 5 +++++ lib/cli/Memoize.php | 3 +-- lib/cli/Notify.php | 2 +- lib/cli/Shell.php | 2 +- lib/cli/Streams.php | 17 ++++++++++------- lib/cli/arguments/HelpScreen.php | 3 +-- lib/cli/cli.php | 24 +++++++++++++++++------- lib/cli/table/Ascii.php | 22 ++++++++++++++-------- lib/cli/table/Tabular.php | 3 +++ phpstan.neon.dist | 2 +- 11 files changed, 59 insertions(+), 30 deletions(-) diff --git a/lib/cli/Arguments.php b/lib/cli/Arguments.php index fe3a0b5..561533d 100644 --- a/lib/cli/Arguments.php +++ b/lib/cli/Arguments.php @@ -91,7 +91,11 @@ public function getHelpScreen() { * @return string */ public function asJSON() { - return json_encode($this->_parsed); + $json = json_encode( $this->_parsed ); + if ( false === $json ) { + throw new \RuntimeException( 'Failed to encode arguments as JSON' ); + } + return $json; } /** diff --git a/lib/cli/Colors.php b/lib/cli/Colors.php index 574994a..fb9e071 100644 --- a/lib/cli/Colors.php +++ b/lib/cli/Colors.php @@ -340,6 +340,10 @@ static public function wrapPreColorized( $string, $width, $encoding = false ) { // Split the string into parts: ANSI codes and text $parts = preg_split( $ansi_pattern, $string, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY ); + if ( false === $parts ) { + $parts = array( $string ); + } + foreach ( $parts as $part ) { // Check if this part is an ANSI code if ( preg_match( $ansi_pattern, $part ) ) { @@ -361,6 +365,7 @@ static public function wrapPreColorized( $string, $width, $encoding = false ) { while ( $offset < $text_length ) { $char = \cli\safe_substr( $part, $offset, 1, false, $encoding ); + assert( is_string( $char ) ); $char_width = \cli\strwidth( $char, $encoding ); // Check if adding this character would exceed the width diff --git a/lib/cli/Memoize.php b/lib/cli/Memoize.php index 4c6b478..b08a619 100644 --- a/lib/cli/Memoize.php +++ b/lib/cli/Memoize.php @@ -36,8 +36,7 @@ public function __get($name) { return ($this->_memoCache[$name] = null); } - $method = array($this, $name); - ($this->_memoCache[$name] = call_user_func($method)); + ($this->_memoCache[$name] = $this->$name()); return $this->_memoCache[$name]; } diff --git a/lib/cli/Notify.php b/lib/cli/Notify.php index 14cdbe9..e5a6271 100644 --- a/lib/cli/Notify.php +++ b/lib/cli/Notify.php @@ -121,7 +121,7 @@ public function speed() { $this->_speed = ($this->_current / $this->_iteration) / $span; } - return $this->_speed; + return (int) $this->_speed; } /** diff --git a/lib/cli/Shell.php b/lib/cli/Shell.php index 3ab2122..a3bb95d 100755 --- a/lib/cli/Shell.php +++ b/lib/cli/Shell.php @@ -50,7 +50,7 @@ static public function columns() { } } else { $size = exec( '/usr/bin/env stty size 2>/dev/null' ); - if ( '' !== $size && preg_match( '/[0-9]+ ([0-9]+)/', $size, $matches ) ) { + if ( $size && preg_match( '/[0-9]+ ([0-9]+)/', $size, $matches ) ) { $columns = (int) $matches[1]; } if ( ! $columns ) { diff --git a/lib/cli/Streams.php b/lib/cli/Streams.php index f6fa7ba..786f55b 100755 --- a/lib/cli/Streams.php +++ b/lib/cli/Streams.php @@ -19,7 +19,8 @@ class Streams { * @return mixed */ static function _call( $func, $args ) { - $method = __CLASS__ . '::' . $func; + $method = array( __CLASS__, $func ); + assert( is_callable( $method ) ); return call_user_func_array( $method, $args ); } @@ -164,7 +165,7 @@ public static function input( $format = null, $hide = false ) { throw new \Exception( 'Caught ^D during input' ); } - return trim( $line ); + return trim( (string) $line ); } /** @@ -188,10 +189,12 @@ public static function prompt( $question, $default = false, $marker = ': ', $hid self::out( $question . $marker ); $line = self::input( null, $hide ); - if ( trim( $line ) !== '' ) + if ( trim( $line ) !== '' ) { return $line; - if( $default !== false ) - return $default; + } + if( $default !== false ) { + return (string) $default; + } } } @@ -213,7 +216,7 @@ public static function choose( $question, $choice = 'yn', $default = 'n' ) { // Make every choice character lowercase except the default $choice = str_ireplace( $default, strtoupper( $default ), strtolower( $choice ) ); // Separate each choice with a forward-slash - $choices = trim( join( '/', preg_split( '//', $choice ) ), '/' ); + $choices = trim( join( '/', str_split( $choice ) ), '/' ); while( true ) { $line = self::prompt( sprintf( '%s? [%s]', $question, $choices ), $default, '' ); @@ -259,7 +262,7 @@ public static function menu( $items, $default = null, $title = 'Choose an item' if( is_numeric( $line ) ) { $line--; if( isset( $map[$line] ) ) { - return array_search( $map[$line], $items ); + return (string) array_search( $map[$line], $items ); } if( $line < 0 || $line >= count( $map ) ) { diff --git a/lib/cli/arguments/HelpScreen.php b/lib/cli/arguments/HelpScreen.php index 28913ce..fd4b346 100644 --- a/lib/cli/arguments/HelpScreen.php +++ b/lib/cli/arguments/HelpScreen.php @@ -116,8 +116,7 @@ private function _renderScreen($options, $max) { foreach ($options as $option => $settings) { $formatted = ' ' . str_pad($option, $max); - $dlen = 80 - 4 - $max; - + $dlen = max( 1, 80 - 4 - $max ); $description = str_split($settings['description'], $dlen); $formatted.= ' ' . array_shift($description); diff --git a/lib/cli/cli.php b/lib/cli/cli.php index 0e473fa..c8cf458 100755 --- a/lib/cli/cli.php +++ b/lib/cli/cli.php @@ -166,8 +166,8 @@ function safe_strlen( $str, $encoding = false ) { // Allow for selective testings - "1" bit set tests grapheme_strlen(), "2" preg_match_all( '/\X/u' ), "4" mb_strlen(), "other" strlen(). $test_safe_strlen = (int) getenv( 'PHP_CLI_TOOLS_TEST_SAFE_STRLEN' ); - // Assume UTF-8 if no encoding given - `grapheme_strlen()` will return null if given non-UTF-8 string. - if ( ( ! $encoding || 'UTF-8' === $encoding ) && can_use_icu() && null !== ( $length = grapheme_strlen( $str ) ) ) { + // Assume UTF-8 if no encoding given - `grapheme_strlen()` will return false on failure. + if ( ( ! $encoding || 'UTF-8' === $encoding ) && can_use_icu() && is_int( $length = grapheme_strlen( $str ) ) ) { if ( ! $test_safe_strlen || ( $test_safe_strlen & 1 ) ) { return $length; } @@ -183,10 +183,12 @@ function safe_strlen( $str, $encoding = false ) { if ( ! $encoding ) { $encoding = mb_detect_encoding( $str, null, true /*strict*/ ); } - $length = $encoding ? mb_strlen( $str, $encoding ) : mb_strlen( $str ); // mbstring funcs can fail if given `$encoding` arg that evals to false. + $length = is_string( $encoding ) ? mb_strlen( $str, $encoding ) : mb_strlen( $str ); // mbstring funcs can fail if given `$encoding` arg that evals to false. if ( 'UTF-8' === $encoding ) { // Subtract combining characters. - $length -= preg_match_all( get_unicode_regexs( 'm' ), $str, $dummy /*needed for PHP 5.3*/ ); + $m_regex = get_unicode_regexs( 'm' ); + assert( is_string( $m_regex ) ); + $length -= preg_match_all( $m_regex, $str, $dummy /*needed for PHP 5.3*/ ); } if ( ! $test_safe_strlen || ( $test_safe_strlen & 4 ) ) { return $length; @@ -217,6 +219,8 @@ function safe_substr( $str, $start, $length = false, $is_width = false, $encodin // Normalize `$length` when not specified - PHP 5.3 substr takes false as full length, PHP > 5.3 takes null. if ( null === $length || false === $length ) { $length = $safe_strlen; + } else { + $length = (int) $length; } // Normalize `$start` - various methods treat this differently. if ( $start > $safe_strlen ) { @@ -250,7 +254,7 @@ function safe_substr( $str, $start, $length = false, $is_width = false, $encodin $encoding = mb_detect_encoding( $str, null, true /*strict*/ ); } // Bug: not adjusting for combining chars. - $try = $encoding ? mb_substr( $str, $start, $length, $encoding ) : mb_substr( $str, $start, $length ); // mbstring funcs can fail if given `$encoding` arg that evals to false. + $try = is_string( $encoding ) ? mb_substr( $str, $start, $length, $encoding ) : mb_substr( $str, $start, $length ); // mbstring funcs can fail if given `$encoding` arg that evals to false. if ( 'UTF-8' === $encoding && $is_width ) { $try = _safe_substr_eaw( $try, $length ); } @@ -271,6 +275,7 @@ function safe_substr( $str, $start, $length = false, $is_width = false, $encodin function _safe_substr_eaw( $str, $length ) { // Set the East Asian Width regex. $eaw_regex = get_unicode_regexs( 'eaw' ); + assert( is_string( $eaw_regex ) ); // If there's any East Asian double-width chars... if ( preg_match( $eaw_regex, $str ) ) { @@ -283,6 +288,9 @@ function _safe_substr_eaw( $str, $length ) { } else { // Explode string into an array of UTF-8 chars. Based on core `_mb_substr()` in "wp-includes/compat.php". $chars = preg_split( '/([\x00-\x7f\xc2-\xf4][^\x00-\x7f\xc2-\xf4]*)/', $str, $length + 1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY ); + if ( false === $chars ) { + $chars = array( $str ); + } $cnt = min( count( $chars ), $length ); $width = $length; @@ -326,7 +334,9 @@ function strwidth( $string, $encoding = false ) { $string = (string) $string; // Set the East Asian Width and Mark regexs. - list( $eaw_regex, $m_regex ) = get_unicode_regexs(); + $regexs = get_unicode_regexs(); + assert( is_array( $regexs ) ); + list( $eaw_regex, $m_regex ) = $regexs; // Allow for selective testings - "1" bit set tests grapheme_strlen(), "2" preg_match_all( '/\X/u' ), "4" mb_strwidth(), "other" safe_strlen(). $test_strwidth = (int) getenv( 'PHP_CLI_TOOLS_TEST_STRWIDTH' ); @@ -348,7 +358,7 @@ function strwidth( $string, $encoding = false ) { if ( ! $encoding ) { $encoding = mb_detect_encoding( $string, null, true /*strict*/ ); } - $width = $encoding ? mb_strwidth( $string, $encoding ) : mb_strwidth( $string ); // mbstring funcs can fail if given `$encoding` arg that evals to false. + $width = is_string( $encoding ) ? mb_strwidth( $string, $encoding ) : mb_strwidth( $string ); // mbstring funcs can fail if given `$encoding` arg that evals to false. if ( 'UTF-8' === $encoding ) { // Subtract combining characters. $width -= preg_match_all( $m_regex, $string, $dummy /*needed for PHP 5.3*/ ); diff --git a/lib/cli/table/Ascii.php b/lib/cli/table/Ascii.php index 4e33461..cb6b0d5 100644 --- a/lib/cli/table/Ascii.php +++ b/lib/cli/table/Ascii.php @@ -90,7 +90,7 @@ public function setWidths(array $widths, $fallback = false) { if ( $widths && $max_width && array_sum( $widths ) > $max_width ) { - $avg = floor( $max_width / count( $widths ) ); + $avg = (int) floor( $max_width / count( $widths ) ); $resize_widths = array(); $extra_width = 0; foreach( $widths as $width ) { @@ -102,7 +102,7 @@ public function setWidths(array $widths, $fallback = false) { } if ( ! empty( $resize_widths ) && $extra_width ) { - $avg_extra_width = floor( $extra_width / count( $resize_widths ) ); + $avg_extra_width = (int) floor( $extra_width / count( $resize_widths ) ); foreach( $widths as &$width ) { if ( in_array( $width, $resize_widths ) ) { $width = $avg + $avg_extra_width; @@ -198,6 +198,9 @@ public function row( array $row ) { $original_val_width = Colors::width( $value, self::isPreColorized( $col ), $encoding ); if ( $col_width && ( $original_val_width > $col_width || strpos( $value, "\n" ) !== false ) ) { $split_lines = preg_split( '/\r\n|\n/', $value ); + if ( false === $split_lines ) { + $split_lines = array( $value ); + } $wrapped_lines = []; foreach ( $split_lines as $line ) { @@ -308,11 +311,11 @@ protected function wrapText( $text, $width, $encoding, $is_precolorized ) { if ( 'truncate' === $this->_wrapping_mode ) { if ( $width <= self::ELLIPSIS_WIDTH ) { // Not enough space for ellipsis, just truncate - return array( \cli\safe_substr( $text, 0, $width, true /*is_width*/, $encoding ) ); + return array( (string) \cli\safe_substr( $text, 0, $width, true /*is_width*/, $encoding ) ); } // Truncate and add ellipsis - $truncated = \cli\safe_substr( $text, 0, $width - self::ELLIPSIS_WIDTH, true /*is_width*/, $encoding ); + $truncated = (string) \cli\safe_substr( $text, 0, $width - self::ELLIPSIS_WIDTH, true /*is_width*/, $encoding ); return array( $truncated . self::ELLIPSIS ); } @@ -331,11 +334,11 @@ protected function wrapText( $text, $width, $encoding, $is_precolorized ) { } else { // For non-colorized content, use character-boundary wrapping do { - $wrapped_value = \cli\safe_substr( $line, 0, $width, true /*is_width*/, $encoding ); + $wrapped_value = (string) \cli\safe_substr( $line, 0, $width, true /*is_width*/, $encoding ); $val_width = Colors::width( $wrapped_value, $is_precolorized, $encoding ); if ( $val_width ) { $wrapped_lines[] = $wrapped_value; - $line = \cli\safe_substr( $line, \cli\safe_strlen( $wrapped_value, $encoding ), null /*length*/, false /*is_width*/, $encoding ); + $line = (string) \cli\safe_substr( $line, \cli\safe_strlen( $wrapped_value, $encoding ), null /*length*/, false /*is_width*/, $encoding ); } } while ( $line ); } @@ -359,6 +362,9 @@ protected function wordWrap( $text, $width, $encoding, $is_precolorized ) { // Split by spaces and hyphens while keeping the delimiters $words = preg_split( '/(\s+|-)/u', $text, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY ); + if ( false === $words ) { + $words = array( $text ); + } foreach ( $words as $word ) { $word_width = Colors::width( $word, $is_precolorized, $encoding ); @@ -375,9 +381,9 @@ protected function wordWrap( $text, $width, $encoding, $is_precolorized ) { // Split the long word at character boundaries $remaining_word = $word; while ( $remaining_word ) { - $chunk = \cli\safe_substr( $remaining_word, 0, $width, true /*is_width*/, $encoding ); + $chunk = (string) \cli\safe_substr( $remaining_word, 0, $width, true /*is_width*/, $encoding ); $wrapped_lines[] = $chunk; - $remaining_word = \cli\safe_substr( $remaining_word, \cli\safe_strlen( $chunk, $encoding ), null /*length*/, false /*is_width*/, $encoding ); + $remaining_word = (string) \cli\safe_substr( $remaining_word, \cli\safe_strlen( $chunk, $encoding ), null /*length*/, false /*is_width*/, $encoding ); } continue; } diff --git a/lib/cli/table/Tabular.php b/lib/cli/table/Tabular.php index 31170ed..142deb2 100644 --- a/lib/cli/table/Tabular.php +++ b/lib/cli/table/Tabular.php @@ -33,6 +33,9 @@ public function row( array $row ) { $value = isset( $value ) ? (string) $value : ''; $value = str_replace( "\t", ' ', $value ); $split_lines = preg_split( '/\r\n|\n/', $value ); + if ( false === $split_lines ) { + $split_lines = array( $value ); + } // Keep anything before the first line break on the original line $row[ $col ] = array_shift( $split_lines ); } diff --git a/phpstan.neon.dist b/phpstan.neon.dist index fb750b9..0367424 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,5 +1,5 @@ parameters: - level: 6 + level: 7 paths: - lib scanDirectories: From ca8ddaac3fa2aabffdd567d8b7cbfbb5544c250b Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Tue, 7 Apr 2026 10:58:27 +0200 Subject: [PATCH 8/9] Level 8 --- lib/cli/Arguments.php | 30 ++++++++++++++++++++---------- lib/cli/Streams.php | 10 +++++++--- lib/cli/arguments/Argument.php | 2 +- lib/cli/arguments/Lexer.php | 9 +++++---- lib/cli/table/Tabular.php | 10 ++++++---- phpstan.neon.dist | 2 +- 6 files changed, 40 insertions(+), 23 deletions(-) diff --git a/lib/cli/Arguments.php b/lib/cli/Arguments.php index 561533d..9b95e95 100644 --- a/lib/cli/Arguments.php +++ b/lib/cli/Arguments.php @@ -73,7 +73,7 @@ public function getArguments() { if (!isset($this->_parsed)) { $this->parse(); } - return $this->_parsed; + return $this->_parsed ?? []; } /** @@ -110,7 +110,7 @@ public function offsetExists($offset) { $offset = $offset->key; } - return array_key_exists($offset, $this->_parsed); + return array_key_exists($offset, $this->_parsed ?? []); } /** @@ -442,15 +442,20 @@ public function parse() { $this->_applyDefaults(); - foreach ($this->_lexer as $argument) { - if ($this->_parseFlag($argument)) { - continue; - } - if ($this->_parseOption($argument)) { - continue; - } + if ($this->_lexer) { + foreach ($this->_lexer as $argument) { + if (null === $argument) { + continue; + } + if ($this->_parseFlag($argument)) { + continue; + } + if ($this->_parseOption($argument)) { + continue; + } - array_push($this->_invalid, $argument->raw()); + array_push($this->_invalid, $argument->raw()); + } } if ($this->_strict && !empty($this->_invalid)) { @@ -523,6 +528,8 @@ private function _parseOption($option) { return false; } + assert(null !== $this->_lexer); + // Peak ahead to make sure we get a value. if ($this->_lexer->end() || !$this->_lexer->peek->isValue) { $optionSettings = $this->getOption($option->key); @@ -546,6 +553,9 @@ private function _parseOption($option) { // Loop until we find a flag in peak-ahead while ( $this->_lexer->valid() ) { $value = $this->_lexer->current(); + if ( null === $value ) { + break; + } array_push( $values, $value->raw ); if ( ! $this->_lexer->end() && ! $this->_lexer->peek->isValue ) { diff --git a/lib/cli/Streams.php b/lib/cli/Streams.php index 786f55b..43da1b6 100755 --- a/lib/cli/Streams.php +++ b/lib/cli/Streams.php @@ -204,7 +204,7 @@ public static function prompt( $question, $default = false, $marker = ': ', $hid * * @param string $question The question to ask the user. * @param string $choice A string of characters allowed as a response. Case is ignored. - * @param string $default The default choice. NULL if a default is not allowed. + * @param string|null $default The default choice. NULL if a default is not allowed. * @return string The users choice. * @see cli\prompt() */ @@ -214,12 +214,16 @@ public static function choose( $question, $choice = 'yn', $default = 'n' ) { } // Make every choice character lowercase except the default - $choice = str_ireplace( $default, strtoupper( $default ), strtolower( $choice ) ); + if ( null !== $default ) { + $choice = str_ireplace( $default, strtoupper( $default ), strtolower( $choice ) ); + } else { + $choice = strtolower( $choice ); + } // Separate each choice with a forward-slash $choices = trim( join( '/', str_split( $choice ) ), '/' ); while( true ) { - $line = self::prompt( sprintf( '%s? [%s]', $question, $choices ), $default, '' ); + $line = self::prompt( sprintf( '%s? [%s]', $question, $choices ), $default ?? false, '' ); if( stripos( $choice, $line ) !== false ) { return strtolower( $line ); diff --git a/lib/cli/arguments/Argument.php b/lib/cli/arguments/Argument.php index c690f26..7ab070b 100644 --- a/lib/cli/arguments/Argument.php +++ b/lib/cli/arguments/Argument.php @@ -140,7 +140,7 @@ public function exploded() { array_push($exploded, $this->_argument[$i - 1]); } - $this->_argument = array_pop($exploded); + $this->_argument = (string) array_pop($exploded); $this->_raw = '-' . $this->_argument; return $exploded; } diff --git a/lib/cli/arguments/Lexer.php b/lib/cli/arguments/Lexer.php index 910110d..e8ac7d8 100644 --- a/lib/cli/arguments/Lexer.php +++ b/lib/cli/arguments/Lexer.php @@ -17,7 +17,7 @@ /** * @property-read Argument $peek * - * @implements \Iterator + * @implements \Iterator */ class Lexer extends Memoize implements \Iterator { /** @var Argument|null */ @@ -42,7 +42,7 @@ public function __construct(array $items) { /** * The current token. * - * @return Argument + * @return Argument|null */ #[\ReturnTypeWillChange] public function current() { @@ -125,7 +125,8 @@ public function end() { * @return void */ private function _shift() { - $this->_item = new Argument(array_shift($this->_items)); + $shifted = array_shift($this->_items); + $this->_item = null !== $shifted ? new Argument($shifted) : null; $this->_index += 1; $this->_explode(); $this->_unmemo('peek'); @@ -135,7 +136,7 @@ private function _shift() { * @return void */ private function _explode() { - if (!$this->_item->canExplode) { + if (null === $this->_item || !$this->_item->canExplode) { return; } diff --git a/lib/cli/table/Tabular.php b/lib/cli/table/Tabular.php index 142deb2..7b58d1f 100644 --- a/lib/cli/table/Tabular.php +++ b/lib/cli/table/Tabular.php @@ -42,11 +42,13 @@ public function row( array $row ) { $rows[] = $row; - foreach ( $split_lines as $i => $line ) { - if ( ! isset( $rows[ $i + 1 ] ) ) { - $rows[ $i + 1 ] = array_fill_keys( array_keys( $row ), '' ); + if ( null !== $col ) { + foreach ( $split_lines as $i => $line ) { + if ( ! isset( $rows[ $i + 1 ] ) ) { + $rows[ $i + 1 ] = array_fill_keys( array_keys( $row ), '' ); + } + $rows[ $i + 1 ][ $col ] = $line; } - $rows[ $i + 1 ][ $col ] = $line; } foreach ( $rows as $r ) { diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 0367424..984098b 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,5 +1,5 @@ parameters: - level: 7 + level: 8 paths: - lib scanDirectories: From 74c6f837e66342875ef51572bb6f38ab6f7f4040 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Tue, 7 Apr 2026 11:45:39 +0200 Subject: [PATCH 9/9] Level 9 --- lib/cli/Arguments.php | 63 +++++++++++-- lib/cli/Streams.php | 115 +++++++++++++---------- lib/cli/Table.php | 152 +++++++++++++++++-------------- lib/cli/arguments/HelpScreen.php | 97 ++++++++++++-------- lib/cli/arguments/Lexer.php | 3 +- lib/cli/cli.php | 2 +- lib/cli/table/Ascii.php | 107 +++++++++++----------- lib/cli/table/Tabular.php | 2 +- phpstan.neon.dist | 2 +- 9 files changed, 318 insertions(+), 225 deletions(-) diff --git a/lib/cli/Arguments.php b/lib/cli/Arguments.php index 9b95e95..c6e41de 100644 --- a/lib/cli/Arguments.php +++ b/lib/cli/Arguments.php @@ -50,17 +50,25 @@ class Arguments implements \ArrayAccess { public function __construct($options = array()) { $options += array( 'strict' => false, - 'input' => array_slice($_SERVER['argv'], 1) + 'input' => isset( $_SERVER['argv'] ) && is_array( $_SERVER['argv'] ) ? array_slice( $_SERVER['argv'], 1 ) : array(), ); - $this->_input = $options['input']; - $this->setStrict($options['strict']); + $input = $options['input']; + if ( ! is_array( $input ) ) { + $input = array(); + } + $this->_input = array_map( function( $item ) { return is_scalar( $item ) ? (string) $item : ''; }, $input ); + $this->setStrict( ! empty( $options['strict'] ) ); - if (isset($options['flags'])) { - $this->addFlags($options['flags']); + if ( isset( $options['flags'] ) && is_array( $options['flags'] ) ) { + /** @var array|string> $flags */ + $flags = $options['flags']; + $this->addFlags( $flags ); } - if (isset($options['options'])) { - $this->addOptions($options['options']); + if ( isset( $options['options'] ) && is_array( $options['options'] ) ) { + /** @var array|string> $opts */ + $opts = $options['options']; + $this->addOptions( $opts ); } } @@ -110,6 +118,10 @@ public function offsetExists($offset) { $offset = $offset->key; } + if ( ! is_string( $offset ) && ! is_int( $offset ) ) { + return false; + } + return array_key_exists($offset, $this->_parsed ?? []); } @@ -125,6 +137,10 @@ public function offsetGet($offset) { $offset = $offset->key; } + if ( ! is_string( $offset ) && ! is_int( $offset ) ) { + return null; + } + if (isset($this->_parsed[$offset])) { return $this->_parsed[$offset]; } @@ -144,6 +160,11 @@ public function offsetSet($offset, $value) { $offset = $offset->key; } + if ( ! is_string( $offset ) && ! is_int( $offset ) ) { + return; + } + + $offset = (string) $offset; $this->_parsed[$offset] = $value; } @@ -158,6 +179,10 @@ public function offsetUnset($offset) { $offset = $offset->key; } + if ( ! is_string( $offset ) && ! is_int( $offset ) ) { + return; + } + unset($this->_parsed[$offset]); } @@ -180,6 +205,11 @@ public function addFlag($flag, $settings = array()) { $settings['aliases'] = $flag; $flag = array_shift($settings['aliases']); } + if ( is_scalar( $flag ) ) { + $flag = (string) $flag; + } else { + $flag = ''; + } if (isset($this->_flags[$flag])) { $this->_warn('flag already exists: ' . $flag); return $this; @@ -235,6 +265,11 @@ public function addOption($option, $settings = array()) { $settings['aliases'] = $option; $option = array_shift($settings['aliases']); } + if ( is_scalar( $option ) ) { + $option = (string) $option; + } else { + $option = ''; + } if (isset($this->_options[$option])) { $this->_warn('option already exists: ' . $option); return $this; @@ -308,6 +343,10 @@ public function getFlag($flag) { $flag = $flag->value(); } + if ( ! is_string( $flag ) && ! is_int( $flag ) ) { + return null; + } + if (isset($this->_flags[$flag])) { return $this->_flags[$flag]; } @@ -381,6 +420,10 @@ public function getOption($option) { $option = $option->value(); } + if ( ! is_string( $option ) && ! is_int( $option ) ) { + return null; + } + if (isset($this->_options[$option])) { return $this->_options[$option]; } @@ -454,7 +497,8 @@ public function parse() { continue; } - array_push($this->_invalid, $argument->raw()); + $raw = $argument->raw(); + array_push($this->_invalid, is_scalar($raw) ? (string) $raw : ''); } } @@ -509,7 +553,8 @@ private function _parseFlag($argument) { $this[$argument->key] = 0; } - $this[$argument->key] += 1; + $current = $this[$argument->key]; + $this[$argument->key] = (is_int($current) ? $current : 0) + 1; } else { $this[$argument->key] = true; } diff --git a/lib/cli/Streams.php b/lib/cli/Streams.php index 43da1b6..22a22e5 100755 --- a/lib/cli/Streams.php +++ b/lib/cli/Streams.php @@ -29,11 +29,11 @@ static function _call( $func, $args ) { * * @return bool */ - static public function isTty() { - if ( function_exists('stream_isatty') ) { - return stream_isatty(static::$out); + public static function isTty() { + if ( function_exists( 'stream_isatty' ) ) { + return stream_isatty( static::$out ); } else { - return (function_exists('posix_isatty') && posix_isatty(static::$out)); + return ( function_exists( 'posix_isatty' ) && posix_isatty( static::$out ) ); } } @@ -48,31 +48,32 @@ static public function isTty() { * @return string The rendered string. */ public static function render( $msg, ...$args ) { - $args = func_get_args(); - // No string replacement is needed - if( count( $args ) == 1 || ( is_string( $args[1] ) && '' === $args[1] ) ) { + if ( empty( $args ) || ( is_string( $args[0] ) && '' === $args[0] ) ) { return Colors::shouldColorize() ? Colors::colorize( $msg ) : $msg; } // If the first argument is not an array just pass to sprintf - if( !is_array( $args[1] ) ) { + if ( ! is_array( $args[0] ) ) { // Normalize color tokens before sprintf: colorize or strip them so no raw %tokens reach sprintf if ( Colors::shouldColorize() ) { - $args[0] = Colors::colorize( $args[0] ); + $msg = Colors::colorize( $msg ); } else { - $args[0] = Colors::decolorize( $args[0] ); + $msg = Colors::decolorize( $msg ); } // Escape percent characters for sprintf - $args[0] = preg_replace('/(%([^\w]|$))/', "%$1", $args[0]); + $msg = (string) preg_replace( '/(%([^\w]|$))/', '%$1', $msg ); - return call_user_func_array( 'sprintf', $args ); + $sprintf_args = array_merge( array( $msg ), $args ); + /** @var string $rendered */ + $rendered = call_user_func_array( 'sprintf', $sprintf_args ); + return $rendered; } // Here we do named replacement so formatting strings are more understandable - foreach( $args[1] as $key => $value ) { - $msg = str_replace( '{:' . $key . '}', $value, $msg ); + foreach ( $args[0] as $key => $value ) { + $msg = str_replace( '{:' . $key . '}', is_scalar( $value ) ? (string) $value : '', $msg ); } return Colors::shouldColorize() ? Colors::colorize( $msg ) : $msg; } @@ -87,7 +88,8 @@ public static function render( $msg, ...$args ) { * @see \cli\render() */ public static function out( $msg, ...$args ) { - fwrite( static::$out, self::_call( 'render', func_get_args() ) ); + $rendered = self::_call( 'render', func_get_args() ); + fwrite( static::$out, is_scalar( $rendered ) ? (string) $rendered : '' ); } /** @@ -99,7 +101,8 @@ public static function out( $msg, ...$args ) { * @see cli\out() */ public static function out_padded( $msg, ...$args ) { - $msg = self::_call( 'render', func_get_args() ); + $rendered = self::_call( 'render', func_get_args() ); + $msg = is_scalar( $rendered ) ? (string) $rendered : ''; self::out( str_pad( $msg, \cli\Shell::columns() ) ); } @@ -113,8 +116,8 @@ public static function out_padded( $msg, ...$args ) { */ public static function line( $msg = '' ) { // func_get_args is empty if no args are passed even with the default above. - $args = array_merge( func_get_args(), array( '' ) ); - $args[0] .= "\n"; + $args = array_merge( func_get_args(), array( '' ) ); + $args[0] = ( is_scalar( $args[0] ) ? (string) $args[0] : '' ) . "\n"; self::_call( 'out', $args ); } @@ -130,9 +133,10 @@ public static function line( $msg = '' ) { */ public static function err( $msg = '', ...$args ) { // func_get_args is empty if no args are passed even with the default above. - $args = array_merge( func_get_args(), array( '' ) ); - $args[0] .= "\n"; - fwrite( static::$err, self::_call( 'render', $args ) ); + $args = array_merge( func_get_args(), array( '' ) ); + $args[0] = ( is_scalar( $args[0] ) ? (string) $args[0] : '' ) . "\n"; + $rendered = self::_call( 'render', $args ); + fwrite( static::$err, is_scalar( $rendered ) ? (string) $rendered : '' ); } /** @@ -147,10 +151,11 @@ public static function err( $msg = '', ...$args ) { * @throws \Exception Thrown if ctrl-D (EOT) is sent as input. */ public static function input( $format = null, $hide = false ) { - if ( $hide ) + if ( $hide ) { Shell::hide(); + } - if( $format ) { + if ( $format ) { fscanf( static::$in, $format . "\n", $line ); } else { $line = fgets( static::$in ); @@ -161,7 +166,7 @@ public static function input( $format = null, $hide = false ) { echo "\n"; } - if( $line === false ) { + if ( $line === false ) { throw new \Exception( 'Caught ^D during input' ); } @@ -181,18 +186,18 @@ public static function input( $format = null, $hide = false ) { * @see cli\input() */ public static function prompt( $question, $default = false, $marker = ': ', $hide = false ) { - if( $default && strpos( $question, '[' ) === false ) { + if ( $default && strpos( $question, '[' ) === false ) { $question .= ' [' . $default . ']'; } - while( true ) { + while ( true ) { self::out( $question . $marker ); $line = self::input( null, $hide ); if ( trim( $line ) !== '' ) { return $line; } - if( $default !== false ) { + if ( $default !== false ) { return (string) $default; } } @@ -209,7 +214,7 @@ public static function prompt( $question, $default = false, $marker = ': ', $hid * @see cli\prompt() */ public static function choose( $question, $choice = 'yn', $default = 'n' ) { - if( !is_string( $choice ) ) { + if ( ! is_string( $choice ) ) { $choice = join( '', $choice ); } @@ -222,13 +227,13 @@ public static function choose( $question, $choice = 'yn', $default = 'n' ) { // Separate each choice with a forward-slash $choices = trim( join( '/', str_split( $choice ) ), '/' ); - while( true ) { + while ( true ) { $line = self::prompt( sprintf( '%s? [%s]', $question, $choices ), $default ?? false, '' ); - if( stripos( $choice, $line ) !== false ) { + if ( stripos( $choice, $line ) !== false ) { return strtolower( $line ); } - if( !empty( $default ) ) { + if ( ! empty( $default ) ) { return strtolower( $default ); } } @@ -250,29 +255,42 @@ public static function choose( $question, $choice = 'yn', $default = 'n' ) { public static function menu( $items, $default = null, $title = 'Choose an item' ) { $map = array_values( $items ); - if( $default && strpos( $title, '[' ) === false && isset( $items[$default] ) ) { - $title .= ' [' . $items[$default] . ']'; + if ( $default && strpos( $title, '[' ) === false && isset( $items[ $default ] ) ) { + $default_item = $items[ $default ]; + $default_str = ''; + if ( is_scalar( $default_item ) ) { + $default_str = (string) $default_item; + } elseif ( is_object( $default_item ) && method_exists( $default_item, '__toString' ) ) { + $default_str = (string) $default_item; + } + $title .= ' [' . $default_str . ']'; } - foreach( $map as $idx => $item ) { - self::line( ' %d. %s', $idx + 1, (string)$item ); + foreach ( $map as $idx => $item ) { + $item_str = ''; + if ( is_scalar( $item ) ) { + $item_str = (string) $item; + } elseif ( is_object( $item ) && method_exists( $item, '__toString' ) ) { + $item_str = (string) $item; + } + self::line( ' %d. %s', $idx + 1, $item_str ); } self::line(); - while( true ) { + while ( true ) { fwrite( static::$out, sprintf( '%s: ', $title ) ); $line = self::input(); - if( is_numeric( $line ) ) { - $line--; - if( isset( $map[$line] ) ) { - return (string) array_search( $map[$line], $items ); + if ( is_numeric( $line ) ) { + --$line; + if ( isset( $map[ $line ] ) ) { + return (string) array_search( $map[ $line ], $items ); } - if( $line < 0 || $line >= count( $map ) ) { + if ( $line < 0 || $line >= count( $map ) ) { self::err( 'Invalid menu selection: out of range' ); } - } else if( isset( $default ) ) { + } elseif ( isset( $default ) ) { return $default; } } @@ -295,15 +313,16 @@ public static function menu( $items, $default = null, $title = 'Choose an item' * @throws \Exception Thrown if $stream is not a resource of the 'stream' type. */ public static function setStream( $whichStream, $stream ) { - if( !is_resource( $stream ) || get_resource_type( $stream ) !== 'stream' ) { + if ( ! is_resource( $stream ) || get_resource_type( $stream ) !== 'stream' ) { throw new \Exception( 'Invalid resource type!' ); } - if( property_exists( __CLASS__, $whichStream ) ) { + if ( property_exists( __CLASS__, $whichStream ) ) { static::${$whichStream} = $stream; } - register_shutdown_function( function() use ($stream) { - fclose( $stream ); - } ); + register_shutdown_function( + function () use ( $stream ) { + fclose( $stream ); + } + ); } - } diff --git a/lib/cli/Table.php b/lib/cli/Table.php index c166792..770de06 100644 --- a/lib/cli/Table.php +++ b/lib/cli/Table.php @@ -60,36 +60,47 @@ class Table { * @param array $footers Footers used in this table. Optional. * @param array $alignments Column alignments. Optional. */ - public function __construct(array $headers = array(), array $rows = array(), array $footers = array(), array $alignments = array()) { - if (!empty($headers)) { + public function __construct( array $headers = array(), array $rows = array(), array $footers = array(), array $alignments = array() ) { + $safe_strval = function ( $v ) { + return ( is_scalar( $v ) || ( is_object( $v ) && method_exists( $v, '__toString' ) ) ) ? (string) $v : ''; + }; + + if ( ! empty( $headers ) ) { // If all the rows is given in $headers we use the keys from the // first row for the header values - if ($rows === array()) { - $rows = $headers; - $keys = array_keys(array_shift($headers)); - $headers = array(); + if ( $rows === array() ) { + $rows = $headers; + $first_row = array_shift( $headers ); + $keys = is_array( $first_row ) ? array_keys( $first_row ) : array(); + $headers = array_map( $safe_strval, $keys ); + } else { + $headers = array_map( $safe_strval, $headers ); + } + + $this->setHeaders( $headers ); - foreach ($keys as $header) { - $headers[$header] = $header; + $safe_rows = array(); + foreach ( $rows as $row ) { + if ( is_array( $row ) ) { + $safe_rows[] = array_map( $safe_strval, $row ); } } - - $this->setHeaders($headers); - $this->setRows($rows); + $this->setRows( $safe_rows ); } - if (!empty($footers)) { - $this->setFooters($footers); + if ( ! empty( $footers ) ) { + $this->setFooters( array_map( $safe_strval, $footers ) ); } - if (!empty($alignments)) { - $this->setAlignments($alignments); + if ( ! empty( $alignments ) ) { + /** @var array|array $alignments */ + $this->setAlignments( $alignments ); } - if (Shell::isPiped()) { - $this->setRenderer(new Tabular()); + if ( Shell::isPiped() ) { + $this->setRenderer( new Tabular() ); } else { - $this->setRenderer(new Ascii()); + $this->setRenderer( new Ascii() ); } } @@ -98,12 +109,11 @@ public function __construct(array $headers = array(), array $rows = array(), arr * * @return $this */ - public function resetTable() - { - $this->_headers = array(); - $this->_width = array(); - $this->_rows = array(); - $this->_footers = array(); + public function resetTable() { + $this->_headers = array(); + $this->_width = array(); + $this->_rows = array(); + $this->_footers = array(); $this->_alignments = array(); return $this; } @@ -113,8 +123,7 @@ public function resetTable() * * @return $this */ - public function resetRows() - { + public function resetRows() { $this->_rows = array(); return $this; } @@ -128,7 +137,7 @@ public function resetRows() * @see table\Tabular * @return void */ - public function setRenderer(Renderer $renderer) { + public function setRenderer( Renderer $renderer ) { $this->_renderer = $renderer; } @@ -138,11 +147,11 @@ public function setRenderer(Renderer $renderer) { * @param array $row The table row. * @return array $row */ - protected function checkRow(array $row) { - foreach ($row as $column => $str) { + protected function checkRow( array $row ) { + foreach ( $row as $column => $str ) { $width = Colors::width( $str, $this->isAsciiPreColorized( $column ) ); - if (!isset($this->_width[$column]) || $width > $this->_width[$column]) { - $this->_width[$column] = $width; + if ( ! isset( $this->_width[ $column ] ) || $width > $this->_width[ $column ] ) { + $this->_width[ $column ] = $width; } } @@ -161,7 +170,7 @@ protected function checkRow(array $row) { * @return void */ public function display() { - foreach( $this->getDisplayLines() as $line ) { + foreach ( $this->getDisplayLines() as $line ) { Streams::line( $line ); } } @@ -175,21 +184,21 @@ public function display() { * @param array $row The row data to display. * @return void */ - public function displayRow(array $row) { + public function displayRow( array $row ) { // Update widths if this row has wider content - $row = $this->checkRow($row); - + $row = $this->checkRow( $row ); + // Recalculate widths for the renderer - $this->_renderer->setWidths($this->_width, false); - - $rendered_row = $this->_renderer->row($row); - $row_lines = explode( PHP_EOL, $rendered_row ); + $this->_renderer->setWidths( $this->_width, false ); + + $rendered_row = $this->_renderer->row( $row ); + $row_lines = explode( PHP_EOL, $rendered_row ); foreach ( $row_lines as $line ) { Streams::line( $line ); } - + $border = $this->_renderer->border(); - if (isset($border)) { + if ( isset( $border ) ) { Streams::line( $border ); } } @@ -203,34 +212,34 @@ public function displayRow(array $row) { * @return array */ public function getDisplayLines() { - $this->_renderer->setWidths($this->_width, $fallback = true); - $this->_renderer->setHeaders($this->_headers); - $this->_renderer->setAlignments($this->_alignments); + $this->_renderer->setWidths( $this->_width, $fallback = true ); + $this->_renderer->setHeaders( $this->_headers ); + $this->_renderer->setAlignments( $this->_alignments ); $border = $this->_renderer->border(); $out = array(); - if (isset($border)) { + if ( isset( $border ) ) { $out[] = $border; } - $out[] = $this->_renderer->row($this->_headers); - if (isset($border)) { + $out[] = $this->_renderer->row( $this->_headers ); + if ( isset( $border ) ) { $out[] = $border; } - foreach ($this->_rows as $row) { - $row = $this->_renderer->row($row); + foreach ( $this->_rows as $row ) { + $row = $this->_renderer->row( $row ); $row = explode( PHP_EOL, $row ); $out = array_merge( $out, $row ); } // Only add final border if there are rows - if (!empty($this->_rows) && isset($border)) { + if ( ! empty( $this->_rows ) && isset( $border ) ) { $out[] = $border; } - if ($this->_footers) { - $out[] = $this->_renderer->row($this->_footers); - if (isset($border)) { + if ( $this->_footers ) { + $out[] = $this->_renderer->row( $this->_footers ); + if ( isset( $border ) ) { $out[] = $border; } } @@ -243,15 +252,18 @@ public function getDisplayLines() { * @param int $column The index of the column to sort by. * @return void */ - public function sort($column) { - if (!isset($this->_headers[$column])) { - trigger_error('No column with index ' . $column, E_USER_NOTICE); + public function sort( $column ) { + if ( ! isset( $this->_headers[ $column ] ) ) { + trigger_error( 'No column with index ' . $column, E_USER_NOTICE ); return; } - usort($this->_rows, function($a, $b) use ($column) { - return strcmp($a[$column], $b[$column]); - }); + usort( + $this->_rows, + function ( $a, $b ) use ( $column ) { + return strcmp( $a[ $column ], $b[ $column ] ); + } + ); } /** @@ -260,8 +272,8 @@ public function sort($column) { * @param array $headers An array of strings containing column header names. * @return void */ - public function setHeaders(array $headers) { - $this->_headers = $this->checkRow($headers); + public function setHeaders( array $headers ) { + $this->_headers = $this->checkRow( $headers ); } /** @@ -270,8 +282,8 @@ public function setHeaders(array $headers) { * @param array $footers An array of strings containing column footers names. * @return void */ - public function setFooters(array $footers) { - $this->_footers = $this->checkRow($footers); + public function setFooters( array $footers ) { + $this->_footers = $this->checkRow( $footers ); } /** @@ -280,7 +292,7 @@ public function setFooters(array $footers) { * @param array|array $alignments An array of alignment constants keyed by column name or index. * @return void */ - public function setAlignments(array $alignments) { + public function setAlignments( array $alignments ) { // Initialize the cached valid alignments map on first use if ( null === self::$_valid_alignments_map ) { self::$_valid_alignments_map = array_flip( array( Column::ALIGN_LEFT, Column::ALIGN_RIGHT, Column::ALIGN_CENTER ) ); @@ -306,8 +318,8 @@ public function setAlignments(array $alignments) { * @see cli\Table::checkRow() * @return void */ - public function addRow(array $row) { - $this->_rows[] = $this->checkRow($row); + public function addRow( array $row ) { + $this->_rows[] = $this->checkRow( $row ); } /** @@ -317,10 +329,10 @@ public function addRow(array $row) { * @see cli\Table::addRow() * @return void */ - public function setRows(array $rows) { + public function setRows( array $rows ) { $this->_rows = array(); - foreach ($rows as $row) { - $this->addRow($row); + foreach ( $rows as $row ) { + $this->addRow( $row ); } } @@ -330,7 +342,7 @@ public function setRows(array $rows) { * @return int */ public function countRows() { - return count($this->_rows); + return count( $this->_rows ); } /** diff --git a/lib/cli/arguments/HelpScreen.php b/lib/cli/arguments/HelpScreen.php index fd4b346..ed4b7b3 100644 --- a/lib/cli/arguments/HelpScreen.php +++ b/lib/cli/arguments/HelpScreen.php @@ -30,8 +30,8 @@ class HelpScreen { /** * @param Arguments $arguments */ - public function __construct(Arguments $arguments) { - $this->setArguments($arguments); + public function __construct( Arguments $arguments ) { + $this->setArguments( $arguments ); } /** @@ -45,19 +45,19 @@ public function __toString() { * @param Arguments $arguments * @return void */ - public function setArguments(Arguments $arguments) { - $this->consumeArgumentFlags($arguments); - $this->consumeArgumentOptions($arguments); + public function setArguments( Arguments $arguments ) { + $this->consumeArgumentFlags( $arguments ); + $this->consumeArgumentOptions( $arguments ); } /** * @param Arguments $arguments * @return void */ - public function consumeArgumentFlags(Arguments $arguments) { - $data = $this->_consume($arguments->getFlags()); + public function consumeArgumentFlags( Arguments $arguments ) { + $data = $this->_consume( $arguments->getFlags() ); - $this->_flags = $data[0]; + $this->_flags = $data[0]; $this->_flagMax = $data[1]; } @@ -65,10 +65,10 @@ public function consumeArgumentFlags(Arguments $arguments) { * @param Arguments $arguments * @return void */ - public function consumeArgumentOptions(Arguments $arguments) { - $data = $this->_consume($arguments->getOptions()); + public function consumeArgumentOptions( Arguments $arguments ) { + $data = $this->_consume( $arguments->getOptions() ); - $this->_options = $data[0]; + $this->_options = $data[0]; $this->_optionMax = $data[1]; } @@ -78,32 +78,32 @@ public function consumeArgumentOptions(Arguments $arguments) { public function render() { $help = array(); - array_push($help, $this->_renderFlags()); - array_push($help, $this->_renderOptions()); + array_push( $help, $this->_renderFlags() ); + array_push( $help, $this->_renderOptions() ); - return join("\n\n", $help); + return join( "\n\n", $help ); } /** * @return string|null */ private function _renderFlags() { - if (empty($this->_flags)) { + if ( empty( $this->_flags ) ) { return null; } - return "Flags\n" . $this->_renderScreen($this->_flags, $this->_flagMax); + return "Flags\n" . $this->_renderScreen( $this->_flags, $this->_flagMax ); } /** * @return string|null */ private function _renderOptions() { - if (empty($this->_options)) { + if ( empty( $this->_options ) ) { return null; } - return "Options\n" . $this->_renderScreen($this->_options, $this->_optionMax); + return "Options\n" . $this->_renderScreen( $this->_options, $this->_optionMax ); } /** @@ -111,51 +111,68 @@ private function _renderOptions() { * @param int $max * @return string */ - private function _renderScreen($options, $max) { + private function _renderScreen( $options, $max ) { $help = array(); - foreach ($options as $option => $settings) { - $formatted = ' ' . str_pad($option, $max); + foreach ( $options as $option => $settings ) { + $formatted = ' ' . str_pad( $option, $max ); - $dlen = max( 1, 80 - 4 - $max ); - $description = str_split($settings['description'], $dlen); - $formatted.= ' ' . array_shift($description); + $dlen = max( 1, 80 - 4 - $max ); + $settings_desc = $settings['description']; + $desc_str = ( is_scalar( $settings_desc ) || ( is_object( $settings_desc ) && method_exists( $settings_desc, '__toString' ) ) ) ? (string) $settings_desc : ''; - if ($settings['default']) { - $formatted .= ' [default: ' . $settings['default'] . ']'; + $description = array(); + if ( '' !== $desc_str ) { + $description = str_split( $desc_str, $dlen ); } - $pad = str_repeat(' ', $max + 3); - while ($desc = array_shift($description)) { + if ( empty( $description ) ) { + $description = array( '' ); + } + + $formatted .= ' ' . array_shift( $description ); + + if ( ! empty( $settings['default'] ) ) { + $default_val = $settings['default']; + $default_str = ( is_scalar( $default_val ) || ( is_object( $default_val ) && method_exists( $default_val, '__toString' ) ) ) ? (string) $default_val : ''; + if ( '' !== $default_str ) { + $formatted .= ' [default: ' . $default_str . ']'; + } + } + + $pad = str_repeat( ' ', $max + 3 ); + while ( $desc = array_shift( $description ) ) { $formatted .= "\n{$pad}{$desc}"; } - array_push($help, $formatted); + array_push( $help, $formatted ); } - return join("\n", $help); + return join( "\n", $help ); } /** * @param array> $options * @return array{0: array>, 1: int} */ - private function _consume($options) { + private function _consume( $options ) { $max = 0; $out = array(); - foreach ($options as $option => $settings) { - $names = array('--' . $option); + foreach ( $options as $option => $settings ) { + $names = array( '--' . $option ); - foreach ($settings['aliases'] as $alias) { - array_push($names, '-' . $alias); + $aliases = $settings['aliases']; + if ( is_array( $aliases ) ) { + foreach ( $aliases as $alias ) { + array_push( $names, '-' . ( is_scalar( $alias ) ? (string) $alias : '' ) ); + } } - $names = join(', ', $names); - $max = max(strlen($names), $max); - $out[$names] = $settings; + $names = join( ', ', $names ); + $max = max( strlen( $names ), $max ); + $out[ $names ] = $settings; } - return array($out, $max); + return array( $out, $max ); } } - diff --git a/lib/cli/arguments/Lexer.php b/lib/cli/arguments/Lexer.php index e8ac7d8..381e3a6 100644 --- a/lib/cli/arguments/Lexer.php +++ b/lib/cli/arguments/Lexer.php @@ -108,7 +108,8 @@ public function valid() { * @return void */ public function unshift($item) { - array_unshift($this->_items, $item); + $item_str = (is_scalar($item) || (is_object($item) && method_exists($item, '__toString'))) ? (string)$item : ''; + array_unshift($this->_items, $item_str); $this->_length += 1; } diff --git a/lib/cli/cli.php b/lib/cli/cli.php index c8cf458..b86eaa6 100755 --- a/lib/cli/cli.php +++ b/lib/cli/cli.php @@ -24,7 +24,7 @@ * @return string The rendered string. */ function render( $msg, ...$args ) { - return Streams::_call( 'render', func_get_args() ); + return Streams::render( $msg, ...$args ); } /** diff --git a/lib/cli/table/Ascii.php b/lib/cli/table/Ascii.php index cb6b0d5..b71d5d4 100644 --- a/lib/cli/table/Ascii.php +++ b/lib/cli/table/Ascii.php @@ -71,10 +71,10 @@ class Ascii extends Renderer { * @param bool $fallback Whether to use these values as fallback only. * @return void */ - public function setWidths(array $widths, $fallback = false) { - if ($fallback) { + public function setWidths( array $widths, $fallback = false ) { + if ( $fallback ) { foreach ( $this->_widths as $index => $value ) { - $widths[$index] = $value; + $widths[ $index ] = $value; } } $this->_widths = $widths; @@ -82,18 +82,18 @@ public function setWidths(array $widths, $fallback = false) { if ( is_null( $this->_constraintWidth ) ) { $this->_constraintWidth = (int) Shell::columns(); } - $col_count = count( $widths ); - $col_borders_count = $col_count ? ( ( $col_count - 1 ) * strlen( $this->_characters['border'] ) ) : 0; + $col_count = count( $widths ); + $col_borders_count = $col_count ? ( ( $col_count - 1 ) * strlen( $this->_characters['border'] ) ) : 0; $table_borders_count = strlen( $this->_characters['border'] ) * 2; - $col_padding_count = $col_count * strlen( $this->_characters['padding'] ) * 2; - $max_width = $this->_constraintWidth - $col_borders_count - $table_borders_count - $col_padding_count; + $col_padding_count = $col_count * strlen( $this->_characters['padding'] ) * 2; + $max_width = $this->_constraintWidth - $col_borders_count - $table_borders_count - $col_padding_count; if ( $widths && $max_width && array_sum( $widths ) > $max_width ) { - $avg = (int) floor( $max_width / count( $widths ) ); + $avg = (int) floor( $max_width / count( $widths ) ); $resize_widths = array(); - $extra_width = 0; - foreach( $widths as $width ) { + $extra_width = 0; + foreach ( $widths as $width ) { if ( $width > $avg ) { $resize_widths[] = $width; } else { @@ -103,7 +103,7 @@ public function setWidths(array $widths, $fallback = false) { if ( ! empty( $resize_widths ) && $extra_width ) { $avg_extra_width = (int) floor( $extra_width / count( $resize_widths ) ); - foreach( $widths as &$width ) { + foreach ( $widths as &$width ) { if ( in_array( $width, $resize_widths ) ) { $width = $avg + $avg_extra_width; array_shift( $resize_widths ); @@ -115,7 +115,6 @@ public function setWidths(array $widths, $fallback = false) { } } } - } $this->_widths = $widths; @@ -155,8 +154,8 @@ public function setWrappingMode( $mode ) { * @param array $characters Characters used in rendering. * @return void */ - public function setCharacters(array $characters) { - $this->_characters = array_merge($this->_characters, $characters); + public function setCharacters( array $characters ) { + $this->_characters = array_merge( $this->_characters, $characters ); } /** @@ -166,10 +165,10 @@ public function setCharacters(array $characters) { * @return string The table border. */ public function border() { - if (!isset($this->_border)) { + if ( ! isset( $this->_border ) ) { $this->_border = $this->_characters['corner']; - foreach ($this->_widths as $width) { - $this->_border .= str_repeat($this->_characters['line'], $width + 2); + foreach ( $this->_widths as $width ) { + $this->_border .= str_repeat( $this->_characters['line'], $width + 2 ); $this->_border .= $this->_characters['corner']; } } @@ -192,7 +191,7 @@ public function row( array $row ) { $extra_rows = array_fill( 0, count( $row ), array() ); foreach ( $row as $col => $value ) { - $value = $value ?: ''; + $value = ( is_scalar( $value ) || ( is_object( $value ) && method_exists( $value, '__toString' ) ) ) ? (string) $value : ''; $col_width = $this->_widths[ $col ]; $encoding = function_exists( 'mb_detect_encoding' ) ? mb_detect_encoding( $value, null, true /*strict*/ ) : false; $original_val_width = Colors::width( $value, self::isPreColorized( $col ), $encoding ); @@ -204,7 +203,7 @@ public function row( array $row ) { $wrapped_lines = []; foreach ( $split_lines as $line ) { - $line_wrapped = $this->wrapText( $line, $col_width, $encoding, self::isPreColorized( $col ) ); + $line_wrapped = $this->wrapText( $line, $col_width, $encoding, self::isPreColorized( $col ) ); $wrapped_lines = array_merge( $wrapped_lines, $line_wrapped ); } @@ -217,34 +216,34 @@ public function row( array $row ) { } } - $row = array_map(array($this, 'padColumn'), $row, array_keys($row)); - array_unshift($row, ''); // First border - array_push($row, ''); // Last border + $row = array_map( array( $this, 'padColumn' ), $row, array_keys( $row ) ); + array_unshift( $row, '' ); // First border + array_push( $row, '' ); // Last border - $ret = join($this->_characters['border'], $row); + $ret = join( $this->_characters['border'], $row ); if ( $extra_row_count ) { - foreach( $extra_rows as $col => $col_values ) { - while( count( $col_values ) < $extra_row_count ) { + foreach ( $extra_rows as $col => $col_values ) { + while ( count( $col_values ) < $extra_row_count ) { $col_values[] = ''; } } do { $row_values = array(); - $has_more = false; - foreach( $extra_rows as $col => &$col_values ) { + $has_more = false; + foreach ( $extra_rows as $col => &$col_values ) { $row_values[ $col ] = ! empty( $col_values ) ? array_shift( $col_values ) : ''; if ( count( $col_values ) ) { $has_more = true; } } - $row_values = array_map(array($this, 'padColumn'), $row_values, array_keys($row_values)); - array_unshift($row_values, ''); // First border - array_push($row_values, ''); // Last border + $row_values = array_map( array( $this, 'padColumn' ), $row_values, array_keys( $row_values ) ); + array_unshift( $row_values, '' ); // First border + array_push( $row_values, '' ); // Last border - $ret .= PHP_EOL . join($this->_characters['border'], $row_values); - } while( $has_more ); + $ret .= PHP_EOL . join( $this->_characters['border'], $row_values ); + } while ( $has_more ); } return $ret; } @@ -272,7 +271,7 @@ private function getColumnAlignment( $column ) { */ private function padColumn( $content, $column ) { $alignment = $this->getColumnAlignment( $column ); - $content = str_replace( "\t", ' ', (string) $content ); + $content = str_replace( "\t", ' ', (string) $content ); return $this->_characters['padding'] . Colors::pad( $content, $this->_widths[ $column ], $this->isPreColorized( $column ), false, $alignment ) . $this->_characters['padding']; } @@ -313,7 +312,7 @@ protected function wrapText( $text, $width, $encoding, $is_precolorized ) { // Not enough space for ellipsis, just truncate return array( (string) \cli\safe_substr( $text, 0, $width, true /*is_width*/, $encoding ) ); } - + // Truncate and add ellipsis $truncated = (string) \cli\safe_substr( $text, 0, $width - self::ELLIPSIS_WIDTH, true /*is_width*/, $encoding ); return array( $truncated . self::ELLIPSIS ); @@ -326,8 +325,8 @@ protected function wrapText( $text, $width, $encoding, $is_precolorized ) { // Default: character-boundary wrapping $wrapped_lines = array(); - $line = $text; - + $line = $text; + // Use the new color-aware wrapping for pre-colorized content if ( $is_precolorized ) { $wrapped_lines = Colors::wrapPreColorized( $line, $width, $encoding ); @@ -338,11 +337,11 @@ protected function wrapText( $text, $width, $encoding, $is_precolorized ) { $val_width = Colors::width( $wrapped_value, $is_precolorized, $encoding ); if ( $val_width ) { $wrapped_lines[] = $wrapped_value; - $line = (string) \cli\safe_substr( $line, \cli\safe_strlen( $wrapped_value, $encoding ), null /*length*/, false /*is_width*/, $encoding ); + $line = (string) \cli\safe_substr( $line, \cli\safe_strlen( $wrapped_value, $encoding ), null /*length*/, false /*is_width*/, $encoding ); } } while ( $line ); } - + return $wrapped_lines; } @@ -356,56 +355,56 @@ protected function wrapText( $text, $width, $encoding, $is_precolorized ) { * @return array Array of wrapped lines. */ protected function wordWrap( $text, $width, $encoding, $is_precolorized ) { - $wrapped_lines = array(); - $current_line = ''; + $wrapped_lines = array(); + $current_line = ''; $current_line_width = 0; - + // Split by spaces and hyphens while keeping the delimiters $words = preg_split( '/(\s+|-)/u', $text, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY ); if ( false === $words ) { $words = array( $text ); } - + foreach ( $words as $word ) { $word_width = Colors::width( $word, $is_precolorized, $encoding ); - + // If this word alone exceeds the width, we need to split it if ( $word_width > $width ) { // Flush current line if not empty if ( $current_line !== '' ) { - $wrapped_lines[] = $current_line; - $current_line = ''; + $wrapped_lines[] = $current_line; + $current_line = ''; $current_line_width = 0; } - + // Split the long word at character boundaries $remaining_word = $word; while ( $remaining_word ) { - $chunk = (string) \cli\safe_substr( $remaining_word, 0, $width, true /*is_width*/, $encoding ); + $chunk = (string) \cli\safe_substr( $remaining_word, 0, $width, true /*is_width*/, $encoding ); $wrapped_lines[] = $chunk; - $remaining_word = (string) \cli\safe_substr( $remaining_word, \cli\safe_strlen( $chunk, $encoding ), null /*length*/, false /*is_width*/, $encoding ); + $remaining_word = (string) \cli\safe_substr( $remaining_word, \cli\safe_strlen( $chunk, $encoding ), null /*length*/, false /*is_width*/, $encoding ); } continue; } - + // Check if adding this word would exceed the width if ( $current_line !== '' && $current_line_width + $word_width > $width ) { // Start a new line - $wrapped_lines[] = $current_line; - $current_line = $word; + $wrapped_lines[] = $current_line; + $current_line = $word; $current_line_width = $word_width; } else { // Add to current line - $current_line .= $word; + $current_line .= $word; $current_line_width += $word_width; } } - + // Add any remaining content if ( $current_line !== '' ) { $wrapped_lines[] = $current_line; } - + return $wrapped_lines ?: array( '' ); } diff --git a/lib/cli/table/Tabular.php b/lib/cli/table/Tabular.php index 7b58d1f..f373799 100644 --- a/lib/cli/table/Tabular.php +++ b/lib/cli/table/Tabular.php @@ -30,7 +30,7 @@ public function row( array $row ) { $col = null; foreach ( $row as $col => $value ) { - $value = isset( $value ) ? (string) $value : ''; + $value = ( isset( $value ) && ( is_scalar( $value ) || ( is_object( $value ) && method_exists( $value, '__toString' ) ) ) ) ? (string) $value : ''; $value = str_replace( "\t", ' ', $value ); $split_lines = preg_split( '/\r\n|\n/', $value ); if ( false === $split_lines ) { diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 984098b..4611d49 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,5 +1,5 @@ parameters: - level: 8 + level: 9 paths: - lib scanDirectories: