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