4 * This file is part of the Symfony package.
6 * (c) Fabien Potencier <fabien@symfony.com>
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
12 namespace Symfony\Component\VarDumper\Dumper;
14 use Symfony\Component\VarDumper\Cloner\Cursor;
15 use Symfony\Component\VarDumper\Cloner\Stub;
18 * CliDumper dumps variables for command line output.
20 * @author Nicolas Grekas <p@tchwork.com>
22 class CliDumper extends AbstractDumper
24 public static $defaultColors;
25 public static $defaultOutput = 'php://stdout';
28 protected $maxStringWidth = 0;
29 protected $styles = array(
30 // See http://en.wikipedia.org/wiki/ANSI_escape_code#graphics
31 'default' => '38;5;208',
33 'const' => '1;38;5;208',
34 'str' => '1;38;5;113',
45 protected static $controlCharsRx = '/[\x00-\x1F\x7F]+/';
46 protected static $controlCharsMap = array(
55 protected $collapseNextHash = false;
56 protected $expandNextHash = false;
61 public function __construct($output = null, $charset = null, $flags = 0)
63 parent::__construct($output, $charset, $flags);
65 if ('\\' === \DIRECTORY_SEPARATOR && !$this->isWindowsTrueColor()) {
66 // Use only the base 16 xterm colors when using ANSICON or standard Windows 10 CLI
67 $this->setStyles(array(
82 * Enables/disables colored output.
86 public function setColors($colors)
88 $this->colors = (bool) $colors;
92 * Sets the maximum number of characters per line for dumped strings.
94 * @param int $maxStringWidth
96 public function setMaxStringWidth($maxStringWidth)
98 $this->maxStringWidth = (int) $maxStringWidth;
104 * @param array $styles A map of style names to style definitions
106 public function setStyles(array $styles)
108 $this->styles = $styles + $this->styles;
114 public function dumpScalar(Cursor $cursor, $type, $value)
116 $this->dumpKey($cursor);
119 $attr = $cursor->attr;
134 case INF === $value: $value = 'INF'; break;
135 case -INF === $value: $value = '-INF'; break;
136 case is_nan($value): $value = 'NAN'; break;
138 $value = (string) $value;
139 if (false === strpos($value, $this->decimalPoint)) {
140 $value .= $this->decimalPoint.'0';
151 $value = $value ? 'true' : 'false';
155 $attr += array('value' => $this->utf8Encode($value));
156 $value = $this->utf8Encode($type);
160 $this->line .= $this->style($style, $value, $attr);
162 $this->endValue($cursor);
168 public function dumpString(Cursor $cursor, $str, $bin, $cut)
170 $this->dumpKey($cursor);
171 $attr = $cursor->attr;
174 $str = $this->utf8Encode($str);
178 $this->endValue($cursor);
181 'length' => 0 <= $cut ? mb_strlen($str, 'UTF-8') + $cut : 0,
184 $str = explode("\n", $str);
185 if (isset($str[1]) && !isset($str[2]) && !isset($str[1][0])) {
189 $m = \count($str) - 1;
192 if (self::DUMP_STRING_LENGTH & $this->flags) {
193 $this->line .= '('.$attr['length'].') ';
200 $this->line .= '"""';
201 $this->dumpLine($cursor->depth);
206 foreach ($str as $str) {
210 if (0 < $this->maxStringWidth && $this->maxStringWidth < $len = mb_strlen($str, 'UTF-8')) {
211 $str = mb_substr($str, 0, $this->maxStringWidth, 'UTF-8');
212 $lineCut = $len - $this->maxStringWidth;
214 if ($m && 0 < $cursor->depth) {
215 $this->line .= $this->indentPad;
218 $this->line .= $this->style('str', $str, $attr);
223 $this->dumpLine($cursor->depth);
224 if (0 < $cursor->depth) {
225 $this->line .= $this->indentPad;
228 $this->line .= '"""';
233 $this->line .= '…';
240 $this->line .= '…'.$lineCut;
245 $this->endValue($cursor);
247 $this->dumpLine($cursor->depth);
256 public function enterHash(Cursor $cursor, $type, $class, $hasChild)
258 $this->dumpKey($cursor);
260 if ($this->collapseNextHash) {
261 $cursor->skipChildren = true;
262 $this->collapseNextHash = $hasChild = false;
265 $class = $this->utf8Encode($class);
266 if (Cursor::HASH_OBJECT === $type) {
267 $prefix = $class && 'stdClass' !== $class ? $this->style('note', $class).' {' : '{';
268 } elseif (Cursor::HASH_RESOURCE === $type) {
269 $prefix = $this->style('note', $class.' resource').($hasChild ? ' {' : ' ');
271 $prefix = $class && !(self::DUMP_LIGHT_ARRAY & $this->flags) ? $this->style('note', 'array:'.$class).' [' : '[';
274 if ($cursor->softRefCount || 0 < $cursor->softRefHandle) {
275 $prefix .= $this->style('ref', (Cursor::HASH_RESOURCE === $type ? '@' : '#').(0 < $cursor->softRefHandle ? $cursor->softRefHandle : $cursor->softRefTo), array('count' => $cursor->softRefCount));
276 } elseif ($cursor->hardRefTo && !$cursor->refIndex && $class) {
277 $prefix .= $this->style('ref', '&'.$cursor->hardRefTo, array('count' => $cursor->hardRefCount));
278 } elseif (!$hasChild && Cursor::HASH_RESOURCE === $type) {
279 $prefix = substr($prefix, 0, -1);
282 $this->line .= $prefix;
285 $this->dumpLine($cursor->depth);
292 public function leaveHash(Cursor $cursor, $type, $class, $hasChild, $cut)
294 $this->dumpEllipsis($cursor, $hasChild, $cut);
295 $this->line .= Cursor::HASH_OBJECT === $type ? '}' : (Cursor::HASH_RESOURCE !== $type ? ']' : ($hasChild ? '}' : ''));
296 $this->endValue($cursor);
300 * Dumps an ellipsis for cut children.
302 * @param Cursor $cursor The Cursor position in the dump
303 * @param bool $hasChild When the dump of the hash has child item
304 * @param int $cut The number of items the hash has been cut by
306 protected function dumpEllipsis(Cursor $cursor, $hasChild, $cut)
309 $this->line .= ' …';
314 $this->dumpLine($cursor->depth + 1);
320 * Dumps a key in a hash structure.
322 * @param Cursor $cursor The Cursor position in the dump
324 protected function dumpKey(Cursor $cursor)
326 if (null !== $key = $cursor->hashKey) {
327 if ($cursor->hashKeyIsBinary) {
328 $key = $this->utf8Encode($key);
330 $attr = array('binary' => $cursor->hashKeyIsBinary);
331 $bin = $cursor->hashKeyIsBinary ? 'b' : '';
333 switch ($cursor->hashType) {
335 case Cursor::HASH_INDEXED:
336 if (self::DUMP_LIGHT_ARRAY & $this->flags) {
341 case Cursor::HASH_ASSOC:
343 $this->line .= $this->style($style, $key).' => ';
345 $this->line .= $bin.'"'.$this->style($style, $key).'" => ';
349 case Cursor::HASH_RESOURCE:
352 case Cursor::HASH_OBJECT:
353 if (!isset($key[0]) || "\0" !== $key[0]) {
354 $this->line .= '+'.$bin.$this->style('public', $key).': ';
355 } elseif (0 < strpos($key, "\0", 1)) {
356 $key = explode("\0", substr($key, 1), 2);
358 switch ($key[0][0]) {
359 case '+': // User inserted keys
360 $attr['dynamic'] = true;
361 $this->line .= '+'.$bin.'"'.$this->style('public', $key[1], $attr).'": ';
365 if (isset($key[0][1])) {
366 parse_str(substr($key[0], 1), $attr);
367 $attr += array('binary' => $cursor->hashKeyIsBinary);
371 $style = 'protected';
375 $attr['class'] = $key[0];
381 if (isset($attr['collapse'])) {
382 if ($attr['collapse']) {
383 $this->collapseNextHash = true;
385 $this->expandNextHash = true;
389 $this->line .= $bin.$this->style($style, $key[1], $attr).(isset($attr['separator']) ? $attr['separator'] : ': ');
391 // This case should not happen
392 $this->line .= '-'.$bin.'"'.$this->style('private', $key, array('class' => '')).'": ';
397 if ($cursor->hardRefTo) {
398 $this->line .= $this->style('ref', '&'.($cursor->hardRefCount ? $cursor->hardRefTo : ''), array('count' => $cursor->hardRefCount)).' ';
404 * Decorates a value with some style.
406 * @param string $style The type of style being applied
407 * @param string $value The value being styled
408 * @param array $attr Optional context information
410 * @return string The value with style decoration
412 protected function style($style, $value, $attr = array())
414 if (null === $this->colors) {
415 $this->colors = $this->supportsColors();
418 if (isset($attr['ellipsis'], $attr['ellipsis-type'])) {
419 $prefix = substr($value, 0, -$attr['ellipsis']);
420 if ('cli' === \PHP_SAPI && 'path' === $attr['ellipsis-type'] && isset($_SERVER[$pwd = '\\' === \DIRECTORY_SEPARATOR ? 'CD' : 'PWD']) && 0 === strpos($prefix, $_SERVER[$pwd])) {
421 $prefix = '.'.substr($prefix, \strlen($_SERVER[$pwd]));
423 if (!empty($attr['ellipsis-tail'])) {
424 $prefix .= substr($value, -$attr['ellipsis'], $attr['ellipsis-tail']);
425 $value = substr($value, -$attr['ellipsis'] + $attr['ellipsis-tail']);
427 $value = substr($value, -$attr['ellipsis']);
430 return $this->style('default', $prefix).$this->style($style, $value);
433 $style = $this->styles[$style];
435 $map = static::$controlCharsMap;
436 $startCchr = $this->colors ? "\033[m\033[{$this->styles['default']}m" : '';
437 $endCchr = $this->colors ? "\033[m\033[{$style}m" : '';
438 $value = preg_replace_callback(static::$controlCharsRx, function ($c) use ($map, $startCchr, $endCchr) {
442 $s .= isset($map[$c[$i]]) ? $map[$c[$i]] : sprintf('\x%02X', \ord($c[$i]));
443 } while (isset($c[++$i]));
446 }, $value, -1, $cchrCount);
449 if ($cchrCount && "\033" === $value[0]) {
450 $value = substr($value, \strlen($startCchr));
452 $value = "\033[{$style}m".$value;
454 if ($cchrCount && $endCchr === substr($value, -\strlen($endCchr))) {
455 $value = substr($value, 0, -\strlen($endCchr));
457 $value .= "\033[{$this->styles['default']}m";
465 * @return bool Tells if the current output stream supports ANSI colors or not
467 protected function supportsColors()
469 if ($this->outputStream !== static::$defaultOutput) {
470 return $this->hasColorSupport($this->outputStream);
472 if (null !== static::$defaultColors) {
473 return static::$defaultColors;
475 if (isset($_SERVER['argv'][1])) {
476 $colors = $_SERVER['argv'];
477 $i = \count($colors);
479 if (isset($colors[$i][5])) {
480 switch ($colors[$i]) {
484 case '--color=force':
485 case '--color=always':
486 return static::$defaultColors = true;
491 case '--color=never':
492 return static::$defaultColors = false;
498 $h = stream_get_meta_data($this->outputStream) + array('wrapper_type' => null);
499 $h = 'Output' === $h['stream_type'] && 'PHP' === $h['wrapper_type'] ? fopen('php://stdout', 'wb') : $this->outputStream;
501 return static::$defaultColors = $this->hasColorSupport($h);
507 protected function dumpLine($depth, $endOfValue = false)
510 $this->line = sprintf("\033[%sm%s\033[m", $this->styles['default'], $this->line);
512 parent::dumpLine($depth);
515 protected function endValue(Cursor $cursor)
517 if (Stub::ARRAY_INDEXED === $cursor->hashType || Stub::ARRAY_ASSOC === $cursor->hashType) {
518 if (self::DUMP_TRAILING_COMMA & $this->flags && 0 < $cursor->depth) {
520 } elseif (self::DUMP_COMMA_SEPARATOR & $this->flags && 1 < $cursor->hashLength - $cursor->hashIndex) {
525 $this->dumpLine($cursor->depth, true);
529 * Returns true if the stream supports colorization.
531 * Reference: Composer\XdebugHandler\Process::supportsColor
532 * https://github.com/composer/xdebug-handler
534 * @param mixed $stream A CLI output stream
538 private function hasColorSupport($stream)
540 if (!\is_resource($stream) || 'stream' !== get_resource_type($stream)) {
544 if ('Hyper' === getenv('TERM_PROGRAM')) {
548 if (\DIRECTORY_SEPARATOR === '\\') {
549 return (\function_exists('sapi_windows_vt100_support')
550 && @sapi_windows_vt100_support($stream))
551 || false !== getenv('ANSICON')
552 || 'ON' === getenv('ConEmuANSI')
553 || 'xterm' === getenv('TERM');
556 if (\function_exists('stream_isatty')) {
557 return @stream_isatty($stream);
560 if (\function_exists('posix_isatty')) {
561 return @posix_isatty($stream);
564 $stat = @fstat($stream);
565 // Check if formatted mode is S_IFCHR
566 return $stat ? 0020000 === ($stat['mode'] & 0170000) : false;
570 * Returns true if the Windows terminal supports true color.
572 * Note that this does not check an output stream, but relies on environment
573 * variables from known implementations, or a PHP and Windows version that
574 * supports true color.
578 private function isWindowsTrueColor()
580 $result = 183 <= getenv('ANSICON_VER')
581 || 'ON' === getenv('ConEmuANSI')
582 || 'xterm' === getenv('TERM')
583 || 'Hyper' === getenv('TERM_PROGRAM');
585 if (!$result && \PHP_VERSION_ID >= 70200) {
588 PHP_WINDOWS_VERSION_MAJOR,
589 PHP_WINDOWS_VERSION_MINOR,
590 PHP_WINDOWS_VERSION_BUILD
592 $result = $version >= '10.0.15063';