@@ -61,11 +61,24 @@ class OutputHelper
6161{
6262 use InflectsString;
6363
64+ /**
65+ * The output writer instance used to write formatted output.
66+ *
67+ * @var Writer
68+ */
6469 protected Writer $ writer ;
65-
66- /** @var int Max width of command name */
70+ /**
71+ * Max width of command name.
72+ *
73+ * @var int
74+ */
6775 protected int $ maxCmdName = 0 ;
6876
77+ /**
78+ * Class constructor.
79+ *
80+ * @param Writer|null $writer The output writer instance used to write formatted output.
81+ */
6982 public function __construct (?Writer $ writer = null )
7083 {
7184 $ this ->writer = $ writer ?? new Writer ;
@@ -79,7 +92,7 @@ public function printTrace(Throwable $e): void
7992 $ eClass = get_class ($ e );
8093
8194 $ this ->writer ->colors (
82- "{ $ eClass} <red> {$ e ->getMessage ()}</end><eol/> " .
95+ "$ eClass <red> {$ e ->getMessage ()}</end><eol/> " .
8396 '( ' . t ('thrown in ' ) . " <yellow> {$ e ->getFile ()}</end><white>: {$ e ->getLine ()})</end> "
8497 );
8598
@@ -107,6 +120,19 @@ public function printTrace(Throwable $e): void
107120 $ this ->writer ->colors ($ traceStr );
108121 }
109122
123+ /**
124+ * Converts an array of arguments into a string representation.
125+ *
126+ * Each array element is converted based on its type:
127+ * - Scalar values (int, float, string, bool) are var_exported
128+ * - Objects are converted using __toString() if available, otherwise class name is used
129+ * - Arrays are recursively processed and wrapped in square brackets
130+ * - Other types are converted to their type name
131+ *
132+ * @param array $args Array of arguments to be stringified
133+ *
134+ * @return string The comma-separated string representation of all arguments
135+ */
110136 public function stringifyArgs (array $ args ): string
111137 {
112138 $ holder = [];
@@ -118,7 +144,14 @@ public function stringifyArgs(array $args): string
118144 return implode (', ' , $ holder );
119145 }
120146
121- protected function stringifyArg ($ arg ): string
147+ /**
148+ * Converts the provided argument into a string representation.
149+ *
150+ * @param mixed $arg The argument to be converted into a string. This can be of any type.
151+ *
152+ * @return string A string representation of the provided argument.
153+ */
154+ protected function stringifyArg (mixed $ arg ): string
122155 {
123156 if (is_scalar ($ arg )) {
124157 return var_export ($ arg , true );
@@ -196,15 +229,17 @@ protected function showHelp(string $for, array $items, string $header = '', stri
196229 return ;
197230 }
198231
199- $ space = 4 ;
200- $ group = $ lastGroup = null ;
232+ $ space = 4 ;
233+ $ lastGroup = null ;
201234
202235 $ withDefault = $ for === 'Options ' || $ for === 'Arguments ' ;
203236 foreach (array_values ($ this ->sortItems ($ items , $ padLen , $ for )) as $ idx => $ item ) {
204237 $ name = $ this ->getName ($ item );
205238 if ($ for === 'Commands ' && $ lastGroup !== $ group = $ item ->group ()) {
206- $ this ->writer ->help_group ($ group ?: '* ' , true );
207239 $ lastGroup = $ group ;
240+ if ($ group !== '' ) {
241+ $ this ->writer ->help_group ($ group , true );
242+ }
208243 }
209244 $ desc = str_replace (["\r\n" , "\n" ], str_pad ("\n" , $ padLen + $ space + 3 ), $ item ->desc ($ withDefault ));
210245
@@ -254,12 +289,21 @@ public function showUsage(string $usage): self
254289 return $ this ;
255290 }
256291
292+ /**
293+ * Shows an error message when a command is not found and suggests similar commands.
294+ * Uses levenshtein distance to find commands that are similar to the attempted one.
295+ *
296+ * @param string $attempted The command name that was attempted to be executed
297+ * @param array $available List of available command names
298+ *
299+ * @return OutputHelper For method chaining
300+ */
257301 public function showCommandNotFound (string $ attempted , array $ available ): self
258302 {
259303 $ closest = [];
260304 foreach ($ available as $ cmd ) {
261305 $ lev = levenshtein ($ attempted , $ cmd );
262- if ($ lev > 0 || $ lev < 5 ) {
306+ if ($ lev > 0 && $ lev < 5 ) {
263307 $ closest [$ cmd ] = $ lev ;
264308 }
265309 }
@@ -278,12 +322,12 @@ public function showCommandNotFound(string $attempted, array $available): self
278322 * Sort items by name. As a side effect sets max length of all names.
279323 *
280324 * @param Parameter[]|Command[] $items
281- * @param int $max
325+ * @param int|null $max
282326 * @param string $for
283327 *
284328 * @return array
285329 */
286- protected function sortItems (array $ items , &$ max = 0 , string $ for = '' ): array
330+ protected function sortItems (array $ items , ? int &$ max = 0 , string $ for = '' ): array
287331 {
288332 $ max = max (array_map (fn ($ item ) => strlen ($ this ->getName ($ item )), $ items ));
289333
@@ -292,8 +336,10 @@ protected function sortItems(array $items, &$max = 0, string $for = ''): array
292336 }
293337
294338 uasort ($ items , static function ($ a , $ b ) {
295- $ aName = $ a instanceof Groupable ? $ a ->group () . $ a ->name () : $ a ->name ();
296- $ bName = $ b instanceof Groupable ? $ b ->group () . $ b ->name () : $ b ->name ();
339+ // Items in the default group (where group() returns empty/falsy) are prefixed with '__'
340+ // to ensure they appear at the top of the sorted list, whilst grouped items follow after
341+ $ aName = $ a instanceof Groupable ? ($ a ->group () ?: '__ ' ) . $ a ->name () : $ a ->name ();
342+ $ bName = $ b instanceof Groupable ? ($ b ->group () ?: '__ ' ) . $ b ->name () : $ b ->name ();
297343
298344 return $ aName <=> $ bName ;
299345 });
@@ -308,7 +354,7 @@ protected function sortItems(array $items, &$max = 0, string $for = ''): array
308354 *
309355 * @return string
310356 */
311- protected function getName ($ item ): string
357+ protected function getName (Parameter | Command $ item ): string
312358 {
313359 $ name = $ item ->name ();
314360
0 commit comments