12afbbbaeSAndreas Gohr<?php 22afbbbaeSAndreas Gohr 32afbbbaeSAndreas Gohrnamespace splitbrain\phpcli; 42afbbbaeSAndreas Gohr 52afbbbaeSAndreas Gohr/** 62afbbbaeSAndreas Gohr * Class CLIBase 72afbbbaeSAndreas Gohr * 82afbbbaeSAndreas Gohr * All base functionality is implemented here. 92afbbbaeSAndreas Gohr * 102afbbbaeSAndreas Gohr * Your commandline should not inherit from this class, but from one of the *CLI* classes 112afbbbaeSAndreas Gohr * 122afbbbaeSAndreas Gohr * @author Andreas Gohr <andi@splitbrain.org> 132afbbbaeSAndreas Gohr * @license MIT 142afbbbaeSAndreas Gohr */ 152afbbbaeSAndreas Gohrabstract class Base 162afbbbaeSAndreas Gohr{ 172afbbbaeSAndreas Gohr /** @var string the executed script itself */ 182afbbbaeSAndreas Gohr protected $bin; 192afbbbaeSAndreas Gohr /** @var Options the option parser */ 202afbbbaeSAndreas Gohr protected $options; 212afbbbaeSAndreas Gohr /** @var Colors */ 222afbbbaeSAndreas Gohr public $colors; 232afbbbaeSAndreas Gohr 242cadabe7SAndreas Gohr /** @var array PSR-3 compatible loglevels and their prefix, color, output channel, enabled status */ 252afbbbaeSAndreas Gohr protected $loglevel = array( 262cadabe7SAndreas Gohr 'debug' => array( 272cadabe7SAndreas Gohr 'icon' => '', 282cadabe7SAndreas Gohr 'color' => Colors::C_RESET, 292cadabe7SAndreas Gohr 'channel' => STDOUT, 302cadabe7SAndreas Gohr 'enabled' => true 312cadabe7SAndreas Gohr ), 322cadabe7SAndreas Gohr 'info' => array( 332cadabe7SAndreas Gohr 'icon' => 'ℹ ', 342cadabe7SAndreas Gohr 'color' => Colors::C_CYAN, 352cadabe7SAndreas Gohr 'channel' => STDOUT, 362cadabe7SAndreas Gohr 'enabled' => true 372cadabe7SAndreas Gohr ), 382cadabe7SAndreas Gohr 'notice' => array( 392cadabe7SAndreas Gohr 'icon' => '☛ ', 402cadabe7SAndreas Gohr 'color' => Colors::C_CYAN, 412cadabe7SAndreas Gohr 'channel' => STDOUT, 422cadabe7SAndreas Gohr 'enabled' => true 432cadabe7SAndreas Gohr ), 442cadabe7SAndreas Gohr 'success' => array( 452cadabe7SAndreas Gohr 'icon' => '✓ ', 462cadabe7SAndreas Gohr 'color' => Colors::C_GREEN, 472cadabe7SAndreas Gohr 'channel' => STDOUT, 482cadabe7SAndreas Gohr 'enabled' => true 492cadabe7SAndreas Gohr ), 502cadabe7SAndreas Gohr 'warning' => array( 512cadabe7SAndreas Gohr 'icon' => '⚠ ', 522cadabe7SAndreas Gohr 'color' => Colors::C_BROWN, 532cadabe7SAndreas Gohr 'channel' => STDERR, 542cadabe7SAndreas Gohr 'enabled' => true 552cadabe7SAndreas Gohr ), 562cadabe7SAndreas Gohr 'error' => array( 572cadabe7SAndreas Gohr 'icon' => '✗ ', 582cadabe7SAndreas Gohr 'color' => Colors::C_RED, 592cadabe7SAndreas Gohr 'channel' => STDERR, 602cadabe7SAndreas Gohr 'enabled' => true 612cadabe7SAndreas Gohr ), 622cadabe7SAndreas Gohr 'critical' => array( 632cadabe7SAndreas Gohr 'icon' => '☠ ', 642cadabe7SAndreas Gohr 'color' => Colors::C_LIGHTRED, 652cadabe7SAndreas Gohr 'channel' => STDERR, 662cadabe7SAndreas Gohr 'enabled' => true 672cadabe7SAndreas Gohr ), 682cadabe7SAndreas Gohr 'alert' => array( 692cadabe7SAndreas Gohr 'icon' => '✖ ', 702cadabe7SAndreas Gohr 'color' => Colors::C_LIGHTRED, 712cadabe7SAndreas Gohr 'channel' => STDERR, 722cadabe7SAndreas Gohr 'enabled' => true 732cadabe7SAndreas Gohr ), 742cadabe7SAndreas Gohr 'emergency' => array( 752cadabe7SAndreas Gohr 'icon' => '✘ ', 762cadabe7SAndreas Gohr 'color' => Colors::C_LIGHTRED, 772cadabe7SAndreas Gohr 'channel' => STDERR, 782cadabe7SAndreas Gohr 'enabled' => true 792cadabe7SAndreas Gohr ), 802afbbbaeSAndreas Gohr ); 812afbbbaeSAndreas Gohr 822cadabe7SAndreas Gohr /** @var string default log level */ 832afbbbaeSAndreas Gohr protected $logdefault = 'info'; 842afbbbaeSAndreas Gohr 852afbbbaeSAndreas Gohr /** 862afbbbaeSAndreas Gohr * constructor 872afbbbaeSAndreas Gohr * 882afbbbaeSAndreas Gohr * Initialize the arguments, set up helper classes and set up the CLI environment 892afbbbaeSAndreas Gohr * 902afbbbaeSAndreas Gohr * @param bool $autocatch should exceptions be catched and handled automatically? 912afbbbaeSAndreas Gohr */ 922afbbbaeSAndreas Gohr public function __construct($autocatch = true) 932afbbbaeSAndreas Gohr { 942afbbbaeSAndreas Gohr if ($autocatch) { 952afbbbaeSAndreas Gohr set_exception_handler(array($this, 'fatal')); 962afbbbaeSAndreas Gohr } 97*9520a435SAndreas Gohr $this->setLogLevel($this->logdefault); 982afbbbaeSAndreas Gohr $this->colors = new Colors(); 992afbbbaeSAndreas Gohr $this->options = new Options($this->colors); 1002afbbbaeSAndreas Gohr } 1012afbbbaeSAndreas Gohr 1022afbbbaeSAndreas Gohr /** 1032afbbbaeSAndreas Gohr * Register options and arguments on the given $options object 1042afbbbaeSAndreas Gohr * 1052afbbbaeSAndreas Gohr * @param Options $options 1062afbbbaeSAndreas Gohr * @return void 1072afbbbaeSAndreas Gohr * 1082afbbbaeSAndreas Gohr * @throws Exception 1092afbbbaeSAndreas Gohr */ 1102afbbbaeSAndreas Gohr abstract protected function setup(Options $options); 1112afbbbaeSAndreas Gohr 1122afbbbaeSAndreas Gohr /** 1132afbbbaeSAndreas Gohr * Your main program 1142afbbbaeSAndreas Gohr * 1152afbbbaeSAndreas Gohr * Arguments and options have been parsed when this is run 1162afbbbaeSAndreas Gohr * 1172afbbbaeSAndreas Gohr * @param Options $options 1182afbbbaeSAndreas Gohr * @return void 1192afbbbaeSAndreas Gohr * 1202afbbbaeSAndreas Gohr * @throws Exception 1212afbbbaeSAndreas Gohr */ 1222afbbbaeSAndreas Gohr abstract protected function main(Options $options); 1232afbbbaeSAndreas Gohr 1242afbbbaeSAndreas Gohr /** 1252afbbbaeSAndreas Gohr * Execute the CLI program 1262afbbbaeSAndreas Gohr * 1272afbbbaeSAndreas Gohr * Executes the setup() routine, adds default options, initiate the options parsing and argument checking 1282afbbbaeSAndreas Gohr * and finally executes main() - Each part is split into their own protected function below, so behaviour 1292afbbbaeSAndreas Gohr * can easily be overwritten 1302afbbbaeSAndreas Gohr * 1312afbbbaeSAndreas Gohr * @throws Exception 1322afbbbaeSAndreas Gohr */ 1332afbbbaeSAndreas Gohr public function run() 1342afbbbaeSAndreas Gohr { 1352afbbbaeSAndreas Gohr if ('cli' != php_sapi_name()) { 1362afbbbaeSAndreas Gohr throw new Exception('This has to be run from the command line'); 1372afbbbaeSAndreas Gohr } 1382afbbbaeSAndreas Gohr 1392afbbbaeSAndreas Gohr $this->setup($this->options); 1402afbbbaeSAndreas Gohr $this->registerDefaultOptions(); 1412afbbbaeSAndreas Gohr $this->parseOptions(); 1422afbbbaeSAndreas Gohr $this->handleDefaultOptions(); 1432afbbbaeSAndreas Gohr $this->setupLogging(); 1442afbbbaeSAndreas Gohr $this->checkArguments(); 1452afbbbaeSAndreas Gohr $this->execute(); 1462afbbbaeSAndreas Gohr } 1472afbbbaeSAndreas Gohr 1482afbbbaeSAndreas Gohr // region run handlers - for easier overriding 1492afbbbaeSAndreas Gohr 1502afbbbaeSAndreas Gohr /** 1512afbbbaeSAndreas Gohr * Add the default help, color and log options 1522afbbbaeSAndreas Gohr */ 1532afbbbaeSAndreas Gohr protected function registerDefaultOptions() 1542afbbbaeSAndreas Gohr { 1552afbbbaeSAndreas Gohr $this->options->registerOption( 1562afbbbaeSAndreas Gohr 'help', 1572afbbbaeSAndreas Gohr 'Display this help screen and exit immediately.', 1582afbbbaeSAndreas Gohr 'h' 1592afbbbaeSAndreas Gohr ); 1602afbbbaeSAndreas Gohr $this->options->registerOption( 1612afbbbaeSAndreas Gohr 'no-colors', 1622afbbbaeSAndreas Gohr 'Do not use any colors in output. Useful when piping output to other tools or files.' 1632afbbbaeSAndreas Gohr ); 1642afbbbaeSAndreas Gohr $this->options->registerOption( 1652afbbbaeSAndreas Gohr 'loglevel', 1662afbbbaeSAndreas Gohr 'Minimum level of messages to display. Default is ' . $this->colors->wrap($this->logdefault, Colors::C_CYAN) . '. ' . 1672afbbbaeSAndreas Gohr 'Valid levels are: debug, info, notice, success, warning, error, critical, alert, emergency.', 1682afbbbaeSAndreas Gohr null, 1692afbbbaeSAndreas Gohr 'level' 1702afbbbaeSAndreas Gohr ); 1712afbbbaeSAndreas Gohr } 1722afbbbaeSAndreas Gohr 1732afbbbaeSAndreas Gohr /** 1742afbbbaeSAndreas Gohr * Handle the default options 1752afbbbaeSAndreas Gohr */ 1762afbbbaeSAndreas Gohr protected function handleDefaultOptions() 1772afbbbaeSAndreas Gohr { 1782afbbbaeSAndreas Gohr if ($this->options->getOpt('no-colors')) { 1792afbbbaeSAndreas Gohr $this->colors->disable(); 1802afbbbaeSAndreas Gohr } 1812afbbbaeSAndreas Gohr if ($this->options->getOpt('help')) { 1822afbbbaeSAndreas Gohr echo $this->options->help(); 1832afbbbaeSAndreas Gohr exit(0); 1842afbbbaeSAndreas Gohr } 1852afbbbaeSAndreas Gohr } 1862afbbbaeSAndreas Gohr 1872afbbbaeSAndreas Gohr /** 1882afbbbaeSAndreas Gohr * Handle the logging options 1892afbbbaeSAndreas Gohr */ 1902afbbbaeSAndreas Gohr protected function setupLogging() 1912afbbbaeSAndreas Gohr { 1922afbbbaeSAndreas Gohr $level = $this->options->getOpt('loglevel', $this->logdefault); 1932cadabe7SAndreas Gohr $this->setLogLevel($level); 1942afbbbaeSAndreas Gohr } 1952afbbbaeSAndreas Gohr 1962afbbbaeSAndreas Gohr /** 1972afbbbaeSAndreas Gohr * Wrapper around the option parsing 1982afbbbaeSAndreas Gohr */ 1992afbbbaeSAndreas Gohr protected function parseOptions() 2002afbbbaeSAndreas Gohr { 2012afbbbaeSAndreas Gohr $this->options->parseOptions(); 2022afbbbaeSAndreas Gohr } 2032afbbbaeSAndreas Gohr 2042afbbbaeSAndreas Gohr /** 2052afbbbaeSAndreas Gohr * Wrapper around the argument checking 2062afbbbaeSAndreas Gohr */ 2072afbbbaeSAndreas Gohr protected function checkArguments() 2082afbbbaeSAndreas Gohr { 2092afbbbaeSAndreas Gohr $this->options->checkArguments(); 2102afbbbaeSAndreas Gohr } 2112afbbbaeSAndreas Gohr 2122afbbbaeSAndreas Gohr /** 2132afbbbaeSAndreas Gohr * Wrapper around main 2142afbbbaeSAndreas Gohr */ 2152afbbbaeSAndreas Gohr protected function execute() 2162afbbbaeSAndreas Gohr { 2172afbbbaeSAndreas Gohr $this->main($this->options); 2182afbbbaeSAndreas Gohr } 2192afbbbaeSAndreas Gohr 2202afbbbaeSAndreas Gohr // endregion 2212afbbbaeSAndreas Gohr 2222afbbbaeSAndreas Gohr // region logging 2232afbbbaeSAndreas Gohr 2242afbbbaeSAndreas Gohr /** 2252cadabe7SAndreas Gohr * Set the current log level 2262cadabe7SAndreas Gohr * 2272cadabe7SAndreas Gohr * @param string $level 2282cadabe7SAndreas Gohr */ 2292cadabe7SAndreas Gohr public function setLogLevel($level) 2302cadabe7SAndreas Gohr { 2312cadabe7SAndreas Gohr if (!isset($this->loglevel[$level])) $this->fatal('Unknown log level'); 2322cadabe7SAndreas Gohr $enable = false; 2332cadabe7SAndreas Gohr foreach (array_keys($this->loglevel) as $l) { 2342cadabe7SAndreas Gohr if ($l == $level) $enable = true; 2352cadabe7SAndreas Gohr $this->loglevel[$l]['enabled'] = $enable; 2362cadabe7SAndreas Gohr } 2372cadabe7SAndreas Gohr } 2382cadabe7SAndreas Gohr 2392cadabe7SAndreas Gohr /** 2402cadabe7SAndreas Gohr * Check if a message with the given level should be logged 2412cadabe7SAndreas Gohr * 2422cadabe7SAndreas Gohr * @param string $level 2432cadabe7SAndreas Gohr * @return bool 2442cadabe7SAndreas Gohr */ 2452cadabe7SAndreas Gohr public function isLogLevelEnabled($level) 2462cadabe7SAndreas Gohr { 2472cadabe7SAndreas Gohr if (!isset($this->loglevel[$level])) $this->fatal('Unknown log level'); 2482cadabe7SAndreas Gohr return $this->loglevel[$level]['enabled']; 2492cadabe7SAndreas Gohr } 2502cadabe7SAndreas Gohr 2512cadabe7SAndreas Gohr /** 2522afbbbaeSAndreas Gohr * Exits the program on a fatal error 2532afbbbaeSAndreas Gohr * 2542afbbbaeSAndreas Gohr * @param \Exception|string $error either an exception or an error message 2552afbbbaeSAndreas Gohr * @param array $context 2562afbbbaeSAndreas Gohr */ 2572afbbbaeSAndreas Gohr public function fatal($error, array $context = array()) 2582afbbbaeSAndreas Gohr { 2592afbbbaeSAndreas Gohr $code = 0; 2602afbbbaeSAndreas Gohr if (is_object($error) && is_a($error, 'Exception')) { 2612afbbbaeSAndreas Gohr /** @var Exception $error */ 2622afbbbaeSAndreas Gohr $this->logMessage('debug', get_class($error) . ' caught in ' . $error->getFile() . ':' . $error->getLine()); 2632afbbbaeSAndreas Gohr $this->logMessage('debug', $error->getTraceAsString()); 2642afbbbaeSAndreas Gohr $code = $error->getCode(); 2652afbbbaeSAndreas Gohr $error = $error->getMessage(); 2662afbbbaeSAndreas Gohr 2672afbbbaeSAndreas Gohr } 2682afbbbaeSAndreas Gohr if (!$code) { 2692afbbbaeSAndreas Gohr $code = Exception::E_ANY; 2702afbbbaeSAndreas Gohr } 2712afbbbaeSAndreas Gohr 2722afbbbaeSAndreas Gohr $this->logMessage('critical', $error, $context); 2732afbbbaeSAndreas Gohr exit($code); 2742afbbbaeSAndreas Gohr } 2752afbbbaeSAndreas Gohr 2762afbbbaeSAndreas Gohr /** 2772afbbbaeSAndreas Gohr * Normal, positive outcome (This is not a PSR-3 level) 2782afbbbaeSAndreas Gohr * 2792afbbbaeSAndreas Gohr * @param string $string 2802afbbbaeSAndreas Gohr * @param array $context 2812afbbbaeSAndreas Gohr */ 2822afbbbaeSAndreas Gohr public function success($string, array $context = array()) 2832afbbbaeSAndreas Gohr { 2842afbbbaeSAndreas Gohr $this->logMessage('success', $string, $context); 2852afbbbaeSAndreas Gohr } 2862afbbbaeSAndreas Gohr 2872afbbbaeSAndreas Gohr /** 2882afbbbaeSAndreas Gohr * @param string $level 2892afbbbaeSAndreas Gohr * @param string $message 2902afbbbaeSAndreas Gohr * @param array $context 2912afbbbaeSAndreas Gohr */ 2922afbbbaeSAndreas Gohr protected function logMessage($level, $message, array $context = array()) 2932afbbbaeSAndreas Gohr { 2942cadabe7SAndreas Gohr // unknown level is always an error 2952cadabe7SAndreas Gohr if (!isset($this->loglevel[$level])) $level = 'error'; 2962afbbbaeSAndreas Gohr 2972cadabe7SAndreas Gohr $info = $this->loglevel[$level]; 2982cadabe7SAndreas Gohr if (!$this->isLogLevelEnabled($level)) return; // no logging for this level 2992afbbbaeSAndreas Gohr 3002afbbbaeSAndreas Gohr $message = $this->interpolate($message, $context); 3012cadabe7SAndreas Gohr 3022cadabe7SAndreas Gohr // when colors are wanted, we also add the icon 3032cadabe7SAndreas Gohr if ($this->colors->isEnabled()) { 3042cadabe7SAndreas Gohr $message = $info['icon'] . $message; 3052cadabe7SAndreas Gohr } 3062cadabe7SAndreas Gohr 3072cadabe7SAndreas Gohr $this->colors->ptln($message, $info['color'], $info['channel']); 3082afbbbaeSAndreas Gohr } 3092afbbbaeSAndreas Gohr 3102afbbbaeSAndreas Gohr /** 3112afbbbaeSAndreas Gohr * Interpolates context values into the message placeholders. 3122afbbbaeSAndreas Gohr * 3132afbbbaeSAndreas Gohr * @param $message 3142afbbbaeSAndreas Gohr * @param array $context 3152afbbbaeSAndreas Gohr * @return string 3162afbbbaeSAndreas Gohr */ 3172afbbbaeSAndreas Gohr protected function interpolate($message, array $context = array()) 3182afbbbaeSAndreas Gohr { 3192afbbbaeSAndreas Gohr // build a replacement array with braces around the context keys 3202afbbbaeSAndreas Gohr $replace = array(); 3212afbbbaeSAndreas Gohr foreach ($context as $key => $val) { 3222afbbbaeSAndreas Gohr // check that the value can be casted to string 3232afbbbaeSAndreas Gohr if (!is_array($val) && (!is_object($val) || method_exists($val, '__toString'))) { 3242afbbbaeSAndreas Gohr $replace['{' . $key . '}'] = $val; 3252afbbbaeSAndreas Gohr } 3262afbbbaeSAndreas Gohr } 3272afbbbaeSAndreas Gohr 3282afbbbaeSAndreas Gohr // interpolate replacement values into the message and return 3292afbbbaeSAndreas Gohr return strtr((string)$message, $replace); 3302afbbbaeSAndreas Gohr } 3312afbbbaeSAndreas Gohr 3322afbbbaeSAndreas Gohr // endregion 3332afbbbaeSAndreas Gohr} 334