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 * @throws Exception 61 */ 62 abstract protected function setup(Options $options); 63 64 /** 65 * Your main program 66 * 67 * Arguments and options have been parsed when this is run 68 * 69 * @param Options $options 70 * @return void 71 * 72 * @throws Exception 73 */ 74 abstract protected function main(Options $options); 75 76 /** 77 * Execute the CLI program 78 * 79 * Executes the setup() routine, adds default options, initiate the options parsing and argument checking 80 * and finally executes main() 81 * 82 * @throws Exception 83 */ 84 public function run() 85 { 86 if ('cli' != php_sapi_name()) { 87 throw new Exception('This has to be run from the command line'); 88 } 89 90 // setup 91 $this->setup($this->options); 92 $this->options->registerOption( 93 'help', 94 'Display this help screen and exit immeadiately.', 95 'h' 96 ); 97 $this->options->registerOption( 98 'no-colors', 99 'Do not use any colors in output. Useful when piping output to other tools or files.' 100 ); 101 $this->options->registerOption( 102 'loglevel', 103 'Minimum level of messages to display. Default is ' . $this->colors->wrap($this->logdefault, Colors::C_CYAN) . '. ' . 104 'Valid levels are: debug, info, notice, success, warning, error, critical, alert, emergency.', 105 null, 106 'level' 107 ); 108 109 // parse 110 $this->options->parseOptions(); 111 112 // handle defaults 113 if ($this->options->getOpt('no-colors')) { 114 $this->colors->disable(); 115 } 116 if ($this->options->getOpt('help')) { 117 echo $this->options->help(); 118 exit(0); 119 } 120 $level = $this->options->getOpt('loglevel', $this->logdefault); 121 if (!isset($this->loglevel[$level])) $this->fatal('Unknown log level'); 122 foreach (array_keys($this->loglevel) as $l) { 123 if ($l == $level) break; 124 unset($this->loglevel[$l]); 125 } 126 127 // check arguments 128 $this->options->checkArguments(); 129 130 // execute 131 $this->main($this->options); 132 133 exit(0); 134 } 135 136 // region logging 137 138 /** 139 * Exits the program on a fatal error 140 * 141 * @param \Exception|string $error either an exception or an error message 142 * @param array $context 143 */ 144 public function fatal($error, array $context = array()) 145 { 146 $code = 0; 147 if (is_object($error) && is_a($error, 'Exception')) { 148 /** @var Exception $error */ 149 $this->debug(get_class($error) . ' caught in ' . $error->getFile() . ':' . $error->getLine()); 150 $this->debug($error->getTraceAsString()); 151 $code = $error->getCode(); 152 $error = $error->getMessage(); 153 154 } 155 if (!$code) { 156 $code = Exception::E_ANY; 157 } 158 159 $this->critical($error, $context); 160 exit($code); 161 } 162 163 /** 164 * System is unusable. 165 * 166 * @param string $message 167 * @param array $context 168 * 169 * @return void 170 */ 171 public function emergency($message, array $context = array()) 172 { 173 $this->log('emergency', $message, $context); 174 } 175 176 /** 177 * Action must be taken immediately. 178 * 179 * Example: Entire website down, database unavailable, etc. This should 180 * trigger the SMS alerts and wake you up. 181 * 182 * @param string $message 183 * @param array $context 184 */ 185 public function alert($message, array $context = array()) 186 { 187 $this->log('alert', $message, $context); 188 } 189 190 /** 191 * Critical conditions. 192 * 193 * Example: Application component unavailable, unexpected exception. 194 * 195 * @param string $message 196 * @param array $context 197 */ 198 public function critical($message, array $context = array()) 199 { 200 $this->log('critical', $message, $context); 201 } 202 203 /** 204 * Runtime errors that do not require immediate action but should typically 205 * be logged and monitored. 206 * 207 * @param string $message 208 * @param array $context 209 */ 210 public function error($message, array $context = array()) 211 { 212 $this->log('error', $message, $context); 213 } 214 215 /** 216 * Exceptional occurrences that are not errors. 217 * 218 * Example: Use of deprecated APIs, poor use of an API, undesirable things 219 * that are not necessarily wrong. 220 * 221 * @param string $message 222 * @param array $context 223 */ 224 public function warning($message, array $context = array()) 225 { 226 $this->log('warning', $message, $context); 227 } 228 229 /** 230 * Normal, positive outcome 231 * 232 * @param string $string 233 * @param array $context 234 */ 235 public function success($string, array $context = array()) 236 { 237 $this->log('success', $string, $context); 238 } 239 240 /** 241 * Normal but significant events. 242 * 243 * @param string $message 244 * @param array $context 245 */ 246 public function notice($message, array $context = array()) 247 { 248 $this->log('notice', $message, $context); 249 } 250 251 /** 252 * Interesting events. 253 * 254 * Example: User logs in, SQL logs. 255 * 256 * @param string $message 257 * @param array $context 258 */ 259 public function info($message, array $context = array()) 260 { 261 $this->log('info', $message, $context); 262 } 263 264 /** 265 * Detailed debug information. 266 * 267 * @param string $message 268 * @param array $context 269 */ 270 public function debug($message, array $context = array()) 271 { 272 $this->log('debug', $message, $context); 273 } 274 275 /** 276 * @param string $level 277 * @param string $message 278 * @param array $context 279 */ 280 public function log($level, $message, array $context = array()) 281 { 282 // is this log level wanted? 283 if (!isset($this->loglevel[$level])) return; 284 285 /** @var string $prefix */ 286 /** @var string $color */ 287 /** @var resource $channel */ 288 list($prefix, $color, $channel) = $this->loglevel[$level]; 289 if(!$this->colors->isEnabled()) $prefix = ''; 290 291 $message = $this->interpolate($message, $context); 292 $this->colors->ptln($prefix . $message, $color, $channel); 293 } 294 295 /** 296 * Interpolates context values into the message placeholders. 297 * 298 * @param $message 299 * @param array $context 300 * @return string 301 */ 302 function interpolate($message, array $context = array()) 303 { 304 // build a replacement array with braces around the context keys 305 $replace = array(); 306 foreach ($context as $key => $val) { 307 // check that the value can be casted to string 308 if (!is_array($val) && (!is_object($val) || method_exists($val, '__toString'))) { 309 $replace['{' . $key . '}'] = $val; 310 } 311 } 312 313 // interpolate replacement values into the message and return 314 return strtr($message, $replace); 315 } 316 317 // endregion 318} 319