WEB Advent 2011 / PHP for All the Things

PHP was originally designed for web sites and is still widely thought of only as a programming language for the Web. But with the approaches below and a variety of useful console libraries, PHP works great for command line scripts, too.

At Etsy, we have a variety of command line utilities that are all written in PHP. Having our web site and command line utilities in the same language means easier mental context switches between the two environments. It also lets us reuse web code from the command line, particularly for data access or asynchronous processing.

Arguments

Just like web pages, command line scripts usually do more interesting things when they take arguments. The command line arguments can be accessed with the $argc and $argv globals. $argc contains the number of arguments, and $argv holds an array that contains each argument; $argv[0] is always the filename of the PHP script being executed.

For simple options, using $argv directly is easy enough. Consider using getopt() or this Args class if you want to allow more complex options and flags like: ./script -n -g --near 4 --orange rectangle

Command line scripts often have different memory requirements than web requests. If you find your script running out of memory, don’t hesitate to explicitly increase the memory limit: ini_set('memory_limit', '256M');

`Backticks`

The first handy scripting tool is the execution operator known as backticks (``). As with shell scripting, backticks will execute a given command and return whatever was written to standard output.

One place where we make extensive use of backticks is in our builda script, which we use to compile CSS and JavaScript as part of our deployment process. Rather than using PHP functions to navigate files and directories, we get everything with one shell command, as in the following example.

$files = explode("\n", trim(`find $js_dir -type f`));

Note that the backticks expand variables like $js_dir just as with double quoted strings. Remember that any input from strangers should be escaped with escapeshellarg() or escapeshellcmd() before sending it to the shell.

Builda was originally written in “not-PHP” but stagnated, because not everyone at Etsy was comfortable enough with that language to jump in and scratch their own itch. Since being rewritten six months ago in PHP, which all engineers know from working with the web code, seven different engineers have contributed to making the tool better.

Standard Error

print and echo are easy ways to write to stdout. But, sometimes, command line programs want to write to standard error. For quick writes to stderr, try the following:

file_put_contents('php://stderr', "error message\n");

Alternatively, you can open stderr like a file and write to it with the standard file functions.

$err = fopen('php://stderr', 'w');
fwrite($err, "another error message\n");

Pipes

In cases where you require more interactivity with child processes, proc_open() is often the right tool. It launches a process and opens pipes so that the two processes can communicate with each other.

As an example for how you might use proc_open, consider the case of loading data into sharded databases. Originally, we had only three shards, and it was easy to manually run one script for each shard. As the number of shards grew, this became more time consuming, so we wrote another script for loading data to all the shards at once.

The new script is actually two scripts. First, the child process, shard_load, is very simple. It opens a connection to a single shard, reads input from stdin and inserts rows into the database. By breaking the job into two scripts, each task is kept simple and can be tested independently.

The main script, load_all_shards, is equally simple. It just reads a stream of input from stdin, opens a child process for each shard, and pipes rows of input to the appropriate child process. The pipes opened by proc_open can be read from and written to with functions like fgets() and fwrite(), just like any other file handle.

do {
    $line = $fgets($stdin);
    list($shard_id,$row) = split("\t", $line, 2);
    fwrite($shards[$shard_id]->stdin, $row);
    $line = fgets($handle);
} while ($line);

All that needs to be done is to connect them with proc_open:

function openShard($shard, $tbl, $fields) {
    $desc = array(
            0 => array('pipe', 'r'),
            1 => array('pipe', 'w'),
        );
    $pipes = array();
    $cmd = "./shard_load $shard $tbl $fields";
    $proc = proc_open($cmd, $desc, $pipes);
    return new ShardProc($shard, $proc, $pipes);
}

PHP Ubiquity

With easy access to the shell and interprocess communication with pipes, there’s no reason not to take advantage of existing engineering experience (and web code) and use PHP for utility and offline scripts as well.

Developer Gift

Have you ever looked at the inside of a hard drive? It’s round and very shiny and perfect for a clock face! Any of these clocks would make a great gift for your friendly neighborhood software developer.

Other posts