diff --git a/lib/cli/Arguments.php b/lib/cli/Arguments.php index 298d1a0..c6e41de 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,37 +45,50 @@ 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( '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 ); } } /** * Get the list of arguments found by the defined definitions. * - * @return array + * @return array */ public function getArguments() { if (!isset($this->_parsed)) { $this->parse(); } - return $this->_parsed; + return $this->_parsed ?? []; } + /** + * Get the help screen. + * + * @return HelpScreen + */ public function getHelpScreen() { return new HelpScreen($this); } @@ -77,7 +99,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; } /** @@ -92,7 +118,11 @@ public function offsetExists($offset) { $offset = $offset->key; } - return array_key_exists($offset, $this->_parsed); + if ( ! is_string( $offset ) && ! is_int( $offset ) ) { + return false; + } + + return array_key_exists($offset, $this->_parsed ?? []); } /** @@ -107,9 +137,15 @@ 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]; } + + return null; } /** @@ -124,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; } @@ -138,6 +179,10 @@ public function offsetUnset($offset) { $offset = $offset->key; } + if ( ! is_string( $offset ) && ! is_int( $offset ) ) { + return; + } + unset($this->_parsed[$offset]); } @@ -145,7 +190,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. @@ -160,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; @@ -181,7 +231,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) { @@ -201,7 +251,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. @@ -215,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; @@ -235,7 +290,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) { @@ -269,7 +324,7 @@ public function setStrict($strict) { /** * Get the list of invalid arguments the parser found. * - * @return array + * @return array */ public function getInvalidArguments() { return $this->_invalid; @@ -280,12 +335,16 @@ 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) { $obj = $flag; - $flag = $flag->value; + $flag = $flag->value(); + } + + if ( ! is_string( $flag ) && ! is_int( $flag ) ) { + return null; } if (isset($this->_flags[$flag])) { @@ -302,12 +361,24 @@ public function getFlag($flag) { return $settings; } } + + 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); } @@ -341,12 +412,16 @@ 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) { $obj = $option; - $option = $option->value; + $option = $option->value(); + } + + if ( ! is_string( $option ) && ! is_int( $option ) ) { + return null; } if (isset($this->_options[$option])) { @@ -362,12 +437,24 @@ public function getOption($option) { return $settings; } } + + 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); } @@ -388,7 +475,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() { @@ -398,15 +485,21 @@ 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); + $raw = $argument->raw(); + array_push($this->_invalid, is_scalar($raw) ? (string) $raw : ''); + } } if ($this->_strict && !empty($this->_invalid)) { @@ -418,6 +511,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) { @@ -432,10 +527,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; @@ -446,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; } @@ -454,11 +562,19 @@ private function _parseFlag($argument) { return true; } + /** + * Parse an option. + * + * @param Argument $option + * @return bool + */ private function _parseOption($option) { if (!$this->isOption($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); @@ -477,13 +593,20 @@ 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 - foreach ($this->_lexer as $value) { - array_push($values, $value->raw); + 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) { + if ( ! $this->_lexer->end() && ! $this->_lexer->peek->isValue ) { break; } + $this->_lexer->next(); } $this[$option->key] = join(' ', $values); diff --git a/lib/cli/Colors.php b/lib/cli/Colors.php index bba9f40..fb9e071 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(); @@ -319,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 ) ) { @@ -340,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 cedbc19..b08a619 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]; @@ -29,11 +36,16 @@ 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]; } + /** + * 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 beaa0b3..e5a6271 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; @@ -109,7 +121,7 @@ public function speed() { $this->_speed = ($this->_current / $this->_iteration) / $span; } - return $this->_speed; + return (int) $this->_speed; } /** @@ -120,7 +132,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); } /** @@ -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 a18c0a4..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(); @@ -97,12 +102,14 @@ public function estimated() { } $estimated = round($this->_total / $speed); - return $estimated; + return (int)$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..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 ) { @@ -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 71b454f..22a22e5 100755 --- a/lib/cli/Streams.php +++ b/lib/cli/Streams.php @@ -4,20 +4,36 @@ 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; + $method = array( __CLASS__, $func ); + assert( is_callable( $method ) ); return call_user_func_array( $method, $args ); } - static public function isTty() { - if ( function_exists('stream_isatty') ) { - return stream_isatty(static::$out); + /** + * Check if the stream is a TTY. + * + * @return bool + */ + 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 ) ); } } @@ -27,36 +43,37 @@ 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 ) { - $args = func_get_args(); - + public static function render( $msg, ...$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; } @@ -66,24 +83,26 @@ 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 ) { - fwrite( static::$out, self::_call( 'render', func_get_args() ) ); + public static function out( $msg, ...$args ) { + $rendered = self::_call( 'render', func_get_args() ); + fwrite( static::$out, is_scalar( $rendered ) ? (string) $rendered : '' ); } /** * 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 ) { - $msg = self::_call( 'render', func_get_args() ); + public static function out_padded( $msg, ...$args ) { + $rendered = self::_call( 'render', func_get_args() ); + $msg = is_scalar( $rendered ) ? (string) $rendered : ''; self::out( str_pad( $msg, \cli\Shell::columns() ) ); } @@ -91,12 +110,14 @@ public static function out_padded( $msg ) { * 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 = '' ) { // 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 ); } @@ -107,14 +128,15 @@ 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"; - 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 : '' ); } /** @@ -129,10 +151,11 @@ public static function err( $msg = '' ) { * @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 ); @@ -143,11 +166,11 @@ public static function input( $format = null, $hide = false ) { echo "\n"; } - if( $line === false ) { + if ( $line === false ) { throw new \Exception( 'Caught ^D during input' ); } - return trim( $line ); + return trim( (string) $line ); } /** @@ -163,18 +186,20 @@ 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 ) !== '' ) + if ( trim( $line ) !== '' ) { return $line; - if( $default !== false ) - return $default; + } + if ( $default !== false ) { + return (string) $default; + } } } @@ -184,27 +209,31 @@ 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() */ public static function choose( $question, $choice = 'yn', $default = 'n' ) { - if( !is_string( $choice ) ) { + if ( ! is_string( $choice ) ) { $choice = join( '', $choice ); } // 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( '/', preg_split( '//', $choice ) ), '/' ); + $choices = trim( join( '/', str_split( $choice ) ), '/' ); - while( true ) { - $line = self::prompt( sprintf( '%s? [%s]', $question, $choices ), $default, '' ); + 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 ); } } @@ -215,8 +244,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() @@ -226,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 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; } } @@ -271,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 8ae90aa..770de06 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,50 +55,65 @@ 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)) { + 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 ); + } - foreach ($keys as $header) { - $headers[$header] = $header; + $this->setHeaders( $headers ); + + $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() ); } } - public function resetTable() - { - $this->_headers = array(); - $this->_width = array(); - $this->_rows = array(); - $this->_footers = array(); + /** + * Reset the table state. + * + * @return $this + */ + public function resetTable() { + $this->_headers = array(); + $this->_width = array(); + $this->_rows = array(); + $this->_footers = array(); $this->_alignments = array(); return $this; } @@ -102,8 +123,7 @@ public function resetTable() * * @return $this */ - public function resetRows() - { + public function resetRows() { $this->_rows = array(); return $this; } @@ -115,22 +135,23 @@ public function resetRows() * @see table\Renderer * @see table\Ascii * @see table\Tabular + * @return void */ - public function setRenderer(Renderer $renderer) { + public function setRenderer( Renderer $renderer ) { $this->_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) { + 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; } } @@ -146,9 +167,10 @@ 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 ) { + foreach ( $this->getDisplayLines() as $line ) { Streams::line( $line ); } } @@ -159,23 +181,24 @@ 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) { + 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 ); } } @@ -186,37 +209,37 @@ 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); - $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; } } @@ -227,42 +250,49 @@ 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])) { - 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 ] ); + } + ); } /** * 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); + public function setHeaders( array $headers ) { + $this->_headers = $this->checkRow( $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); + public function setFooters( array $footers ) { + $this->_footers = $this->checkRow( $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) { + 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 ) ); @@ -284,35 +314,43 @@ 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); + public function addRow( array $row ) { + $this->_rows[] = $this->checkRow( $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) { + public function setRows( array $rows ) { $this->_rows = array(); - foreach ($rows as $row) { - $this->addRow($row); + foreach ( $rows as $row ) { + $this->addRow( $row ); } } + /** + * Count the number of rows in the table. + * + * @return int + */ public function countRows() { - return count($this->_rows); + return count( $this->_rows ); } /** * 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() + * @return void */ public function setAsciiPreColorized( $pre_colorized ) { if ( $this->_renderer instanceof Ascii ) { @@ -324,8 +362,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 9bc01f9..7ab070b 100644 --- a/lib/cli/arguments/Argument.php +++ b/lib/cli/arguments/Argument.php @@ -16,16 +16,26 @@ /** * 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 + * @property-read bool $canExplode + * @property-read array $exploded + * @property-read string $raw + * @property-read bool $isValue */ class Argument extends Memoize { /** * The canonical name of this argument, used for aliasing. * - * @param string + * @var string */ public $key; + /** @var string */ private $_argument; + /** @var string */ private $_raw; /** @@ -121,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(); @@ -130,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/HelpScreen.php b/lib/cli/arguments/HelpScreen.php index f800788..ed4b7b3 100644 --- a/lib/cli/arguments/HelpScreen.php +++ b/lib/cli/arguments/HelpScreen.php @@ -18,105 +18,161 @@ * 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; - public function __construct(Arguments $arguments) { - $this->setArguments($arguments); + /** + * @param Arguments $arguments + */ + public function __construct( Arguments $arguments ) { + $this->setArguments( $arguments ); } + /** + * @return string + */ public function __toString() { return $this->render(); } - public function setArguments(Arguments $arguments) { - $this->consumeArgumentFlags($arguments); - $this->consumeArgumentOptions($arguments); + /** + * @param Arguments $arguments + * @return void + */ + public function setArguments( Arguments $arguments ) { + $this->consumeArgumentFlags( $arguments ); + $this->consumeArgumentOptions( $arguments ); } - public function consumeArgumentFlags(Arguments $arguments) { - $data = $this->_consume($arguments->getFlags()); + /** + * @param Arguments $arguments + * @return void + */ + public function consumeArgumentFlags( Arguments $arguments ) { + $data = $this->_consume( $arguments->getFlags() ); - $this->_flags = $data[0]; + $this->_flags = $data[0]; $this->_flagMax = $data[1]; } - public function consumeArgumentOptions(Arguments $arguments) { - $data = $this->_consume($arguments->getOptions()); + /** + * @param Arguments $arguments + * @return void + */ + public function consumeArgumentOptions( Arguments $arguments ) { + $data = $this->_consume( $arguments->getOptions() ); - $this->_options = $data[0]; + $this->_options = $data[0]; $this->_optionMax = $data[1]; } + /** + * @return string + */ 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 ); } - private function _renderScreen($options, $max) { + /** + * @param array> $options + * @param int $max + * @return string + */ + 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 = 80 - 4 - $max; + $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 : ''; - $description = str_split($settings['description'], $dlen); - $formatted.= ' ' . array_shift($description); + $description = array(); + if ( '' !== $desc_str ) { + $description = str_split( $desc_str, $dlen ); + } - if ($settings['default']) { - $formatted .= ' [default: ' . $settings['default'] . ']'; + if ( empty( $description ) ) { + $description = array( '' ); } - $pad = str_repeat(' ', $max + 3); - while ($desc = array_shift($description)) { + $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 ); } - private function _consume($options) { + /** + * @param array> $options + * @return array{0: array>, 1: int} + */ + 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/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 3fb054b..381e3a6 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; @@ -32,7 +42,7 @@ public function __construct(array $items) { /** * The current token. * - * @return string + * @return Argument|null */ #[\ReturnTypeWillChange] public function current() { @@ -95,9 +105,11 @@ 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); + $item_str = (is_scalar($item) || (is_object($item) && method_exists($item, '__toString'))) ? (string)$item : ''; + array_unshift($this->_items, $item_str); $this->_length += 1; } @@ -110,16 +122,23 @@ public function end() { return ($this->_index + 1) == $this->_length; } + /** + * @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'); } + /** + * @return void + */ private function _explode() { - if (!$this->_item->canExplode) { - return false; + if (null === $this->_item || !$this->_item->canExplode) { + return; } foreach ($this->_item->exploded as $piece) { diff --git a/lib/cli/cli.php b/lib/cli/cli.php index ccc2b51..b86eaa6 100755 --- a/lib/cli/cli.php +++ b/lib/cli/cli.php @@ -19,12 +19,12 @@ * 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 ) { - return Streams::_call( 'render', func_get_args() ); +function render( $msg, ...$args ) { + return Streams::render( $msg, ...$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() ); } @@ -56,6 +56,8 @@ function out_padded( $msg ) { * 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 = '' ) { @@ -68,10 +70,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() ); } @@ -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. @@ -162,10 +164,10 @@ 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 ) ) ) { + // 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; } @@ -181,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; @@ -215,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 ) { @@ -225,7 +231,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 ) ) ) { @@ -248,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 ); } @@ -262,11 +268,14 @@ 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 ) { // 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 ) ) { @@ -279,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; @@ -322,10 +334,12 @@ 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 = 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 ) ) ) { @@ -344,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*/ ); @@ -393,8 +407,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 9c58f76..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() @@ -71,7 +76,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 +96,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); @@ -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 fd20bc9..b71d5d4 100644 --- a/lib/cli/table/Ascii.php +++ b/lib/cli/table/Ascii.php @@ -34,27 +34,47 @@ 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) { + public function setWidths( array $widths, $fallback = false ) { + if ( $fallback ) { foreach ( $this->_widths as $index => $value ) { - $widths[$index] = $value; + $widths[ $index ] = $value; } } $this->_widths = $widths; @@ -62,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 = 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 { @@ -82,8 +102,8 @@ public function setWidths(array $widths, $fallback = false) { } if ( ! empty( $resize_widths ) && $extra_width ) { - $avg_extra_width = floor( $extra_width / count( $resize_widths ) ); - foreach( $widths as &$width ) { + $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; array_shift( $resize_widths ); @@ -95,7 +115,6 @@ public function setWidths(array $widths, $fallback = false) { } } } - } $this->_widths = $widths; @@ -107,6 +126,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 +137,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,10 +151,11 @@ 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); + public function setCharacters( array $characters ) { + $this->_characters = array_merge( $this->_characters, $characters ); } /** @@ -143,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']; } } @@ -157,27 +179,31 @@ 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 ) { $extra_row_count = 0; + $extra_rows = []; if ( count( $row ) > 0 ) { $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 ); 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 ) { - $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 ); } @@ -190,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; } @@ -236,16 +262,24 @@ 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 ); + $content = str_replace( "\t", ' ', (string) $content ); return $this->_characters['padding'] . Colors::pad( $content, $this->_widths[ $column ], $this->isPreColorized( $column ), false, $alignment ) . $this->_characters['padding']; } /** * 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. + * @return void */ public function setPreColorized( $pre_colorized ) { $this->_pre_colorized = $pre_colorized; @@ -258,7 +292,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 ) { @@ -276,11 +310,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 ); } @@ -291,23 +325,23 @@ 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 ); } 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 ); } - + return $wrapped_lines; } @@ -318,56 +352,59 @@ 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(); - $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 = \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; } - + // 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/Renderer.php b/lib/cli/table/Renderer.php index 6bf6df7..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) { @@ -62,7 +82,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; @@ -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 0675b4c..f373799 100644 --- a/lib/cli/table/Tabular.php +++ b/lib/cli/table/Tabular.php @@ -19,32 +19,40 @@ 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 ) { - $rows = []; - $output = ''; + /** @var array> $rows */ + $rows = []; + $output = ''; + $split_lines = []; + $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 ) { + $split_lines = array( $value ); + } // Keep anything before the first line break on the original line $row[ $col ] = array_shift( $split_lines ); } $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 ) { - $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 new file mode 100644 index 0000000..4611d49 --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,9 @@ +parameters: + level: 9 + paths: + - lib + scanDirectories: + - vendor/wp-cli/wp-cli/php + scanFiles: + - vendor/php-stubs/wordpress-stubs/wordpress-stubs.php + treatPhpDocTypesAsCertain: false