1*2afbbbaeSAndreas Gohr<?php 2*2afbbbaeSAndreas Gohr 3*2afbbbaeSAndreas Gohrnamespace splitbrain\phpcli; 4*2afbbbaeSAndreas Gohr 5*2afbbbaeSAndreas Gohr/** 6*2afbbbaeSAndreas Gohr * Class CLIBase 7*2afbbbaeSAndreas Gohr * 8*2afbbbaeSAndreas Gohr * All base functionality is implemented here. 9*2afbbbaeSAndreas Gohr * 10*2afbbbaeSAndreas Gohr * Your commandline should not inherit from this class, but from one of the *CLI* classes 11*2afbbbaeSAndreas Gohr * 12*2afbbbaeSAndreas Gohr * @author Andreas Gohr <andi@splitbrain.org> 13*2afbbbaeSAndreas Gohr * @license MIT 14*2afbbbaeSAndreas Gohr */ 15*2afbbbaeSAndreas Gohrabstract class Base 16*2afbbbaeSAndreas Gohr{ 17*2afbbbaeSAndreas Gohr /** @var string the executed script itself */ 18*2afbbbaeSAndreas Gohr protected $bin; 19*2afbbbaeSAndreas Gohr /** @var Options the option parser */ 20*2afbbbaeSAndreas Gohr protected $options; 21*2afbbbaeSAndreas Gohr /** @var Colors */ 22*2afbbbaeSAndreas Gohr public $colors; 23*2afbbbaeSAndreas Gohr 24*2afbbbaeSAndreas Gohr /** @var array PSR-3 compatible loglevels and their prefix, color, output channel */ 25*2afbbbaeSAndreas Gohr protected $loglevel = array( 26*2afbbbaeSAndreas Gohr 'debug' => array('', Colors::C_RESET, STDOUT), 27*2afbbbaeSAndreas Gohr 'info' => array('ℹ ', Colors::C_CYAN, STDOUT), 28*2afbbbaeSAndreas Gohr 'notice' => array('☛ ', Colors::C_CYAN, STDOUT), 29*2afbbbaeSAndreas Gohr 'success' => array('✓ ', Colors::C_GREEN, STDOUT), 30*2afbbbaeSAndreas Gohr 'warning' => array('⚠ ', Colors::C_BROWN, STDERR), 31*2afbbbaeSAndreas Gohr 'error' => array('✗ ', Colors::C_RED, STDERR), 32*2afbbbaeSAndreas Gohr 'critical' => array('☠ ', Colors::C_LIGHTRED, STDERR), 33*2afbbbaeSAndreas Gohr 'alert' => array('✖ ', Colors::C_LIGHTRED, STDERR), 34*2afbbbaeSAndreas Gohr 'emergency' => array('✘ ', Colors::C_LIGHTRED, STDERR), 35*2afbbbaeSAndreas Gohr ); 36*2afbbbaeSAndreas Gohr 37*2afbbbaeSAndreas Gohr protected $logdefault = 'info'; 38*2afbbbaeSAndreas Gohr 39*2afbbbaeSAndreas Gohr /** 40*2afbbbaeSAndreas Gohr * constructor 41*2afbbbaeSAndreas Gohr * 42*2afbbbaeSAndreas Gohr * Initialize the arguments, set up helper classes and set up the CLI environment 43*2afbbbaeSAndreas Gohr * 44*2afbbbaeSAndreas Gohr * @param bool $autocatch should exceptions be catched and handled automatically? 45*2afbbbaeSAndreas Gohr */ 46*2afbbbaeSAndreas Gohr public function __construct($autocatch = true) 47*2afbbbaeSAndreas Gohr { 48*2afbbbaeSAndreas Gohr if ($autocatch) { 49*2afbbbaeSAndreas Gohr set_exception_handler(array($this, 'fatal')); 50*2afbbbaeSAndreas Gohr } 51*2afbbbaeSAndreas Gohr 52*2afbbbaeSAndreas Gohr $this->colors = new Colors(); 53*2afbbbaeSAndreas Gohr $this->options = new Options($this->colors); 54*2afbbbaeSAndreas Gohr } 55*2afbbbaeSAndreas Gohr 56*2afbbbaeSAndreas Gohr /** 57*2afbbbaeSAndreas Gohr * Register options and arguments on the given $options object 58*2afbbbaeSAndreas Gohr * 59*2afbbbaeSAndreas Gohr * @param Options $options 60*2afbbbaeSAndreas Gohr * @return void 61*2afbbbaeSAndreas Gohr * 62*2afbbbaeSAndreas Gohr * @throws Exception 63*2afbbbaeSAndreas Gohr */ 64*2afbbbaeSAndreas Gohr abstract protected function setup(Options $options); 65*2afbbbaeSAndreas Gohr 66*2afbbbaeSAndreas Gohr /** 67*2afbbbaeSAndreas Gohr * Your main program 68*2afbbbaeSAndreas Gohr * 69*2afbbbaeSAndreas Gohr * Arguments and options have been parsed when this is run 70*2afbbbaeSAndreas Gohr * 71*2afbbbaeSAndreas Gohr * @param Options $options 72*2afbbbaeSAndreas Gohr * @return void 73*2afbbbaeSAndreas Gohr * 74*2afbbbaeSAndreas Gohr * @throws Exception 75*2afbbbaeSAndreas Gohr */ 76*2afbbbaeSAndreas Gohr abstract protected function main(Options $options); 77*2afbbbaeSAndreas Gohr 78*2afbbbaeSAndreas Gohr /** 79*2afbbbaeSAndreas Gohr * Execute the CLI program 80*2afbbbaeSAndreas Gohr * 81*2afbbbaeSAndreas Gohr * Executes the setup() routine, adds default options, initiate the options parsing and argument checking 82*2afbbbaeSAndreas Gohr * and finally executes main() - Each part is split into their own protected function below, so behaviour 83*2afbbbaeSAndreas Gohr * can easily be overwritten 84*2afbbbaeSAndreas Gohr * 85*2afbbbaeSAndreas Gohr * @throws Exception 86*2afbbbaeSAndreas Gohr */ 87*2afbbbaeSAndreas Gohr public function run() 88*2afbbbaeSAndreas Gohr { 89*2afbbbaeSAndreas Gohr if ('cli' != php_sapi_name()) { 90*2afbbbaeSAndreas Gohr throw new Exception('This has to be run from the command line'); 91*2afbbbaeSAndreas Gohr } 92*2afbbbaeSAndreas Gohr 93*2afbbbaeSAndreas Gohr $this->setup($this->options); 94*2afbbbaeSAndreas Gohr $this->registerDefaultOptions(); 95*2afbbbaeSAndreas Gohr $this->parseOptions(); 96*2afbbbaeSAndreas Gohr $this->handleDefaultOptions(); 97*2afbbbaeSAndreas Gohr $this->setupLogging(); 98*2afbbbaeSAndreas Gohr $this->checkArguments(); 99*2afbbbaeSAndreas Gohr $this->execute(); 100*2afbbbaeSAndreas Gohr } 101*2afbbbaeSAndreas Gohr 102*2afbbbaeSAndreas Gohr // region run handlers - for easier overriding 103*2afbbbaeSAndreas Gohr 104*2afbbbaeSAndreas Gohr /** 105*2afbbbaeSAndreas Gohr * Add the default help, color and log options 106*2afbbbaeSAndreas Gohr */ 107*2afbbbaeSAndreas Gohr protected function registerDefaultOptions() 108*2afbbbaeSAndreas Gohr { 109*2afbbbaeSAndreas Gohr $this->options->registerOption( 110*2afbbbaeSAndreas Gohr 'help', 111*2afbbbaeSAndreas Gohr 'Display this help screen and exit immediately.', 112*2afbbbaeSAndreas Gohr 'h' 113*2afbbbaeSAndreas Gohr ); 114*2afbbbaeSAndreas Gohr $this->options->registerOption( 115*2afbbbaeSAndreas Gohr 'no-colors', 116*2afbbbaeSAndreas Gohr 'Do not use any colors in output. Useful when piping output to other tools or files.' 117*2afbbbaeSAndreas Gohr ); 118*2afbbbaeSAndreas Gohr $this->options->registerOption( 119*2afbbbaeSAndreas Gohr 'loglevel', 120*2afbbbaeSAndreas Gohr 'Minimum level of messages to display. Default is ' . $this->colors->wrap($this->logdefault, Colors::C_CYAN) . '. ' . 121*2afbbbaeSAndreas Gohr 'Valid levels are: debug, info, notice, success, warning, error, critical, alert, emergency.', 122*2afbbbaeSAndreas Gohr null, 123*2afbbbaeSAndreas Gohr 'level' 124*2afbbbaeSAndreas Gohr ); 125*2afbbbaeSAndreas Gohr } 126*2afbbbaeSAndreas Gohr 127*2afbbbaeSAndreas Gohr /** 128*2afbbbaeSAndreas Gohr * Handle the default options 129*2afbbbaeSAndreas Gohr */ 130*2afbbbaeSAndreas Gohr protected function handleDefaultOptions() 131*2afbbbaeSAndreas Gohr { 132*2afbbbaeSAndreas Gohr if ($this->options->getOpt('no-colors')) { 133*2afbbbaeSAndreas Gohr $this->colors->disable(); 134*2afbbbaeSAndreas Gohr } 135*2afbbbaeSAndreas Gohr if ($this->options->getOpt('help')) { 136*2afbbbaeSAndreas Gohr echo $this->options->help(); 137*2afbbbaeSAndreas Gohr exit(0); 138*2afbbbaeSAndreas Gohr } 139*2afbbbaeSAndreas Gohr } 140*2afbbbaeSAndreas Gohr 141*2afbbbaeSAndreas Gohr /** 142*2afbbbaeSAndreas Gohr * Handle the logging options 143*2afbbbaeSAndreas Gohr */ 144*2afbbbaeSAndreas Gohr protected function setupLogging() 145*2afbbbaeSAndreas Gohr { 146*2afbbbaeSAndreas Gohr $level = $this->options->getOpt('loglevel', $this->logdefault); 147*2afbbbaeSAndreas Gohr if (!isset($this->loglevel[$level])) $this->fatal('Unknown log level'); 148*2afbbbaeSAndreas Gohr foreach (array_keys($this->loglevel) as $l) { 149*2afbbbaeSAndreas Gohr if ($l == $level) break; 150*2afbbbaeSAndreas Gohr unset($this->loglevel[$l]); 151*2afbbbaeSAndreas Gohr } 152*2afbbbaeSAndreas Gohr } 153*2afbbbaeSAndreas Gohr 154*2afbbbaeSAndreas Gohr /** 155*2afbbbaeSAndreas Gohr * Wrapper around the option parsing 156*2afbbbaeSAndreas Gohr */ 157*2afbbbaeSAndreas Gohr protected function parseOptions() 158*2afbbbaeSAndreas Gohr { 159*2afbbbaeSAndreas Gohr $this->options->parseOptions(); 160*2afbbbaeSAndreas Gohr } 161*2afbbbaeSAndreas Gohr 162*2afbbbaeSAndreas Gohr /** 163*2afbbbaeSAndreas Gohr * Wrapper around the argument checking 164*2afbbbaeSAndreas Gohr */ 165*2afbbbaeSAndreas Gohr protected function checkArguments() 166*2afbbbaeSAndreas Gohr { 167*2afbbbaeSAndreas Gohr $this->options->checkArguments(); 168*2afbbbaeSAndreas Gohr } 169*2afbbbaeSAndreas Gohr 170*2afbbbaeSAndreas Gohr /** 171*2afbbbaeSAndreas Gohr * Wrapper around main 172*2afbbbaeSAndreas Gohr */ 173*2afbbbaeSAndreas Gohr protected function execute() 174*2afbbbaeSAndreas Gohr { 175*2afbbbaeSAndreas Gohr $this->main($this->options); 176*2afbbbaeSAndreas Gohr } 177*2afbbbaeSAndreas Gohr 178*2afbbbaeSAndreas Gohr // endregion 179*2afbbbaeSAndreas Gohr 180*2afbbbaeSAndreas Gohr // region logging 181*2afbbbaeSAndreas Gohr 182*2afbbbaeSAndreas Gohr /** 183*2afbbbaeSAndreas Gohr * Exits the program on a fatal error 184*2afbbbaeSAndreas Gohr * 185*2afbbbaeSAndreas Gohr * @param \Exception|string $error either an exception or an error message 186*2afbbbaeSAndreas Gohr * @param array $context 187*2afbbbaeSAndreas Gohr */ 188*2afbbbaeSAndreas Gohr public function fatal($error, array $context = array()) 189*2afbbbaeSAndreas Gohr { 190*2afbbbaeSAndreas Gohr $code = 0; 191*2afbbbaeSAndreas Gohr if (is_object($error) && is_a($error, 'Exception')) { 192*2afbbbaeSAndreas Gohr /** @var Exception $error */ 193*2afbbbaeSAndreas Gohr $this->logMessage('debug', get_class($error) . ' caught in ' . $error->getFile() . ':' . $error->getLine()); 194*2afbbbaeSAndreas Gohr $this->logMessage('debug', $error->getTraceAsString()); 195*2afbbbaeSAndreas Gohr $code = $error->getCode(); 196*2afbbbaeSAndreas Gohr $error = $error->getMessage(); 197*2afbbbaeSAndreas Gohr 198*2afbbbaeSAndreas Gohr } 199*2afbbbaeSAndreas Gohr if (!$code) { 200*2afbbbaeSAndreas Gohr $code = Exception::E_ANY; 201*2afbbbaeSAndreas Gohr } 202*2afbbbaeSAndreas Gohr 203*2afbbbaeSAndreas Gohr $this->logMessage('critical', $error, $context); 204*2afbbbaeSAndreas Gohr exit($code); 205*2afbbbaeSAndreas Gohr } 206*2afbbbaeSAndreas Gohr 207*2afbbbaeSAndreas Gohr /** 208*2afbbbaeSAndreas Gohr * Normal, positive outcome (This is not a PSR-3 level) 209*2afbbbaeSAndreas Gohr * 210*2afbbbaeSAndreas Gohr * @param string $string 211*2afbbbaeSAndreas Gohr * @param array $context 212*2afbbbaeSAndreas Gohr */ 213*2afbbbaeSAndreas Gohr public function success($string, array $context = array()) 214*2afbbbaeSAndreas Gohr { 215*2afbbbaeSAndreas Gohr $this->logMessage('success', $string, $context); 216*2afbbbaeSAndreas Gohr } 217*2afbbbaeSAndreas Gohr 218*2afbbbaeSAndreas Gohr /** 219*2afbbbaeSAndreas Gohr * @param string $level 220*2afbbbaeSAndreas Gohr * @param string $message 221*2afbbbaeSAndreas Gohr * @param array $context 222*2afbbbaeSAndreas Gohr */ 223*2afbbbaeSAndreas Gohr protected function logMessage($level, $message, array $context = array()) 224*2afbbbaeSAndreas Gohr { 225*2afbbbaeSAndreas Gohr // is this log level wanted? 226*2afbbbaeSAndreas Gohr if (!isset($this->loglevel[$level])) return; 227*2afbbbaeSAndreas Gohr 228*2afbbbaeSAndreas Gohr /** @var string $prefix */ 229*2afbbbaeSAndreas Gohr /** @var string $color */ 230*2afbbbaeSAndreas Gohr /** @var resource $channel */ 231*2afbbbaeSAndreas Gohr list($prefix, $color, $channel) = $this->loglevel[$level]; 232*2afbbbaeSAndreas Gohr if (!$this->colors->isEnabled()) $prefix = ''; 233*2afbbbaeSAndreas Gohr 234*2afbbbaeSAndreas Gohr $message = $this->interpolate($message, $context); 235*2afbbbaeSAndreas Gohr $this->colors->ptln($prefix . $message, $color, $channel); 236*2afbbbaeSAndreas Gohr } 237*2afbbbaeSAndreas Gohr 238*2afbbbaeSAndreas Gohr /** 239*2afbbbaeSAndreas Gohr * Interpolates context values into the message placeholders. 240*2afbbbaeSAndreas Gohr * 241*2afbbbaeSAndreas Gohr * @param $message 242*2afbbbaeSAndreas Gohr * @param array $context 243*2afbbbaeSAndreas Gohr * @return string 244*2afbbbaeSAndreas Gohr */ 245*2afbbbaeSAndreas Gohr protected function interpolate($message, array $context = array()) 246*2afbbbaeSAndreas Gohr { 247*2afbbbaeSAndreas Gohr // build a replacement array with braces around the context keys 248*2afbbbaeSAndreas Gohr $replace = array(); 249*2afbbbaeSAndreas Gohr foreach ($context as $key => $val) { 250*2afbbbaeSAndreas Gohr // check that the value can be casted to string 251*2afbbbaeSAndreas Gohr if (!is_array($val) && (!is_object($val) || method_exists($val, '__toString'))) { 252*2afbbbaeSAndreas Gohr $replace['{' . $key . '}'] = $val; 253*2afbbbaeSAndreas Gohr } 254*2afbbbaeSAndreas Gohr } 255*2afbbbaeSAndreas Gohr 256*2afbbbaeSAndreas Gohr // interpolate replacement values into the message and return 257*2afbbbaeSAndreas Gohr return strtr((string)$message, $replace); 258*2afbbbaeSAndreas Gohr } 259*2afbbbaeSAndreas Gohr 260*2afbbbaeSAndreas Gohr // endregion 261*2afbbbaeSAndreas Gohr} 262