Pop PHP
The Console

Building Console Applications

A console application in Pop is a stand-alone CLI application object serving a different kind of request. It is intentionally separate and distinct from kettle, providing a separate place to organize and build out a large console application that could potentially have many commands.

The router matches command arguments instead of a URL path, dispatches a controller or a command class, and that code writes back through Pop\Console\Console instead of an HTTP response. This page covers the console side: the entry script, the command surface, and everything Console can put on a terminal.

Controllers vs Commands#

Two different dispatchable objects can be configured and deployed via the console application: commands and controllers. The difference is really organization. A command class handles one command. A controller class handles a group of them, one action method per command, giving you the ability to organize a subset of commands in one class. For example, your App\Console\Controller\UsersController class may have the methods list(), create() and delete() in it, all providing functionality related to users. Conversely, you could wire up three command classes App\Console\Command\UsersList, App\Console\Command\UsersCreate and App\Console\Command\UsersDelete that each deploy their handle() method.

A CLI Application in Pop#

The entry point is a PHP script with a shebang, sitting in the project root or in script/. It builds an application from a CLI routes config and runs it:

PHPapp
#!/usr/bin/env php
<?php
$autoloader = include __DIR__ . '/vendor/autoload.php';

$app = new Pop\Application($autoloader, include __DIR__ . '/app/config/app.cli.php');
$app->run();

Pop\Application decides which router to build by looking at how PHP was invoked, so nothing in that script says "this is CLI". The route table is what makes it a console application:

PHPapp/config/app.cli.php
<?php
return [
    'routes' => [
        'users list [--limit=]' => [
            'controller' => 'App\Console\Controller\UsersController',
            'action'     => 'index',
            'help'       => 'List the users'
        ],
        'help' => [
            'controller' => 'App\Console\Controller\ConsoleController',
            'action'     => 'help',
            'help'       => 'Show the help screen'
        ],
        '*' => [
            'controller' => 'App\Console\Controller\ConsoleController',
            'action'     => 'error'
        ]
    ]
];

A controller reaches the console the same way an HTTP controller reaches the request and response — through a trait. Pop\Dispatch\ConsoleTrait supplies a constructor taking the application and a Pop\Console\Console, the second defaulted, so $this->console and $this->application are both there:

PHPapp/src/Console/Controller/UsersController.php
<?php

namespace App\Console\Controller;

use Pop\Controller\AbstractController;
use Pop\Dispatch\ConsoleTrait;

class UsersController extends AbstractController
{

    use ConsoleTrait;

    public function index(array $options = []): void
    {
        $limit = $options['limit'] ?? 'all';
        $this->console->write('Listing ' . $limit . ' users.');
    }

}
BASH
./app users list --limit=25
TEXT
    Listing 25 users.

That leading indent is the console's margin, not something the sample printed. A Console a controller receives by default is new Console(120) — a wrap width of 120 columns and a margin of four spaces — while a Console you construct yourself with no arguments wraps at 80. Both are covered under The response buffer.

The '*' catch-all is not optional in practice. Without it a mistyped command produces no output at all, and the user has no way to tell a typo from a command that did nothing. Route it at an action that says so:

PHPapp/src/Console/Controller/ConsoleController.php
<?php

namespace App\Console\Controller;

use Pop\Controller\AbstractController;
use Pop\Dispatch\ConsoleTrait;

class ConsoleController extends AbstractController
{

    use ConsoleTrait;

    public function help(): void
    {
        $this->console->addCommandsFromRoutes(
            $this->application->router()->getRouteMatch(), './app'
        );
        $this->console->help();
    }

    public function error(): void
    {
        $this->console->alertDanger('Invalid command', 40);
    }

}

Two routes, a controller each, and the application answers both a real command and a wrong one. The rest of this page is what goes inside those actions.

CLI Routes#

