How to create a simple CLI app with MiniCLI.
Tags: php, cli, tutorial, minicli
Published: 2021-07-11 17:47:19

---
title: How to create a simple CLI app with MiniCLI.
published: true
date: 2021-06-10 14:12:21 UTC
tags: php,cli,tutorial,minicli
canonical_url: https://devdojo.com/thinkverse/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.

```bash
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`.

```bash
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.

```bash
#!/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.

```php
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`.

```php
$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.

```php
$app->runCommand($argv);
```

The `$argv` variable is a reserved variable provided to us by PHP, you can read more about it in the [documentation](https://php.net/manual/en/reserved.variables.argv.php).

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

```php
./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`.

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

EOD);
```

This will give us the following signature.

![Screenshot of new CLI signature](https://raw.githubusercontent.com/thinkverse/ping-cli/main/assets/cli-signature.png?token=AAQ6NMWFSAKV2WPI74FGG3DAZNTOW)

## 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`.

```php
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.

```php
$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.

```php
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.

```php
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.

```php
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`.

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

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

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

This will produce the following result.

![Screenshot of successful ping](https://raw.githubusercontent.com/thinkverse/ping-cli/main/assets/cli-successful-ping.png?token=AAQ6NMUNCOZM3BKTW3YDNY3AZOCRQ)


## 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.

```php
$start_time = microtime(as_float: true);
```

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

```php
$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.

```php
$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.

![Screenshot of estimated time](https://raw.githubusercontent.com/thinkverse/ping-cli/main/assets/cli-estimated-time.png?token=AAQ6NMQKJY3F3KLND7XG7XLAZOEFW)

## 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](https://docs.minicli.dev/en/latest/), also follow the creator Erika Heidi on [twitter](https://twitter.com/erikaheidi).

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

Complete source available on my `ping-cli` [repository](https://github.com/thinkverse/ping-cli).

## 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.

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

[MiniCLI]: https://github.com/minicli/minicli
[GitHub]: https://github.com/minicli/minicli