PHP - - By Nicola Pietroluongo

Console Wars – PHP CLI Libraries

I have always been a big fan of console commands and I try to provide a command line interface (CLI) as much as possible in most of my PHP projects.

In this article, I’ll briefly compare three PHP console command libraries:

Image of terminal screen

Origin Stories

The Symfony console is the oldest and the most popular one, used in many projects (and obviously part of the Symfony framework). With dozens of contributors, it became the first choice for many developers.

Hoa is a modular, extensible and structured set of PHP libraries that includes the Hoa console. It aims to be a bridge between industrial and research worlds, and this makes that project quite interesting.

The Webmozart console is the newest project, wants to be easier, test friendly and add new functionality on top of the Symfony console.

Dependencies, Size, and Complexity

The Symfony console has only suggested dependencies, as opposed to the Hoa console library that depends on some Hoa project libraries. The Webmozart project, too, directly depends on the Symfony console.

The Hoa console has the smallest number of LOC (Logical Lines of Code) ~1397, followed by the Symfony console ~2226 and the Webmozart ~3126 (without dependencies).

In order to have a rough indicator of the complexity of these projects, below is some data from their PHPLOC analysis*:


Description Symfony Hoa Webmozart
Cyclomatic Complexity
Average Complexity per LLOC 0.37 0.36 0.26
Average Complexity per Class 14.73 25.14 8.84
Average Complexity per Method 2.55 3.38 1.99
Dependencies
Global Accesses 3 20 1
Attribute Accesses 807 217 1285
Method Calls 1103 324 1320

*The analysis is performed only in the main source directory, excluding the test folder when present.

Practical example

Description

To have an overview of each library’s functionality and see the code in action, let’s write a business feature to describe a usage example:

Feature: I want to output a message to several people.
	The message should be passed via the `--message` option and should be optional (default="Hello"),
	the message should be followed by two or more names,
	the message should be coloured with `--color=` (default="white") and/or in uppercase with `--up` (default=lowercase).

The final console call should be something like:
somemsg --message='Good Morning' Nicola Bruno --color=green --up
and the output should be:

GOOD MORNING NICOLA AND BRUNO

Implementation

First, we need to define a PHP Message, used in every console implementation, to handle the example.

Below is some pretty straightforward code:

class Message
{
    /**
     * Construct the class and initializes the properties.
     * 
     * @param        $names
     * @param string $message
     * @param bool   $uppercase
     */
    public function __construct($names, $message="Hello", $uppercase=false)
    {
        $this->names = implode(' and ', $names);
        $this->message = $message;
        $this->uppercase = $uppercase;
    }

    /**
     * Generates the output.
     * 
     * @return string
     */
    public function getMessage()
    {
        $output =  $this->message . ' ' . $this->names;
        if ($this->uppercase) {
            $output = strtoupper($output);
        }

        return $output;
    }
}

Symfony console

In order to create a console command in Symfony, it’s necessary to:

  • Create the command class
    – Configure the arguments and options
    – Write the logic
  • Create the application

Create The command

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;

class MessageCommand extends Command
{
    /**
     * Configures the argument and options
     */
    protected function configure()
    {
        $this
            ->setName('demo:msg')
            ->setDescription('Simple message delivery')
            ->addArgument('names', InputArgument::IS_ARRAY | InputArgument::REQUIRED, 'Who do you want to message?')
            ->addOption('message', null, InputOption::VALUE_REQUIRED, 'Set the message', 'Hello')
            ->addOption('up', null, InputOption::VALUE_NONE, 'Set the output in uppercase')
            ->addOption('color', null, InputOption::VALUE_REQUIRED, 'Which colors do you like?', 'white')
        ;
    }

    /**
     * Executes the logic and creates the output.
     * 
     * @param InputInterface $input
     * @param OutputInterface $output
     */
    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $names = $input->getArgument('names');
        $message = $input->getOption('message');
        $uppercase = $input->getOption('up');
        $color = $input->getOption('color');