A CLI route pattern is a command signature: bare words are literal, <name> is required, [<name>] optional, [--flag] a boolean and [-l|--limit=] an option taking a value. Parameters reach the action positionally and options arrive in one trailing array. The full syntax table is in Routing.

What is console-specific is that a route can carry a help key. Nothing dispatches on it; it's read by the console object when it builds the help screen, described under Help screens.

For an application that puts one command per class, the route table does not have to be written by hand. Pop\Console\CommandRegistry::loadRoutes() scans a directory and returns a routes array built from what it finds:

PHPapp
#!/usr/bin/env php
<?php
$autoloader = include __DIR__ . '/vendor/autoload.php';

$config = include __DIR__ . '/app/config/app.cli.php';
$config['routes'] = Pop\Console\CommandRegistry::loadRoutes(
    $config['routes'], __DIR__ . '/app/src/Console/Command'
);

$app = new Pop\Application($autoloader, $config);
$app->run();

Each .php file holds one loadable class named after the file, and every class in a directory shares the namespace parsed from the first file scanned. Each is constructed with no arguments and keyed by its own string cast, so a command class names itself. A UsersList class naming itself users list with params [--limit=] comes back as:

PHP
$config['routes'] = [
    'users list [--limit=]' => [
        'controller' => 'App\Console\Command\UsersList',
        'help'       => 'List the users'
    ],
    'help' => [
        'controller' => 'App\Console\Controller\ConsoleController',
        'action'     => 'help'
    ]
];

The third argument, $prepend, defaults to true and merges discovered routes before the ones you passed, so your explicit route wins a key collision. Pass false to let the scanned directory override the config.

loadRoutes() is the only part of CommandRegistry you call directly; the rest belongs to a registry Console builds and holds for itself.

Commands#

Pop\Console\Command is a command's name, parameters and help text as an object. Registering one with the console is what puts it on the help screen:

PHP
use Pop\Console\Command;
use Pop\Console\Console;

$console = new Console();

$users = new Command(name: 'users list');
$users->setParams('[--limit=]')
    ->setHelp('List the users');

$roles = new Command(name: 'roles list', params: '[--limit=]', help: 'List the roles');

$console->addCommands([$users, $roles]);

$console->hasCommand('users list');   // true
$console->getCommand('roles list');   // the $roles object, or null
$console->getCommands();              // ['users list' => $users, 'roles list' => $roles]

addCommand() takes one, addCommands() takes an array. The named arguments are the constructor's third, fourth and fifth parameters — the first two are the Application and the Console the command works through, which is why naming them is worth the keystrokes.

A command can also be the dispatch target itself rather than a description of one. Command implements Pop\Dispatch\DispatchableInterface, the same contract Pop\Controller\AbstractController implements, so a route can point at a Command subclass with no action key at all. Write a handle() method with whatever signature the route's parameters call for:

PHPapp/src/Console/Command/UsersShow.php
<?php

namespace App\Console\Command;

use Pop\Console\Command;

class UsersShow extends Command
{

    public function __construct(...$args)
    {
        parent::__construct(...$args);
        $this->setName('users show')
            ->setParams('<id>')
            ->setHelp('Show one user');
    }

    public function handle(string $id): void
    {
        $this->console()->write('Showing user ' . $id);
    }

}
BASH
./app users show 1001
TEXT
    Showing user 1001

Setting the name in the constructor rather than accepting it from outside is what makes the class self-describing, and it's the shape CLI routes requires — loadRoutes() constructs each class with no arguments and asks it what it is called. (string)$command returns the name and params joined, users show <id> here, which is exactly the route key.

dispatch() resolves to handle() whenever no action name is given, and always forwards the route's parameters. A subclass without a handle() throws Pop\Dispatch\Exception with The action to handle the route is not defined. rather than doing nothing.

A command reaches the application and the console through Pop\Dispatch\ConsoleTrait, the same trait a console controller uses, so application(), console(), hasApplication(), hasConsole(), setApplication() and setConsole() are all there. Console defaults to a fresh new Console(120) if none is supplied, so hasConsole() is true from the start; Application stays null until something gives the command one — the router does, when it dispatches it.

