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