bkdotcom/PHPDebugConsole

View on GitHub
src/Debug/Dump/TextAnsiValue.php

Summary

Maintainability
A
0 mins
Test Coverage
A
100%
<?php

/**
 * This file is part of PHPDebugConsole
 *
 * @package   PHPDebugConsole
 * @author    Brad Kent <bkfake-github@yahoo.com>
 * @license   http://opensource.org/licenses/MIT MIT
 * @copyright 2014-2024 Brad Kent
 * @version   v3.0
 */

namespace bdk\Debug\Dump;

use bdk\Debug\Abstraction\Abstraction;
use bdk\Debug\Abstraction\AbstractObject;
use bdk\Debug\Abstraction\Type;

/**
 * Base output plugin
 */
class TextAnsiValue extends TextValue
{
    /** @var string */
    public $escapeReset = "\e[0m";

    /** @var bool */
    protected $dumpKeys = false;

    /**
     * Add ansi escape sequences for classname type strings
     *
     * @param mixed $val        classname or classname(::|->)name (method/property/const)
     * @param bool  $asFunction (false) specify we're marking up a function
     *
     * @return string
     */
    public function markupIdentifier($val, $asFunction = false)
    {
        $parts = $this->parseIdentifier($val, $asFunction);
        $classname = '';
        $operator = $this->cfg['escapeCodes']['operator'] . $parts['operator'] . $this->escapeReset;
        $identifier = '';
        if ($parts['classname']) {
            $idx = \strrpos($parts['classname'], '\\');
            $classname = $parts['classname'];
            $classname = $idx
                ? $this->cfg['escapeCodes']['muted'] . \substr($classname, 0, $idx + 1) . $this->escapeReset
                    . "\e[1m" . \substr($classname, $idx + 1) . "\e[22m"
                : "\e[1m" . $classname . "\e[22m";
        }
        if ($parts['identifier']) {
            $identifier = "\e[1m" . $parts['identifier'] . "\e[22m";
        }
        $parts = \array_filter(array($classname, $identifier), 'strlen');
        return \implode($operator, $parts);
    }

    /**
     * Dump array as text
     *
     * @param array $array Array to display
     *
     * @return string
     */
    protected function dumpArray($array)
    {
        $this->valDepth++;
        $isNested = $this->valDepth > 0;
        $escapeCodes = $this->cfg['escapeCodes'];
        $regexRemoveReset = '/' . \preg_quote($this->escapeReset) . '$/';
        if ($this->getDumpOpt('isMaxDepth')) {
            return $this->cfg['escapeCodes']['keyword'] . 'array '
                . $this->cfg['escapeCodes']['recursion'] . '*MAX DEPTH*'
                . $this->escapeReset;
        }
        $str = $escapeCodes['keyword'] . 'array' . $escapeCodes['punct'] . '(' . $this->escapeReset . "\n";
        foreach ($array as $key => $val) {
            // key gets 'numeric' from dump...   apply arrayKey if string
            //  remove escapeReset
            $key = (\is_int($key) ? '' : $escapeCodes['arrayKey']) . $this->dump($key, array('addQuotes' => false));
            $key = \preg_replace($regexRemoveReset, '', $key);
            $str .= '    '
                . $escapeCodes['punct'] . '[' . $key . $escapeCodes['punct'] . ']'
                . $escapeCodes['operator'] . ' => ' . $this->escapeReset
                . $this->dump($val) . "\n";
        }
        $str .= $this->cfg['escapeCodes']['punct'] . ')' . $this->escapeReset;
        if (!$array) {
            $str = \str_replace("\n", '', $str);
        } elseif ($isNested) {
            $str = \str_replace("\n", "\n    ", $str);
        }
        return $str;
    }

    /**
     * Dump boolean
     *
     * @param bool $val boolean value
     *
     * @return string
     */
    protected function dumpBool($val)
    {
        return $val
            ? $this->cfg['escapeCodes']['true'] . 'true' . $this->escapeReset
            : $this->cfg['escapeCodes']['false'] . 'false' . $this->escapeReset;
    }

