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