Skip to content

Refactor DisplayHelper and add unit tests for improved clarity #22

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Aug 12, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
271 changes: 136 additions & 135 deletions src/Install/Cli/DisplayHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,183 +4,184 @@

namespace Laravel\Boost\Install\Cli;

use InvalidArgumentException;

class DisplayHelper
{
private const UNICODE_TOP_LEFT = '╭';
private const UNICODE_TOP_RIGHT = '╮';
private const UNICODE_BOTTOM_LEFT = '╰';
private const UNICODE_BOTTOM_RIGHT = '╯';
private const UNICODE_HORIZONTAL = '─';
private const UNICODE_VERTICAL = '│';
private const UNICODE_CROSS = '┼';
private const UNICODE_TOP_T = '┬';
private const UNICODE_BOTTOM_T = '┴';
private const UNICODE_LEFT_T = '├';
private const UNICODE_RIGHT_T = '┤';

private const BORDER_TOP = 'top';
private const BORDER_MIDDLE = 'middle';
private const BORDER_BOTTOM = 'bottom';

private const CELL_PADDING = 2;
private const GRID_CELL_PADDING = 4;
private const ANSI_BOLD = "\e[1m";
private const ANSI_RESET = "\e[0m";
private const SPACE = ' ';

/**
* @param array<int, array<int|string, mixed>> $data
*/
public static function datatable(array $data, int $cols = 80): void
public static function datatable(array $data, int $maxWidth = 80): void
{
if (empty($data)) {
if (! $data) {
return;
}

// Calculate column widths
$columnWidths = [];
foreach ($data as $row) {
$colIndex = 0;
foreach ($row as $cell) {
$length = mb_strlen((string) $cell);
if (! isset($columnWidths[$colIndex]) || $length > $columnWidths[$colIndex]) {
$columnWidths[$colIndex] = $length;
}
$colIndex++;
}
}
$columnWidths = self::calculateColumnWidths($data);
$columnWidths = array_map(fn ($width) => $width + self::CELL_PADDING, $columnWidths);

// Add padding
$columnWidths = array_map(fn ($width) => $width + 2, $columnWidths);

// Unicode box drawing characters
$topLeft = '╭';
$topRight = '╮';
$bottomLeft = '╰';
$bottomRight = '╯';
$horizontal = '─';
$vertical = '│';
$cross = '┼';
$topT = '┬';
$bottomT = '┴';
$leftT = '├';
$rightT = '┤';

// Draw top border
$topBorder = $topLeft;
foreach ($columnWidths as $index => $width) {
$topBorder .= str_repeat($horizontal, $width);
if ($index < count($columnWidths) - 1) {
$topBorder .= $topT;
}
}
$topBorder .= $topRight;
echo $topBorder.PHP_EOL;
[$leftChar, $rightChar, $joinChar] = self::getBorderChars(self::BORDER_TOP);
echo self::buildBorder($columnWidths, $leftChar, $rightChar, $joinChar).PHP_EOL;

// Draw rows
$rowCount = 0;
foreach ($data as $row) {
$line = $vertical;
$colIndex = 0;
foreach ($row as $cell) {
$cellStr = ($colIndex === 0) ? "\e[1m".$cell."\e[0m" : $cell;
$padding = $columnWidths[$colIndex] - mb_strlen($cell);
$line .= ' '.$cellStr.str_repeat(' ', $padding - 1).$vertical;
$colIndex++;
}
echo $line.PHP_EOL;
echo self::buildDataRow($row, $columnWidths).PHP_EOL;

// Draw separator between rows (except after last row)
if ($rowCount < count($data) - 1) {
$separator = $leftT;
foreach ($columnWidths as $index => $width) {
$separator .= str_repeat($horizontal, $width);
if ($index < count($columnWidths) - 1) {
$separator .= $cross;
}
}
$separator .= $rightT;
echo $separator.PHP_EOL;
[$leftChar, $rightChar, $joinChar] = self::getBorderChars(self::BORDER_MIDDLE);
echo self::buildBorder($columnWidths, $leftChar, $rightChar, $joinChar).PHP_EOL;
}
$rowCount++;
}

// Draw bottom border
$bottomBorder = $bottomLeft;
foreach ($columnWidths as $index => $width) {
$bottomBorder .= str_repeat($horizontal, $width);
if ($index < count($columnWidths) - 1) {
$bottomBorder .= $bottomT;
}
}
$bottomBorder .= $bottomRight;
echo $bottomBorder.PHP_EOL;
[$leftChar, $rightChar, $joinChar] = self::getBorderChars(self::BORDER_BOTTOM);
echo self::buildBorder($columnWidths, $leftChar, $rightChar, $joinChar).PHP_EOL;
}

/**
* @param array<int, string> $items
*/
public static function grid(array $items, int $cols = 80): void
public static function grid(array $items, int $maxWidth = 80): void
{
if (empty($items)) {
return;
}

$cols -= 2;
// Calculate the longest item length
$maxWidth -= 2; // account for grid margins
$maxItemLength = max(array_map('mb_strlen', $items));

// Add padding (2 spaces on each side + 1 for border)
$cellWidth = $maxItemLength + 4;

// Calculate how many cells can fit per row
$cellsPerRow = max(1, (int) floor(($cols - 1) / ($cellWidth + 1)));

// Unicode box drawing characters
$topLeft = '╭';
$topRight = '╮';
$bottomLeft = '╰';
$bottomRight = '╯';
$horizontal = '─';
$vertical = '│';
$cross = '┼';
$topT = '┬';
$bottomT = '┴';
$leftT = '├';
$rightT = '┤';

// Group items into rows
$cellWidth = $maxItemLength + self::GRID_CELL_PADDING;
$cellsPerRow = max(1, (int) floor(($maxWidth - 1) / ($cellWidth + 1)));
$rows = array_chunk($items, $cellsPerRow);

// Draw top border
$topBorder = $topLeft;
for ($i = 0; $i < $cellsPerRow; $i++) {
$topBorder .= str_repeat($horizontal, $cellWidth);
if ($i < $cellsPerRow - 1) {
$topBorder .= $topT;
}
}
$topBorder .= $topRight;
echo ' '.$topBorder.PHP_EOL;
$cellWidths = array_fill(0, $cellsPerRow, $cellWidth);

[$leftChar, $rightChar, $joinChar] = self::getBorderChars(self::BORDER_TOP);
echo self::SPACE.self::buildBorder($cellWidths, $leftChar, $rightChar, $joinChar).PHP_EOL;

// Draw rows
$rowCount = 0;
foreach ($rows as $row) {
$line = $vertical;
for ($i = 0; $i < $cellsPerRow; $i++) {
if (isset($row[$i])) {
$item = $row[$i];
$padding = $cellWidth - mb_strlen($item) - 2;
$line .= ' '.$item.str_repeat(' ', $padding + 1).$vertical;
} else {
// Empty cell
$line .= str_repeat(' ', $cellWidth).$vertical;
}
}
echo ' '.$line.PHP_EOL;
echo self::SPACE.self::buildGridRow($row, $cellWidth, $cellsPerRow).PHP_EOL;

// Draw separator between rows (except after last row)
if ($rowCount < count($rows) - 1) {
$separator = $leftT;
for ($i = 0; $i < $cellsPerRow; $i++) {
$separator .= str_repeat($horizontal, $cellWidth);
if ($i < $cellsPerRow - 1) {
$separator .= $cross;
}
}
$separator .= $rightT;
echo ' '.$separator.PHP_EOL;
[$leftChar, $rightChar, $joinChar] = self::getBorderChars(self::BORDER_MIDDLE);
echo self::SPACE.self::buildBorder($cellWidths, $leftChar, $rightChar, $joinChar).PHP_EOL;
}
$rowCount++;
}

// Draw bottom border
$bottomBorder = $bottomLeft;
for ($i = 0; $i < $cellsPerRow; $i++) {
$bottomBorder .= str_repeat($horizontal, $cellWidth);
if ($i < $cellsPerRow - 1) {
$bottomBorder .= $bottomT;
[$leftChar, $rightChar, $joinChar] = self::getBorderChars(self::BORDER_BOTTOM);
echo self::SPACE.self::buildBorder($cellWidths, $leftChar, $rightChar, $joinChar).PHP_EOL;
}

private static function getBorderChars(string $type): array
{
return match($type) {
self::BORDER_TOP => [self::UNICODE_TOP_LEFT, self::UNICODE_TOP_RIGHT, self::UNICODE_TOP_T],
self::BORDER_MIDDLE => [self::UNICODE_LEFT_T, self::UNICODE_RIGHT_T, self::UNICODE_CROSS],
self::BORDER_BOTTOM => [self::UNICODE_BOTTOM_LEFT, self::UNICODE_BOTTOM_RIGHT, self::UNICODE_BOTTOM_T],
default => throw new InvalidArgumentException('Border type should be valid'),
};
}

/**
* @param array<int, array<int|string, mixed>> $data
* @return array<int, int>
*/
private static function calculateColumnWidths(array $data): array
{
$columnWidths = [];
foreach ($data as $row) {
foreach ($row as $colIndex => $cell) {
$length = mb_strlen((string) $cell);
$columnWidths[$colIndex] = max($columnWidths[$colIndex] ?? 0, $length);
}
}

return $columnWidths;
}

/**
* @param array<int, int> $widths
*/
private static function buildBorder(array $widths, string $leftChar, string $rightChar, string $joinChar): string
{
$border = $leftChar;
foreach ($widths as $index => $width) {
$border .= str_repeat(self::UNICODE_HORIZONTAL, $width);
if ($index < count($widths) - 1) {
$border .= $joinChar;
}
}
$bottomBorder .= $bottomRight;
echo ' '.$bottomBorder.PHP_EOL;
$border .= $rightChar;

return $border;
}

/**
* @param array<int|string, mixed> $row
* @param array<int, int> $columnWidths
*/
private static function buildDataRow(array $row, array $columnWidths): string
{
$line = self::UNICODE_VERTICAL;
$colIndex = 0;
foreach ($row as $cell) {
$cellStr = ($colIndex === 0) ? self::ANSI_BOLD.$cell.self::ANSI_RESET : $cell;
$padding = $columnWidths[$colIndex] - mb_strlen((string) $cell);
$line .= self::SPACE.$cellStr.str_repeat(self::SPACE, $padding - 1).self::UNICODE_VERTICAL;
$colIndex++;
}

return $line;
}

/**
* @param array<int, string> $row
*/
private static function buildGridRow(array $row, int $cellWidth, int $cellsPerRow): string
{
$line = self::UNICODE_VERTICAL;

$cells = array_map(
fn ($index) => self::formatGridCell($row[$index] ?? '', $cellWidth),
range(0, $cellsPerRow - 1)
);

$line .= implode(self::UNICODE_VERTICAL, $cells).self::UNICODE_VERTICAL;

return $line;
}

private static function formatGridCell(string $item, int $cellWidth): string
{
if (! $item) {
return str_repeat(self::SPACE, $cellWidth);
}

$padding = $cellWidth - mb_strlen($item) - 2;

return self::SPACE.$item.str_repeat(self::SPACE, $padding + 1);
}
}
Loading
Loading