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.
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).
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 :)
Top comments (4)
Tip:
The sub-commands can extend the
DefaultController
for the command, over-writing thehandle()
method, but inheriting the other methods from theDefaultController
, in case for example, you want a method in theDefaultController
which handles flags, but don't want to re-define that in all the sub-commands.That's a great idea Sherri, thanks for sharing! Updates coming soon π
While using the framework, I run into this problem. Have checked everything and it is in the correct order
Fatal error: Uncaught Error: Class '\CreateController' not found in C:\Users\use
r pc\Desktop\myapp\vendor\minicli\minicli\lib\Command\CommandNamespace.php:71
Stack trace:
0 C:\Users\user pc\Desktop\myapp\vendor\minicli\minicli\lib\Command\CommandName
space.php(36): Minicli\Command\CommandNamespace->loadCommandMap('C:\Users\user
p...')
1 C:\Users\user pc\Desktop\myapp\vendor\minicli\minicli\lib\Command\CommandRegi
stry.php(51): Minicli\Command\CommandNamespace->loadControllers('C:\Users\user
p...')
2 C:\Users\user pc\Desktop\myapp\vendor\minicli\minicli\lib\Command\CommandRegi
stry.php(40): Minicli\Command\CommandRegistry->registerNamespace('Controller')
3 C:\Users\user pc\Desktop\myapp\vendor\minicli\minicli\lib\Command\CommandRegi
stry.php(31): Minicli\Command\CommandRegistry->autoloadNamespaces()
4 C:\Users\user pc\Desktop\myapp\vendor\minicli\minicli\lib\App.php(66): Minicl
i\Command\CommandRegistry->load(Object(Minicli\App))
5 C:\Users\user pc\Desktop\myapp\vendor\minicli\minicli\lib\App.php(46): Minicl
i\App->loa in C:\Users\user pc\Desktop\myapp\vendor\minicli\minicli\lib\Command\
CommandNamespace.php on line 71
Nicely done. Reminded me of the simple framework (if you can even call it that) I made for running cli tasks created in PHP.
jordonr / php-tc
PHP Task Command
php-tc
PHP Task Command
Basic layout or framework to make php tasks that are to be run from the command-line.
I have included a few packages that were used for my needs so make sure to edit the composer.json as needed and run composer.
Wrote this out of a random need to check and update a SOAP API every 5 minutes. I tried Mono, Ruby, Perl and Python but only PHP generated the correct SOAP calls for this specific API.