kintParser.class.php

File

kint/kint/inc/kintParser.class.php

View source
<?php

abstract class kintParser extends kintVariableData {
    private static $_level = 0;
    private static $_customDataTypes;
    private static $_objectParsers;
    private static $_objects;
    private static $_marker;
    private static $_skipAlternatives = false;
    private static $_placeFullStringInValue = false;
    private static function _init() {
        $fh = opendir(KINT_DIR . 'parsers/custom/');
        while ($fileName = readdir($fh)) {
            if (substr($fileName, -4) !== '.php') {
                continue;
            }
            require KINT_DIR . 'parsers/custom/' . $fileName;
            self::$_customDataTypes[] = substr($fileName, 0, -4);
        }
        $fh = opendir(KINT_DIR . 'parsers/objects/');
        while ($fileName = readdir($fh)) {
            if (substr($fileName, -4) !== '.php') {
                continue;
            }
            require KINT_DIR . 'parsers/objects/' . $fileName;
            self::$_objectParsers[] = substr($fileName, 0, -4);
        }
    }
    public static function reset() {
        self::$_level = 0;
        self::$_objects = self::$_marker = null;
    }
    
    /**
     * main and usually single method a custom parser must implement
     *
     * @param mixed $variable
     *
     * @return mixed [!!!] false is returned if the variable is not of current type
     */
    protected abstract function _parse(&$variable);
    
