Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
84.39% covered (success)
84.39%
146 / 173
90.62% covered (success)
90.62%
29 / 32
CRAP
0.00% covered (danger)
0.00%
0 / 1
Console
84.39% covered (success)
84.39%
146 / 173
90.62% covered (success)
90.62%
29 / 32
131.03
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 setWidth
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setIndent
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setHeader
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setFooter
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setHeaderSent
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setHelpColors
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 getWidth
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getIndent
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getHeader
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 getFooter
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 getHeaderSent
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getHelpColors
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getServer
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 getEnv
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 addCommand
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 addCommands
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getCommands
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getCommand
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 hasCommand
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getCommandsFromRoutes
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
6
 addCommandsFromRoutes
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 help
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 colorize
83.33% covered (success)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
4.07
 prompt
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
156
 append
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
5
 write
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 send
75.00% covered (success)
75.00%
6 / 8
0.00% covered (danger)
0.00%
0 / 1
5.39
 displayHelp
100.00% covered (success)
100.00%
52 / 52
100.00% covered (success)
100.00%
1 / 1
19
 clear
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getColorCode
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 formatTemplate
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2/**
3 * Pop PHP Framework (http://www.popphp.org/)
4 *
5 * @link       https://github.com/popphp/popphp-framework
6 * @author     Nick Sagona, III <dev@nolainteractive.com>
7 * @copyright  Copyright (c) 2009-2023 NOLA Interactive, LLC. (http://www.nolainteractive.com)
8 * @license    http://www.popphp.org/license     New BSD License
9 */
10
11/**
12 * @namespace
13 */
14namespace Pop\Console;
15
16use Pop\Router\Match;
17
18/**
19 * Console class
20 *
21 * @category   Pop
22 * @package    Pop\Console
23 * @author     Nick Sagona, III <dev@nolainteractive.com>
24 * @copyright  Copyright (c) 2009-2023 NOLA Interactive, LLC. (http://www.nolainteractive.com)
25 * @license    http://www.popphp.org/license     New BSD License
26 * @version    3.2.0
27 */
28class Console
29{
30
31    /**
32     * Color indices
33     */
34    const NORMAL        = 0;
35    const BLACK         = 1;
36    const RED           = 2;
37    const GREEN         = 3;
38    const YELLOW        = 4;
39    const BLUE          = 5;
40    const MAGENTA       = 6;
41    const CYAN          = 7;
42    const WHITE         = 8;
43    const GRAY          = 9;
44    const BOLD_RED     = 10;
45    const BOLD_GREEN   = 11;
46    const BOLD_YELLOW  = 12;
47    const BOLD_BLUE    = 13;
48    const BOLD_MAGENTA = 14;
49    const BOLD_CYAN    = 15;
50    const BOLD_WHITE   = 16;
51
52    /**
53     * Console character width
54     * @var int
55     */
56    protected $width = 80;
57
58    /**
59     * Console indentation
60     * @var string
61     */
62    protected $indent = null;
63
64    /**
65     * Console response body
66     * @var string
67     */
68    protected $response = null;
69
70    /**
71     * Commands
72     * @var array
73     */
74    protected $commands = [];
75
76    /**
77     * Console header
78     * @var string
79     */
80    protected $header = null;
81
82    /**
83     * Flag for if console header has been sent
84     * @var boolean
85     */
86    protected $headerSent = false;
87
88    /**
89     * Console footer
90     * @var string
91     */
92    protected $footer = null;
93
94    /**
95     * Help colors
96     * @var array
97     */
98    protected $helpColors = [];
99
100    /**
101     * SERVER array
102     * @var array
103     */
104    protected $server = [];
105
106    /**
107     * ENV array
108     * @var array
109     */
110    protected $env    = [];
111
112    /**
113     * Color map of ansi values
114     *
115     * @var array
116     */
117    protected static $colorMap = [
118        'foreground' => [
119            self::NORMAL       => '22;39',
120            self::BLACK        => '0;30',
121            self::RED          => '0;31',
122            self::GREEN        => '0;32',
123            self::YELLOW       => '0;33',
124            self::BLUE         => '0;34',
125            self::MAGENTA      => '0;35',
126            self::CYAN         => '0;36',
127            self::WHITE        => '0;37',
128            self::GRAY         => '1;30',
129            self::BOLD_RED     => '1;31',
130            self::BOLD_GREEN   => '1;32',
131            self::BOLD_YELLOW  => '1;33',
132            self::BOLD_BLUE    => '1;34',
133            self::BOLD_MAGENTA => '1;35',
134            self::BOLD_CYAN    => '1;36',
135            self::BOLD_WHITE   => '1;37'
136        ],
137        'background' => [
138            self::NORMAL       => '0;49',
139            self::BLACK        => '40',
140            self::RED          => '41',
141            self::GREEN        => '42',
142            self::YELLOW       => '43',
143            self::BLUE         => '44',
144            self::MAGENTA      => '45',
145            self::CYAN         => '46',
146            self::WHITE        => '47'
147        ]
148    ];
149
150    /**
151     * Instantiate a new console object
152     *
153     * @param  int    $width
154     * @param  string $indent
155     */
156    public function __construct($width = 80, $indent = '    ')
157    {
158        $this->setWidth($width);
159        $this->setIndent($indent);
160
161        $this->server = (isset($_SERVER)) ? $_SERVER : [];
162        $this->env    = (isset($_ENV))    ? $_ENV    : [];
163    }
164
165    /**
166     * Set the wrap width of the console object
167     *
168     * @param  int $width
169     * @return Console
170     */
171    public function setWidth($width)
172    {
173        $this->width = (int)$width;
174        return $this;
175    }
176
177    /**
178     * Set the indentation of the console object
179     *
180     * @param  string $indent
181     * @return Console
182     */
183    public function setIndent($indent = null)
184    {
185        $this->indent = $indent;
186        return $this;
187    }
188
189    /**
190     * Set the console header
191     *
192     * @param  string $header
193     * @return Console
194     */
195    public function setHeader($header)
196    {
197        $this->header = $header;
198        return $this;
199    }
200
201    /**
202     * Set the console footer
203     *
204     * @param  string $footer
205     * @return Console
206     */
207    public function setFooter($footer)
208    {
209        $this->footer = $footer;
210        return $this;
211    }
212
213    /**
214     * Set the console header sent flag
215     *
216     * @param  boolean $headerSent
217     * @return Console
218     */
219    public function setHeaderSent($headerSent = true)
220    {
221        $this->headerSent = (bool)$headerSent;
222        return $this;
223    }
224
225    /**
226     * Set the console help colors
227     *
228     * @param  int $color1
229     * @param  int $color2
230     * @param  int $color3
231     * @return Console
232     */
233    public function setHelpColors($color1, $color2 = null, $color3 = null)
234    {
235        $this->helpColors = [
236            $color1
237        ];
238        if (null !== $color2) {
239            $this->helpColors[] = $color2;
240        }
241        if (null !== $color3) {
242            $this->helpColors[] = $color3;
243        }
244
245        return $this;
246    }
247
248    /**
249     * Get the wrap width of the console object
250     *
251     * @return int
252     */
253    public function getWidth()
254    {
255        return $this->width;
256    }
257
258    /**
259     * Get the indentation of the console object
260     *
261     * @return string
262     */
263    public function getIndent()
264    {
265        return $this->indent;
266    }
267
268    /**
269     * Get the console header
270     *
271     * @param  boolean $formatted
272     * @return string
273     */
274    public function getHeader($formatted = false)
275    {
276        return ($formatted) ? $this->formatTemplate($this->header) : $this->header;
277    }
278
279    /**
280     * Get the console footer
281     *
282     * @param  boolean $formatted
283     * @return string
284     */
285    public function getFooter($formatted = false)
286    {
287        return ($formatted) ? $this->formatTemplate($this->footer) : $this->footer;
288    }
289
290    /**
291     * Get the console header sent flag
292     *
293     * @return boolean
294     */
295    public function getHeaderSent()
296    {
297        return $this->headerSent;
298    }
299
300    /**
301     * Get the console help colors
302     *
303     * @return array
304     */
305    public function getHelpColors()
306    {
307        return $this->helpColors;
308    }
309
310    /**
311     * Get a value from $_SERVER, or the whole array
312     *
313     * @param  string $key
314     * @return string|array
315     */
316    public function getServer($key = null)
317    {
318        if (null === $key) {
319            return $this->server;
320        } else {
321            return (isset($this->server[$key])) ? $this->server[$key] : null;
322        }
323    }
324
325    /**
326     * Get a value from $_ENV, or the whole array
327     *
328     * @param  string $key
329     * @return string|array
330     */
331    public function getEnv($key = null)
332    {
333        if (null === $key) {
334            return $this->env;
335        } else {
336            return (isset($this->env[$key])) ? $this->env[$key] : null;
337        }
338    }
339
340    /**
341     * Add a command
342     *
343     * @param  Command $command
344     * @return Console
345     */
346    public function addCommand(Command $command)
347    {
348        $this->commands[$command->getName()] = $command;
349        return $this;
350    }
351
352    /**
353     * Add commands
354     *
355     * @param  array $commands
356     * @return Console
357     */
358    public function addCommands(array $commands)
359    {
360        foreach ($commands as $command) {
361            $this->addCommand($command);
362        }
363        return $this;
364    }
365
366    /**
367     * Get commands
368     *
369     * @return array
370     */
371    public function getCommands()
372    {
373        return $this->commands;
374    }
375
376    /**
377     * Get a command
378     *
379     * @param  string $command
380     * @return Command
381     */
382    public function getCommand($command)
383    {
384        return (isset($this->commands[$command])) ? $this->commands[$command] : null;
385    }
386
387    /**
388     * Check if the console object has a command
389     *
390     * @param  string $command
391     * @return boolean
392     */
393    public function hasCommand($command)
394    {
395        return isset($this->commands[$command]);
396    }
397
398    /**
399     * Get commands from routes
400     *
401     * @param  Match\Cli $routeMatch
402     * @param  string    $scriptName
403     * @return array
404     */
405    public function getCommandsFromRoutes(Match\Cli $routeMatch, $scriptName = null)
406    {
407        $routeMatch->match();
408
409        $commandRoutes = $routeMatch->getRoutes();
410        $commands      = $routeMatch->getCommands();
411        $commandsToAdd = [];
412
413        foreach ($commands as $name => $command) {
414            $commandName = implode(' ', $command);
415            $params      = trim(substr((string)$name, strlen((string)$commandName)));
416            $params      = (!empty($params)) ? $params : null;
417            $help        = (isset($commandRoutes[$name]) && isset($commandRoutes[$name]['help'])) ?
418                $commandRoutes[$name]['help'] : null;
419
420            if (null !== $scriptName) {
421                $commandName = $scriptName . ' ' . $commandName;
422            }
423
424            $commandsToAdd[] = new Command($commandName, $params, $help);
425        }
426
427        return $commandsToAdd;
428    }
429
430    /**
431     * Add commands from routes
432     *
433     * @param  Match\Cli $routeMatch
434     * @param  string    $scriptName
435     * @return Console
436     */
437    public function addCommandsFromRoutes(Match\Cli $routeMatch, $scriptName = null)
438    {
439        $commands = $this->getCommandsFromRoutes($routeMatch, $scriptName);
440
441        if (!empty($commands)) {
442            $this->addCommands($commands);
443        }
444
445        return $this;
446    }
447
448    /**
449     * Get a help
450     *
451     * @param  string $command
452     * @return string
453     */
454    public function help($command = null)
455    {
456        if (null !== $command) {
457            return (isset($this->commands[$command])) ? $this->commands[$command]->getHelp() : null;
458        } else {
459            $this->displayHelp();
460        }
461    }
462
463    /**
464     * Colorize a string for output
465     *
466     * @param  string $string
467     * @param  int    $fg
468     * @param  int    $bg
469     * @return string
470     */
471    public function colorize($string, $fg = null, $bg = null)
472    {
473        if (stripos(PHP_OS, 'win') === false) {
474            $fgColor = $this->getColorCode($fg, 'foreground');
475            $bgColor = $this->getColorCode($bg, 'background');
476            return ($fgColor !== null ? "\x1b[" . $fgColor . 'm' : '') .
477                ($bgColor !== null ? "\x1b[" . $bgColor . 'm' : '') . $string . "\x1b[0m";
478        } else {
479            return $string;
480        }
481    }
482
483    /**
484     * Get input from the prompt
485     *
486     * @param  string  $prompt
487     * @param  array   $options
488     * @param  boolean $caseSensitive
489     * @param  int     $length
490     * @param  boolean $withHeaders
491     * @return string
492     */
493    public function prompt($prompt, array $options = null, $caseSensitive = false, $length = 500, $withHeaders = true)
494    {
495        if (($withHeaders) && (null !== $this->header)) {
496            $this->headerSent = true;
497            echo $this->formatTemplate($this->header) . $this->indent . $prompt;
498        } else {
499            echo $this->indent . $prompt;
500        }
501
502        $input = null;
503
504        if (null !== $options) {
505            $length = 0;
506            foreach ($options as $key => $value) {
507                $options[$key] = ($caseSensitive) ? $value : strtolower((string)$value);
508                if (strlen((string)$value) > $length) {
509                    $length = strlen((string)$value);
510                }
511            }
512
513            while (!in_array($input, $options)) {
514                if (null !== $input) {
515                    echo $this->indent . $prompt;
516                }
517                $promptInput = fopen('php://stdin', 'r');
518                $input       = fgets($promptInput, strlen((string)$prompt) . $length);
519                $input       = ($caseSensitive) ? rtrim($input) : strtolower(rtrim($input));
520                fclose($promptInput);
521            }
522        } else {
523            while (null === $input) {
524                $promptInput = fopen('php://stdin', 'r');
525                $input       = fgets($promptInput, strlen((string)$prompt) + $length);
526                $input       = ($caseSensitive) ? rtrim($input) : strtolower(rtrim($input));
527                fclose($promptInput);
528            }
529        }
530
531        return $input;
532    }
533
534    /**
535     * Append a string of text to the response body
536     *
537     * @param  string  $text
538     * @param  boolean $newline
539     * @return Console
540     */
541    public function append($text = null, $newline = true)
542    {
543        if ($this->width != 0) {
544            $lines = (strlen((string)$text) > $this->width) ?
545                explode(PHP_EOL, wordwrap($text, $this->width, PHP_EOL)) : [$text];
546        } else {
547            $lines = [$text];
548        }
549
550        foreach ($lines as $line) {
551            $this->response .=  $this->indent . $line . (($newline) ? PHP_EOL : null);
552        }
553        return $this;
554    }
555
556    /**
557     * Write a string of text to the response body and send the response
558     *
559     * @param  string $text
560     * @param  boolean $newline
561     * @param  boolean $withHeaders
562     * @return Console
563     */
564    public function write($text = null, $newline = true, $withHeaders = true)
565    {
566        $this->append($text, $newline);
567        $this->send($withHeaders);
568        return $this;
569    }
570
571    /**
572     * Send the response
573     *
574     * @param  boolean $withHeaders
575     * @return Console
576     */
577    public function send($withHeaders = true)
578    {
579        if ($withHeaders) {
580            if ((null !== $this->header) && !($this->headerSent)) {
581                $this->response = $this->formatTemplate($this->header) . $this->response;
582            }
583            if (null !== $this->footer) {
584                $this->response .= $this->formatTemplate($this->footer);
585            }
586        }
587
588        echo $this->response;
589        $this->response = null;
590        return $this;
591    }
592
593    /**
594     * Display console help
595     *
596     * @return void
597     */
598    public function displayHelp()
599    {
600        $this->response = null;
601        $commands       = [];
602        $commandLengths = [];
603
604        if (null !== $this->header) {
605            $this->response .= $this->formatTemplate($this->header);
606        }
607
608        foreach ($this->commands as $key => $command) {
609            $name   = $command->getName();
610            $params = $command->getParams();
611            $length = strlen((string)$name);
612
613            if (count($this->helpColors) > 0) {
614                if (strpos((string)$name, ' ') !== false) {
615                    $name1 = substr($name, 0, strpos($name, ' '));
616                    $name2 = substr($name, strpos($name, ' ') + 1);
617                    if (isset($this->helpColors[0])) {
618                        $name1 = $this->colorize($name1, $this->helpColors[0]);
619                    }
620                    if (isset($this->helpColors[1])) {
621                        $name2 = $this->colorize($name2, $this->helpColors[1]);
622                    }
623                    $name = $name1 . ' ' . $name2;
624                } else if (isset($this->helpColors[0])){
625                    $name = $this->colorize($name, $this->helpColors[0]);
626                }
627            }
628
629            if (null !== $params) {
630                $length += (strlen((string)$params) + 1);
631                $name   .= ' ' . ((isset($this->helpColors[2])) ? $this->colorize($params, $this->helpColors[2]) : $params);
632            }
633
634            $commands[$key]       = $this->indent . $name;
635            $commandLengths[$key] = $length;
636        }
637
638        $maxLength = max($commandLengths);
639        $wrapped   = false;
640        $i         = 0;
641
642        foreach ($commands as $key => $command) {
643            if ($this->commands[$key]->hasHelp()) {
644                $help = $this->commands[$key]->getHelp();
645                $pad  = ($commandLengths[$key] < $maxLength) ?
646                    str_repeat(' ', $maxLength - $commandLengths[$key]) . '    ' : '    ';
647
648                if (strlen((string)$this->commands[$key] . $pad . $help) > $this->width) {
649                    if (!$wrapped) {
650                        $this->response .= PHP_EOL;
651                    }
652
653                    $offset = $this->width - strlen((string)$this->commands[$key] . $pad);
654                    $lines  = explode(PHP_EOL, wordwrap($help, $offset, PHP_EOL));
655                    foreach ($lines as $i => $line) {
656                        $this->response .= ($i == 0) ?
657                            $command . $pad . $line . PHP_EOL :
658                            $this->indent . str_repeat(' ', strlen((string)$this->commands[$key])) . $pad . $line . PHP_EOL;
659                    }
660
661                    if ($i < count($commands) - 1) {
662                        $this->response .= PHP_EOL;
663                    }
664                    $wrapped = true;
665                } else {
666                    $this->response .= $command . $pad . $help . PHP_EOL;
667                    $wrapped = false;
668                }
669            } else {
670                $this->response .= $command . $this->commands[$key]->getHelp() . PHP_EOL;
671            }
672            $i++;
673        }
674
675        if (null !== $this->footer) {
676            $this->response .= $this->formatTemplate($this->footer);
677        }
678
679        $this->send(false);
680    }
681
682    /**
683     * Clear the console
684     *
685     * @return void
686     */
687    public function clear()
688    {
689        echo chr(27) . "[2J" . chr(27) . "[;H";
690    }
691
692    /**
693     * Get the color code from the color map
694     *
695     * @param  int    $color
696     * @param  string $type
697     * @return mixed
698     */
699    protected function getColorCode($color, $type = 'foreground')
700    {
701        if (isset(static::$colorMap[$type]) && isset(static::$colorMap[$type][$color])) {
702            return static::$colorMap[$type][$color];
703        }
704        return null;
705    }
706
707    /**
708     * Format header or footer template
709     *
710     * @param  string $template
711     * @return string
712     */
713    protected function formatTemplate($template)
714    {
715        $format = null;
716
717        if (strpos($template, "\n") !== false) {
718            $templateLines = explode("\n", $template);
719            foreach ($templateLines as $line) {
720                $line = trim($line);
721                $format .= $this->indent . $line . PHP_EOL;
722            }
723        } else {
724            $format = $this->indent . $template . PHP_EOL;
725        }
726
727        return $format;
728    }
729
730}