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