wp-cli/php-cli-tools
PHP Command Line Tools is a collection of functions and classes for building command-line apps in PHP. It provides formatted output, input prompts, menus, progress indicators, ASCII tables, tree displays, and argument parsing. Requires PHP 5.6 or newer.
Installation:
composer require wp-cli/php-cli-tools
Add to composer.json under require-dev if only needed for testing/scaffolding.
First Use Case: Display a formatted table in a Laravel Artisan command:
use cli\Table;
$table = new Table(['ID', 'Name', 'Status']);
$table->addRow([1, 'User 1', 'Active']);
$table->addRow([2, 'User 2', 'Inactive']);
$table->display();
Where to Look First:
vendor/wp-cli/php-cli-tools/examples/ for ready-to-use snippets.cli\Table, cli\progress\Bar, and cli\out() for daily use.cli\is_tty() for conditional CLI output logic.examples/table.php in the installed version for the iterator example before choosing between addRow() and streaming rows.Replace echo/var_dump with structured output:
use cli\out;
out('Processing...', 'green');
out('Done!', 'bold green');
Use progress bars for long-running tasks (e.g., migrations, imports):
use cli\progress\Bar;
$bar = new Bar('Migrating data', 100);
for ($i = 0; $i < 100; $i++) {
$bar->tick();
// Simulate work
sleep(0.1);
}
Collect user input in CLI tools:
use cli\prompt;
$confirm = prompt('Overwrite existing file?', 'n');
if ($confirm === 'y') {
// Proceed
}
Display query results or logs:
use cli\Table;
$table = new Table(['User ID', 'Email', 'Role']);
foreach ($users as $user) {
$table->addRow([$user->id, $user->email, $user->role]);
}
$table->display();
Since v0.13.2 a table can render rows fed from a Traversable/Generator, which lets you stream rows without first materialising a big array:
use cli\Table;
$table = new Table(['ID', 'Email', 'Role']);
$rows = (function () use ($users) {
foreach ($users->cursor() as $user) {
yield [$user->id, $user->email, $user->role];
}
})();
$table->setRows($rows); // check examples/table.php for the exact method name in your installed version
$table->display();
Prefer this over addRow() in a loop when the underlying query is a cursor(), a LazyCollection, or a PDOStatement — the point is to keep memory flat while still producing an aligned table.
Visualize directory/file hierarchies:
use cli\Tree;
$tree = new Tree();
$tree->setData(['folder' => ['file1.txt', 'subfolder' => ['file2.txt']]]);
$tree->display();
Extend Laravel’s Command class with CLI tools:
use Illuminate\Console\Command;
use cli\out;
class MyCommand extends Command {
protected $signature = 'my:command {--force}';
public function handle() {
out('Command executed!', 'yellow');
// ...
}
}
Use cli\Table/cli\progress\Bar in PHPUnit tests:
use cli\Table;
use Tests\TestCase;
class TableTest extends TestCase {
public function testTableOutput() {
$table = new Table(['ID', 'Name']);
$table->addRow([1, 'Test']);
$this->expectOutputRegex('/\|\s*1\s*\|\s*Test\s*\|\s*/');
$table->display();
}
}
Combine cursor() with the v0.13.2 iterator support so a php artisan report:export command doesn’t blow the memory limit:
$rows = (function () {
foreach (User::query()->cursor() as $user) {
yield [$user->id, $user->email, $user->role];
}
})();
(new Table(['ID', 'Email', 'Role']))->setRows($rows)->display();
Build interactive CLI menus (e.g., for admin tools):
use cli\menu;
$choices = ['Option 1', 'Option 2', 'Quit'];
$choice = menu('Select an option:', $choices, 'Option 1');
Handle Windows/Linux differences:
if (cli\is_windows()) {
// Windows-specific logic
}
Add to composer.json for reusable CLI tasks:
"scripts": {
"test": "phpunit && php-cs-fixer fix --dry-run",
"lint": "php-cs-fixer fix"
}
Extend cli\table\Renderer for custom table styles:
class MyRenderer extends \cli\table\Renderer {
public function renderCell($value) {
return '<span style="color: red;">' . parent::renderCell($value) . '</span>';
}
}
Detect piped vs. interactive output:
if (cli\is_tty()) {
out('Interactive mode: ', 'green');
} else {
out('Piped output: ', 'yellow');
}
Run shell commands with error handling:
use cli\run_command;
$result = run_command('git status');
if ($result['success']) {
out($result['output']);
} else {
err('Git error: ' . $result['error']);
}
If you rely on iterator-backed tables, pin "wp-cli/php-cli-tools": "^0.13.2" rather than a looser constraint so CI and teammates on older installs don’t silently lose the feature.
cli\is_tty() may return false in CI/CD pipelines even when output is to a terminal.$isTty = !defined('CI') && posix_isatty(STDERR);
cli\is_windows() and adjust rendering logic:
if (cli\is_windows()) {
$bar->setInterval(200); // Slower updates for Windows
}
if (!cli\is_tty()) {
cli\Colors::disable();
}
$table->setColumnWidths([10, 30, 'auto']); // Fixed, fixed, auto
$table->setWrap(true);
Generator can only be traversed once. Feeding the same generator to two tables (or to display() twice) throws Cannot traverse an already closed generator.$makeRows = fn () => (function () { yield [1, 'A']; })();
(new Table(['ID', 'Name']))->setRows($makeRows())->display();
If you need to re-render (e.g. after a retry), call the factory again rather than reusing the object. Also avoid mixing addRow() and setRows() on the same instance, and remember that lazy iteration means the first row isn’t rendered until the iterator is actually consumed.err() + an explicit exit code, or pre-validate the query before rendering.grapheme_strlen) may not work on older PHP versions.if (!function_exists('grapheme_strlen')) {
require 'vendor/wp-cli/php-cli-tools/src/cli/fallbacks.php';
}
cli\out('Debug: ' . print_r($variable, true), 'red');
out('TTY: ' . (cli\is_tty() ? 'Yes' : 'No'), 'blue');
$table->setHeaders(['ID', 'Name']);
$table->setRows([['1', 'Test']]);
$table->display();
$bar = new Bar('Test', 5);
$bar->setInterval(50); // Faster updates for debugging
Materialise the iterator temporarily to confirm the data is correct before switching back to lazy mode:
$rows = $makeRows();
$preview = iterator_to_array($rows, false);
out('Row count: ' . count($preview), 'cyan');
(new Table(['ID', 'Name']))->setRows($preview)->display();
If the count is right but the table is empty, you’re likely reusing a spent generator.
Override cli\table\Renderer for custom styles (e.g., Markdown, HTML):
class MarkdownRenderer extends \cli\table\Renderer {
public function renderHeader() { /* ... */ }
public function renderRow() { /* ... */ }
}
Customize cli\ArgumentParser for project-specific CLI flags:
$parser = new \cli\ArgumentParser();
$parser->addOption('verbose', 'v', 'Enable verbose output');
$args = $parser->parse($_SERVER['argv']);
Extend cli\notify\Notifier for new progress indicators (e.g., ASCII art):
class HeartbeatNotifier extends \cli\notify\Notifier {
public function render($msg) { /* ... */ }
}
Override cli\out() globally via autoloading:
// In a service provider
cli\out::setHandler(function ($msg, $color) {
// Custom logic (e.g., log to file)
echo $msg . PHP_EOL;
});
When you standardise table rendering in a base command, accept a Closure that returns an iterable rather than the iterable itself. This gives you one place to add iterator re-creation, error wrapping, and row-count estimation for the new iterator-aware table API.
php-cli-tools is configured entirely in code; there’s nothing published to config/.cli\Colors::disable() mutates process-wide state — call it once early (e.g. in a service provider or base command handle()) rather than per-table, or subsequent commands in the same process (queue workers, test suites) inherit it.composer show wp-cli/php-cli-tools when a teammate reports a missing method.How can I help you explore Laravel packages today?