The Workshop: Easy CLI PHP with Symfony Console 5 - php[architect] Magazine April 2020

Joe • January 10, 2021

learning phparch writing php

Warning:

This post content may not be current, please double check the official documentation as needed.

This post may also be in an unedited form with grammatical or spelling mistakes, purchase the April 2020 issue from http://phparch.com for the professionally edited version.

EZ CLI PHP With Symfony Console

Writing console commands with PHP is something I have always taken for granted. I started my career having learned Linux and command line server configuration and was very comfortable writing small applications with BASH scripts, Python, and even dabbling in C and Microsoft Qbasic. These commands copied files to a backup location, process batched data records in CSV, XML, and various other formats. By the time I discovered PHP, the community had just seen early builds of PHP 5, and the entire ecosystem was in for a shock. Once comfortable with PHP, I began writing small CLI commands in PHP instead of Python (or BASH). It was quite liberating to flex the same language for both web and command line applications.

PHP 4.3 released on December 27th, 2002; this was the first release that supported the command line interface. You may have seen the term SAPI or PHP SAPI; these acronyms stand for Server Application Programming Interface, where the CLI SAPI refers to the Command-Line Application Server Programming Interface. The CGI SAPI is the Common Gateway Interface Application Server Programming Interface, or other implementations connecting PHP to a web server instance such as Apache modphp, or PHP-FPM. These interfaces are how the PHP interpreter passes user input, sets up server and environment values for our script, and executes our PHP code during run time.

The most important thing to remember about writing PHP command line applications is the PHP configuration is completely separate from the configuration you’re currently using with a web server. You’ll also find the configuration files in a cli folder instead of fpm or other SAPI folder. For example, a typical PHP installation using Ondřej Surý’s repository on Ubuntu 18.04 has the /etc/php folder structure shown in Figure 1.

Figure 1

We can see our fpm, cli, cgi, and other folders where those SAPIs store their configuration files. This means FPM does not use the same configuration as CLI. Each SAPI folder contains its own php.ini and conf.d directory for further configuration files, which may be needed.

There are some important differences between the CLI SAPI and the others which are tightly coupled to web servers. Most notably are the absence of headers in the output (because there is no request!), and error message output is in plain text instead of HTML.

The most basic PHP command line script is:

#!/usr/bin/env php
<?php
echo “Have you read the latest issue of php[architect]? https://phparch.com/”;

Don’t worry if #!/usr/local/bin/php looks odd; this is called a Shebang, which tells our operating system to interpret it as an executable application. The path is to my local PHP binary (/usr/local/bin/php), which may not match your location, so we should use /usr/bin/env php, which tells our operating system to search for PHP in the system path. This also allows for instances where PHP may be installed to /usr/local/bin or a location already within the system PATH environment variable. Make sure you point this line to your PHP; you can typically find this path by running which php in the command line.

Running our command outputs:

$ php basic.php                                                                                                                                                                          
Have you read the latest issue of php[architect]? https://phparch.com/
$

We could also make our basic.php script executable and run via ./basic.php. For this to work, our shebang must point to a locally installed PHP executable; otherwise, you get an error message.

$ chmod +x basic.php                                                                                                                                                                          
$ ./basic.php
Have you read the latest issue of php[architect]? https://phparch.com/

Why Console Commands

The primary reason you should be writing command line applications in PHP is because you’re already comfortable with PHP, or you’re getting there! I enjoy PHP on the command line because I find BASH script syntax difficult to read. PHP can be written with a high degree of readability, which makes the human element of maintaining and understanding our code much more efficient. We can eliminate the context switching PHP to BASH and vice versa. We can also reuse code and classes from our web-based applications giving us access to the same resources such as external APIs, databases, helper classes, etc.

Some of my early PHP command line applications were built to promote and manage users to various roles giving them administrator ability in the web application. This resulted in the requirement of having command line access to the web host to manipulate user groups and permissions. Looking back at this practice, it seems silly. I think I was trying to avoid building all of the user interface for doing this in the primary application.

