Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
42 / 42
98.00% covered (success)
98.00%
49 / 50
51.02% covered (warning)
51.02%
25 / 49
92.86% covered (success)
92.86%
13 / 14
CRAP
0.00% covered (danger)
0.00%
0 / 1
Output
100.00% covered (success)
100.00%
42 / 42
98.00% covered (success)
98.00%
49 / 50
51.02% covered (warning)
51.02%
25 / 49
100.00% covered (success)
100.00%
14 / 14
207.67
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 echo
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 echoln
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 echoErr
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 echolnErr
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 styled
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
6 / 6
50.00% covered (danger)
50.00%
2 / 4
100.00% covered (success)
100.00%
1 / 1
4.12
 write
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 color
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
11 / 11
29.41% covered (danger)
29.41%
5 / 17
100.00% covered (success)
100.00%
1 / 1
18.66
 indent
100.00% covered (success)
100.00%
6 / 6
87.50% covered (warning)
87.50%
7 / 8
37.50% covered (danger)
37.50%
3 / 8
100.00% covered (success)
100.00%
1 / 1
5.20
 terminalWidth
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
6 / 6
66.67% covered (warning)
66.67%
2 / 3
100.00% covered (success)
100.00%
1 / 1
4.59
 formatText
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
9 / 9
50.00% covered (danger)
50.00%
4 / 8
100.00% covered (success)
100.00%
1 / 1
8.12
 stdout
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 stderr
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasColorSupport
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
9
1<?php
2
3declare(strict_types=1);
4
5namespace Celemas\Cli;
6
7final class Output
8{
9    private mixed $stream = null;
10    private mixed $errorStream = null;
11    private ?int $width = null;
12    private array $fg = [
13        'black' => [0, 30],
14        'gray' => [1, 30],
15        'grey' => [1, 30],
16        'red' => [0, 31],
17        'lightred' => [1, 31],
18        'green' => [0, 32],
19        'lightgreen' => [1, 32],
20        'brown' => [0, 33],
21        'yellow' => [1, 33],
22        'blue' => [0, 34],
23        'lightblue' => [1, 34],
24        'purple' => [0, 35],
25        'lightpurple' => [1, 35],
26        'magenta' => [0, 35],
27        'lightmagenta' => [1, 35],
28        'cyan' => [0, 36],
29        'lightcyan' => [1, 36],
30        'lightgray' => [0, 37],
31        'lightgrey' => [0, 37],
32        'white' => [1, 37],
33    ];
34    private array $bg = [
35        'black' => 40,
36        'red' => 41,
37        'green' => 42,
38        'yellow' => 43,
39        'blue' => 44,
40        'purple' => 45,
41        'magenta' => 45,
42        'cyan' => 46,
43        'gray' => 47,
44        'grey' => 47,
45        'white' => 47,
46    ];
47
48    public function __construct(
49        protected readonly string $target = 'php://output',
50        protected readonly string $errorTarget = 'php://stderr',
51    ) {}
52
53    public function echo(string $text, string $color = '', string $background = ''): void
54    {
55        $this->write($this->stdout(), $this->styled($text, $color, $background));
56    }
57
58    public function echoln(string $text, string $color = '', string $background = ''): void
59    {
60        $this->write($this->stdout(), $this->styled($text, $color, $background) . PHP_EOL);
61    }
62
63    public function echoErr(string $text, string $color = '', string $background = ''): void
64    {
65        $this->write($this->stderr(), $this->styled($text, $color, $background));
66    }
67
68    public function echolnErr(string $text, string $color = '', string $background = ''): void
69    {
70        $this->write($this->stderr(), $this->styled($text, $color, $background) . PHP_EOL);
71    }
72
73    private function styled(string $text, string $color, string $background): string
74    {
75        return $color || $background ? $this->color($text, $color, $background) : $text;
76    }
77
78    private function write(mixed $stream, string $text): void
79    {
80        fwrite($stream, $text);
81        fflush($stream);
82    }
83
84    public function color(string $text, string $color = '', string $background = ''): string
85    {
86        if (!$this->hasColorSupport()) {
87            return $text;
88        }
89
90        $colorCode = '';
91        $backgroundCode = '';
92
93        if ($color && array_key_exists($color, $this->fg)) {
94            [$first, $second] = $this->fg[$color];
95            $colorCode = "{$first};{$second}";
96        }
97
98        if ($background && array_key_exists($background, $this->bg)) {
99            $backgroundCode = $this->bg[$background];
100        }
101
102        return $this->formatText($text, $colorCode, $backgroundCode);
103    }
104
105    public function indent(
106        string $text,
107        int $indent,
108        ?int $max = null,
109    ): string {
110        $spaces = str_repeat(' ', $indent);
111        $width = $this->terminalWidth() - $indent;
112
113        if ($max !== null && $max < $width) {
114            $width = $max;
115        }
116
117        $lines = explode("\n", wordwrap($text, $width, break: "\n"));
118
119        return implode("\n", array_map(static fn($line) => $spaces . $line, $lines));
120    }
121
122    private function terminalWidth(): int
123    {
124        if ($this->width !== null) {
125            return $this->width;
126        }
127
128        $columns = (int) getenv('COLUMNS');
129
130        if ($columns < 1) {
131            /** @psalm-suppress ForbiddenCode */
132            $columns = (int) shell_exec('tput cols');
133        }
134
135        if ($columns < 1) {
136            // @codeCoverageIgnoreStart
137            $columns = 80;
138
139            // @codeCoverageIgnoreEnd
140        }
141
142        return $this->width = $columns;
143    }
144
145    private function formatText(string $text, string $colorCode, string|int $backgroundCode): string
146    {
147        if ($colorCode && $backgroundCode) {
148            return "\033[{$colorCode};{$backgroundCode}m{$text}\033[0m";
149        }
150
151        if ($colorCode) {
152            return "\033[{$colorCode}m{$text}\033[0m";
153        }
154
155        if ($backgroundCode) {
156            return "\033[{$backgroundCode}m{$text}\033[0m";
157        }
158
159        return $text;
160    }
161
162    private function stdout(): mixed
163    {
164        return $this->stream ??= fopen($this->target, mode: 'w');
165    }
166
167    private function stderr(): mixed
168    {
169        return $this->errorStream ??= fopen($this->errorTarget, mode: 'w');
170    }
171
172    private function hasColorSupport(): bool
173    {
174        if (getenv('NO_COLOR') !== false) {
175            return false;
176        }
177
178        // @codeCoverageIgnoreStart
179        if (getenv('FORCE_COLOR') !== false || getenv('COLORTERM') !== false) {
180            return true;
181        }
182
183        // Windows
184        if (DIRECTORY_SEPARATOR === '\\') {
185            if (function_exists('sapi_windows_vt100_support')) {
186                return sapi_windows_vt100_support(STDOUT);
187            }
188
189            return (
190                getenv('ANSICON') !== false
191                || getenv('ConEmuANSI') === 'ON'
192                || getenv('TERM') === 'xterm'
193            );
194        }
195
196        if (function_exists('stream_isatty')) {
197            return stream_isatty(STDOUT);
198        }
199
200        return false;
201
202        // @codeCoverageIgnoreEnd
203    }
204}

