1*cbeaa4a0SAndreas Gohr<?php 2*cbeaa4a0SAndreas Gohr 3*cbeaa4a0SAndreas Gohrnamespace splitbrain\phpcli; 4*cbeaa4a0SAndreas Gohr 5*cbeaa4a0SAndreas Gohr/** 6*cbeaa4a0SAndreas Gohr * Class CLI 7*cbeaa4a0SAndreas Gohr * 8*cbeaa4a0SAndreas Gohr * Your commandline script should inherit from this class and implement the abstract methods. 9*cbeaa4a0SAndreas Gohr * 10*cbeaa4a0SAndreas Gohr * @author Andreas Gohr <andi@splitbrain.org> 11*cbeaa4a0SAndreas Gohr * @license MIT 12*cbeaa4a0SAndreas Gohr */ 13*cbeaa4a0SAndreas Gohrabstract class CLI 14*cbeaa4a0SAndreas Gohr{ 15*cbeaa4a0SAndreas Gohr /** @var string the executed script itself */ 16*cbeaa4a0SAndreas Gohr protected $bin; 17*cbeaa4a0SAndreas Gohr /** @var Options the option parser */ 18*cbeaa4a0SAndreas Gohr protected $options; 19*cbeaa4a0SAndreas Gohr /** @var Colors */ 20*cbeaa4a0SAndreas Gohr public $colors; 21*cbeaa4a0SAndreas Gohr 22*cbeaa4a0SAndreas Gohr /** @var array PSR-3 compatible loglevels and their prefix, color, output channel */ 23*cbeaa4a0SAndreas Gohr protected $loglevel = array( 24*cbeaa4a0SAndreas Gohr 'debug' => array('', Colors::C_LIGHTGRAY, STDOUT), 25*cbeaa4a0SAndreas Gohr 'info' => array('ℹ ', Colors::C_CYAN, STDOUT), 26*cbeaa4a0SAndreas Gohr 'notice' => array('☛ ', Colors::C_CYAN, STDOUT), 27*cbeaa4a0SAndreas Gohr 'success' => array('✓ ', Colors::C_GREEN, STDOUT), 28*cbeaa4a0SAndreas Gohr 'warning' => array('⚠ ', Colors::C_BROWN, STDERR), 29*cbeaa4a0SAndreas Gohr 'error' => array('✗ ', Colors::C_RED, STDERR), 30*cbeaa4a0SAndreas Gohr 'critical' => array('☠ ', Colors::C_LIGHTRED, STDERR), 31*cbeaa4a0SAndreas Gohr 'alert' => array('✖ ', Colors::C_LIGHTRED, STDERR), 32*cbeaa4a0SAndreas Gohr 'emergency' => array('✘ ', Colors::C_LIGHTRED, STDERR), 33*cbeaa4a0SAndreas Gohr ); 34*cbeaa4a0SAndreas Gohr 35*cbeaa4a0SAndreas Gohr protected $logdefault = 'info'; 36*cbeaa4a0SAndreas Gohr 37*cbeaa4a0SAndreas Gohr /** 38*cbeaa4a0SAndreas Gohr * constructor 39*cbeaa4a0SAndreas Gohr * 40*cbeaa4a0SAndreas Gohr * Initialize the arguments, set up helper classes and set up the CLI environment 41*cbeaa4a0SAndreas Gohr * 42*cbeaa4a0SAndreas Gohr * @param bool $autocatch should exceptions be catched and handled automatically? 43*cbeaa4a0SAndreas Gohr */ 44*cbeaa4a0SAndreas Gohr public function __construct($autocatch = true) 45*cbeaa4a0SAndreas Gohr { 46*cbeaa4a0SAndreas Gohr if ($autocatch) { 47*cbeaa4a0SAndreas Gohr set_exception_handler(array($this, 'fatal')); 48*cbeaa4a0SAndreas Gohr } 49*cbeaa4a0SAndreas Gohr 50*cbeaa4a0SAndreas Gohr $this->colors = new Colors(); 51*cbeaa4a0SAndreas Gohr $this->options = new Options($this->colors); 52*cbeaa4a0SAndreas Gohr } 53*cbeaa4a0SAndreas Gohr 54*cbeaa4a0SAndreas Gohr /** 55*cbeaa4a0SAndreas Gohr * Register options and arguments on the given $options object 56*cbeaa4a0SAndreas Gohr * 57*cbeaa4a0SAndreas Gohr * @param Options $options 58*cbeaa4a0SAndreas Gohr * @return void 59*cbeaa4a0SAndreas Gohr */ 60*cbeaa4a0SAndreas Gohr abstract protected function setup(Options $options); 61*cbeaa4a0SAndreas Gohr 62*cbeaa4a0SAndreas Gohr /** 63*cbeaa4a0SAndreas Gohr * Your main program 64*cbeaa4a0SAndreas Gohr * 65*cbeaa4a0SAndreas Gohr * Arguments and options have been parsed when this is run 66*cbeaa4a0SAndreas Gohr * 67*cbeaa4a0SAndreas Gohr * @param Options $options 68*cbeaa4a0SAndreas Gohr * @return void 69*cbeaa4a0SAndreas Gohr */ 70*cbeaa4a0SAndreas Gohr abstract protected function main(Options $options); 71*cbeaa4a0SAndreas Gohr 72*cbeaa4a0SAndreas Gohr /** 73*cbeaa4a0SAndreas Gohr * Execute the CLI program 74*cbeaa4a0SAndreas Gohr * 75*cbeaa4a0SAndreas Gohr * Executes the setup() routine, adds default options, initiate the options parsing and argument checking 76*cbeaa4a0SAndreas Gohr * and finally executes main() 77*cbeaa4a0SAndreas Gohr */ 78*cbeaa4a0SAndreas Gohr public function run() 79*cbeaa4a0SAndreas Gohr { 80*cbeaa4a0SAndreas Gohr if ('cli' != php_sapi_name()) { 81*cbeaa4a0SAndreas Gohr throw new Exception('This has to be run from the command line'); 82*cbeaa4a0SAndreas Gohr } 83*cbeaa4a0SAndreas Gohr 84*cbeaa4a0SAndreas Gohr // setup 85*cbeaa4a0SAndreas Gohr $this->setup($this->options); 86*cbeaa4a0SAndreas Gohr $this->options->registerOption( 87*cbeaa4a0SAndreas Gohr 'help', 88*cbeaa4a0SAndreas Gohr 'Display this help screen and exit immeadiately.', 89*cbeaa4a0SAndreas Gohr 'h' 90*cbeaa4a0SAndreas Gohr ); 91*cbeaa4a0SAndreas Gohr $this->options->registerOption( 92*cbeaa4a0SAndreas Gohr 'no-colors', 93*cbeaa4a0SAndreas Gohr 'Do not use any colors in output. Useful when piping output to other tools or files.' 94*cbeaa4a0SAndreas Gohr ); 95*cbeaa4a0SAndreas Gohr $this->options->registerOption( 96*cbeaa4a0SAndreas Gohr 'loglevel', 97*cbeaa4a0SAndreas Gohr 'Minimum level of messages to display. Default is ' . $this->colors->wrap($this->logdefault, Colors::C_CYAN) . '. ' . 98*cbeaa4a0SAndreas Gohr 'Valid levels are: debug, info, notice, success, warning, error, critical, alert, emergency.', 99*cbeaa4a0SAndreas Gohr null, 100*cbeaa4a0SAndreas Gohr 'level' 101*cbeaa4a0SAndreas Gohr ); 102*cbeaa4a0SAndreas Gohr 103*cbeaa4a0SAndreas Gohr // parse 104*cbeaa4a0SAndreas Gohr $this->options->parseOptions(); 105*cbeaa4a0SAndreas Gohr 106*cbeaa4a0SAndreas Gohr // handle defaults 107*cbeaa4a0SAndreas Gohr if ($this->options->getOpt('no-colors')) { 108*cbeaa4a0SAndreas Gohr $this->colors->disable(); 109*cbeaa4a0SAndreas Gohr } 110*cbeaa4a0SAndreas Gohr if ($this->options->getOpt('help')) { 111*cbeaa4a0SAndreas Gohr echo $this->options->help(); 112*cbeaa4a0SAndreas Gohr exit(0); 113*cbeaa4a0SAndreas Gohr } 114*cbeaa4a0SAndreas Gohr $level = $this->options->getOpt('loglevel', $this->logdefault); 115*cbeaa4a0SAndreas Gohr if (!isset($this->loglevel[$level])) $this->fatal('Unknown log level'); 116*cbeaa4a0SAndreas Gohr foreach (array_keys($this->loglevel) as $l) { 117*cbeaa4a0SAndreas Gohr if ($l == $level) break; 118*cbeaa4a0SAndreas Gohr unset($this->loglevel[$l]); 119*cbeaa4a0SAndreas Gohr } 120*cbeaa4a0SAndreas Gohr 121*cbeaa4a0SAndreas Gohr // check arguments 122*cbeaa4a0SAndreas Gohr $this->options->checkArguments(); 123*cbeaa4a0SAndreas Gohr 124*cbeaa4a0SAndreas Gohr // execute 125*cbeaa4a0SAndreas Gohr $this->main($this->options); 126*cbeaa4a0SAndreas Gohr 127*cbeaa4a0SAndreas Gohr exit(0); 128*cbeaa4a0SAndreas Gohr } 129*cbeaa4a0SAndreas Gohr 130*cbeaa4a0SAndreas Gohr // region logging 131*cbeaa4a0SAndreas Gohr 132*cbeaa4a0SAndreas Gohr /** 133*cbeaa4a0SAndreas Gohr * Exits the program on a fatal error 134*cbeaa4a0SAndreas Gohr * 135*cbeaa4a0SAndreas Gohr * @param \Exception|string $error either an exception or an error message 136*cbeaa4a0SAndreas Gohr * @param array $context 137*cbeaa4a0SAndreas Gohr */ 138*cbeaa4a0SAndreas Gohr public function fatal($error, array $context = array()) 139*cbeaa4a0SAndreas Gohr { 140*cbeaa4a0SAndreas Gohr $code = 0; 141*cbeaa4a0SAndreas Gohr if (is_object($error) && is_a($error, 'Exception')) { 142*cbeaa4a0SAndreas Gohr /** @var Exception $error */ 143*cbeaa4a0SAndreas Gohr $this->debug(get_class($error) . ' caught in ' . $error->getFile() . ':' . $error->getLine()); 144*cbeaa4a0SAndreas Gohr $this->debug($error->getTraceAsString()); 145*cbeaa4a0SAndreas Gohr $code = $error->getCode(); 146*cbeaa4a0SAndreas Gohr $error = $error->getMessage(); 147*cbeaa4a0SAndreas Gohr 148*cbeaa4a0SAndreas Gohr } 149*cbeaa4a0SAndreas Gohr if (!$code) { 150*cbeaa4a0SAndreas Gohr $code = Exception::E_ANY; 151*cbeaa4a0SAndreas Gohr } 152*cbeaa4a0SAndreas Gohr 153*cbeaa4a0SAndreas Gohr $this->critical($error, $context); 154*cbeaa4a0SAndreas Gohr exit($code); 155*cbeaa4a0SAndreas Gohr } 156*cbeaa4a0SAndreas Gohr 157*cbeaa4a0SAndreas Gohr /** 158*cbeaa4a0SAndreas Gohr * System is unusable. 159*cbeaa4a0SAndreas Gohr * 160*cbeaa4a0SAndreas Gohr * @param string $message 161*cbeaa4a0SAndreas Gohr * @param array $context 162*cbeaa4a0SAndreas Gohr * 163*cbeaa4a0SAndreas Gohr * @return void 164*cbeaa4a0SAndreas Gohr */ 165*cbeaa4a0SAndreas Gohr public function emergency($message, array $context = array()) 166*cbeaa4a0SAndreas Gohr { 167*cbeaa4a0SAndreas Gohr $this->log('emergency', $message, $context); 168*cbeaa4a0SAndreas Gohr } 169*cbeaa4a0SAndreas Gohr 170*cbeaa4a0SAndreas Gohr /** 171*cbeaa4a0SAndreas Gohr * Action must be taken immediately. 172*cbeaa4a0SAndreas Gohr * 173*cbeaa4a0SAndreas Gohr * Example: Entire website down, database unavailable, etc. This should 174*cbeaa4a0SAndreas Gohr * trigger the SMS alerts and wake you up. 175*cbeaa4a0SAndreas Gohr * 176*cbeaa4a0SAndreas Gohr * @param string $message 177*cbeaa4a0SAndreas Gohr * @param array $context 178*cbeaa4a0SAndreas Gohr */ 179*cbeaa4a0SAndreas Gohr public function alert($message, array $context = array()) 180*cbeaa4a0SAndreas Gohr { 181*cbeaa4a0SAndreas Gohr $this->log('alert', $message, $context); 182*cbeaa4a0SAndreas Gohr } 183*cbeaa4a0SAndreas Gohr 184*cbeaa4a0SAndreas Gohr /** 185*cbeaa4a0SAndreas Gohr * Critical conditions. 186*cbeaa4a0SAndreas Gohr * 187*cbeaa4a0SAndreas Gohr * Example: Application component unavailable, unexpected exception. 188*cbeaa4a0SAndreas Gohr * 189*cbeaa4a0SAndreas Gohr * @param string $message 190*cbeaa4a0SAndreas Gohr * @param array $context 191*cbeaa4a0SAndreas Gohr */ 192*cbeaa4a0SAndreas Gohr public function critical($message, array $context = array()) 193*cbeaa4a0SAndreas Gohr { 194*cbeaa4a0SAndreas Gohr $this->log('critical', $message, $context); 195*cbeaa4a0SAndreas Gohr } 196*cbeaa4a0SAndreas Gohr 197*cbeaa4a0SAndreas Gohr /** 198*cbeaa4a0SAndreas Gohr * Runtime errors that do not require immediate action but should typically 199*cbeaa4a0SAndreas Gohr * be logged and monitored. 200*cbeaa4a0SAndreas Gohr * 201*cbeaa4a0SAndreas Gohr * @param string $message 202*cbeaa4a0SAndreas Gohr * @param array $context 203*cbeaa4a0SAndreas Gohr */ 204*cbeaa4a0SAndreas Gohr public function error($message, array $context = array()) 205*cbeaa4a0SAndreas Gohr { 206*cbeaa4a0SAndreas Gohr $this->log('error', $message, $context); 207*cbeaa4a0SAndreas Gohr } 208*cbeaa4a0SAndreas Gohr 209*cbeaa4a0SAndreas Gohr /** 210*cbeaa4a0SAndreas Gohr * Exceptional occurrences that are not errors. 211*cbeaa4a0SAndreas Gohr * 212*cbeaa4a0SAndreas Gohr * Example: Use of deprecated APIs, poor use of an API, undesirable things 213*cbeaa4a0SAndreas Gohr * that are not necessarily wrong. 214*cbeaa4a0SAndreas Gohr * 215*cbeaa4a0SAndreas Gohr * @param string $message 216*cbeaa4a0SAndreas Gohr * @param array $context 217*cbeaa4a0SAndreas Gohr */ 218*cbeaa4a0SAndreas Gohr public function warning($message, array $context = array()) 219*cbeaa4a0SAndreas Gohr { 220*cbeaa4a0SAndreas Gohr $this->log('warning', $message, $context); 221*cbeaa4a0SAndreas Gohr } 222*cbeaa4a0SAndreas Gohr 223*cbeaa4a0SAndreas Gohr /** 224*cbeaa4a0SAndreas Gohr * Normal, positive outcome 225*cbeaa4a0SAndreas Gohr * 226*cbeaa4a0SAndreas Gohr * @param string $string 227*cbeaa4a0SAndreas Gohr * @param array $context 228*cbeaa4a0SAndreas Gohr */ 229*cbeaa4a0SAndreas Gohr public function success($string, array $context = array()) 230*cbeaa4a0SAndreas Gohr { 231*cbeaa4a0SAndreas Gohr $this->log('success', $string, $context); 232*cbeaa4a0SAndreas Gohr } 233*cbeaa4a0SAndreas Gohr 234*cbeaa4a0SAndreas Gohr /** 235*cbeaa4a0SAndreas Gohr * Normal but significant events. 236*cbeaa4a0SAndreas Gohr * 237*cbeaa4a0SAndreas Gohr * @param string $message 238*cbeaa4a0SAndreas Gohr * @param array $context 239*cbeaa4a0SAndreas Gohr */ 240*cbeaa4a0SAndreas Gohr public function notice($message, array $context = array()) 241*cbeaa4a0SAndreas Gohr { 242*cbeaa4a0SAndreas Gohr $this->log('notice', $message, $context); 243*cbeaa4a0SAndreas Gohr } 244*cbeaa4a0SAndreas Gohr 245*cbeaa4a0SAndreas Gohr /** 246*cbeaa4a0SAndreas Gohr * Interesting events. 247*cbeaa4a0SAndreas Gohr * 248*cbeaa4a0SAndreas Gohr * Example: User logs in, SQL logs. 249*cbeaa4a0SAndreas Gohr * 250*cbeaa4a0SAndreas Gohr * @param string $message 251*cbeaa4a0SAndreas Gohr * @param array $context 252*cbeaa4a0SAndreas Gohr */ 253*cbeaa4a0SAndreas Gohr public function info($message, array $context = array()) 254*cbeaa4a0SAndreas Gohr { 255*cbeaa4a0SAndreas Gohr $this->log('info', $message, $context); 256*cbeaa4a0SAndreas Gohr } 257*cbeaa4a0SAndreas Gohr 258*cbeaa4a0SAndreas Gohr /** 259*cbeaa4a0SAndreas Gohr * Detailed debug information. 260*cbeaa4a0SAndreas Gohr * 261*cbeaa4a0SAndreas Gohr * @param string $message 262*cbeaa4a0SAndreas Gohr * @param array $context 263*cbeaa4a0SAndreas Gohr */ 264*cbeaa4a0SAndreas Gohr public function debug($message, array $context = array()) 265*cbeaa4a0SAndreas Gohr { 266*cbeaa4a0SAndreas Gohr $this->log('debug', $message, $context); 267*cbeaa4a0SAndreas Gohr } 268*cbeaa4a0SAndreas Gohr 269*cbeaa4a0SAndreas Gohr /** 270*cbeaa4a0SAndreas Gohr * @param string $level 271*cbeaa4a0SAndreas Gohr * @param string $message 272*cbeaa4a0SAndreas Gohr * @param array $context 273*cbeaa4a0SAndreas Gohr */ 274*cbeaa4a0SAndreas Gohr public function log($level, $message, array $context = array()) 275*cbeaa4a0SAndreas Gohr { 276*cbeaa4a0SAndreas Gohr // is this log level wanted? 277*cbeaa4a0SAndreas Gohr if (!isset($this->loglevel[$level])) return; 278*cbeaa4a0SAndreas Gohr 279*cbeaa4a0SAndreas Gohr /** @var string $prefix */ 280*cbeaa4a0SAndreas Gohr /** @var string $color */ 281*cbeaa4a0SAndreas Gohr /** @var resource $channel */ 282*cbeaa4a0SAndreas Gohr list($prefix, $color, $channel) = $this->loglevel[$level]; 283*cbeaa4a0SAndreas Gohr if(!$this->colors->isEnabled()) $prefix = ''; 284*cbeaa4a0SAndreas Gohr 285*cbeaa4a0SAndreas Gohr $message = $this->interpolate($message, $context); 286*cbeaa4a0SAndreas Gohr $this->colors->ptln($prefix . $message, $color, $channel); 287*cbeaa4a0SAndreas Gohr } 288*cbeaa4a0SAndreas Gohr 289*cbeaa4a0SAndreas Gohr /** 290*cbeaa4a0SAndreas Gohr * Interpolates context values into the message placeholders. 291*cbeaa4a0SAndreas Gohr * 292*cbeaa4a0SAndreas Gohr * @param $message 293*cbeaa4a0SAndreas Gohr * @param array $context 294*cbeaa4a0SAndreas Gohr * @return string 295*cbeaa4a0SAndreas Gohr */ 296*cbeaa4a0SAndreas Gohr function interpolate($message, array $context = array()) 297*cbeaa4a0SAndreas Gohr { 298*cbeaa4a0SAndreas Gohr // build a replacement array with braces around the context keys 299*cbeaa4a0SAndreas Gohr $replace = array(); 300*cbeaa4a0SAndreas Gohr foreach ($context as $key => $val) { 301*cbeaa4a0SAndreas Gohr // check that the value can be casted to string 302*cbeaa4a0SAndreas Gohr if (!is_array($val) && (!is_object($val) || method_exists($val, '__toString'))) { 303*cbeaa4a0SAndreas Gohr $replace['{' . $key . '}'] = $val; 304*cbeaa4a0SAndreas Gohr } 305*cbeaa4a0SAndreas Gohr } 306*cbeaa4a0SAndreas Gohr 307*cbeaa4a0SAndreas Gohr // interpolate replacement values into the message and return 308*cbeaa4a0SAndreas Gohr return strtr($message, $replace); 309*cbeaa4a0SAndreas Gohr } 310*cbeaa4a0SAndreas Gohr 311*cbeaa4a0SAndreas Gohr // endregion 312*cbeaa4a0SAndreas Gohr} 313