Processing large data sets were the next challenge I solved via command line PHP applications. Running PHP code on a web server, we’re limited to various timeouts. Web requests are designed to be as fast as possible. When you need to process thousands or millions of rows of a database, you often find your application running into these timeouts. Command line applications do not have these same short timeouts, so they continue to run until you stop the process or it completes. These long-running PHP applications are well suited for background tasks, or cron jobs where you may be updating a document store with fresh or newer data from a data store, or you need to run statistical analysis on a complete day or month worth of data. By removing web server request timeouts, the next limitation we’re likely to run against would be RAM on the system running our CLI application. Remember to include the memory needs of these apps in addition to your web server memory needs.

Debugging Console Commands

We can debug command line PHP applications just as easily as we can via HTTP remote debugging. Make sure you have installed Xdebug, and we can tell PhpStorm how we want to debug our script by opening the “Run” menu and clicking on “Edit Configurations.”

Figure 2

Make sure you select your interpreter; I’m using PHP 7.4.3 on macOS via Homebrew, and I’ve installed Xdebug via PECL as in the documentation. Once configured, we can go back to the “Run” menu and select “Debug.” Xdebug stops us at our first breakpoint located on our echo() line in basic.php, as shown in Figure 3.

Figure 3

We don’t have access to $_GET or $_REQUEST because we’re not running via a web server SAPI, however, PHP still gives us our environment values in $_SERVER. We also have access to any arguments passed to the script via the argv array, but there’s a much cleaner way to access user input with Symfony’s Console Component. While many developers may prefer to “print and die” or “dump and die” style debugging, step debugging has improved my ability to find and locate bugs faster, especially in the context of large full-stack frameworks.

Symfony Console Component

The Symfony Console Component is a PHP package for building modern command line applications in PHP. Most command line PHP applications such as Laravel’s Artisan command, Phing---a PHP build tool, and Doctrine’s migrations tool are built on the symfony/console component. Laravel also supports creating your own command line artisan commands, which further extend and add syntactic sugar to the Symfony console package.

Let’s install the component package into our current working folder with Composer (see Figure 4).

Figure 4

Before we dive into writing commands, we need to set up our console application and scaffold out some folders of our project. A common convention with command line applications is locating the primary entry point into the application at bin/application (in web applications, this is your index.php script). The bin/ folder is for binary or executable files as src/ represent source or source code. The application file has no file extension but does have a shebang line to instruct our operating system we intend to execute the code via PHP.

bin/application:

#!/usr/bin/env php
<?php
require __DIR__ . ‘/../vendor/autoload.php’;

use Symfony\Component\Console\Application;

$application = new Application();

$application->run();

Remember to set your application executable via chmod +x bin/application.

Running our application shows the default output because we have not yet created any of our custom commands. Notice how Symfony already sets up common command line switches for us.

$ ./bin/application
Console Tool

Usage:
  command [options] [arguments]

Options:
  -h, --help            Display this help message
  -q, --quiet           Do not output any message
  -V, --version         Display this application version
      --ansi            Force ANSI output
      --no-ansi         Disable ANSI output
  -n, --no-interaction  Do not ask any interactive question
  -v|vv|vvv, --verbose  Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug

Available commands:
  help  Displays help for a command
  list  Lists commands

Note: You can use ./bin/application or php bin/application. The syntax on the command line is the same, executing our PHP file application via our command line PHP SAPI.

We need to update our composer.json file to tell Composer how we plan to autoload our classes. We use a src/ folder and a generic \App\ namespace for our console commands by adding a psr-4 section:

{
  “require”: {
    “symfony/console”: “^5.0”
  },
  “autoload”: {
    “psr-4”: {
      “App\\”: “src”
    }
  }
}

To highlight the difference between outputting information to a console instead of a web browser, we create a new file at src/BasicOutput.php and demonstrate how to send output from our command.

There are three basic sections of each command we are building, the command name, configuration, and execution. The command name is a protected static variable on our BasicOutput command class: protected static $defaultName = ‘basic’; This syntax tells the Symfony Component we want to run ./bin/application basic to run our BasicOutput class. The configuration method of our class contains the description of our command, which is what is displayed in our bin/application and is run without any arguments in the Available commands output. We also have helper text which is displayed when running ./bin/application help basic so our users have helpful context when running our code.

src/BasicOutput.php:

<?php
namespace App\Command;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class BasicOutput extends Command
{
    protected static $defaultName = ‘basic’;