        $message = new Message($names, $message, $uppercase);
        $messageString = $message->getMessage();
        $coloredMsg = '<fg='.$color.'>'.$messageString.'</fg='.$color.'>';

        $output->writeln($coloredMsg);
    }
}

The configure method is used to set up the arguments and options for the command.

The addArgument method can receive the following parameters:
addArgument($name, $mode, $description, $default)


type name description
string $name The argument name
int $mode The argument mode: InputArgument::REQUIRED or InputArgument::OPTIONAL
string $description A description text
mixed $default The default value (for InputArgument::OPTIONAL mode only)

The addOption can receive the following parameters:
addArgument($name, $shortcut, $mode, $description, $default)


type name description
string $name The option name
string $shortcut The shortcut (can be null)
int $mode The option mode: One of the InputOption::VALUE_* constants
string $description A description text
mixed $default The default value (must be null for InputOption::VALUE_REQUIRED or InputOption::VALUE_NONE)

There are three options available to color the output:

  • Use a preset tag (es: $output->writeln('<info>foo</info>'); for green output)
  • Define a style using the OutputFormatterStyle class
  • Set the color inside the tag name es:
// red text on a cyan background
 $output->writeln('<fg=red;bg=cyan>foo</fg=red;bg=cyan>');

The available foreground and background colors are: black, red, green, yellow, blue, magenta, cyan and white. The available options are: bold, underscore, blink, reverse and conceal.

More information in the official Symfony documentation.

Note: by default, the Windows command console doesn’t support output coloring. You’ll need (and should install) Git tools or another more advanced command console.

Create The application

After the configuration and the execution, we’re almost done. The last step is creating a PHP file to run the command.

//file myconsole.php

require __DIR__.'/vendor/autoload.php';

use MessageCommand;
use Symfony\Component\Console\Application;

$application = new Application();
$application->add(new MessageCommand());
$application->run();

Console call example:

php myconsole.php demo:msg Nicola Bruno --message='Good Morning' --color=blue --up

The Symfony console also automatically provides the output helper with the --help argument.

Hoa console

The Hoa console follows a less structured approach to configuring the console command.

That process consists of the following steps:

  • Parse the command
  • Get options and input
  • Execute the logic

Parse the command

/**
 * $argv contains an array of all the arguments passed to the script,
 * the first argument is always the name of the PHP file.
 * the Hoa Parser->parse method accept a string in input, so it's necessary to convert the $argv array in a string without the first argument as below.
 */
unset($argv[0]);
$command = implode(' ', $argv);

$parser = new Hoa\Console\Parser();
$parser->parse($command);

//options definition
//['longname', TYPE, 'shortname']
$options = new Hoa\Console\GetOption(
    [
        ['up', Hoa\Console\GetOption::NO_ARGUMENT, 'u'],
        ['message', Hoa\Console\GetOption::REQUIRED_ARGUMENT, 'm'],
        ['color', Hoa\Console\GetOption::OPTIONAL_ARGUMENT, 'c']
    ],
    $parser
);

Get options and inputs

//definition of default values
$uppercase = false;
$message = "Hello";
$color = "white";

$names = $parser->getInputs();

//The following while with the switch will assign the values to the variables.
while (false !== $shortName = $options->getOption($value)) {
    switch ($shortName) {
        case 'u':
            $uppercase = true;
            break;
        case 'm':
            $message = $value;
            break;
        case 'c':
            $color = $value;
            break;
    }
}

Execute the logic

$message = new Message($names, $message, $uppercase);
$messageString = $message->getMessage();

Hoa\Console\Cursor::colorize('fg('.$color.')');
echo $messageString;
Hoa\Console\Cursor::colorize('fg(white)'); //reset the cursor to default white

For coloring the output, it’s possible to change the Cursor color.

The Hoa console supports a wide range of colors.
The color can be set by name (black, red, green, yellow…), by number (from 0 to 256 representing the 264 color palette) or by hexadecimal code #rrggbb, example:
Hoa\Console\Cursor::colorize('fg(yellow) bg(#932e2e) underlined');

