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