    protected function configure()
    {
        $this
            ->setDescription(‘Demonstrates Basic Output’)
            ->setHelp(‘Outputs line to the console’);
    }

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $output->writeln(‘<info>This line will be green</info>’);
        $output->writeln(‘<error>Something went wrong!</error>’);
        $output->writeln(‘<href=https://phparch.com>Clickable Link!</>’);
        return 0;
    }
}

We use $this-> writeln() because we want a newline character at the end. This could also be this->write() if you want to concatenate strings on the same output line.

Before we run our command we need to tell our bin/application script about it and add() it to our application:

#!/usr/bin/env php
<?php
require __DIR__ . ‘/../vendor/autoload.php’;

use App\Command\BasicOutput;
use Symfony\Component\Console\Application;

$application = new Application();
$application->add(new BasicOutput());
$application->run();

We see our description and help from the output of help basic:

$ ./bin/application help basic
Description:
  Demonstrates Basic Output

Usage:
  basic

Options:
  -h, --help            Display this help message
  -q, --quiet           Do not output any message
  -V, --version         Display this application version
      --ansi            Force ANSI output
      --no-ansi         Disable ANSI output
  -n, --no-interaction  Do not ask any interactive question
  -v|vv|vvv, --verbose  Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug

Help:
  Outputs line to the console

Running our BasicOutput class via ./bin/application basic gives us the output shown in Figure 5.

Figure 5

If we hover over the “Clickable Link!” text while holding the command (control) key on the keyboard, it allows us to click and follow the link to phparch.com (see Figure 6).

Figure 6

If you’re using the Laravel framework, you’re likely used to accomplishing the same functionality with: $this->info() or $this->error(). If we rebuild our BasicOutput class as a Laravel Artisan command, we can see the difference between pure Symfony Console and how Laravel implements the package as Artisan commands:

<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;

class Basic extends Command
{
    protected $signature = ‘basic’;
    protected $description = ‘Demonstrates Basic Output’;
    public function __construct()
    {
        parent::__construct();
    }
    public function handle()
    {
        $this->info(‘This line will be green’);
        $this->error(‘Something went wrong’);
        $this->line(‘<href=https://phparch.com>Clickable Link!</>’);
    }
}

Running our artisan command gives us an output similar to our previous example:

Figure 7

While Laravel doesn’t have its own link() method, you can use the same syntax supported by Symfony to accomplish the goal of clickable links in the console output.

Arguments and Options

We have two options available for taking input from the user: arguments and options. Arguments are pieces of information your command requires to accomplish a task. Think of the commands we’ve already seen. When we run ./bin/application basic, we’re already working with arguments; basic is the first argument passed to bin/application. An example of an option would be --help in the usage of ./bin/application --help so we can see a list of all the commands and their help information. The --help string is optional---the application runs even if you don’t pass in the help option. To help keep these clear, I always think of arguments as required and options as optional. One step further, formalizing the difference would be to keep our application behavior modifications to options, and input(s) as arguments. Arguments must always be passed into the command line in the same order you define them in the configure() method of the command class. Options can be passed to the command line in any order.

To better visualize the differences between arguments and options, let’s build a new command to greet the user based on arguments, which is required for our Greetings.php command to run.

<?php

namespace App\Command;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class Greeting extends Command
{
    protected static $defaultName = ‘greeting’;

    protected function configure()
    {
        $this->setDescription(‘Greeting Command’)->setHelp(‘Usage: greeting <name>’);
        $this->addArgument(
            ‘name’,
            InputArgument::REQUIRED,
            ‘What is your name?’
        );
    }

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $output->writeln(‘Hello, ‘.$input->getArgument(‘name’));
        return 0;
    }
}

Returning 0: The standard return code for an application where everything processed without errors or exceptions returns 0. We assume our commands always work, so it’s important to return 0; at the end of our execute() method, and if we catch an exception, we should return 1. You can read more about exit codes and their particular meanings from The Linux Documentation Project.

If we run our Greeting.php command without any arguments, we see an error (Figure 8) because we have specified the mode of the argument name is InputArgument::*REQUIRED*.

Figure 8

