DEV Community

Cover image for Bootstrapping a CLI PHP application in Vanilla PHP
Erika Heidi
Erika Heidi

Posted on • Updated on

Bootstrapping a CLI PHP application in Vanilla PHP

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!

Top comments (21)

Collapse
 
syntaxseed profile image
SyntaxSeed (Sherri W) • Edited

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
/**
 * Example project-specific auto-loading implementation.
 *
 * After registering this autoload function with SPL, the namespaces on the left of the loaders array will load classes found in the paths on the right.
 *
 * @param string $class The fully-qualified class name.
 * @return void
 */

 spl_autoload_register(function ($class) {
    $loaders = [
        'Minicli\\' => '/lib/',
        'App\\' => '/app/'
    ];

foreach($loaders as $prefix => $base_dir){
    $len = strlen($prefix);
    if(strncmp($prefix, $class,$len) !== 0){
        continue;
    }
    $relative_class = substr($class, $len);

    $file = __DIR__ . $base_dir .
       str_replace('\\', '/',
         $relative_class) .
       '.php';

    if (file_exists($file)) {
        require $file;
        return;
    }
} //end foreach
});
Enter fullscreen mode Exit fullscreen mode
Collapse
 
rescatado182 profile image
Diego Pinzón

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!!

Collapse
 
nueaf profile image
Michael Als

Excellent article. Thank you!

Collapse
 
_ezell_ profile image
Ezell Frazier

PHP as a general purpose tool? I'm intrigued.

Collapse
 
q2dg profile image
Osqui LittleRiver

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

Collapse
 
erikaheidi profile image
Erika Heidi

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

Collapse
 
syntaxseed profile image
SyntaxSeed (Sherri W)

Following along using Termux on my phone, while camping. 😊

Collapse
 
erikaheidi profile image
Erika Heidi

that's AMAZING!

Collapse
 
jodyshop profile image
Waleed Barakat

Nice, but what is the output of this in real life, is there is a demo or something to try?

Thanks

Collapse
 
riccycastro profile image
Ricardo Castro

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

Collapse
 
erikaheidi profile image
Erika Heidi

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.

Collapse
 
anwar_nairi profile image
Anwar

I always found PHP appealing for CLI, your article demonstrate it! Very cool 😉 Looking forward the next parts!

Collapse
 
erikaheidi profile image
Erika Heidi

Thank you! :-)

Collapse
 
tabraizbukhari profile image
Tabraizbukhari

Great

Collapse
 
doctor_brown profile image
James Miranda

Great tutorial! Thank you for it.

Collapse
 
silverman42 profile image
Sylvester Nkeze

This is really awesome.

Collapse
 
erikaheidi profile image
Erika Heidi

Thank you!

Collapse
 
kod7dev profile image
KodSeven.Dev

p-e-r-f-e-c-t

Collapse
 
erikaheidi profile image
Erika Heidi

Thank you 🤗

Collapse
 
mateuschmitz profile image
Mateus Schmitz

Great article. Thanks!

Collapse
 
coopz profile image
Leonard Cooper

except that we are not working with a front end anymore - yay!
Erica Heidi circa 2019

Bravo! Words so sweet, bringing a tear of pure joy down my cheek. BRAVO