xref: /dokuwiki/vendor/splitbrain/php-cli/src/CLI.php (revision 13ce475d48f846c56b4bf52ae5fc6c1216dc8deb)
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