Running our command again with an argument of Joe outputs our greeting as expected (see Figure 9).

Figure 9

The most common option I build into nearly every command is a —test option to perform a dry run or execution of our command without any write operations. This allows writing the command in a very flexible way in an attempt to prevent unexpected outcomes. If we wanted to write a command which fetches database rows and changes the first character of the name to an uppercase letter we could pass the name argument to the ucfirst() method:

protected function execute(InputInterface $input, OutputInterface $output)
{
    $users = $this->db->getUsers();
    foreach ($users as $user)
    {
        $user->name = ucfirst($user->name);
        $user->save();
    }
    return 0;
}

We can then add a test argument to this command and only write the updated user to the database if the test option is missing, or false:

if($input->getOption(‘test’) === false)
{
    $user->save();
}

When we run our command with ./bin/application uppername the command runs and saves our changes to the user’s name because if we don’t specify an option, the value is false. If we want to test-run our command use:

./bin/application uppername --test` 

The user record is not saved because $input->getOption(‘test’) is true .

Verbosity

Sometimes it can be difficult to see what exactly a task is doing as it’s sitting there on the command line happily chewing through the thousands of database records you may be processing. We can build in spot checks for our commands and only show the output depending on the verbosity of the command by adding -v, -vv, or -vvv going from low verbosity to most verbose. In PHP, this translates to -v is OutputInterface::VERBOSITY_VERBOSE, -vv is OutputInterface::VERBOSITY_VERY_VERBOSE, and -vvv is OutputInterface::VERBOSITY_DEBUG.

We can refactor our uppername script to account for different verbosity levels:

protected function execute(InputInterface $input, OutputInterface $output)
{
    if ($output->isDebug()) { # -vvv
        $output->writeln(‘Running execute()’);
    }

    if ($output->isVeryVerbose()) { # -vv
        $output->writeln(‘Getting All Users from DB’);
    }

    $users = $this->db->getUsers();

    foreach ($users as $user)
    {
        if ($output->isVerbose()) { # -v
            $output->writeln(‘Processing ‘ .$user->name);
        }
        $user->name = ucfirst($user->name);
        $user->save();
        if ($output->isVeryVerbose()) { # -vv
            $output->writeln(‘Saved ‘ .$user->name);
        }
    }

    if ($output->isDebug()) { # -vvv
        $output->writeln(‘Command complete!’);
    }

    return 0;
}

Assuming we have two users in our database named “Joe” and “Paul,” when we run the command with no added verbosity, we see no output as intended, but when we start increasing verbosity, we see more details about what our command is doing as shown in Figure 10.

Figure 10

Depending on the complexity of my application, I may use one or more of the verbosity options especially, if the command is for general use, and I know I may not be the only person running the command. Think of verbose output as helper context for other developers on your team who may also run commands.

Advanced Commands

One of the open source projects I maintain is Laravel Homestead, a virtual machine-based development environment for PHP. The underlying concept is you download a virtual machine and run it to do your local PHP development. Thes downloads are called Vagrant Boxes. Vagrant is the program which utilizes a virtual machine provider such as Virtualbox or VMware. You can discover public Vagrant boxes from Vagrant Cloud. You can also see how many downloads each box has. If we look at Homestead, we can see 13,895,010 million downloads.

What we don’t have visible are the downloads over time. I wrote a Laravel Artisan command to check Vagrant Cloud’s public API for the number of downloads of Homestead, and I saved that in my application’s database. The entire process revolves around one Arisan command, which we can review to highlight differences from using Symfony’s Console package without the extra syntactic sugar from Laravel.

Commandeering the Command Line

Now you’re ready to venture forth and build commands to process large data sets, replace hard to read bash scripts, or even build a robust cron job library of PHP tasks. Keep in mind to add the output to help the user understand what the command is doing even if you hide some of the output behind -v, -vv, vvv. Symfony Console is the heart of what makes command line PHP so approachable and comfortable feeling for PHP developers. To dive even further into building command line applications, check out the Symfony Console documentation.

Have fun storming the command line castle!

Warning:

This post content may not be current, please double check the official documentation as needed.

This post may also be in an unedited form with grammatical or spelling mistakes, purchase the April 2020 issue from http://phparch.com for the professionally edited version.