Overview

Description

Caldera Console is an abstraction layer for CLI applications.

As with the other Caldera components it has been built to be swappable and modular.

Installation

The easisest way to install it is to use Composer:

composer require vecode/caldera-console

Requires

  • php: >=8.1
  • ext-mbstring: *
  • psr/container: ^2.0

Basic usage

At its core, the console package consist in a series of helpers to make it easier to build CLI applications, by enabling you to interact with the users in a streamlined way.

There are three core components, the Console class and two traits that give I/O funcionality to your classes: HasInput and HasOutput.

Let's discover first the two traits.

HasOutput

With the HasOutput trait you can add some helpers for generating output on the console, primarily aimed to display messages to the user; just add the trait to your classes:

use Caldera\Console\HasOutput;

class MyConsoleApp {

    use HasOutput;
}

The most basic method is blank, that outputs a blank line:

$app = new MyConsoleApp();
$app->blank();

However, what's the joy in showing blank lines? Enter the line method, that shows any message and ensures that it ends in a new line:

$app->line('Hey! This is a line!');

Combined with blank you can create space around your lines:

$app->blank()->line('Hey! This is a line!');

Note that those methods are chainable. But it get's better, as there are four additional methods that allow you to print colored text! (if your terminal supports it):

$app->info('Showing some info for you');
$app->success('That thing is now done!');
$app->warning('Are you sure you want to continue?');
$app->error('Oops! an error ocurred');

Modern terminals allow some colors and Caldera Console includes a colorization helper called, colorize:

$app->line( $app->colorize('Whoa, I am some purple text!', Color::fgPurple) );

As you can see, the colorize method takes an string and a color (or two, one for the text and other for its background), that uses the Color enum, which defines a set of text and background colors. Please only use colors that start with fg for the text and bg for the background or you will get an error:

$app->line( $app->colorize('Whoa, I am some purple text!', Color::bgRed, Color::fgYellow) ); # Throws an exception

All of the above methods allow you to use placeholders in your strings, for example:

$app->error('An error ocurred: {error}', ['error' => $e->getMessage()]);

If you don't pass a value for a given placeholder, you will get an error:

$app->error('An error ocurred: {error}'); # Throws an exception

Finally, you can show progress bars easily with a single call to the progressBar method:

$app->progressBar(65); # Show a 65% bar

If you call the progressBar again with another value without showing more output it will update the current value:

$app->progressBar(65); # Show a 65% bar

# <do some processing>

$app->progressBar(75); # Update the bar to show a 75% progress
HasInput

If you want to also receive input from the user, add the HasInput trait too:

use Caldera\Console\HasInput;
use Caldera\Console\HasOutput;

class MyConsoleApp {

    use HasInput, HasOutput;
}

Note that HasInput requires the HasOutput trait as it may need to show messages as a result of some actions.

The most common use case is reading a line of text, and for that we have the getString method:

$name = $app->getString('Hello, enter your name:');

It will show a prompt and return the entered text. If you are prompting for passwords it would be a good idea to hide the entered text, and for that you just have to specify an extra paameter:

$password = $app->getString('Now please enter your password:', true);

Another thing usually done is confirming actions, and for that you just need a key, not an entire string. The getKey method does that:

$key = $app->getKey(['y', 'n'], 'You are about to delete your data, continue (y/n):');

This is specially useful because you pass an array of valid characters, and the function will keep prompting for an answer until the user presses a valid key.

The Console class

Those traits supercharge your classes should you need to interact with the console. But there is also a powerful class that leverages your interaction possibilities: enter the Console class.

As you may have guessed, it already has both the HasInput and HasOutput traits, but its special power is that it allows you to process the input arguments and call specialized classes that handle particular prompts.

For example, you can code your app and ask there for input, show some output and call it a day. But if the logic starts to get beefy a single monolithic class just won't do.

So for complex apps, it is better to isolate the procedures into self-contained classes that we call Commands. And the best part is that you can define custom prompts for each one, allowing a series of arguments that may be optional or required.

So let's go at it.

Creating commands

Let's define a simple command here:

use Caldera\Console\Command\AbstractCommand;
use Caldera\Console\HasInput;
use Caldera\Console\HasOutput;

class GreetCommand extends AbstractCommand {

