Build a self-executing phar file is easy with Pulp, the streaming build pipeline. You know you've always wanted to build your own phar.

I've always liked phar files. They're so easy, you install them in /usr/local/bin, chmod a+x, and now you have some nice functionality available globally onyour machine. Some phar files that I just can't live without are:

Building your own

We're going to use Pulp, the streaming build pipeline. I originally copied the build-phar script from Altax here.

$pharSettings = [
	'srcRoot'    => "src/**",
	'vendorRoot' => "vendor/**",
	'buildRoot'  => "build/",
	'pharFile'   => "build/pulp.phar",
	'pharAlias'  => 'pulp-'.time().'.phar',
	'entryPoint' => 'bin/pulp',
];

Let's start by defining the settings we'll need for our own project. Here we can see that pulp is building itself.

The entryPoint setting points to an executable shell script that has the proper shebang (#!) for command line execution. The shebang will be removed from the file when inserting into the phar file and a new wrapper stub will be installed that will call this entry point. This is necessary because we cannot write one script that functions both inside and outside a phar - there's no run-time check to see if you're inside a phar.

The pharAlias is important to randomize. If we call two phar files with the same alias, the results are unpredictable. You cannot - for example - build a phar with the alias "pulp.phar" from a far with the alias "pulp.phar".


	$phar = new Phar($pharSettings['pharFile'], 
		FilesystemIterator::CURRENT_AS_FILEINFO | FilesystemIterator::KEY_AS_FILENAME,
		$pharSettings['pharAlias']
	);

	$phar->startBuffering();

	$dirRootList = [ $pharSettings['srcRoot'], $pharSettings['vendorRoot'] ];
	$cout = 0;
	$p->src($dirRootList)
		->pipe(new \Pulp\DataPipe(function($file, $pipe) use(&$count, $phar) {
			$count++;
			$pipe->emit('log', ["Adding to phar build: ".$file->getPathName()]);
			$phar->addFromString($file->getPathname(), file_get_contents($file->getPathname()));
			$pipe->push($file);
		}))

This is the main section of code, it will add the file to the phar by reading the contents from disk with "file_get_contents". Pulp always streams SplFileInfo objects so we can call getPathname() to get the file name on disk.


		->pipe(new \Pulp\DataPipe(NULL, function($pipe) use(&$count, $phar, $pharSettings) {
			$alias = $pharSettings['pharAlias'];
			$ep    = $pharSettings['entryPoint'];
			$pfile = $pharSettings['pharFile'];
	
			//pack the main bin file, remove bash shebang (#!)
			$content = file_get_contents($ep);
			$content = preg_replace('{^#!/usr/bin/env php\s*}', '', $content);
			$phar->addFromString($ep, $content);

			$phar->setDefaultStub($ep); //tell the defaults stub of our CLI entry point
			$phar->stopBuffering();

			unset($phar); //free memory
			chmod($pfile, 0755);
			$pipe->log("Generated $pfile");
			$pipe->log("File size is ".round(filesize($pfile) / 1024 / 1024, 2)." MB.");
		}));

This next pipe only works AFTER all the files are done streaming. The callback is in the second position of the DataPipe constructor, so it will only be triggered on end events - not for every file on data events.

Here, we simply finish the phar file by setting a stub and removing the unix-only shebang from the top of the main execution file. When you run a script file on linux the first line, #!/usr/bin/php, will be removed when the kernel launches the interpreter.

We can use the defaultStub, but we have to give it the name of our start file in the phar archive. I'm not scanning bin/* to build the phar, only src/* and vendor/* so I have to handle this one file seperately.

Complete example

Your phar file is built. There are a few things we can do to enhance it, like supply our own stub and remove documentation and other unnecessary files from the final build.

Here is the complete pulp task that uses a custom stub and filters out docs. Customize it to fit your project.


$p->task('build-phar', function() use($p, $pharSettings) {
	 
	$phar = new Phar($pharSettings['pharFile'], 
		FilesystemIterator::CURRENT_AS_FILEINFO | FilesystemIterator::KEY_AS_FILENAME,
		$pharSettings['pharAlias']
	);

	$phar->startBuffering();

	$dirRootList = [ $pharSettings['srcRoot'], $pharSettings['vendorRoot'] ];
	$count = 0;
	return $p->src($dirRootList)
		->pipe(new \Pulp\DataPipe(function($file, $pipe) {
			if ( 0 == strpos( $file->getFilename(), '.')) { return; }
			if ( FALSE !== strpos( $file->getFilename(), '.xml.dist')) { return; }
			if ( FALSE !== strpos( $file->getPathName(), 'README.md')) { return; }
			if ( FALSE !== strpos( $file->getPathName(), 'tests/'))    { return; }
			if ( FALSE !== strpos( $file->getPathName(), 'doc/'))      { return; }
			if ( FALSE !== strpos( $file->getPathName(), 'examples/')) { return; }
			$pipe->push($file);
		}))
		->pipe(new \Pulp\DataPipe(function($file, $pipe) use(&$count, $phar) {
			$count++;
			$pipe->emit('log', ["Adding to phar build: ".$file->getPathName()]);
			$phar->addFromString($file->getPathname(), file_get_contents($file->getPathname()));
			$pipe->push($file);
		}))
		->pipe(new \Pulp\DataPipe(NULL, function($pipe) use(&$count, $phar, $pharSettings) {
			$alias = $pharSettings['pharAlias'];
			$ep    = $pharSettings['entryPoint'];
			$pfile = $pharSettings['pharFile'];
$pharStub = <<<EOL
#!/usr/bin/env php
$pfile");
			$pipe->log("File size is ".round(filesize($pfile) / 1024 / 1024, 2)." MB.");
		}));
});

Go get pulp: Github Page

Github Project Page