    /**
     * Dump float value
     *
     * @param float       $val float value
     * @param Abstraction $abs (optional) full abstraction
     *
     * @return float|string
     */
    protected function dumpFloat($val, Abstraction $abs = null)
    {
        if ($val === Type::TYPE_FLOAT_INF) {
            $val = 'INF';
        } elseif ($val === Type::TYPE_FLOAT_NAN) {
            $val = 'NaN';
        }
        $date = $this->checkTimestamp($val, $abs);
        $val = $this->cfg['escapeCodes']['numeric'] . $val . $this->escapeReset;
        return $date
            ? '📅 ' . $val . ' ' . $this->cfg['escapeCodes']['muted'] . '(' . $date . ')' . $this->escapeReset
            : $val;
    }

    /**
     * Dump null value
     *
     * @return string
     */
    protected function dumpNull()
    {
        return $this->cfg['escapeCodes']['muted'] . 'null' . $this->escapeReset;
    }

    /**
     * Dump object as text
     *
     * @param Abstraction $abs Object Abstraction instance
     *
     * @return string
     */
    protected function dumpObject(Abstraction $abs)
    {
        $className = $this->markupIdentifier($abs['className']);
        $escapeCodes = $this->cfg['escapeCodes'];
        if ($abs['isRecursion']) {
            return $className . ' ' . $escapeCodes['recursion'] . '*RECURSION*' . $this->escapeReset;
        }
        if ($abs['isMaxDepth']) {
            return $className . ' ' . $escapeCodes['recursion'] . '*MAX DEPTH*' . $this->escapeReset;
        }
        if ($abs['isExcluded']) {
            return $className . ' ' . $escapeCodes['excluded'] . 'NOT INSPECTED' . $this->escapeReset;
        }
        $isNested = $this->valDepth > 0;
        $this->valDepth++;
        $str = $className . "\n"
            . $this->dumpObjectProperties($abs)
            . $this->dumpObjectMethods($abs);
        $str = \trim($str);
        if ($isNested) {
            $str = \str_replace("\n", "\n    ", $str);
        }
        return $str;
    }

    /**
     * Dump object methods as text
     *
     * @param Abstraction $abs Object Abstraction instance
     *
     * @return string html
     */
    protected function dumpObjectMethods(Abstraction $abs)
    {
        $methodCollect = $abs['cfgFlags'] & AbstractObject::METHOD_COLLECT;
        $methodOutput = $abs['cfgFlags'] & AbstractObject::METHOD_OUTPUT;
        if (!$methodCollect || !$methodOutput) {
            return '';
        }
        // phpcs:ignore SlevomatCodingStandard.Arrays.AlphabeticallySortedByKeys.IncorrectKeyOrder
        $counts = array(
            'public' => 0,
            'protected' => 0,
            'private' => 0,
            'magic' => 0,
        );
        foreach ($abs['methods'] as $info) {
            $counts[ $info['visibility'] ]++;
        }
        $counts = \array_filter($counts);
        $header = $counts
            ? "\e[4mMethods:\e[24m"
            : 'Methods: none!';
        $counts = \array_map(function ($vis, $count) {
            return '    ' . $vis
                . $this->cfg['escapeCodes']['punct'] . ':' . $this->escapeReset . ' '
                . $this->cfg['escapeCodes']['numeric'] . $count
                . $this->escapeReset . "\n";
        }, \array_keys($counts), $counts);
        return '  ' . $header . "\n" . \implode('', $counts);
    }

    /**
     * Dump object properties as text with ANSI escape codes
     *
     * @param Abstraction $abs Object Abstraction instance
     *
     * @return string
     */
    protected function dumpObjectProperties(Abstraction $abs)
    {
        $str = '';
        if (isset($abs['methods']['__get'])) {
            $str .= '    ' . $this->cfg['escapeCodes']['muted']
                . '✨ This object has a __get() method'
                . $this->escapeReset
                . "\n";
        }
        $properties = $abs->sort($abs['properties'], $abs['sort']);
        foreach ($properties as $name => $info) {
            $info['className'] = $abs['className'];
            $info['isInherited'] = $info['declaredLast'] && $info['declaredLast'] !== $abs['className'];
            $str .= $this->dumpProp($name, $info);
        }
        $header = $str
            ? "\e[4mProperties:\e[24m"
            : 'Properties: none!';
        return '  ' . $header . "\n" . $str;
    }

