Console   F
last analyzed

Complexity

Total Complexity 70

Size/Duplication

Total Lines 350
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 154
dl 0
loc 350
ccs 0
cts 167
cp 0
rs 2.8
c 1
b 0
f 0
wmc 70

18 Methods

Rating   Name   Duplication   Size   Complexity  
A getTerminalWidth() 0 5 1
A getSttyColumns() 0 17 3
A getTerminalHeight() 0 5 1
B getTerminalDimensions() 0 25 8
A __construct() 0 7 1
A setDecorated() 0 3 1
C renderException() 0 65 16
A write() 0 23 6
A hasColorSupport() 0 11 6
A doWrite() 0 10 4
A isRunningOS400() 0 8 2
A openOutputStream() 0 6 3
A getMode() 0 20 4
A openErrorStream() 0 3 2
A hasStderrSupport() 0 3 1
A hasStdoutSupport() 0 3 1
A stringWidth() 0 11 3
B splitStringByWidth() 0 28 7

How to fix   Complexity   

Complex Class

Complex classes like Console often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Console, and based on these observations, apply Extract Interface, too.

1
<?php
2
// +----------------------------------------------------------------------
3
// | ThinkPHP [ WE CAN DO IT JUST THINK IT ]
4
// +----------------------------------------------------------------------
5
// | Copyright (c) 2006-2016 http://thinkphp.cn All rights reserved.
6
// +----------------------------------------------------------------------
7
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
8
// +----------------------------------------------------------------------
9
// | Author: yunwuxin <[email protected]>
10
// +----------------------------------------------------------------------
11
12
namespace think\console\output\driver;
13
14
use think\console\Output;
15
use think\console\output\Formatter;
16
17
class Console
18
{
19
20
    /** @var  Resource */
21
    private $stdout;
22
23
    /** @var  Formatter */
24
    private $formatter;
25
26
    private $terminalDimensions;
27
28
    /** @var  Output */
29
    private $output;
30
31
    public function __construct(Output $output)
32
    {
33
        $this->output    = $output;
34
        $this->formatter = new Formatter();
35
        $this->stdout    = $this->openOutputStream();
36
        $decorated       = $this->hasColorSupport($this->stdout);
37
        $this->formatter->setDecorated($decorated);
38
    }
39
40
    public function setDecorated($decorated)
41
    {
42
        $this->formatter->setDecorated($decorated);
43
    }
44
45
    public function write($messages, bool $newline = false, int $type = 0, $stream = null)
46
    {
47
        if (Output::VERBOSITY_QUIET === $this->output->getVerbosity()) {
48
            return;
49
        }
50
51
        $messages = (array) $messages;
52
53
        foreach ($messages as $message) {
54
            switch ($type) {
55
                case Output::OUTPUT_NORMAL:
56
                    $message = $this->formatter->format($message);
57
                    break;
58
                case Output::OUTPUT_RAW:
59
                    break;
60
                case Output::OUTPUT_PLAIN:
61
                    $message = strip_tags($this->formatter->format($message));
62
                    break;
63
                default:
64
                    throw new \InvalidArgumentException(sprintf('Unknown output type given (%s)', $type));
65
            }
66
67
            $this->doWrite($message, $newline, $stream);
68
        }
69
    }
70
71
    public function renderException(\Throwable $e)
72
    {
73
        $stderr    = $this->openErrorStream();
74
        $decorated = $this->hasColorSupport($stderr);
75
        $this->formatter->setDecorated($decorated);
76
77
        do {
78
            $title = sprintf('  [%s]  ', get_class($e));
79
80
            $len = $this->stringWidth($title);
81
82
            $width = $this->getTerminalWidth() ? $this->getTerminalWidth() - 1 : PHP_INT_MAX;
83
84
            if (defined('HHVM_VERSION') && $width > 1 << 31) {
85
                $width = 1 << 31;
86
            }
87
            $lines = [];
88
            foreach (preg_split('/\r?\n/', $e->getMessage()) as $line) {
89
                foreach ($this->splitStringByWidth($line, $width - 4) as $line) {
0 ignored issues
show
Comprehensibility Bug introduced by
$line is overwriting a variable from outer foreach loop.
Loading history...
90
91
                    $lineLength = $this->stringWidth(preg_replace('/\[[^m]*m/', '', $line)) + 4;
92
                    $lines[]    = [$line, $lineLength];
93
94
                    $len = max($lineLength, $len);
95
                }
96
            }
97
98
            $messages   = ['', ''];
99
            $messages[] = $emptyLine = sprintf('<error>%s</error>', str_repeat(' ', $len));
100
            $messages[] = sprintf('<error>%s%s</error>', $title, str_repeat(' ', max(0, $len - $this->stringWidth($title))));
101
            foreach ($lines as $line) {
102
                $messages[] = sprintf('<error>  %s  %s</error>', $line[0], str_repeat(' ', $len - $line[1]));
103
            }
104
            $messages[] = $emptyLine;
105
            $messages[] = '';
106
            $messages[] = '';
107
108
            $this->write($messages, true, Output::OUTPUT_NORMAL, $stderr);
109
110
            if (Output::VERBOSITY_VERBOSE <= $this->output->getVerbosity()) {
111
                $this->write('<comment>Exception trace:</comment>', true, Output::OUTPUT_NORMAL, $stderr);
112
113
                // exception related properties
114
                $trace = $e->getTrace();
115
                array_unshift($trace, [
116
                    'function' => '',
117
                    'file'     => $e->getFile() !== null ? $e->getFile() : 'n/a',
118
                    'line'     => $e->getLine() !== null ? $e->getLine() : 'n/a',
119
                    'args'     => [],
120
                ]);
121
122
                for ($i = 0, $count = count($trace); $i < $count; ++$i) {
123
                    $class    = isset($trace[$i]['class']) ? $trace[$i]['class'] : '';
124
                    $type     = isset($trace[$i]['type']) ? $trace[$i]['type'] : '';
125
                    $function = $trace[$i]['function'];
126
                    $file     = isset($trace[$i]['file']) ? $trace[$i]['file'] : 'n/a';
127
                    $line     = isset($trace[$i]['line']) ? $trace[$i]['line'] : 'n/a';
128
129
                    $this->write(sprintf(' %s%s%s() at <info>%s:%s</info>', $class, $type, $function, $file, $line), true, Output::OUTPUT_NORMAL, $stderr);
130
                }
131
132
                $this->write('', true, Output::OUTPUT_NORMAL, $stderr);
133
                $this->write('', true, Output::OUTPUT_NORMAL, $stderr);
134
            }
135
        } while ($e = $e->getPrevious());
136
137
    }
138
139
    /**
140
     * 获取终端宽度
141
     * @return int|null
142
     */
143
    protected function getTerminalWidth()
144
    {
145
        $dimensions = $this->getTerminalDimensions();
146
147
        return $dimensions[0];
148
    }
149
150
    /**
151
     * 获取终端高度
152
     * @return int|null
153
     */
154
    protected function getTerminalHeight()
155
    {
156
        $dimensions = $this->getTerminalDimensions();
157
158
        return $dimensions[1];
159
    }
160
161
    /**
162
     * 获取当前终端的尺寸
163
     * @return array
164
     */
165
    public function getTerminalDimensions(): array
166
    {
167
        if ($this->terminalDimensions) {
168
            return $this->terminalDimensions;
169
        }
170
171
        if ('\\' === DIRECTORY_SEPARATOR) {
172
            if (preg_match('/^(\d+)x\d+ \(\d+x(\d+)\)$/', trim(getenv('ANSICON')), $matches)) {
173
                return [(int) $matches[1], (int) $matches[2]];
174
            }
175
            if (preg_match('/^(\d+)x(\d+)$/', $this->getMode(), $matches)) {
176
                return [(int) $matches[1], (int) $matches[2]];
177
            }
178
        }
179
180
        if ($sttyString = $this->getSttyColumns()) {
181
            if (preg_match('/rows.(\d+);.columns.(\d+);/i', $sttyString, $matches)) {
182
                return [(int) $matches[2], (int) $matches[1]];
183
            }
184
            if (preg_match('/;.(\d+).rows;.(\d+).columns/i', $sttyString, $matches)) {
185
                return [(int) $matches[2], (int) $matches[1]];
186
            }
187
        }
188
189
        return [null, null];
190
    }
191
192
    /**
193
     * 获取stty列数
194
     * @return string
195
     */
196
    private function getSttyColumns()
197
    {
198
        if (!function_exists('proc_open')) {
199
            return;
200
        }
201
202
        $descriptorspec = [1 => ['pipe', 'w'], 2 => ['pipe', 'w']];
203
        $process        = proc_open('stty -a | grep columns', $descriptorspec, $pipes, null, null, ['suppress_errors' => true]);
204
        if (is_resource($process)) {
205
            $info = stream_get_contents($pipes[1]);
206
            fclose($pipes[1]);
207
            fclose($pipes[2]);
208
            proc_close($process);
209
210
            return $info;
211
        }
212
        return;
213
    }
214
215
    /**
216
     * 获取终端模式
217
     * @return string <width>x<height>
218
     */
219
    private function getMode()
220
    {
221
        if (!function_exists('proc_open')) {
222
            return '';
223
        }
224
225
        $descriptorspec = [1 => ['pipe', 'w'], 2 => ['pipe', 'w']];
226
        $process        = proc_open('mode CON', $descriptorspec, $pipes, null, null, ['suppress_errors' => true]);
227
        if (is_resource($process)) {
228
            $info = stream_get_contents($pipes[1]);
229
            fclose($pipes[1]);
230
            fclose($pipes[2]);
231
            proc_close($process);
232
233
            if (preg_match('/--------+\r?\n.+?(\d+)\r?\n.+?(\d+)\r?\n/', $info, $matches)) {
234
                return $matches[2] . 'x' . $matches[1];
235
            }
236
        }
237
238
        return '';
239
    }
240
241
    private function stringWidth(string $string): int
242
    {
243
        if (!function_exists('mb_strwidth')) {
244
            return strlen($string);
245
        }
246
247
        if (false === $encoding = mb_detect_encoding($string)) {
248
            return strlen($string);
249
        }
250
251
        return mb_strwidth($string, $encoding);
252
    }
253
254
    private function splitStringByWidth(string $string, int $width): array
255
    {
256
        if (!function_exists('mb_strwidth')) {
257
            return str_split($string, $width);
258
        }
259
260
        if (false === $encoding = mb_detect_encoding($string)) {
261
            return str_split($string, $width);
262
        }
263
264
        $utf8String = mb_convert_encoding($string, 'utf8', $encoding);
265
        $lines      = [];
266
        $line       = '';
267
        foreach (preg_split('//u', $utf8String) as $char) {
0 ignored issues
show
Bug introduced by
It seems like $utf8String can also be of type array; however, parameter $subject of preg_split() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

267
        foreach (preg_split('//u', /** @scrutinizer ignore-type */ $utf8String) as $char) {
Loading history...
268
            if (mb_strwidth($line . $char, 'utf8') <= $width) {
269
                $line .= $char;
270
                continue;
271
            }
272
            $lines[] = str_pad($line, $width);
273
            $line    = $char;
274
        }
275
        if (strlen($line)) {
276
            $lines[] = count($lines) ? str_pad($line, $width) : $line;
277
        }
278
279
        mb_convert_variables($encoding, 'utf8', $lines);
280
281
        return $lines;
282
    }
283
284
    private function isRunningOS400(): bool
285
    {
286
        $checks = [
287
            function_exists('php_uname') ? php_uname('s') : '',
288
            getenv('OSTYPE'),
289
            PHP_OS,
290
        ];
291
        return false !== stripos(implode(';', $checks), 'OS400');
292
    }
293
294
    /**
295
     * 当前环境是否支持写入控制台输出到stdout.
296
     *
297
     * @return bool
298
     */
299
    protected function hasStdoutSupport(): bool
300
    {
301
        return false === $this->isRunningOS400();
302
    }
303
304
    /**
305
     * 当前环境是否支持写入控制台输出到stderr.
306
     *
307
     * @return bool
308
     */
309
    protected function hasStderrSupport(): bool
310
    {
311
        return false === $this->isRunningOS400();
312
    }
313
314
    /**
315
     * @return resource
316
     */
317
    private function openOutputStream()
318
    {
319
        if (!$this->hasStdoutSupport()) {
320
            return fopen('php://output', 'w');
321
        }
322
        return @fopen('php://stdout', 'w') ?: fopen('php://output', 'w');
323
    }
324
325
    /**
326
     * @return resource
327
     */
328
    private function openErrorStream()
329
    {
330
        return fopen($this->hasStderrSupport() ? 'php://stderr' : 'php://output', 'w');
331
    }
332
333
    /**
334
     * 将消息写入到输出。
335
     * @param string $message 消息
336
     * @param bool   $newline 是否另起一行
337
     * @param null   $stream
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $stream is correct as it would always require null to be passed?
Loading history...
338
     */
339
    protected function doWrite($message, $newline, $stream = null)
340
    {
341
        if (null === $stream) {
0 ignored issues
show
introduced by
The condition null === $stream is always true.
Loading history...
342
            $stream = $this->stdout;
343
        }
344
        if (false === @fwrite($stream, $message . ($newline ? PHP_EOL : ''))) {
345
            throw new \RuntimeException('Unable to write output.');
346
        }
347
348
        fflush($stream);
349
    }
350
351
    /**
352
     * 是否支持着色
353
     * @param $stream
354
     * @return bool
355
     */
356
    protected function hasColorSupport($stream): bool
357
    {
358
        if (DIRECTORY_SEPARATOR === '\\') {
359
            return
360
            '10.0.10586' === PHP_WINDOWS_VERSION_MAJOR . '.' . PHP_WINDOWS_VERSION_MINOR . '.' . PHP_WINDOWS_VERSION_BUILD
361
            || false !== getenv('ANSICON')
362
            || 'ON' === getenv('ConEmuANSI')
363
            || 'xterm' === getenv('TERM');
364
        }
365
366
        return function_exists('posix_isatty') && @posix_isatty($stream);
367
    }
368
369
}
370