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