This is the other half of Controllers vs Commands: a Command subclass is one class per command, where a console controller groups related actions that share setup. A command class is also what directory scanning picks up. Either way both dispatch through the same router and reach the same help screen.

Output: Lines, Headers and Alerts#

write() is the workhorse — one line of text, printed immediately. Around it are three shaping methods that exist so an application does not spend its time counting dashes.

PHP
use Pop\Console\Console;

$console = new Console(40);

$console->write('Migrating the database.');
$console->line();
$console->line('=', 20);
$console->header('Hello World');
$console->header('Hello World', '=', 40, 'center');
TEXT
    Migrating the database.
    ----------------------------------------
    ====================
    Hello World
    -----------
                   Hello World
    ========================================

line() draws a horizontal rule, defaulting to - at the console's wrap width. header() writes text with a rule under it, and takes the character, the width and an alignment. headerLeft(), headerCenter() and headerRight() are the same thing with the alignment fixed and the size defaulted to 'auto' — the wrap width, falling back to the terminal's own width when there is no wrap set.

Alerts are the loud option: a padded, colored block for something the user has to notice.

PHP
use Pop\Console\Console;

$console = new Console(40);

$console->alertDanger('Migration failed', 'auto');
$console->alertWarning('Two tables were skipped', 'auto');
$console->alertSuccess('Migration complete', 'auto');
$console->alertInfo('Nothing to do', 'auto');

Each renders three lines — a blank line, the centered message, a blank line — filled with the alert's background color. alertPrimary(), alertSecondary(), alertDark() and alertLight() complete the set. alertBox() is the colorless one, drawing a border out of characters you choose instead:

PHP
use Pop\Console\Console;

$console = new Console(40);

$console->alertBox('Boxed', '-', '|', 'auto');
TEXT
    ----------------------------------------
    |                                      |
    |                 Boxed                |
    |                                      |
    ----------------------------------------

Passing null as alertBox()'s vertical character drops the side borders and leaves the two rules.

line(), header() and every alert take a final bool $return argument. Pass true and the method hands back the rendered string instead of printing it, which is how you put one of these inside something else — a string you are building, a value you are testing against:

PHP
use Pop\Console\Console;

$console = new Console(40);

$header = $console->header('Hello World', '-', 40, 'left', true, true);
$alert  = $console->alertSuccess('Done', 'auto', 'center', 4, true, true);

The argument before $return is $newline in both, so the flags are positional and the defaults in between have to be repeated. $size accepts the string 'auto' as well as an integer, and 'auto' is what you want whenever the console's own width should decide.

The Response Buffer#

Console has two ways to put text on the terminal, and they are not interchangeable. write() prints immediately. append() adds to an internal buffer that stays there until send() flushes it.

PHP
use Pop\Console\Console;

$console = new Console();

$console->setHeader('My Application');
$console->setFooter('The End');

$console->append('Here is some console information.');
$console->append('Hope you enjoyed it!');
$console->send();
TEXT
    My Application
    
    Here is some console information.
    Hope you enjoyed it!
    
    The End

setHeader() and setFooter() are what the buffer buys you. send() prints the header, the buffered content and the footer as one response, which is the right shape for a command that decides what to say as it goes and says it at the end. Calling send() again prints nothing — the buffer is cleared on the way out.

Reach for write() when the user needs to see progress as it happens: a long migration reporting each step, a worker logging each job. The output goes out immediately, and nothing is held back.

A header and footer wrap every write(), so build multi-line output as one write() call when you have them set.

Two constructor arguments control the shape of everything written either way: the wrap width and the margin.

PHP
use Pop\Console\Console;

$console = new Console(40, 2);

