How to create a simple CLI app with MiniCLI.

In this post we're covering how to create a simple CLI app with the help of MiniCLI. What we'll be creating is a small CLI tool to ping websites.

Setting up our requirements.

The first thing we do is to create a folder for our new tool, which I'll name ping-cli and require the minicli/minicli package.

composer require minicli/minicli

Let's also create a .gitignore to make sure the /vendor folder doesn't get added to source control.

vendor/
composer.lock

Creating our entry-point.

With the requirements done we can continue on to creating our entry-point. This is what'll be called from the command-line. Create a bin folder and a file named ping-cli.

mkdir bin && touch bin/ping-cli

Now we can use bash and the minicli package to create our new tool. Start by using a shebang to load our new ping-cli file in a php environment, and add out opening php tags.

#!/usr/bin/env php
<?php declare(strict_types = 1);

You can omit declare if you don't want strict typing.

Before moving on to using minicli, we must first make sure we're calling this file from the CLI. We do this with the php_sapi_name functions, which returns the string of our current interface.

if (php_sapi_name() !== 'cli') {
    exit;
}

Integrating with MiniCLI.

To integrate minicli we include the composer autoloader and instantiate a new Minicli class.

We do this by establishing a new variable $root to hold the parent directory of the directory bin/ping-cli. Adding a check to see if we can grab the autoloader using that path. Should that operation be false, we will set the $root to be 4 levels up from our parent directory.

This is done so that if our tool is required and is symlinked to vendor/bin/ping-cli, we use the autoloader generated by the outside project correctly.

Lastly we require the autoloader and instantiate a new Minicli\App.

$root = dirname(__DIR__);

if (! is_file($root . '/vendor/autoload.php')) {
    $root = dirname(__DIR__, 4);
}

require $root . '/vendor/autoload.php';

use Minicli\App;

$app = new App();

To get the CLI tool to run, we have to call the runCommand method on our $app. Add the following line to the end of our ping-cli file.

$app->runCommand($argv);

The $argv variable is a reserved variable provided to us by PHP, you can read more about it in the documentation.

Running our command now will output ./minicli help to the console, try it yourself by running the command.

./bin/ping-cli

Updating our CLI signature.

Before embarking on the journey of creating the new command, let us first update our signature. This signature will replace the default ./minicli help signature ran when running our tool with no input.

We set our new signature using the setSignature method on the minicli $app.

$app->setSignature(<<<EOD
           _                        ___ 
    ____  (_)___  ____ _      _____/ (_)
   / __ \/ / __ \/ __ `/_____/ ___/ / / 
  / /_/ / / / / / /_/ /_____/ /__/ / /  
 / .___/_/_/ /_/\__, /      \___/_/_/   
/_/            /____/ \e[0;32m Ver. 0.0.1

EOD);

This will give us the following signature.

Registering our command.

With the basic setup out-the-way, let's continue on by registering our ping command. Since this tool will only have a single command, we will forgo registering a namespace and just register a single command with the registerCommand method.

To register the command, we pass it the name and the callable which will get passed the $input. For convenience we will also use the $app in out callable.

use Minicli\Command\CommandCall;

$app->registerCommand('ping', function (CommandCall $input) use ($app) {
    $app->getPrinter()->success('We have lift-off! 🚀');
});

Calling the getPrinter method access the output handler, which we then use to print a success message to the console.

Running ./bin/ping-cli ping will now print back We have lift-off! 🚀 in our console.

Updating our command.

With the command registered, let us move swiftly along to implementing our real ping command.

This command will take a url parameter, in either of two formats, HTTP or HTTPS. With this parameter, we will send a request to the url and see if it's accessible.

To grab the parameter we call getParam on our $input and specify which parameter we want. Let's store our url parameter in a $url variable, or default to null.

With this we can sent a error output using getPrinter and exit early if no url was provided.

$url = $input->getParam('url') ?? null;

if (! $url) {
    $app->getPrinter()->error('Parameter <url> was not provided');
    return 1;
}

Now we can continue on and validate the incoming $url parameter. To do that we will use RegEx and filter_var.

We first check for a valid URL - since URLs can be valid even without the HTTP protocol, we add this check first. Returning early is the check fails.

Next we use RegEx to check if the $url provided starts with a valid HTTP(s) protocol. Returning early with a message if that fails.

if (! filter_var($url, FILTER_VALIDATE_URL)) {
    $app->getPrinter()->error('Parameter <url> not a valid URL.');
    return 1;
}

if (! preg_match('#^https?://#i', $url)) {
    $app->getPrinter()->error('Parameter <url> has to include a valid HTTP protocol.');
    return 1;
}

While this isn't the end-all be-all of validating the $url, it'll do for a simple ping command.

A streaming context

Before we try and access the $url provided, we need to create a stream context for file_get_contents to use. In this context we will set the HTTP method and some HTTP headers.

To keep things a bit separated, let's create a new function called getStreamContext where we'll create our new context.

function getStreamContext() {
    $headers = implode("\r\n", [
        'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_16_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36',
        'Accept: text/html,application/xhtml+xml,application/xml;q=0.9',
        'Accept-Language: en-US,en;q=0.9',
        'Upgrade-Insecure-Requests: 1',
        'X-Application: PingCLI/0.0.1',
        'Sec-GPC: 1',
    ]);

    return stream_context_create([
        'http' => [
            'method' => 'GET', 
            'header' => $headers,
        ]
    ]);
}

We set some headers so the $url we're accessing thinks the site is being used from a Chrome browser running on a Mac.

Finishing up the command.

With the context created we can now use file_get_contents to ping the $url. Since we won't use the returning content for anything, we can use file_get_contents directly in our if statement.

if (! @file_get_contents($url, context: getStreamContext())) {
    $app->getPrinter()->error('Parameter <url> could not be reached.');
    return 1;
}

The reasoning being the silencing (@) is because we can provide a valid URL that file_get_contents might not be able to access, that will give of warnings unless silenced.

If all those checks are cleared, we can safely assume the site is reachable and up. Let's give the user an indication the site was reached by adding the following code to the end of our callable.

$app->getPrinter()->success(sprintf('URL <%s> is up.', $url));

We can now test our ping command by running the following.

./bin/ping-cli ping url=https://devdojo.com

This will produce the following result.

Extending our command.

Before I leave you, I'd like to add one more feature to our command. That's the estimated ping time, which is an estimate of how long it took to access the website.

We'll do this with the microtime function and some subtraction. We'll Add the following right above our file_get_contents check.

$start_time = microtime(as_float: true);

Below our if, and before we output our success message we add the following.

$end_time = microtime(as_float: true);

These two variables will now be used to estimate how long it took to access the site. Since they're places in between file_get_contents we should get a fairly close estimate.

Let's print that estimate to our user below out success output.

$app->getPrinter()->display(sprintf('Est. ping time: %.02f sec', ($end_time - $start_time)));

This will now add an estimated ping time to the end of our output, as displayed below.

Conclusion.

This is how you can create a simple CLI tool with the help of MiniCLI. You can read up more about minicli on their GitHub page, or their documentation, also follow the creator Erika Heidi on twitter.

That's all for me folks, thank you for reading. 👋

Complete source available on my ping-cli repository.

Troubleshooting.

If you're having issues running your new CLI tool, make sure the permissions are turned on to use the file as an executable. On Linux system this can be done with chmod, use the equivalent command for windows.

ping-cli ~ chmod +x bin/ping-cli

17