Branches

Below are the source code lines that represent each code branch as identified by Xdebug. Please note a branch is not necessarily coterminous with a line, a line may contain multiple branches and therefore show up more than once. Please also be aware that some branches may be implicit rather than explicit, e.g. an if statement always has an else as part of its logical flow even if you didn't write one.

Output->__construct
49        protected readonly string $target = 'php://output',
50        protected readonly string $errorTarget = 'php://stderr',
51    ) {}
Output->color
84    public function color(string $text, string $color = '', string $background = ''): string
85    {
86        if (!$this->hasColorSupport()) {
87            return $text;
90        $colorCode = '';
91        $backgroundCode = '';
92
93        if ($color && array_key_exists($color, $this->fg)) {
93        if ($color && array_key_exists($color, $this->fg)) {
93        if ($color && array_key_exists($color, $this->fg)) {
94            [$first, $second] = $this->fg[$color];
95            $colorCode = "{$first};{$second}";
96        }
97
98        if ($background && array_key_exists($background, $this->bg)) {
98        if ($background && array_key_exists($background, $this->bg)) {
98        if ($background && array_key_exists($background, $this->bg)) {
98        if ($background && array_key_exists($background, $this->bg)) {
99            $backgroundCode = $this->bg[$background];
100        }
101
102        return $this->formatText($text, $colorCode, $backgroundCode);
102        return $this->formatText($text, $colorCode, $backgroundCode);
103    }
Output->echo
53    public function echo(string $text, string $color = '', string $background = ''): void
54    {
55        $this->write($this->stdout(), $this->styled($text, $color, $background));
56    }
Output->echoErr
63    public function echoErr(string $text, string $color = '', string $background = ''): void
64    {
65        $this->write($this->stderr(), $this->styled($text, $color, $background));
66    }
Output->echoln
58    public function echoln(string $text, string $color = '', string $background = ''): void
59    {
60        $this->write($this->stdout(), $this->styled($text, $color, $background) . PHP_EOL);
61    }
Output->echolnErr
68    public function echolnErr(string $text, string $color = '', string $background = ''): void
69    {
70        $this->write($this->stderr(), $this->styled($text, $color, $background) . PHP_EOL);
71    }
Output->formatText
145    private function formatText(string $text, string $colorCode, string|int $backgroundCode): string
146    {
147        if ($colorCode && $backgroundCode) {
147        if ($colorCode && $backgroundCode) {
147        if ($colorCode && $backgroundCode) {
148            return "\033[{$colorCode};{$backgroundCode}m{$text}\033[0m";
151        if ($colorCode) {
152            return "\033[{$colorCode}m{$text}\033[0m";
155        if ($backgroundCode) {
156            return "\033[{$backgroundCode}m{$text}\033[0m";
159        return $text;
160    }
Output->hasColorSupport
174        if (getenv('NO_COLOR') !== false) {
175            return false;
Output->indent
106        string $text,
107        int $indent,
108        ?int $max = null,
109    ): string {
110        $spaces = str_repeat(' ', $indent);
111        $width = $this->terminalWidth() - $indent;
112
113        if ($max !== null && $max < $width) {
113        if ($max !== null && $max < $width) {
113        if ($max !== null && $max < $width) {
114            $width = $max;
115        }
116
117        $lines = explode("\n", wordwrap($text, $width, break: "\n"));
117        $lines = explode("\n", wordwrap($text, $width, break: "\n"));
118
119        return implode("\n", array_map(static fn($line) => $spaces . $line, $lines));
119        return implode("\n", array_map(static fn($line) => $spaces . $line, $lines));
119        return implode("\n", array_map(static fn($line) => $spaces . $line, $lines));
119        return implode("\n", array_map(static fn($line) => $spaces . $line, $lines));
120    }
Output->stderr
169        return $this->errorStream ??= fopen($this->errorTarget, mode: 'w');
170    }
Output->stdout
164        return $this->stream ??= fopen($this->target, mode: 'w');
165    }
Output->styled
73    private function styled(string $text, string $color, string $background): string
74    {
75        return $color || $background ? $this->color($text, $color, $background) : $text;
75        return $color || $background ? $this->color($text, $color, $background) : $text;
75        return $color || $background ? $this->color($text, $color, $background) : $text;
75        return $color || $background ? $this->color($text, $color, $background) : $text;
75        return $color || $background ? $this->color($text, $color, $background) : $text;
75        return $color || $background ? $this->color($text, $color, $background) : $text;
76    }
Output->terminalWidth
124        if ($this->width !== null) {
125            return $this->width;
128        $columns = (int) getenv('COLUMNS');
129
130        if ($columns < 1) {
132            $columns = (int) shell_exec('tput cols');
133        }
134
135        if ($columns < 1) {
135        if ($columns < 1) {
142        return $this->width = $columns;
143    }
Output->write
78    private function write(mixed $stream, string $text): void
79    {
80        fwrite($stream, $text);
81        fflush($stream);
82    }
{closure:/workspace/celemas/cli/src/Output.php:119-119}
119        return implode("\n", array_map(static fn($line) => $spaces . $line, $lines));