Introduction
PHP is well known for its popularity with web applications and CMSs, but what many people don't know is that PHP is also a great language for building command line applications that don't require a web server. Its ease of use and familiar syntax make it a low-barrier language for complimentary tools and little applications that might communicate with APIs or execute scheduled tasks via Crontab, for instance, but without the need to be exposed to external users.
Certainly, you can build a one-file PHP script to attend your needs and that might work reasonably well for tiny things; but that makes it very hard for maintaining, extending or reusing that code in the future. The same principles of web development can be applied when building command-line applications, except that we are not working with a front end anymore - yay! Additionally, the application is not accessible to outside users, which adds security and creates more room for experimentation.
I'm a bit sick of web applications and the complexity that is built around front-end lately, so toying around with PHP in the command line was very refreshing to me personally. In this post/series, we'll build together a minimalist / dependency-free CLI AppKit (think a tiny framework) - minicli - that can be used as base for your experimental CLI apps in PHP.
PS.: if all you need is a git clone
, please go here.
This is part 1 of the Building Minicli series.
Prerequisites
In order to follow this tutorial, you'll need php-cli
installed on your local machine or development server, and Composer for generating the autoload files.
1. Setting Up Directory Structure & Entry Point
Let's start by creating the main project directory:
mkdir minicli
cd minicli
Next, we'll create the entry point for our CLI application. This is the equivalent of an index.php
file on modern PHP web apps, where a single entry point redirect requests to the relevant Controllers. However, since our application is CLI only, we will use a different file name and include some safeguards to not allow execution from a web server.
Open a new file named minicli
using your favorite text editor:
vim minicli
You will notice that we didn't include a .php
extension here. Because we are running this script on the command line, we can include a special descriptor to tell your shell program that we're using PHP to execute this script.
#!/usr/bin/php
<?php
if (php_sapi_name() !== 'cli') {
exit;
}
echo "Hello World\n";
The first line is the application shebang. It tells the shell that is running this script to use /usr/bin/php
as interpreter for that code.
Make the script executable with chmod
:
chmod +x minicli
Now you can run the application with:
./minicli
You should see a Hello World
as output.
2. Setting Up Source Dirs and Autoload
To facilitate reusing this framework for several applications, we'll create two source directories:
-
app
: this namespace will be reserved for Application-specific models and controllers. -
lib
: this namespace will be used by the core framework classes, which can be reused throughout various applications.
Create both directories with:
mkdir app
mkdir lib
Now let's create a composer.json
file to set up autoload. This will help us better organize our application while using classes and other object oriented resources from PHP.
Create a new composer.json
file in your text editor and include the following content:
{
"autoload": {
"psr-4": {
"Minicli\\": "lib/",
"App\\": "app/"
}
}
}
After saving and closing the file, run the following command to set up autoload files:
composer dump-autoload
To test that the autoload is working as expected, we'll create our first class. This class will represent the Application object, responsible for handling command execution. We'll keep it simple and name it App
.
Create a new App.php
file inside your lib
folder, using your text editor of choice:
vim lib/App.php
The App
class implements a runCommand
method replacing the "Hello World" code we had previously set up in our minicli
executable.
We will modify this method later so that it can handle several commands. For now, it will output a "Hello $name" text using a parameter passed along when executing the script; if no parameter is passed, it will use world
as default value for the $name
variable.
Insert the following content in your App.php
file, saving and closing the file when you're finished:
<?php
namespace Minicli;
class App
{
public function runCommand(array $argv)
{
$name = "World";
if (isset($argv[1])) {
$name = $argv[1];
}
echo "Hello $name!!!\n";
}
}
Now go to your minicli
script and replace the current content with the following code, which we'll explain in a minute:
#!/usr/bin/php
<?php
if (php_sapi_name() !== 'cli') {
exit;
}
require __DIR__ . '/vendor/autoload.php';
use Minicli\App;
$app = new App();
$app->runCommand($argv);
Here, we are requiring the auto-generated autoload.php
file in order to automatically include class files when creating new objects. After creating the App
object, we call the runCommand
method, passing along the global $argv
variable that contains all parameters used when running that script. The $argv
variable is an array where the first position (0) is the name of the script, and the subsequent positions are occupied by extra parameters passed to the command call. This is a predefined variable available in PHP scripts executed from the command line.
Now, to test that everything works as expected, run:
./minicli your-name
And you should see the following output:
Hello your-name!!!
Now, if you don't pass any additional parameters to the script, it should print:
Hello World!!!
3. Creating an Output Helper
Because the command line interface is text-only, sometimes it can be hard to identify errors or alert messages from an application, or to format data in a way that is more human-readable. We'll outsource some of these tasks to a helper class that will handle output to the terminal.
Create a new class inside the lib
folder using your text editor of choice:
vim lib/CliPrinter.php
The following class defines three public methods: a basic out
method to output a message; a newline
method to print a new line; and a display
method that combines those two in order to give emphasis to a text, wrapping it with new lines. We'll expand this class later to include more formatting options.
<?php
namespace Minicli;
class CliPrinter
{
public function out($message)
{
echo $message;
}
public function newline()
{
$this->out("\n");
}
public function display($message)
{
$this->newline();
$this->out($message);
$this->newline();
$this->newline();
}
}
Now let's update the App
class to use the CliPrinter helper class. We will create a property named $printer
that will reference a CliPrinter
object. The object is created in the App
constructor method. We'll then create a getPrinter
method and use it in the runCommand
method to display our message, instead of using echo
directly:
<?php
namespace Minicli;
class App
{
protected $printer;
public function __construct()
{
$this->printer = new CliPrinter();
}
public function getPrinter()
{
return $this->printer;
}
public function runCommand($argv)
{
$name = "World";
if (isset($argv[1])) {
$name = $argv[1];
}
$this->getPrinter()->display("Hello $name!!!");
}
}
Now run the application again with:
./minicli your_name
You should get output like this (with newlines surrounding the message):
Hello your_name!!!
In the next step, we'll move the command logic outside the App
class, making it easier to include new commands whenever you need.
4. Creating a Command Registry
We'll now refactor the App
class to handle multiple commands through a generic runCommand
method and a Command Registry. New commands will be registered much like routes are typically defined in some popular PHP web frameworks.
The updated App
class will now include a new property, an array named command_registry
. The method registerCommand
will use this variable to store the application commands as anonymous functions identified by a name.
The runCommand
method now checks if $argv[1]
is set to a registered command name. If no command is set, it will try to execute a help
command by default. If no valid command is found, it will print an error message.
This is how the updated App.php
class looks like after these changes. Replace the current content of your App.php
file with the following code:
<?php
namespace Minicli;
class App
{
protected $printer;
protected $registry = [];
public function __construct()
{
$this->printer = new CliPrinter();
}
public function getPrinter()
{
return $this->printer;
}
public function registerCommand($name, $callable)
{
$this->registry[$name] = $callable;
}
public function getCommand($command)
{
return isset($this->registry[$command]) ? $this->registry[$command] : null;
}
public function runCommand(array $argv = [])
{
$command_name = "help";
if (isset($argv[1])) {
$command_name = $argv[1];
}
$command = $this->getCommand($command_name);
if ($command === null) {
$this->getPrinter()->display("ERROR: Command \"$command_name\" not found.");
exit;
}
call_user_func($command, $argv);
}
}
Next, we'll update our minicli
script and register two commands: hello
and help
. These will be registered as anonymous functions within our App
object, using the newly created registerCommand
method.
Copy the updated minicli
script and update your file:
#!/usr/bin/php
<?php
if (php_sapi_name() !== 'cli') {
exit;
}
require __DIR__ . '/vendor/autoload.php';
use Minicli\App;
$app = new App();
$app->registerCommand('hello', function (array $argv) use ($app) {
$name = isset ($argv[2]) ? $argv[2] : "World";
$app->getPrinter()->display("Hello $name!!!");
});
$app->registerCommand('help', function (array $argv) use ($app) {
$app->getPrinter()->display("usage: minicli hello [ your-name ]");
});
$app->runCommand($argv);
Now your application has two working commands: help
and hello
. To test it out, run:
./minicli help
This will print:
usage: minicli hello [ your-name ]
Now test the hello
command with:
./minicli hello your_name
Hello your_name!!!
You have now a working CLI app using a minimalist structure that will serve as base to implement more commands and features.
This is how your directory structure will look like at this point:
.
├── app
├── lib
│ ├── App.php
│ └── CliPrinter.php
├── vendor
│ ├── composer
│ └── autoload.php
├── composer.json
└── minicli
In the next part of this series, we'll refactor minicli
to use Command Controllers, moving the command logic to dedicated classes inside the application-specific namespace. See you next time!
Top comments (21)
Following this on Termux on my phone & can't get Composer to work.
So here's an autoloader for anyone who needs to do this without composer:
(Include this file into the minicli file.)
PHP as a general purpose tool? I'm intrigued.
Well, it isn't useful for building native graphical applications, for instance. Also it has some problems to manage serial port (I use Arduino boards a lot). So...about this aspect Php is far behind Python, I think
That's precisely why the post uses the therm CLI, because it's for the command line only: scripts. Surely, PHP can't solve all problems, but it is a very low barrier language that many people are already familiar with. I said all these things in the article's introduction by the way
Excellent article. Thank you!
Great tutorial Erika, it's really very helpful for my daily working. I just wanna mention that I've worked on Windows 10 and xampp, so, if you get those too, for running on a console, yo need, for example: #!C:\xampp\php\php.exe.
BTW, vim is my least favorite editor, hahahahaha!!
Nice, but what is the output of this in real life, is there is a demo or something to try?
Thanks
Well, you can do a lot with this, if you use to use Symfony or Laravel, for sure, at some point you die use the cli to generate new controller, entities/models, services, database and the tables... Your imagination is the limit
Hi! The idea is that we build together a little framework for creating PHP applications in the command line, we'll refactor a few things in the next posts of this series, but the way it is now it has two example commands that you can use as base to build something else. It's very versatile. You can find this code here: erikaheidi/minicli:0.1.0.
Following along using Termux on my phone, while camping. 😊
that's AMAZING!
I always found PHP appealing for CLI, your article demonstrate it! Very cool 😉 Looking forward the next parts!
Thank you! :-)
Great
This is really awesome.
Thank you!
Great article. Thanks!