    use HasInput, HasOutput;

    protected string $signature = 'greet';

    protected string $arguments = '{name?}';

    public function handle() {
        $name = $this->argument('name');
        if (! $name ) {
            $name = $this->getString('Please tell me your name:');
        }
        $this->info("Hey {name}, what's up?", ['name' => $name]);
    }
}

This simple command greets the user by its name, to invoke would do something like this:

php myapp greet John

First you define a $signature, that is the name of the command. In this case is greet, so that you just run your app with a greet argument to identify the command that you want to run.

Also note that there is something called $arguments: this defines which arguments your command accepts, its type and related constraints. We added a single one, called name, but you need to use curly-braces so it will be {name}. There is also a question-mark there {name?} and that makes the argument optional.

Then you need a handle method, that will be called when the command is invoked.

In this example, we try to get name from the arguments, but as it is optional, it may be blank, so we use the getString method to prompt the user for its name if not provided. Then we simply greet the user using the info method.

Your commands must implement the CommandInterface interface, the easiest way to do it is by extending AbstractCommand.

Arguments

Arguments are described by using curly-braces to delimit items, for example:

protected string $arguments = '{first_name} {last_name?}';

This will accept two parameters, one required (first_name) and the other optional (last_name).

You can also define default values by using the = symbol:

protected string $arguments = '{first_name} {last_name?} {prefix=Mr?}';

That will have three arguments, with prefix being optional and with a default value of Mr.

Also you can have arrays of arguments, for example:

protected string $arguments = '{name*}';

That way you will be able to accept multiple items:

php myapp greet John Mary Peter
public function handle() {
    $name = $this->argument('name');
    $this->info("Hey {name}, what's up?", ['name' => implode(', ', $name)]);
}

Please keep in mind that array arguments should always be the last ones and naturally you can have only one. Also, arguments have an order.

Options

Options are another kind of arguments, they usually are optional and can be boolean or have an specific value, for example:

protected string $signature = 'server:listen';

protected string $arguments = '{action} {--v|verbose} {--h|host=localhost?} {--h|port=8080?} {--d|domain*}';

This command accepts up to five arguments:

  • action - A textual argument
  • verbose - A boolean option (true if present, false otherwise)
  • host - An option with a value, with a default value of localhost
  • port - An option with a value, with a default value of 8080
  • domain - An array option

You can call it this way:

php myapp server:listen run --verbose --host=192.168.1.110 --port=8888 --domain=example.org --domain=example.net

Or this way:

php myapp server:listen run -v -h 192.168.1.110 -p 8888 -d example.org -d example.net

Note how you can define option arguments with a shortcut, also note that shortcuts require just a single dash in front of them.

In fact, you can use full options with a = or a space: --domain=example.org and --domain example.org are equivalent.

And shortcuts with an space, or nothing: -p 8080 and -d8080 are the same.

Registering commands

Back to the Console class, to register your commands there are two ways: one by one or in bulk.

For a single command just call the command method:

use MyApp\Commands\GreetCommand;

$console = new Console();
$console->command(GreetCommand::class);

For adding more than one command you can just add a directory with the path method and let the Console class autodiscover all the classes when needed:

$console->path('<path-to-commands-dir>/Commands');

Once the paths are added they will be inspected to autoload the available commands.

Calling commands

To call a command use the call method:

$console->call('greet', ['John']);

It accepts two parameters, the command name and an array of arguments:

$console->call('server:listen', ['run', '-v', '-h 192.168.1.110', '--domain=example.org']);

This way you can easily wire your Console instance to directly accept CLI arguments:

use Caldera\Console\Console;

# Remove the app command
array_shift($argv);

# Get the command name, if any
$command = array_shift($argv);

# Create a Console instance and call the command
$console = new Console();
$console->call($command, $argv);

Advanced usage

Resolving command classes

By default, the Console class will try to instantiate the registered CommandInterface implementation for a given command signature.

But if you are using a ContainerInterface implementation that deals with autowiring, it is better to offload that job to it; so if you are sure that your Container can create scoped classes AND autowire them you may want to pass it to the Console constructor:

$console = new Console($container); # $container is the ContainerInterface implementation instance

By the way, the Caldera Container supports creation of scoped classes and autowiring.