Bootstrapping a CLI PHP application in Vanilla PHP

Erika Heidi - Sep 20 '19 - - Dev Community

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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";
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Now you can run the application with:

./minicli
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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/"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

After saving and closing the file, run the following command to set up autoload files:

composer dump-autoload
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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";
    }
}
Enter fullscreen mode Exit fullscreen mode

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);

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

And you should see the following output:

Hello your-name!!!
Enter fullscreen mode Exit fullscreen mode

Now, if you don't pass any additional parameters to the script, it should print:

Hello World!!!
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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!!!");
    }
}
Enter fullscreen mode Exit fullscreen mode

Now run the application again with:

./minicli your_name
Enter fullscreen mode Exit fullscreen mode

You should get output like this (with newlines surrounding the message):


Hello your_name!!!


Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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);

Enter fullscreen mode Exit fullscreen mode

Now your application has two working commands: help and hello. To test it out, run:

./minicli help
Enter fullscreen mode Exit fullscreen mode

This will print:


usage: minicli hello [ your-name ]


Enter fullscreen mode Exit fullscreen mode

Now test the hello command with:

./minicli hello your_name
Enter fullscreen mode Exit fullscreen mode

Hello your_name!!!


Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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!

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Terabox Video Player