$console->append(
    'Here is some console information. This is a really long string. It will have to wrap.'
);
$console->send();
TEXT
  Here is some console information. This
  is a really long string. It will have to
  wrap.

A bare new Console() wraps at 80 with a margin of 4; a console handed to a dispatched controller or command is new Console(120). Passing null as the wrap width leaves it unset, at which point the methods that take 'auto' fall back to the terminal's real width, detected at construction.

Both write() and append() add the margin at the start and a newline at the end. Two boolean flags turn each off, which is how several calls compose one line:

PHP
use Pop\Console\Console;

$console = new Console(40);

$console->write('Here ', false);          // margin, no newline
$console->write('is ', false, false);     // no margin, no newline
$console->write('some ', false, false);   // no margin, no newline
$console->write('content.', true, false); // no margin, newline
TEXT
    Here is some content.

Colors#

colorize() wraps a string in ANSI escape codes. It returns the wrapped string rather than printing it, so a colored word sits inside an ordinary line of output:

PHP
use Pop\Console\Color;
use Pop\Console\Console;

$console = new Console();

$console->write(
    'Here is some ' .
    $console->colorize('IMPORTANT', Color::BOLD_RED) .
    ' console information.'
);

The same method is a static on Pop\Console\Color, which is what you reach for in code that has no console object in hand — a formatter, a value object, anything below the output layer:

PHP
use Pop\Console\Color;

$important = Color::colorize('IMPORTANT', Color::BOLD_RED);

The second argument is the foreground and the third is the background, so a highlighted run of text is one call:

PHP
use Pop\Console\Color;
use Pop\Console\Console;

$console = new Console();

$console->write($console->colorize('white on red', Color::BOLD_WHITE, Color::RED));

The constants come in four families of eight — RED through WHITE plus BLACK, then BRIGHT_, BOLD_ and BRIGHT_BOLD_ prefixed versions of each — with NORMAL making thirty-three in all. getAvailableColors() returns the lot as a name-to-value array, which is the reliable way to enumerate them:

PHP
use Pop\Console\Console;

$console = new Console();

$console->getAvailableColors();   // ['NORMAL' => 0, 'BLACK' => 1, 'RED' => 2, ...]

Two more helpers answer questions about the terminal rather than about the text. isColor() reports whether the TERM environment variable indicates color support, and isWindows() reports the platform:

PHP
use Pop\Console\Color;
use Pop\Console\Console;

$console = new Console();

$label = ($console->isColor())
    ? $console->colorize('OK', Color::BOLD_GREEN)
    : 'OK';

$console->write($label);

Gate colorized output on isColor() so output piped into a file or another program stays clean — colorize() emits its escape codes wherever it is sent.

Tables#

table() renders headers and rows into a bordered grid, sizing every column to its widest cell.

PHP
use Pop\Console\Color;
use Pop\Console\Console;

$console = new Console();

$console->table(
    ['Name', 'Status'],
    [
        ['foo', 'active'],
        ['brew',   'idle'],
    ],
    '-', '|', Color::BOLD_GREEN
);
TEXT
    +--------+--------+
    | Name   | Status |
    +--------+--------+
    | foo    | active |
    | brew   | idle   |
    +--------+--------+

The third and fourth arguments are the horizontal and vertical border characters, and the fifth and sixth are a foreground and background color for the header row. Passing null as the vertical character drops the column dividers and the box corners, leaving two rules around a plainer listing:

PHP
use Pop\Console\Console;

$console = new Console();

$console->table([], [['a', 'bb'], ['ccc', 'd']], '-', null);
TEXT
    ----------
     a     bb 
     ccc   d  
    ----------

An empty headers array is what removes the header row, as it does there. The last two arguments are $newline and $return, matching header() and the alerts — pass true for $return and you get the grid back as a string instead of on the terminal.

Give table rows as plain lists rather than associative arrays, so the values line up under the headers.

PHP
use Pop\Console\Console;

$console = new Console();