    /**
     * Dump object property
     *
     * @param string $name Property name
     * @param array  $info Property info
     *
     * @return string
     */
    protected function dumpProp($name, array $info)
    {
        $name = $this->cfg['escapeCodes']['property'] . $name . $this->escapeReset;
        $prefix = $this->dumpPropPrefix($info);
        $vis = $this->cfg['escapeCodes']['muted'] . '(' . $this->dumpPropVis($info) . ')' . $this->escapeReset;
        $val = $info['debugInfoExcluded']
            ? ''
            : \sprintf(
                ' %s=%s %s',
                $this->cfg['escapeCodes']['operator'],
                $this->escapeReset,
                $this->dump($info['value'])
            );
        return \sprintf('    %s%s %s%s', $prefix, $vis, $name, $val) . "\n";
    }

    /**
     * Get inherited/dynamic/override indicator
     *
     * @param array $info Property info
     *
     * @return string
     */
    protected function dumpPropPrefix(array $info)
    {
        return \strtr(parent::dumpPropPrefix($info), array(
            '↳' => $this->cfg['escapeCodes']['muted'] . '↳' . $this->escapeReset,
            'âš ' => $this->cfg['escapeCodesMethods']['warn'] . 'âš ' . $this->escapeReset,
            '⟳' => $this->cfg['escapeCodes']['muted'] . '⟳' . $this->escapeReset,
        ));
    }

    /**
     * Dump recursion (array recursion)
     *
     * @return string
     */
    protected function dumpRecursion()
    {
        return $this->cfg['escapeCodes']['keyword'] . 'array '
            . $this->cfg['escapeCodes']['recursion'] . '*RECURSION*'
            . $this->escapeReset;
    }

    /**
     * Dump string
     *
     * @param string      $val string value
     * @param Abstraction $abs (optional) full abstraction
     *
     * @return string
     */
    protected function dumpString($val, Abstraction $abs = null)
    {
        $addQuotes = $this->getDumpOpt('addQuotes');
        if (\is_numeric($val)) {
            return $this->dumpStringNumeric($val, $addQuotes, $abs);
        }
        $escapeCodes = $this->cfg['escapeCodes'];
        $val = $this->debug->utf8->dump($val);
        if ($addQuotes) {
            $ansiQuote = $escapeCodes['quote'] . '"' . $this->escapeReset;
            $val = $ansiQuote . $val . $ansiQuote;
        }
        $diff = $abs && $abs['strlen']
            ? $abs['strlen'] - \strlen($abs['value'])
            : 0;
        if ($diff) {
            $val .= $escapeCodes['maxlen']
                . '[' . $diff . ' more bytes (not logged)]'
                . $this->escapeReset;
        }
        return $val;
    }

    /**
     * Dump numeric string
     *
     * @param string      $val       numeric string value
     * @param bool        $addQuotes whether to add quotes
     * @param Abstraction $abs       (optional) full abstraction
     *
     * @return string
     */
    private function dumpStringNumeric($val, $addQuotes, Abstraction $abs = null)
    {
        $escapeCodes = $this->cfg['escapeCodes'];
        $date = $this->checkTimestamp($val, $abs);
        $val = $escapeCodes['numeric'] . $val;
        if ($addQuotes) {
            $val = $escapeCodes['quote'] . '"'
                . $val
                . $escapeCodes['quote'] . '"';
        }
        $val .= $this->escapeReset;
        return $date
            ? '📅 ' . $val . ' ' . $escapeCodes['muted'] . '(' . $date . ')' . $this->escapeReset
            : $val;
    }

    /**
     * Dump undefined
     *
     * @return string
     */
    protected function dumpUndefined()
    {
        return "\e[2mundefined\e[22m";
    }
}