The basic usage in the example doesn’t provide an automatic helper output, and is not strongly OOP oriented but extending the Hoa\Console\Dispatcher\Kit (requires hoa/dispatcher) could add more flexibility (more information in the official documentation)

The command can be called with:

php message.php  -u --message=Hello --color=green Nicola Bruno

One of the strongpoints of the Hoa console is that it provides additional API classes to manipulate important elements, supporting the different terminal profiles:

  • The Cursor (move, clear, show, colorize, …)
  • The Mouse (listening to mouse action)
  • The Window (setSize, scroll, minimize, …)
  • The terminal line with Readline (history, autocompletion, etc)

Webmozart console

The Webmozart console command creation workflow consists of:

  • configuring the argument and options
  • writing the logic
  • creating the application

Webmozart’s console follows an approach similar to the Symfony Console, but with a clear separation between the configuration and the logical execution.

Configuration

use Webmozart\Console\Api\Args\Format\Argument;
use Webmozart\Console\Api\Args\Format\Option;
use Webmozart\Console\Config\DefaultApplicationConfig;

/**
 * Configuration of arguments and options
 */
class MsgApplicationConfig extends DefaultApplicationConfig
{
    protected function configure()
    {
        parent::configure();

        $this
            ->setName('msg')
            ->setVersion('0.1')
            ->beginCommand('msg')
                ->setDescription('Show a nice message')
                ->setHandler(new MsgCommandHandler())
                ->addArgument('names', Argument::MULTI_VALUED | Argument::REQUIRED, 'Who do you want to message?')
                ->addOption('message', null, Option::OPTIONAL_VALUE, 'Set the message', 'Hello')
                ->addOption('up', null, Option::NO_VALUE, 'Set the output in uppercase')
                ->addOption('color', null, Option::REQUIRED_VALUE, 'Which colors do you like?', 'white')
            ->end()
        ;
    }
}

Logic

use Webmozart\Console\Api\Args\Args;
use Webmozart\Console\Api\IO\IO;

/**
 * Handling the command logic
 */
class MsgCommandHandler
{
    public function handle(Args $args, IO $io)
    {
        //gets the argument and option	
        $names = $args->getArgument('names');
        $message = $args->getOption('message');
        $uppercase = $args->getOption('up');
        $color = $args->getOption('color');
        
        $message = new Message($names, $message, $uppercase);
        $messageString = $message->getMessage();
        
        $coloredMsg = '<fg='.$color.'>'.$messageString.'</fg='.$color.'>';
        
        $io->writeLine($coloredMsg);
    }
}

The strong separation of configuration and logic allows more flexibility for easy testing and for a project that will grow with additional commands.

Other advantages of the Webmozart’s console are:

  • sub commands support:

    php mycommand.php msg send --arg1 --arg2
    	 php mycommand.php msg receive --someoptions=somevalue
  • support for manpage documentation (like with “git help remote”)

  • adapters for the Symfony console (to use Symfony’s classes like ProgressBar)

Creating The application

The application file to run the command is similar to the Symfony one:

require 'vendor/autoload.php';
use Webmozart\Console\ConsoleApplication;

$cli = new ConsoleApplication(new MsgApplicationConfig());
$cli->run();

Console call:

php myconsole.php msg --message='Good Morning' Nicola Bruno --color=blue --up

Final Thoughts

Each console covered above provides different functionalities for different use types and user preferences.

  • The Symfony console is well tested, robust, with good documentation and features to solve most of the average use cases.
  • The Hoa console is more industry oriented, perfect for manipulating the terminal environment (mouse, cursor, window, and so on).
  • The Webmozart console is new (there will be a stable release soon) but it’s very useful for handling projects that tend to grow to large sizes.

Do you use any of them regularly? If so, which one? Why? What would you say the pros and cons of each are? Do you have other contenders to suggest? Let us know!

Sponsors