Distributing a PHP CLI app with ease

Something I love with PHP is how easily you can build a CLI application. Some libraries like the Symfony Console Component has greatly improved this process. Also, the ability to package a whole application into a single PHAR container make the distribution and usage even easier.

Such as composer or php-cs-fixer, let's see how to quickly build a PHAR file for your application with self-update capabilities.

The Symfony Console

First we need to create our CLI app. Let's call it Cliph. For the example, we will build a typical Symfony console app with one « hello world » command. Basically this results in only 2 files. The first one is the command in the src folder:

<?php

namespace Cliph\Command;

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

class HelloCommand extends Command
{
    protected function configure()
    {
        $this
            ->setName('hello')
            ->setDescription('Say hello')
        ;
    }

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $output->writeln('Hello World');
    }
}

And the second is the launcher in bin/cliph:

#!/usr/bin/env php
<?php

require __DIR__.'/../vendor/autoload.php';

use Cliph\Command;
use Symfony\Component\Console\Application;

$application = new Application('Cliph', '0.1-dev');
$application->add(new Command\HelloCommand());
$application->run();

Our application is now ready to run:

$ ./bin/cliph --version
Cliph version 0.1-dev

$ ./bin/cliph hello
Hello World

The Box Project

The next step is to package all the sources into one single executable PHAR file.

If you tried to build a PHAR file for your own CLI app, you may already have read the Compiler.php script from Composer. This script is launched via ./bin/compile and produces the composer.phar file.

Now we want the same thing for our app, but without writing any line of PHP. That would be redundant for each project. This is where the Box Project comes in. The idea behing this tool is to replace the usual compile script with a simple JSON file.

The JSON file contains the location of the PHP files to combine, and the executable to run. This is typically what we need for our app:

{
    "chmod": "0755",
    "directories": [
        "src"
    ],
    "files": [
        "LICENSE"
    ],
    "finder": [
        {
            "name": "*.php",
            "exclude": ["Tests"],
            "in": "vendor"
        }
    ],
    "git-version": "package_version",
    "main": "bin/cliph",
    "output": "cliph.phar",
    "stub": true
}

As you notice, you can specify the source files using three different directives:

  • directories: to import a whole directory, typically the source folder
  • files: ideal to import in single file — here the license
  • finder: if you want more advanced filters, this use the Symfony Finder component. This is very handy to exclude some useless files like the tests.

Then come the options to customise the PHAR file. The output specifies the name of the script; the main is the launcher script; we use chmod to make it executable by default; and of course the stub must be enabled for a CLI app.

Finally you may wonder what the git-version stands for. The value of this parameter is used to replace any string in the source files. Here, any string @package_version@ is replaced by a version number based on the git repository. To take advantage of this, we place it as the version parameter on our main launcher bin/cliph:

<?php
//...
$application = new Application('Cliph', [email protected]_version@');

Time to see the result. Install Box if you did not already, then run:

$ box build
Building...
$ ./cliph.phar --version
Cliph version ad2fc07

Simple, isn't it? It's even better if use git tags to have a proper version number:

$ git tag 1.0.0
$ box build
Building...
$ ./cliph.phar --version
Cliph version 1.0.0

Box will automatically use the git tag number if any, or fallback to the commit hash. If a tag was created on a previous commit, the version may look somthing like 1.0.0-1-gae87139 (ie. use of tag + commit hash).

Auto updates

Edit Jan. 2016: I now recommend you to also read this great blog post "Secure PHAR Automation" by @mwop which makes use of the padraic/phar-updater library and GitHub pages to automate and sign your PHAR file.

Now that we have our PHAR, it would be awesome to add a command to auto update the app. Think of something like composer self-update, but as usual with minimal code.

The library which will help us this time is php-phar-update. This library handles the whole update process. To make it work, we need to instanciate a Manager object from a Manifest then run the update method. The Manifest contains all the possible updates. Ideally it reads a remote JSON file which contains the list of all available versions. To host the manifest.json file, we will use GitHub pages.

Here is what look like our update command:

<?php

namespace Cliph\Command;

use Herrera\Phar\Update\Manager;
use Herrera\Phar\Update\Manifest;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class UpdateCommand extends Command
{
    const MANIFEST_FILE = 'http://mattketmo.github.io/cliph/manifest.json';

    protected function configure()
    {
        $this
            ->setName('update')
            ->setDescription('Updates cliph.phar to the latest version')
        ;
    }

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $manager = new Manager(Manifest::loadFile(self::MANIFEST_FILE));
        $manager->update($this->getApplication()->getVersion(), true);
    }
}

Only two lines of code. Awesome, right? The first parameter of the update method is the current version number (in order to compare with available versions), while the second allows to download major version. Here is the full diff of that new update feature.

Let's tag our new functionnality, and re-build the PHAR:

$ git tag 1.1.0

$ box build
Building...

$ ./cliph.phar
Cliph version 1.1.0

Available commands:
  hello   Say hello
  help    Displays help for a command
  list    Lists commands
  update  Updates cliph.phar to the latest version

Now we just have to put a manifest.json plus the build PHAR into the gh-pages branch and deploy it to GitHub. Note that I used the openssl sha1 <file> command to get the hash of the PHAR file.

$ cat manifest.json
[
    {
        "name": "cliph.phar",
        "sha1": "fbcded58df0ea838c17d56a5e3cdace56127d538",
        "url": "http://mattketmo.github.io/cliph/downloads/my-cli-1.1.0.phar",
        "version": "1.1.0"
    }
]
$ tree
.
├── downloads
│   └── cliph-1.1.0.phar
└── manifest.json

Yeah, we've just acheived the update command step. To try it, let's upgrade Cliph version to 1.1.1 with that new commit, and the corresponding manifest.json. Then, from a 1.1.0 vesion of our app, run the update command:

$ ./cliph.phar --version
Cliph version 1.1.0

$ ./cliph.phar update
Updated to the latest version

$ ./cliph.phar --version
Cliph version 1.1.1

Sign your application

Now as a bonus part, we would like to sign our application. PHAR files can be signed with a set of public & private keys — using for instance OpenSSL. The archive is built with the private key, and to run the file the public key must be put next to it:

« If the phar archive is saved as /path/to/my.phar, the public key must be saved as /path/to/my.phar.pubkey, or phar will be unable to verify the signature. » — php.net

To generate an OpenSSL private key, you have choice to directly use the openssl command, or the box key:create. Then, to sign the PHAR the only thing to do is to add the key option into your box.json file:

{
    "chmod": "0755",
    "...": "...",
    "stub": true,
    "key": "private.key"
}

Then if you run a box build you will see a new file cliph.phar.pubkey. If you try to move or remove this file, you will get a PharException.

Note that if you have set a passphrase for your private key, you must report it in the box.json ("key-pass": "mypass"). But if you do so, please don't version this file. Instead use a box.json.dist and overwrite it in your local environment. However I must admit I'm not really fan of that solution. In my opinion, an option in the box command would be clever (eg. box build --key private.key -p).

Automation

Last thing we want to do is to automate the whole process. For the example, I wrote a stupid bash script which handle all the actions described in this post (git tag, box build, copy to gh-pages, update manifest). It works with jsawk to manipulate the JSON file.

Now I just have to run:

$ ./bump-version.sh 1.3.0

And a new PHAR file is automatically created with the given version number, and pushed to the gh-pages.

Thus we've finally learned in a few steps how to make our CLI app easily distributable. The Box project is really something you have to look at if you are building PHAR files. And of course, you can adapt the "self-update" part to your need. Big thanks to Kevin Herrera for those tools, that's very helpful.