$rows = [
    ['id' => 1001, 'username' => 'admin'],
    ['id' => 1002, 'username' => 'editor'],
];

$console->table(['id' => 'Id', 'username' => 'Username'], $rows);
TEXT
    +------+----------+
    | Id   | Username |
    +------+----------+
    | 1001 | admin    |
    | 1002 | editor   |
    +------+----------+

Progress Bars#

progressBar() is the one output method that does not print and return. It hands back a Pop\Console\ProgressBar object that redraws a single terminal line in place as you advance it:

PHP
use Pop\Console\Console;

$console = new Console();
$items   = range(1, 100);

$bar = $console->progressBar(count($items), 'Processing');

foreach ($items as $item) {
    // do the work for $item
    $bar->advance();
}

$bar->finish();
TEXT
    Processing [===================>        ]  70% (70/100)

advance() moves the bar on by one, or by whatever step you pass. setProgress() sets an absolute position instead, which suits work whose progress you measure rather than count — bytes written, rows imported. Both clamp to the range, so setProgress(999) against a total of 50 lands on 50 rather than overflowing the bar.

Call finish() when the loop ends, including when it completed on its own. It forces the bar to 100% and prints a newline so the next write() starts on a clean line, and isFinished() stays false until you call it.

The bar's third constructor argument is its width in characters, and three setters change its appearance:

PHP
use Pop\Console\Color;
use Pop\Console\Console;

$console = new Console();

$bar = $console->progressBar(50, 'Importing', 40);
$bar->setChars('#', '#', '.')
    ->setColor(Color::BOLD_CYAN);

