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