Creating PHAR archives with external config files
PHAR is a method for taking a PHP project and creating a single artifact from that project. This is useful to me for making small scripts that I'd ordinarily use Shell, Python or Perl for and making a PHP script that fills the bill, while still using contributed packages.
For this example, I'm just trying to create a simple utility the mimics the 'logger' command in UNIX, but uses monolog rather than sending it's output to syslog. This is useful as an example to centralize your PHP logging with Monolog, and how you'd go about adding that functionality into a command line script/app.
Additionally, I wanted my resulting PHAR to execute without having the use php myApp.phar
. I just wanted to type myApp
at the command line to use the script.
I started with a blank project in PHPStorm. None of what I describe here is PHPStorm specific, though it does help out when it comes to adding composer packages to your project.
For my project I set up ${PROJECT}/bin
, ${PROJECT}/etc
, and ${PROJECT}/lib
directories to hold the various parts of my app/script. Additionally, I added createPhar.php
to the project root (${PROJECT}
).
I added monolog/monolog with composer to get the ${PROJECT}/vendor
directory with all of the composer contributed libraries.
Finally I added build.xml so that I could use Apache Ant to build my app. You don't technically need to do this, but it's what I've settled on to manage app builds.
I have a library that wraps monolog to add additional data to what is logged, and externalize some of the configuration of monolog. This is created as ${PROJECT}/lib/Cli_logger.php
<?php
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
/**
* Class cli_logger
* Wrapper around monolog to provide logging command line apps.
*
* 19 MAY 2014 (gary-rogers@uiowa.edu)
*/
class Cli_logger
{
private static $time_zone;
private static $log_name;
private static $user_id;
private static $service_name;
private static $log_directory;
private static $logger;
public function __construct(
$timezone = null,
$logname = null,
$userid = null,
$servicename = null,
$logdirectory = null)
{
$configFile = Phar::running() . '/etc/quicklog.ini';
$this->config = parse_ini_string( file_get_contents($configFile) );
if ($timezone != null) {
$this::$time_zone = $timezone;
} else {
$this::$time_zone = $this->config['time_zone'];
}
if ($logname != null) {
$this::$log_name = $logname;
} else {
$this::$log_name = 'quicklog';
}
if ( $userid != null ) {
$this::$user_id = $userid;
} else {
$this::$user_id = get_current_user();
}
if ( $servicename != null ) {
$this::$service_name = $servicename;
} else {
$this::$service_name = $this->config['service_name'];
}
if ( $logdirectory != null ) {
$this::$log_directory = $logdirectory;
} else {
$this::$log_directory = $this->config['log_directory'];
}
date_default_timezone_set($this::$time_zone);
$this::$logger = new Logger($this::$log_name);
# Set up the date and line formats for the TextFile output.
$dateFormat = "Y-m-d H:i:s";
$outputFormat = "[%datetime%] [%level_name%] [context:%context%] %message%\n";
# Create a stream for the text file output
$textFormatter = new \Monolog\Formatter\LineFormatter($outputFormat, $dateFormat);
$stream = new StreamHandler($this::$log_directory . '/' . $this::$log_name . '.log', Logger::DEBUG);
$stream->setFormatter($textFormatter);
$this::$logger->pushHandler($stream);
}
public function debug($message)
{
$this->_addRecord(100, $message);
}
public function info($message)
{
$this->_addRecord(200, $message);
}
public function warn($message)
{
$this->_addRecord(300, $message);
}
public function error($message)
{
$this->_addRecord(400, $message);
}
private function _addRecord($level, $message)
{
$callers = debug_backtrace();
$context = array(
'user_id' => $this::$user_id,
'service' => $this::$service_name,
);
$this::$logger->addRecord($level, $message, $context);
}
}
Next, I created the main bits of quicklog, at ${PROJECT}/bin/quicklog.php
<?php
/**
* Quick command line script to log into a monolog format.
*/
require(dirname(__FILE__) . '/../vendor/autoload.php');
include_once(dirname(__FILE__) . '/../lib/Cli_logger.php');
$logger = new Cli_logger();
$opts = getopt('diwe');
$level = 'info';
$message = null;
$tempArgv = $argv;
unset($tempArgv[0]);
switch ($argv[1]) {
case "-d":
$level = 'debug';
unset($tempArgv[1]);
$message = implode(' ',$tempArgv);
$logger->debug($message);
break;
case "-i":
$level = 'info';
unset($tempArgv[1]);
$message = implode(' ',$tempArgv);
$logger->info($message);
break;
case "-w":
$level = 'warn';
unset($tempArgv[1]);
$message = implode(' ',$tempArgv);
$logger->warn($message);
break;
case "-e":
$level = 'error';
unset($tempArgv[1]);
$logger->error($message);
$message = implode(' ',$tempArgv);
break;
default:
$level = 'info';
$message = implode(' ',$tempArgv);
$logger->info($message);
break;
}
${PROJECT}/createPhar.php
<?php
# Create an executable phar
# Name of our archive. (We'd creating this in ${PROJECT}/build)
$phar = new Phar("build/quicklog.phar");
# Have to do buffering to make things executable.
# See http://stackoverflow.com/questions/11082337/how-to-make-an-executable-phar
$phar->startBuffering();
# Default executable. This is what executes when we invoke the PHAR
$defaultStub = $phar->createDefaultStub('bin/quicklog.php');
# Set up the header row for *NIX systems. This lets us execute the PHAR without calling `php quicklog.phar`
# Also set up support for external .ini file.
$preStub = "#!/usr/bin/env php
<?php
if ( file_exists( __DIR__ . '/quicklog.ini')) {
Phar::mount( 'etc/quicklog.ini', __DIR__ . '/quicklog.ini');
} else {
Phar::mount('etc/quicklog.ini', 'phar://' . __FILE__ . '/etc/default_quicklog.ini');
}
?>
";
# Build from the project directory. Assumes that createPhar.php (this file) is in the project root.
$phar->buildFromDirectory(dirname(__FILE__));
# Add the preStub to the default stub.
$stub = $preStub . $defaultStub;
# Set the stub.
$phar->setStub($stub);
# Wrap up.
$phar->stopBuffering();
I was initially very confused about the external config file support. It took me a bit to realize (it was right in the documentation of course) that Phar::mount
won't mount over an existing file. What the $preStub
code does is look for a file named quicklog.ini
in the same directory as the PHAR and mounts that inside the PHAR as /etc/quicklog.ini. If that files doesn't exist, then it uses the bundled default_quicklog.ini
file.
/etc/quicklog.ini
is read by the Cli_logger
library to set up a few configration settings. we use $configFile = Phar::running() . '/etc/quicklog.ini';
to refer to the mounted quicklog.ini
file.
From here it's just a php createPhar.php
to build the app into a PHAR.