cover image

Introducing minicli: a microframework for CLI-centric PHP applications

In the previous posts of the Building Minicli series, we demonstrated the steps to bootstrap a dependency-free microframework for CLI-only applications in PHP.

Minicli was created as an educational experiment and also as a lightweight base that I could reuse for my personal projects. I like to think of it as the most basic unit I was able to put together, on top of which I could build my toys.

It's been months since I shared my latest post on this series, and I was reluctant to share what I've been working on because it always feels like an incomplete work. But will it ever be complete (or feel like so)? Probably not. Minicli is open source since day 0, and although I never intended to turn it into a mainstream project, I also think it can help people who are interested in building simple things in the command line without the overkill of dozens of external requirements.

So I'd like to officially introduce you to Minicli, a highly experimental dependency-free microframework for CLI-centric PHP apps.

While I don't advocate for reinventing all the wheels in an application, I believe there should be a starting point that doesn't require 10+ different libraries for basic parsing and routing of commands. From there, you should be able to consciously choose what you'll be depending on, in terms of external libraries. Minicli is what I came up with in order to solve this situation.

What I've built with Minicli so far:

Dolphin, a command-line tool for managing DigitalOcean droplets from the command line. Dolphin screenshot

My website, which is a static-content CMS pulling from my DEV posts. I'm open sourcing this as a separate project called Librarian (WIP).

Screenshot of eheidi.dev

In this post, you will learn how to create a simple CLI application in PHP using Minicli.

Creating a Project

You'll need php-cli and Composer to get started.

Create a new project with:

composer create-project --prefer-dist minicli/application myapp

Once the installation is finished, you can run minicli with:

cd myapp
./minicli

This will show you the default app signature.

The help command that comes with minicli, defined in app/Command/Help/DefaultController.php, auto-generates a tree of available commands:

./minicli help
Available Commands

help
└──test

The help test command, defined in app/Command/Help/TestController.php, shows an echo test of parameters:

./minicli help test user=erika name=value
Hello, erika!

Array
(
    [user] => erika
    [name] => value
)

Creating your First Command

The simplest way to create a command is to edit the minicli script and define a new command as an anonymous function within the Application via registerCommand:

#!/usr/bin/php
<?php

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

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

use Minicli\App;
use Minicli\Command\CommandCall;

$app = new App();
$app->setSignature('./minicli mycommand');

$app->registerCommand('mycommand', function(CommandCall $input) {
    echo "My Command!";

    var_dump($input);
});

$app->runCommand($argv);

You could then execute the new command with:

./minicli mycommand

Using Command Controllers

To organize your commands into controllers, you'll need to use Command Namespaces.

Let's say you want to create a command named hello. You should start by creating a new directory under the app/Commands folder:

mkdir app/Commands/Hello

Now Hello is your Command Namespace. Inside that directory, you'll need to create at least one Command Controller. You can start with the DefaultController, which will be called by default when no subcommand is provided.

This is how this DefaultController class could look like:

<?php

namespace App\Command\Hello;

use Minicli\Command\CommandController;

class DefaultController extends CommandController
{
    public function handle()
    {       
        $this->getPrinter()->display("Hello World!");
    }
}

This command would be available as:

./minicli hello

Becase a subcommand was not provided, it is inferred that you want to execute the default command. This command can also be invoked as:

./minicli hello default

Any other Command Controller placed inside the Hello namespace will be available in a similar way. For instance, let's say you want to create a new subcommand like hello caps.

You would then create a new Command Controller named CapsController:

<?php

namespace App\Command\Hello;

use Minicli\Command\CommandController;

class CapsController extends CommandController
{
    public function handle()
    {       
        $this->getPrinter()->display("HELLO WORLD!");
    }
}

And this new command would be available as:

./minicli hello caps

Working with Parameters

Minicli uses a few conventions for command call arguments:

  • Args / Arguments: Parsed arguments - anything that comes from $argv that is not a key=value and not a --flag.
  • Params / Parameters: Key-value pairs such as user=erika
  • Flags: single arguments prefixed with -- such as --update

The parent CommandController class exposes a few handy methods to work with the command call parameters. For instance, let's say you want to update the previous hello command to use an optional parameter to tell the name of the person that will be greeted.

<?php

namespace App\Command\Hello;

use Minicli\Command\CommandController;
use Minicli\Input;

class HelloController extends CommandController
{
    public function handle()
    {       
        $name = $this->hasParam('user') ? $this->getParam('user') : 'World';
        $this->getPrinter()->display(sprintf("Hello, %s!", $name));
    }
}

Now, to use the custom version of the command, you'll need to run:

./minicli hello user=erika 

And you'll get the output:

Hello, erika!

CommandCall Class Methods

  • hasParam(string $key) : bool - Returns true if a parameter exists.
  • getParam(string $key) : string - Returns a parameter, or null if its non existent.
  • hasFlag(string $key) : bool - Returns whether or not a flag was passed along in the command call.

Printing Output

The CliPrinter class has shortcut methods to print messages with various colors and styles. It comes with two bundled themes: regular and unicorn. This is set up within the App bootstrap config array, and by default it's configured to use the regular theme.

    public function handle()
    {       
        $this->getPrinter()->info("Starting Minicli...");
        if (!$this->hasParam('message')) {
            $this->getPrinter()->error("Error: you must provide a message.");
            exit;
        }

        $this->getPrinter()->success($this->getParam('message'));
    }

CliPrinter Class Methods

  • display(string $message) : void - Displays a message wrapped in new lines.
  • error(string $message) : void - Displays an error message wrapped in new lines, using the current theme colors.
  • success(string $message) : void - Displays a success message wrapped in new lines, using the current theme colors.
  • info(string $message) : void - Displays an info message wrapped in new lines, using the current theme colors.
  • newline() : void - Prints a new line.
  • format(string $message, string $style="default") : string - Returns a formatted string with the desired style.
  • out(string $message) : void - Prints a message.

Wrapping Up

Minicli is a work in progress, but you can already use it as a minimalist base on top of which you can build fun toy projects and/or helpful command line tools, like Dolphin.

Here's a few ideas I'd like to build with Minicli but haven't had the time so far (and I definitely wouldn't mind if anyone build these):

  • a text-based rpg game
  • a Twitter bot
  • a tool for finding your Twitter mutuals
  • a cli-based quizz game

If you'd like to give Minicli a try, check the documentation for more details and don't hesitate to leave a comment if you have any questions :)

showdev, php, cli