Creating PHP Phar archives for command line programs

On Linux / Mac / Cygwin / *nix flavor you like.

I had a program to write that would be run via a cron job that reads from a database, creates a report in an excel file format, and then emails it.

I wanted a fairly self contained executable that has most of what it needs to run packaged up as much as possible. Similar to Composer. So how does that work?

Info on the Phar format

At a first look on php.net, the information on creating Phar files seems a little on the sparse side. However there is enough there, a long with a few bits and pieces found on StackOverflow.com and the Composer source on GitHub.

So what is a Phar file? Basically it is an archive with a small bootstrap which starts the rest. You can bundle up all the files you need, keep the directory structure and use it almost the exact same way as a library of code, or even a whole site.

A very simple one can be created like this:

<?php
$phar = new Phar( 'simple.phar' );
$phar['a.php'] = '<?php echo "Hola Mundo\n"; ';
$stub = $phar->createDefaultStub( 'a.php' );
$phar->setStub( $stub );

The first line should be self explanatory. There are several ways of getting files into a Phar archive - you can give a list of files, or set them like above. Here the createDefaultStub function is used to make start up code that runs a.php, and then we set that as the Phar file's stub.

Now you should have a file called simple.phar, that you can run with PHP on the command line that will print the traditional 'first program' text albeit in Español. That's just to make it slightly more interesting (for me anyhow - I'm learning Spanish).

If you echo the $stub, you will see that it is not trivial, however it does a lot of things you may not need, and a couple of handy little tricks that I will get to in a moment.

One thing the default stub does not do, is have a sha-bang line. This means you can not mark the phar file as executable and run it in a terminal like:

$> ./simple.phar

The command shell doesn't know what to do with it. We can make our own stub that does, and as it turns out it need only be a few lines long.

Replace the $stub = ....... line with:

$pharfile = 'simple.phar';
$startfile = 'a.php';
$stub = <<< "EOT"
#!/usr/bin/env php
Phar::mapPhar( '$pharfile' );
set_include_path( 'phar://$pharfile' . PATH_SEPARATOR . get_include_path() );
require( '$startfile' );
__HALT_COMPILER();
EOT;

I arrived at these 5 lines of stub code after much googling, experimenting, and coffee. Later I had the bright idea of printing out the default stub from the createDefaultStub function, and then realised I should have done that at the start. [Insert your favorite expression of annoyance here].

Anyhow - starting at the top. The first line is the she-bang - telling bash, dash, ksh, zsh, or any other of the myriad of shells how to execute this file by running it through PHP.

The next line (mapPhar) sets the alias for specifying files in the archive, and the line after that sets the include path so that you don't have to worry about it.

Without setting the path to include the phar file, any files you want to include or require would require you to add something like 'phar://myphar.phar/' to the beginning to each file name. Also if you want to be able to run the code as a Phar file or normally as a bunch of files, you have to figure out which is happening and then add it or not. Annoying slightly. So with setting the path like this, we don't have to worry about it at all. Maravilloso!

You can also add Phar::interceptFileFuncs which will cause files to be loaded from the Phar archive when using the likes of file_get_contents, and the file name is relative.

After the require line that loads and runs our actual program you will see __HALT_COMPILER(); Every stub needs this, and the Phar::setStub method will error if you don't have it. I think the stub is simply put at the start of the archive - so this stops PHP from gallivanting off into the rest of the archive for wreck and ruin - getting drunk on the wrong files and generally scaring the locals.

A bigger example with Composer.

In my experiments I wanted to confirm I could do a couple of things with a Phar.

So first I made a tiny app with a config file, and used a library managed by composer.

$> mkdir pharout
$> cd pharout
$> composer init

Mostly accepting the defaults that composer asks - except 'proprietary' for license to shut 'composer validate' up and requiring the package league/climate to use in testing the autoloader from inside a Phar.

Now to tell composer to take the shopping list and go get the groceries.

$> composer install

Then I made a few directories, one for the config file, one for the actual program, and finally one for the Phar build script.

$> mkdir {config,src,build}

config/config.php

<?php
return [
    'greeting' => '¡Buen día!',
];

src/program.php

<?php

$config = require( 'config/config.php' );
require( 'vendor/autoload.php' );

$text = file_get_contents( 'test.txt' );

$climate = new League\CLImate\CLImate;

$climate->whisper( $config['greeting'] );
$climate->info( $text );
And finally a file to remain outside the Phar, to test file_get_contents.

test.txt

Esto es una prueba
At this point, running:
$> php src/program.php

Should output a greeting and the contents of test.txt in nice colours.

Build it and it will run

Now for a script to package this all together. We could throw every file into the archive - but that would waste disk space and slow things down a bit. So I grabbed a list of the files, pruned out what I did not need to be packaged and went from there.

#!/usr/bin/env php
<?php
$pharfile = 'simple.phar';
$startfile = 'src/program.php';

$basedir = dirname(dirname( __FILE__ ));

function prunePrefix( $files, $prefix ) {
    $newlist = array();
    $prefix_len = strlen( $prefix );
    foreach( $files as $file => $data ) {
        if( substr( $file, 0, $prefix_len ) !== $prefix ) {
            $newlist[$file] = $data;
        }
    }
    return $newlist;
}
function pruneSuffix( $files, $suffix ) {
    $newlist = array();
    $suffix_len = strlen( $suffix );
    foreach( $files as $file => $data ) {
        if( substr( $file, -$suffix_len ) !== $suffix ) {
            $newlist[$file] = $data;
        }
    }
    return $newlist;
}
$phar = new Phar($pharfile);
$fileIter = new RecursiveIteratorIterator(
    new RecursiveDirectoryIterator( $basedir, FileSystemIterator::SKIP_DOTS ) );
$files = iterator_to_array( $fileIter );
$files = prunePrefix( $files, $basedir . '/.git' );
$files = prunePrefix( $files, $basedir . '/build' );
$files = prunePrefix( $files, $basedir . '/composer' );
$files = prunePrefix( $files, $basedir . '/test.txt' );
$files = pruneSuffix( $files, 'swp' );
$files = pruneSuffix( $files, '~' );
foreach( $files as $file ) {
    echo "Will add $file ...\n";
}
$phar->buildFromIterator( new ArrayIterator($files), $basedir );

$stub = <<<"EOT"
#!/usr/bin/env php
<?php
Phar::mapPhar( '$pharfile' );
set_include_path( 'phar://$pharfile' . PATH_SEPARATOR . get_include_path() );
require('$startfile');
__HALT_COMPILER();
EOT;

$phar->setStub( $stub );
system( "chmod +x $pharfile" );

This is designed to be run from the build directory.

The two functions prunePrefix and pruneSuffix iterate through an array, and filter out anything that either starts with or ends with the given string respectively.

Interestingly the RecursiveDirectoryIterator needed the SKIP_DOTS flag, in order to not recurse UP the directory tree when it hit the .. directory. That was unexpected!

From the file list I remove the git info, the build directory, the composer files, and the text file I'm using to make sure that it can be accessed outside of the Phar file.

Once the files and the stub are added, chmod is called to mark the Phar file as executable.

And there we have it!

$> ./simple.phar

Should give you the same results as before. One self contained (except for php itself) program you can copy to any machine with the PHP command line interface installed.

The above code grew into an addition to my little library of PHP gadgets. You can see it here on GitHub

Notes:

Back to Top