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