    /**
     * the only public entry point to return a parsed representation of a variable
     *
     * @static
     *
     * @param      $variable
     * @param null $name
     *
     * @throws Exception
     * @return \kintParser
     */
    public static final function factory(&$variable, $name = null) {
        isset(self::$_customDataTypes) or self::_init();
        
        # save internal data to revert after dumping to properly handle recursions etc
        $revert = array(
            'level' => self::$_level,
            'objects' => self::$_objects,
        );
        self::$_level++;
        $varData = new kintVariableData();
        $varData->name = $name;
        
        # first parse the variable based on its type
        $varType = gettype($variable);
        $varType === 'unknown type' and $varType = 'unknown';
        
        # PHP 5.4 inconsistency
        $methodName = '_parse_' . $varType;
        
        # objects can be presented in a different way altogether, INSTEAD, not ALONGSIDE the generic parser
        if ($varType === 'object') {
            foreach (self::$_objectParsers as $parserClass) {
                $className = 'Kint_Objects_' . $parserClass;
                
                /** @var $object KintObject */
                $object = new $className();
                if (($alternativeTabs = $object->parse($variable)) !== false) {
                    self::$_skipAlternatives = true;
                    $alternativeDisplay = new kintVariableData();
                    $alternativeDisplay->type = $object->name;
                    $alternativeDisplay->value = $object->value;
                    $alternativeDisplay->name = $name;
                    foreach ($alternativeTabs as $name => $values) {
                        $alternative = kintParser::factory($values);
                        $alternative->type = $name;
                        if (Kint::enabled() === Kint::MODE_RICH) {
                            empty($alternative->value) and $alternative->value = $alternative->extendedValue;
                            $alternativeDisplay->_alternatives[] = $alternative;
                        }
                        else {
                            $alternativeDisplay->extendedValue[] = $alternative;
                        }
                    }
                    self::$_skipAlternatives = false;
                    self::$_level = $revert['level'];
                    self::$_objects = $revert['objects'];
                    return $alternativeDisplay;
                }
            }
        }
        
        # base type parser returning false means "stop processing further": e.g. recursion
        if (self::$methodName($variable, $varData) === false) {
            self::$_level--;
            return $varData;
        }
        if (Kint::enabled() === Kint::MODE_RICH && !self::$_skipAlternatives) {
            
            # if an alternative returns something that can be represented in an alternative way, don't :)
            self::$_skipAlternatives = true;
            
            # now check whether the variable can be represented in a different way
            foreach (self::$_customDataTypes as $parserClass) {
                $className = 'Kint_Parsers_' . $parserClass;
                
                /** @var $parser kintParser */
                $parser = new $className();
                $parser->name = $name;
                
                # the parser may overwrite the name value, so set it first
                if ($parser->_parse($variable) !== false) {
                    $varData->_alternatives[] = $parser;
                }
            }
            
            # if alternatives exist, push extendedValue to their front and display it as one of alternatives
            if (!empty($varData->_alternatives) && isset($varData->extendedValue)) {
                $_ = new kintVariableData();
                $_->value = $varData->extendedValue;
                $_->type = 'contents';
                $_->size = null;
                array_unshift($varData->_alternatives, $_);
                $varData->extendedValue = null;
            }
            self::$_skipAlternatives = false;
        }
        self::$_level = $revert['level'];
        self::$_objects = $revert['objects'];
        if (strlen($varData->name) > 80) {
            $varData->name = self::_substr($varData->name, 0, 37) . '...' . self::_substr($varData->name, -38, null);
        }
        return $varData;
    }
    private static function _checkDepth() {
        return Kint::$maxLevels != 0 && self::$_level >= Kint::$maxLevels;
    }
    private static function _isArrayTabular(array $variable) {
        if (Kint::enabled() !== Kint::MODE_RICH) {
            return false;
        }
        $arrayKeys = array();
        $keys = null;
        $closeEnough = false;
        foreach ($variable as $row) {
            if (!is_array($row) || empty($row)) {
                return false;
            }
            foreach ($row as $col) {
                if (!empty($col) && !is_scalar($col)) {
                    return false;
                }
                // todo add tabular "tolerance"
            }
            if (isset($keys) && !$closeEnough) {
                
                # let's just see if the first two rows have same keys, that's faster and has the
                
                # positive side effect of easily spotting missing keys in later rows
                if ($keys !== array_keys($row)) {
                    return false;
                }
                $closeEnough = true;
            }
            else {
                $keys = array_keys($row);
            }
            $arrayKeys = array_unique(array_merge($arrayKeys, $keys));
        }
        return $arrayKeys;
    }
    private static function _decorateCell(kintVariableData $kintVar) {
        if ($kintVar->extendedValue !== null || !empty($kintVar->_alternatives)) {
            return '<td>' . Kint_Decorators_Rich::decorate($kintVar) . '</td>';
        }
        $output = '<td';
        if ($kintVar->value !== null) {
            $output .= ' title="' . $kintVar->type;
            if ($kintVar->size !== null) {
                $output .= " (" . $kintVar->size . ")";
            }
            $output .= '">' . $kintVar->value;
        }
        else {
            $output .= '>';
            if ($kintVar->type !== 'NULL') {
                $output .= '<u>' . $kintVar->type;
                if ($kintVar->size !== null) {
                    $output .= "(" . $kintVar->size . ")";
                }
                $output .= '</u>';
            }
            else {
                $output .= '<u>NULL</u>';
            }
        }
        return $output . '</td>';
    }
    public static function escape($value, $encoding = null) {
        if (empty($value)) {
            return $value;
        }
        if (Kint::enabled() === Kint::MODE_CLI) {
            $value = str_replace("\x1b", "\\x1b", $value);
        }
        if (Kint::enabled() === Kint::MODE_CLI || Kint::enabled() === Kint::MODE_WHITESPACE) {
            return $value;
        }
        $encoding or $encoding = self::_detectEncoding($value);
        $value = htmlspecialchars($value, ENT_NOQUOTES, $encoding === 'ASCII' ? 'UTF-8' : $encoding);
        if ($encoding === 'UTF-8') {
            // todo we could make the symbols hover-title show the code for the invisible symbol
            
            # when possible force invisible characters to have some sort of display (experimental)
            $value = preg_replace('/[\\x00-\\x08\\x0B\\x0C\\x0E-\\x1F\\x80-\\x9F]/u', '?', $value);
        }
        
        # this call converts all non-ASCII characters into html chars of format
        if (function_exists('mb_encode_numericentity')) {
            $value = mb_encode_numericentity($value, array(
                0x80,
                0xffff,
                0,
                0xffff,
            ), $encoding);
        }
        return $value;
    }
    private static $_dealingWithGlobals = false;
    private static function _parse_array(&$variable, kintVariableData $variableData) {
        isset(self::$_marker) or self::$_marker = "\x00" . uniqid();
        
        # naturally, $GLOBALS variable is an intertwined recursion nightmare, use black magic
        $globalsDetector = false;
        if (array_key_exists('GLOBALS', $variable) && is_array($variable['GLOBALS'])) {
            $globalsDetector = "\x01" . uniqid();
            $variable['GLOBALS'][$globalsDetector] = true;
            if (isset($variable[$globalsDetector])) {
                unset($variable[$globalsDetector]);
                self::$_dealingWithGlobals = true;
            }
            else {
                unset($variable['GLOBALS'][$globalsDetector]);
                $globalsDetector = false;
            }
        }
        $variableData->type = 'array';
        $variableData->size = count($variable);
        if ($variableData->size === 0) {
            return;
        }
        if (isset($variable[self::$_marker])) {
            
            # recursion; todo mayhaps show from where
            if (self::$_dealingWithGlobals) {
                $variableData->value = '*RECURSION*';
            }
            else {
                unset($variable[self::$_marker]);
                $variableData->value = self::$_marker;
            }
            return false;
        }
        if (self::_checkDepth()) {
            $variableData->extendedValue = "*DEPTH TOO GREAT*";
            return false;
        }
        $isSequential = self::_isSequential($variable);
        if ($variableData->size > 1 && ($arrayKeys = self::_isArrayTabular($variable)) !== false) {
            $variable[self::$_marker] = true;
            
            # this must be AFTER _isArrayTabular
            $firstRow = true;
            $extendedValue = '<table class="kint-report"><thead>';
            foreach ($variable as $rowIndex => &$row) {
                
                # display strings in their full length
                self::$_placeFullStringInValue = true;
                if ($rowIndex === self::$_marker) {
                    continue;
                }
                if (isset($row[self::$_marker])) {
                    $variableData->value = "*RECURSION*";
                    return false;
                }
                $extendedValue .= '<tr>';
                if ($isSequential) {
                    $output = '<td>' . '#' . ($rowIndex + 1) . '</td>';
                }
                else {
                    $output = self::_decorateCell(kintParser::factory($rowIndex));
                }
                if ($firstRow) {
                    $extendedValue .= '<th>&nbsp;</th>';
                }
                
                # we iterate the known full set of keys from all rows in case some appeared at later rows,
                
                # as we only check the first two to assume
                foreach ($arrayKeys as $key) {
                    if ($firstRow) {
                        $extendedValue .= '<th>' . self::escape($key) . '</th>';
                    }
                    if (!array_key_exists($key, $row)) {
                        $output .= '<td class="kint-empty"></td>';
                        continue;
                    }
                    $var = kintParser::factory($row[$key]);
                    if ($var->value === self::$_marker) {
                        $variableData->value = '*RECURSION*';
                        return false;
                    }
                    elseif ($var->value === '*RECURSION*') {
                        $output .= '<td class="kint-empty"><u>*RECURSION*</u></td>';
                    }
                    else {
                        $output .= self::_decorateCell($var);
                    }
                    unset($var);
                }
                if ($firstRow) {
                    $extendedValue .= '</tr></thead><tr>';
                    $firstRow = false;
                }
                $extendedValue .= $output . '</tr>';
            }
            self::$_placeFullStringInValue = false;
            $variableData->extendedValue = $extendedValue . '</table>';
        }
        else {
            $variable[self::$_marker] = true;
            $extendedValue = array();
            foreach ($variable as $key => &$val) {
                if ($key === self::$_marker) {
                    continue;
                }
                $output = kintParser::factory($val);
                if ($output->value === self::$_marker) {
                    $variableData->value = "*RECURSION*";
                    // recursion occurred on a higher level, thus $this is recursion
                    return false;
                }
                if (!$isSequential) {
                    $output->operator = '=>';
                }
                $output->name = $isSequential ? null : "'" . $key . "'";
                $extendedValue[] = $output;
            }
            $variableData->extendedValue = $extendedValue;
        }
        if ($globalsDetector) {
            self::$_dealingWithGlobals = false;
        }
        unset($variable[self::$_marker]);
    }
    private static function _parse_object(&$variable, kintVariableData $variableData) {
        if (function_exists('spl_object_hash')) {
            $hash = spl_object_hash($variable);
        }
        else {
            ob_start();
            var_dump($variable);
            preg_match('[#(\\d+)]', ob_get_clean(), $match);
            $hash = $match[1];
        }
        $castedArray = (array) $variable;
        $variableData->type = get_class($variable);
        $variableData->size = count($castedArray);
        if (isset(self::$_objects[$hash])) {
            $variableData->value = '*RECURSION*';
            return false;
        }
        if (self::_checkDepth()) {
            $variableData->extendedValue = "*DEPTH TOO GREAT*";
            return false;
        }
        
        # ArrayObject (and maybe ArrayIterator, did not try yet) unsurprisingly consist of mainly dark magic.
        
        # What bothers me most, var_dump sees no problem with it, and ArrayObject also uses a custom,
        
        # undocumented serialize function, so you can see the properties in internal functions, but
        
        # can never iterate some of them if the flags are not STD_PROP_LIST. Fun stuff.
        if ($variableData->type === 'ArrayObject' || is_subclass_of($variable, 'ArrayObject')) {
            $arrayObjectFlags = $variable->getFlags();
            $variable->setFlags(ArrayObject::STD_PROP_LIST);
        }
        self::$_objects[$hash] = true;
        // todo store reflectorObject here for alternatives cache
        $reflector = new ReflectionObject($variable);
        
        # add link to definition of userland objects
        if (Kint::enabled() === Kint::MODE_RICH && Kint::$fileLinkFormat && $reflector->isUserDefined()) {
            $url = Kint::getIdeLink($reflector->getFileName(), $reflector->getStartLine());
            $class = strpos($url, 'http://') === 0 ? 'class="kint-ide-link" ' : '';
            $variableData->type = "<a {$class}href=\"{$url}\">{$variableData->type}</a>";
        }
        $variableData->size = 0;
        $extendedValue = array();
        $encountered = array();
        
        # copy the object as an array as it provides more info than Reflection (depends)
        foreach ($castedArray as $key => $value) {
            
            /* casting object to array:
             * integer properties are inaccessible;
             * private variables have the class name prepended to the variable name;
             * protected variables have a '*' prepended to the variable name.
             * These prepended values have null bytes on either side.
             * http://www.php.net/manual/en/language.types.array.php#language.types.array.casting
             */
            if ($key[0] === "\x00") {
                $access = $key[1] === "*" ? "protected" : "private";
                // Remove the access level from the variable name
                $key = substr($key, strrpos($key, "\x00") + 1);
            }
            else {
                $access = "public";
            }
            $encountered[$key] = true;
            $output = kintParser::factory($value, self::escape($key));
            $output->access = $access;
            $output->operator = '->';
            $extendedValue[] = $output;
            $variableData->size++;
        }
        foreach ($reflector->getProperties() as $property) {
            $name = $property->name;
            if ($property->isStatic() || isset($encountered[$name])) {
                continue;
            }
            if ($property->isProtected()) {
                $property->setAccessible(true);
                $access = "protected";
            }
            elseif ($property->isPrivate()) {
                $property->setAccessible(true);
                $access = "private";
            }
            else {
                $access = "public";
            }
            $value = $property->getValue($variable);
            $output = kintParser::factory($value, self::escape($name));
            $output->access = $access;
            $output->operator = '->';
            $extendedValue[] = $output;
            $variableData->size++;
        }
        if (isset($arrayObjectFlags)) {
            $variable->setFlags($arrayObjectFlags);
        }
        if ($variableData->size) {
            $variableData->extendedValue = $extendedValue;
        }
    }
    private static function _parse_boolean(&$variable, kintVariableData $variableData) {
        $variableData->type = 'bool';
        $variableData->value = $variable ? 'TRUE' : 'FALSE';
    }
    private static function _parse_double(&$variable, kintVariableData $variableData) {
        $variableData->type = 'float';
        $variableData->value = $variable;
    }
    private static function _parse_integer(&$variable, kintVariableData $variableData) {
        $variableData->type = 'integer';
        $variableData->value = $variable;
    }
    private static function _parse_null(&$variable, kintVariableData $variableData) {
        $variableData->type = 'NULL';
    }
    private static function _parse_resource(&$variable, kintVariableData $variableData) {
        $resourceType = get_resource_type($variable);
        $variableData->type = "resource ({$resourceType})";
        if ($resourceType === 'stream' && ($meta = stream_get_meta_data($variable))) {
            if (isset($meta['uri'])) {
                $file = $meta['uri'];
                if (function_exists('stream_is_local')) {
                    // Only exists on PHP >= 5.2.4
                    if (stream_is_local($file)) {
                        $file = Kint::shortenPath($file);
                    }
                }
                $variableData->value = $file;
            }
        }
    }
    private static function _parse_string(&$variable, kintVariableData $variableData) {
        $variableData->type = 'string';
        $encoding = self::_detectEncoding($variable);
        if ($encoding !== 'ASCII') {
            $variableData->type .= ' ' . $encoding;
        }
        $variableData->size = self::_strlen($variable, $encoding);
        if (Kint::enabled() !== Kint::MODE_RICH) {
            $variableData->value = '"' . self::escape($variable, $encoding) . '"';
            return;
        }
        if (!self::$_placeFullStringInValue) {
            $strippedString = preg_replace('[\\s+]', ' ', $variable);
            if (Kint::$maxStrLength && $variableData->size > Kint::$maxStrLength) {
                // encode and truncate
                $variableData->value = '"' . self::escape(self::_substr($strippedString, 0, Kint::$maxStrLength, $encoding), $encoding) . '&hellip;"';
                $variableData->extendedValue = self::escape($variable, $encoding);
                return;
            }
            elseif ($variable !== $strippedString) {
                // omit no data from display
                $variableData->value = '"' . self::escape($variable, $encoding) . '"';
                $variableData->extendedValue = self::escape($variable, $encoding);
                return;
            }
        }
        $variableData->value = '"' . self::escape($variable, $encoding) . '"';
    }
    private static function _parse_unknown(&$variable, kintVariableData $variableData) {
        $type = gettype($variable);
        $variableData->type = "UNKNOWN" . (!empty($type) ? " ({$type})" : '');
        $variableData->value = var_export($variable, true);
    }

}

Classes

Title Deprecated Summary
kintParser