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 */ 25 protected $loglevel = array( 26 'debug' => array('', Colors::C_RESET, STDOUT), 27 'info' => array('ℹ ', Colors::C_CYAN, STDOUT), 28 'notice' => array('☛ ', Colors::C_CYAN, STDOUT), 29 'success' => array('✓ ', Colors::C_GREEN, STDOUT), 30 'warning' => array('⚠ ', Colors::C_BROWN, STDERR), 31 'error' => array('✗ ', Colors::C_RED, STDERR), 32 'critical' => array('☠ ', Colors::C_LIGHTRED, STDERR), 33 'alert' => array('✖ ', Colors::C_LIGHTRED, STDERR), 34 'emergency' => array('✘ ', Colors::C_LIGHTRED, STDERR), 35 ); 36 37 protected $logdefault = 'info'; 38 39 /** 40 * constructor 41 * 42 * Initialize the arguments, set up helper classes and set up the CLI environment 43 * 44 * @param bool $autocatch should exceptions be catched and handled automatically? 45 */ 46 public function __construct($autocatch = true) 47 { 48 if ($autocatch) { 49 set_exception_handler(array($this, 'fatal')); 50 } 51 52 $this->colors = new Colors(); 53 $this->options = new Options($this->colors); 54 } 55 56 /** 57 * Register options and arguments on the given $options object 58 * 59 * @param Options $options 60 * @return void 61 * 62 * @throws Exception 63 */ 64 abstract protected function setup(Options $options); 65 66 /** 67 * Your main program 68 * 69 * Arguments and options have been parsed when this is run 70 * 71 * @param Options $options 72 * @return void 73 * 74 * @throws Exception 75 */ 76 abstract protected function main(Options $options); 77 78 /** 79 * Execute the CLI program 80 * 81 * Executes the setup() routine, adds default options, initiate the options parsing and argument checking 82 * and finally executes main() - Each part is split into their own protected function below, so behaviour 83 * can easily be overwritten 84 * 85 * @throws Exception 86 */ 87 public function run() 88 { 89 if ('cli' != php_sapi_name()) { 90 throw new Exception('This has to be run from the command line'); 91 } 92 93 $this->setup($this->options); 94 $this->registerDefaultOptions(); 95 $this->parseOptions(); 96 $this->handleDefaultOptions(); 97 $this->setupLogging(); 98 $this->checkArguments(); 99 $this->execute(); 100 } 101 102 // region run handlers - for easier overriding 103 104 /** 105 * Add the default help, color and log options 106 */ 107 protected function registerDefaultOptions() 108 { 109 $this->options->registerOption( 110 'help', 111 'Display this help screen and exit immediately.', 112 'h' 113 ); 114 $this->options->registerOption( 115 'no-colors', 116 'Do not use any colors in output. Useful when piping output to other tools or files.' 117 ); 118 $this->options->registerOption( 119 'loglevel', 120 'Minimum level of messages to display. Default is ' . $this->colors->wrap($this->logdefault, Colors::C_CYAN) . '. ' . 121 'Valid levels are: debug, info, notice, success, warning, error, critical, alert, emergency.', 122 null, 123 'level' 124 ); 125 } 126 127 /** 128 * Handle the default options 129 */ 130 protected function handleDefaultOptions() 131 { 132 if ($this->options->getOpt('no-colors')) { 133 $this->colors->disable(); 134 } 135 if ($this->options->getOpt('help')) { 136 echo $this->options->help(); 137 exit(0); 138 } 139 } 140 141 /** 142 * Handle the logging options 143 */ 144 protected function setupLogging() 145 { 146 $level = $this->options->getOpt('loglevel', $this->logdefault); 147 if (!isset($this->loglevel[$level])) $this->fatal('Unknown log level'); 148 foreach (array_keys($this->loglevel) as $l) { 149 if ($l == $level) break; 150 unset($this->loglevel[$l]); 151 } 152 } 153 154 /** 155 * Wrapper around the option parsing 156 */ 157 protected function parseOptions() 158 { 159 $this->options->parseOptions(); 160 } 161 162 /** 163 * Wrapper around the argument checking 164 */ 165 protected function checkArguments() 166 { 167 $this->options->checkArguments(); 168 } 169 170 /** 171 * Wrapper around main 172 */ 173 protected function execute() 174 { 175 $this->main($this->options); 176 } 177 178 // endregion 179 180 // region logging 181 182 /** 183 * Exits the program on a fatal error 184 * 185 * @param \Exception|string $error either an exception or an error message 186 * @param array $context 187 */ 188 public function fatal($error, array $context = array()) 189 { 190 $code = 0; 191 if (is_object($error) && is_a($error, 'Exception')) { 192 /** @var Exception $error */ 193 $this->logMessage('debug', get_class($error) . ' caught in ' . $error->getFile() . ':' . $error->getLine()); 194 $this->logMessage('debug', $error->getTraceAsString()); 195 $code = $error->getCode(); 196 $error = $error->getMessage(); 197 198 } 199 if (!$code) { 200 $code = Exception::E_ANY; 201 } 202 203 $this->logMessage('critical', $error, $context); 204 exit($code); 205 } 206 207 /** 208 * Normal, positive outcome (This is not a PSR-3 level) 209 * 210 * @param string $string 211 * @param array $context 212 */ 213 public function success($string, array $context = array()) 214 { 215 $this->logMessage('success', $string, $context); 216 } 217 218 /** 219 * @param string $level 220 * @param string $message 221 * @param array $context 222 */ 223 protected function logMessage($level, $message, array $context = array()) 224 { 225 // is this log level wanted? 226 if (!isset($this->loglevel[$level])) return; 227 228 /** @var string $prefix */ 229 /** @var string $color */ 230 /** @var resource $channel */ 231 list($prefix, $color, $channel) = $this->loglevel[$level]; 232 if (!$this->colors->isEnabled()) $prefix = ''; 233 234 $message = $this->interpolate($message, $context); 235 $this->colors->ptln($prefix . $message, $color, $channel); 236 } 237 238 /** 239 * Interpolates context values into the message placeholders. 240 * 241 * @param $message 242 * @param array $context 243 * @return string 244 */ 245 protected function interpolate($message, array $context = array()) 246 { 247 // build a replacement array with braces around the context keys 248 $replace = array(); 249 foreach ($context as $key => $val) { 250 // check that the value can be casted to string 251 if (!is_array($val) && (!is_object($val) || method_exists($val, '__toString'))) { 252 $replace['{' . $key . '}'] = $val; 253 } 254 } 255 256 // interpolate replacement values into the message and return 257 return strtr((string)$message, $replace); 258 } 259 260 // endregion 261} 262