Completed
Branch 6.0 (d30585)
by yun
04:17
created

Console::write()   B

Complexity

Conditions 6
Paths 6

Size

Total Lines 25

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

Changes 0
Metric Value
cc 6
nc 6
nop 4
dl 0
loc 25
rs 8.8977
c 0
b 0
f 0
ccs 0
cts 17
cp 0
crap 42
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) {
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 View Code Duplication
            if (preg_match('/rows.(\d+);.columns.(\d+);/i', $sttyString, $matches)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
182
                return [(int) $matches[2], (int) $matches[1]];
183
            }
184 View Code Duplication
            if (preg_match('/;.(\d+).rows;.(\d+).columns/i', $sttyString, $matches)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
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> 或 null
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
        return;
238
    }
239
240
    private function stringWidth(string $string): int
241
    {
242
        if (!function_exists('mb_strwidth')) {
243
            return strlen($string);
244
        }
245
246
        if (false === $encoding = mb_detect_encoding($string)) {
247
            return strlen($string);
248
        }
249
250
        return mb_strwidth($string, $encoding);
251
    }
252
253
    private function splitStringByWidth(string $string, int $width): array
254
    {
255
        if (!function_exists('mb_strwidth')) {
256
            return str_split($string, $width);
257
        }
258
259
        if (false === $encoding = mb_detect_encoding($string)) {
260
            return str_split($string, $width);
261
        }
262
263
        $utf8String = mb_convert_encoding($string, 'utf8', $encoding);
264
        $lines      = [];
265
        $line       = '';
266
        foreach (preg_split('//u', $utf8String) as $char) {
267
            if (mb_strwidth($line . $char, 'utf8') <= $width) {
268
                $line .= $char;
269
                continue;
270
            }
271
            $lines[] = str_pad($line, $width);
272
            $line    = $char;
273
        }
274
        if (strlen($line)) {
275
            $lines[] = count($lines) ? str_pad($line, $width) : $line;
276
        }
277
278
        mb_convert_variables($encoding, 'utf8', $lines);
279
280
        return $lines;
281
    }
282
283
    private function isRunningOS400(): bool
284
    {
285
        $checks = [
286
            function_exists('php_uname') ? php_uname('s') : '',
287
            getenv('OSTYPE'),
288
            PHP_OS,
289
        ];
290
        return false !== stripos(implode(';', $checks), 'OS400');
291
    }
292
293
    /**
294
     * 当前环境是否支持写入控制台输出到stdout.
295
     *
296
     * @return bool
297
     */
298
    protected function hasStdoutSupport(): bool
299
    {
300
        return false === $this->isRunningOS400();
301
    }
302
303
    /**
304
     * 当前环境是否支持写入控制台输出到stderr.
305
     *
306
     * @return bool
307
     */
308
    protected function hasStderrSupport(): bool
309
    {
310
        return false === $this->isRunningOS400();
311
    }
312
313
    /**
314
     * @return resource
315
     */
316
    private function openOutputStream()
317
    {
318
        if (!$this->hasStdoutSupport()) {
319
            return fopen('php://output', 'w');
320
        }
321
        return @fopen('php://stdout', 'w') ?: fopen('php://output', 'w');
322
    }
323
324
    /**
325
     * @return resource
326
     */
327
    private function openErrorStream()
328
    {
329
        return fopen($this->hasStderrSupport() ? 'php://stderr' : 'php://output', 'w');
330
    }
331
332
    /**
333
     * 将消息写入到输出。
334
     * @param string $message 消息
335
     * @param bool   $newline 是否另起一行
336
     * @param null   $stream
337
     */
338
    protected function doWrite($message, $newline, $stream = null)
339
    {
340
        if (null === $stream) {
341
            $stream = $this->stdout;
342
        }
343
        if (false === @fwrite($stream, $message . ($newline ? PHP_EOL : ''))) {
344
            throw new \RuntimeException('Unable to write output.');
345
        }
346
347
        fflush($stream);
348
    }
349
350
    /**
351
     * 是否支持着色
352
     * @param $stream
353
     * @return bool
354
     */
355
    protected function hasColorSupport($stream): bool
356
    {
357
        if (DIRECTORY_SEPARATOR === '\\') {
358
            return
359
            '10.0.10586' === PHP_WINDOWS_VERSION_MAJOR . '.' . PHP_WINDOWS_VERSION_MINOR . '.' . PHP_WINDOWS_VERSION_BUILD
360
            || false !== getenv('ANSICON')
361
            || 'ON' === getenv('ConEmuANSI')
362
            || 'xterm' === getenv('TERM');
363
        }
364
365
        return function_exists('posix_isatty') && @posix_isatty($stream);
366
    }
367
368
}
369