$bar->setProgress(25);
TEXT
    Importing [####################....................]  50% (25/50)

setChars() takes the filled character, the leading-edge character and the empty character in that order — passing the same character for the first two, as above, gives a bar with no arrow head. setColor() takes a foreground and an optional background from the same Color constants as Colors, and setMessage(), setWidth() and setIndent() change the rest after construction. getCurrent() and getTotal() read the position back.

A progress bar redraws its line with control characters, so reach for it when the destination is a terminal and print plain lines when it might not be.

Prompts#

prompt() writes a question and returns what the user typed:

PHP
use Pop\Console\Console;

$console = new Console();

$name = $console->prompt('Please provide your name: ', null, true);
$console->write('Hello ' . $name . '!');
TEXT
    Please provide your name: Nick
    Hello Nick!

prompt()'s third argument is $caseSensitive and defaults to false, which lowercases the answer. Pass true when you want it back as the user typed it.

The second argument constrains the answer to a set. The prompt then re-asks until it gets one of them, so the value you get back is always valid and needs no checking:

PHP
use Pop\Console\Console;

$console = new Console();

$letter = $console->prompt(
    'Which is your favorite letter: A, B, C, or D? ',
    ['A', 'B', 'C', 'D'],
    true
);

$console->write('Your favorite letter is ' . $letter . '.');
TEXT
    Which is your favorite letter: A, B, C, or D? x
    Which is your favorite letter: A, B, C, or D? B
    Your favorite letter is B.

promptMulti() is the same idea for more than one answer. The user types a comma-separated list and it returns an array of the values that matched:

PHP
use Pop\Console\Console;

$console = new Console();

$types = $console->promptMulti('Select one or more, comma-separated: ', ['1', '2', '3']);

Typing 1,3 returns ['1', '3'].

confirm() is the shorthand for a yes/no gate. It asks, and on a N it ends the process rather than returning:

PHP
use Pop\Console\Console;

$console = new Console();

$console->confirm('Drop every table?');
$console->write('The user said yes.');

The exit status on N is 127, which is what a shell script wrapping the command should test. Pass false as confirm()'s sixth argument to have it return the answer instead of exiting, when you want to handle a refusal yourself.

Testing a Command that Prompts#

prompt() and confirm() read php://stdin, which makes any command that asks a question untestable by default. setInputStream() swaps that for a stream you control — one line per expected answer, read exactly the way real input is:

PHP
use Pop\Console\Console;

$stream = fopen('php://memory', 'r+');
fwrite($stream, 'Nick' . PHP_EOL);
rewind($stream);

$console = new Console();
$console->setInputStream($stream);

$name = $console->prompt('Please provide your name: ', null, true);

$name is 'Nick' with no terminal involved. hasInputStream() and getInputStream() read the injected stream back, and setInputStream() throws Pop\Console\Exception when handed anything that is not a resource. A stream that runs out mid-run does not hang: a prompt reading end-of-file returns what it has rather than looping forever on input that will never arrive.

Help Screens#

The console builds the help screen from the commands registered with it. Register them by hand and help() prints the list:

PHP
use Pop\Console\Command;
use Pop\Console\Console;

$console = new Console();

$console->addCommands([
    new Command(name: 'db:migrate', help: 'Migrate the database'),
    new Command(name: 'db:seed',    help: 'Seed the database'),
    new Command(name: 'user:list',  help: 'List users'),
]);

$console->help();
TEXT
    db:migrate    Migrate the database
    db:seed       Seed the database
    user:list     List users

Passing a command name returns that command's help string instead of printing anything, which is what a help <command> route wants: $console->help('db:seed') gives back 'Seed the database'.

An application does not usually register its commands twice, though — they are already in the route table. addCommandsFromRoutes() reads them straight out of the CLI route match, taking each route's help value as the description and prefixing every line with the script name you pass:

PHP
use Pop\Console\Console;

$console = new Console();

$console->addCommandsFromRoutes($routeMatch, './app');
$console->help();

Those are the two lines inside the help() action of the ConsoleController under A CLI application in Pop, where $routeMatch is $this->application->router()->getRouteMatch(). The screen it produces for that application's routes:

TEXT
    ./app users show <id>          Show one user
    ./app users list [--limit=]    List the users
    ./app help

A route with no help key is not necessarily undescribed. When its controller is a Command subclass, the class is constructed and its own getHelp() is used — which is why users show above carries a description its route never set. A route with neither leaves the description blank, as help does there.

Two arguments narrow or plain-text the output, and they are the same two on every method that renders help. help(?string $command, bool $raw, ?string $subCommand) takes them second and third; displayHelp(bool $raw, ?string $subCommand) takes them first and second.

PHP
use Pop\Console\Console;

$console = new Console();

$console->help(null, false, 'db');   // only commands under 'db'
$console->help(null, true);          // no ANSI codes
TEXT
    db:migrate    Migrate the database
    db:seed       Seed the database

The trailing colon on a namespace is optional — 'db' and 'db:' filter identically — and matching is a plain prefix test against each command's own name with any registered script name stripped off first. That makes it work for space-separated names too, so 'users' matches users list and users show. A filter matching nothing renders an empty list rather than throwing.

displayHelp() is the version that goes through the response buffer, so it picks up setHeader() and setFooter() the way The response buffer describes:

PHP
use Pop\Console\Console;

$console = new Console();

$console->setHeader('My Application');
$console->setFooter('The End');
$console->displayHelp();

Finally, setHelpColors() takes up to four colors and uses them to break each command line into its parts — the first word of the command name, the second word, then the parameters:

PHP
use Pop\Console\Color;
use Pop\Console\Console;

$console = new Console();

$console->setHelpColors(Color::BOLD_CYAN, Color::BOLD_GREEN, Color::BOLD_MAGENTA);

Against users list [-l|--limit=] that colors users cyan, list green and [-l|--limit=] magenta; against a single-word command like db:seed the second color goes unused and the parameters still take the third. Two lines in a base controller's constructor — the colors and addCommandsFromRoutes() — are the whole of what produces Kettle's own help screen.

pop-console has more surface than this page covers — the Console utilities, and every argument on every render method — see the pop-console README.

See Also#