You are here

Kint.class.php in Devel 8

Same filename and directory in other branches
  1. 8.2 kint/kint/Kint.class.php

File

kint/kint/Kint.class.php
View source
<?php

/**
 * Kint is a zero-setup debugging tool to output information about variables and stack traces prettily and comfortably.
 *
 * https://github.com/raveren/kint
 */
if (defined('KINT_DIR')) {
  return;
}
define('KINT_DIR', dirname(__FILE__) . '/');
define('KINT_PHP53', version_compare(PHP_VERSION, '5.3.0') >= 0);
require KINT_DIR . 'config.default.php';
require KINT_DIR . 'inc/kintVariableData.class.php';
require KINT_DIR . 'inc/kintParser.class.php';
require KINT_DIR . 'inc/kintObject.class.php';
require KINT_DIR . 'decorators/rich.php';
require KINT_DIR . 'decorators/plain.php';
if (is_readable(KINT_DIR . 'config.php')) {
  require KINT_DIR . 'config.php';
}

# init settings
if (!empty($GLOBALS['_kint_settings'])) {
  Kint::enabled($GLOBALS['_kint_settings']['enabled']);
  foreach ($GLOBALS['_kint_settings'] as $key => $val) {
    property_exists('Kint', $key) and Kint::${$key} = $val;
  }
  unset($GLOBALS['_kint_settings'], $key, $val);
}
class Kint {

  // these are all public and 1:1 config array keys so you can switch them easily
  private static $_enabledMode;

  # stores mode and active statuses
  public static $returnOutput;
  public static $fileLinkFormat;
  public static $displayCalledFrom;
  public static $charEncodings;
  public static $maxStrLength;
  public static $appRootDirs;
  public static $maxLevels;
  public static $theme;
  public static $expandedByDefault;
  public static $cliDetection;
  public static $cliColors;
  const MODE_RICH = 'r';
  const MODE_WHITESPACE = 'w';
  const MODE_CLI = 'c';
  const MODE_PLAIN = 'p';
  public static $aliases = array(
    'methods' => array(
      array(
        'kint',
        'dump',
      ),
      array(
        'kint',
        'trace',
      ),
    ),
    'functions' => array(
      'd',
      'dd',
      'ddd',
      's',
      'sd',
    ),
  );
  private static $_firstRun = true;

  /**
   * Enables or disables Kint, can globally enforce the rendering mode. If called without parameters, returns the
   * current mode.
   *
   * @param mixed $forceMode
   *      null or void - return current mode
   *      false        - disable (no output)
   *      true         - enable and detect cli automatically
   *      Kint::MODE_* - enable and force selected mode disregarding detection and function
   *                     shorthand (s()/d()), note that you can still override this
   *                     with the "~" modifier
   *
   * @return mixed        previously set value if a new one is passed
   */
  public static function enabled($forceMode = null) {

    # act both as a setter...
    if (isset($forceMode)) {
      $before = self::$_enabledMode;
      self::$_enabledMode = $forceMode;
      return $before;
    }

    # ...and a getter
    return self::$_enabledMode;
  }

  /**
   * Prints a debug backtrace, same as Kint::dump(1)
   *
   * @param array $trace [OPTIONAL] you can pass your own trace, otherwise, `debug_backtrace` will be called
   *
   * @return mixed
   */
  public static function trace($trace = null) {
    if (!self::enabled()) {
      return '';
    }
    return self::dump(isset($trace) ? $trace : debug_backtrace(true));
  }

  /**
   * Dump information about variables, accepts any number of parameters, supports modifiers:
   *
   *  clean up any output before kint and place the dump at the top of page:
   *   - Kint::dump()
   *  *****
   *  expand all nodes on display:
   *   ! Kint::dump()
   *  *****
   *  dump variables disregarding their depth:
   *   + Kint::dump()
   *  *****
   *  return output instead of displaying it:
   *   @ Kint::dump()
   *  *****
   *  force output as plain text
   *   ~ Kint::dump()
   *
   * Modifiers are supported by all dump wrapper functions, including Kint::trace(). Space is optional.
   *
   *
   * You can also use the following shorthand to display debug_backtrace():
   *   Kint::dump( 1 );
   *
   * Passing the result from debug_backtrace() to kint::dump() as a single parameter will display it as trace too:
   *   $trace = debug_backtrace( true );
   *   Kint::dump( $trace );
   *  Or simply:
   *   Kint::dump( debug_backtrace() );
   *
   *
   * @param mixed $data
   *
   * @return void|string
   */
  public static function dump($data = null) {
    if (!self::enabled()) {
      return '';
    }
    list($names, $modifiers, $callee, $previousCaller, $miniTrace) = self::_getCalleeInfo(defined('DEBUG_BACKTRACE_IGNORE_ARGS') ? debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS) : debug_backtrace());
    $modeOldValue = self::enabled();
    $firstRunOldValue = self::$_firstRun;

    # process modifiers: @, +, !, ~ and -
    if (strpos($modifiers, '-') !== false) {
      self::$_firstRun = true;
      while (ob_get_level()) {
        ob_end_clean();
      }
    }
    if (strpos($modifiers, '!') !== false) {
      $expandedByDefaultOldValue = self::$expandedByDefault;
      self::$expandedByDefault = true;
    }
    if (strpos($modifiers, '+') !== false) {
      $maxLevelsOldValue = self::$maxLevels;
      self::$maxLevels = false;
    }
    if (strpos($modifiers, '@') !== false) {
      $returnOldValue = self::$returnOutput;
      self::$returnOutput = true;
      self::$_firstRun = true;
    }
    if (strpos($modifiers, '~') !== false) {
      self::enabled(self::MODE_WHITESPACE);
    }

    # set mode for current run
    $mode = self::enabled();
    if ($mode === true) {
      $mode = PHP_SAPI === 'cli' ? self::MODE_CLI : self::MODE_RICH;
    }
    self::enabled($mode);
    $decorator = self::enabled() === self::MODE_RICH ? 'Kint_Decorators_Rich' : 'Kint_Decorators_Plain';
    $output = '';
    if (self::$_firstRun) {
      $output .= call_user_func(array(
        $decorator,
        'init',
      ));
    }
    $trace = false;
    if ($names === array(
      null,
    ) && func_num_args() === 1 && $data === 1) {

      # Kint::dump(1) shorthand
      $trace = KINT_PHP53 ? debug_backtrace(true) : debug_backtrace();
    }
    elseif (func_num_args() === 1 && is_array($data)) {
      $trace = $data;

      # test if the single parameter is result of debug_backtrace()
    }
    $trace and $trace = self::_parseTrace($trace);
    $output .= call_user_func(array(
      $decorator,
      'wrapStart',
    ));
    if ($trace) {
      $output .= call_user_func(array(
        $decorator,
        'decorateTrace',
      ), $trace);
    }
    else {
      $data = func_num_args() === 0 ? array(
        "[[no arguments passed]]",
      ) : func_get_args();
      foreach ($data as $k => $argument) {
        kintParser::reset();

        # when the dump arguments take long to generate output, user might have changed the file and

        # Kint might not parse the arguments correctly, so check if names are set and while the

        # displayed names might be wrong, at least don't throw an error
        $output .= call_user_func(array(
          $decorator,
          'decorate',
        ), kintParser::factory($argument, isset($names[$k]) ? $names[$k] : ''));
      }
    }
    $output .= call_user_func(array(
      $decorator,
      'wrapEnd',
    ), $callee, $miniTrace, $previousCaller);
    self::enabled($modeOldValue);
    self::$_firstRun = false;
    if (strpos($modifiers, '~') !== false) {
      self::$_firstRun = $firstRunOldValue;
    }
    else {
      self::enabled($modeOldValue);
    }
    if (strpos($modifiers, '!') !== false) {
      self::$expandedByDefault = $expandedByDefaultOldValue;
    }
    if (strpos($modifiers, '+') !== false) {
      self::$maxLevels = $maxLevelsOldValue;
    }
    if (strpos($modifiers, '@') !== false) {
      self::$returnOutput = $returnOldValue;
      self::$_firstRun = $firstRunOldValue;
      return $output;
    }
    if (self::$returnOutput) {
      return $output;
    }
    echo $output;
    return '';
  }

  /**
   * generic path display callback, can be configured in the settings; purpose is to show relevant path info and hide
   * as much of the path as possible.
   *
   * @param string $file
   *
   * @return string
   */
  public static function shortenPath($file) {
    $file = str_replace('\\', '/', $file);
    $shortenedName = $file;
    $replaced = false;
    if (is_array(self::$appRootDirs)) {
      foreach (self::$appRootDirs as $path => $replaceString) {
        if (empty($path)) {
          continue;
        }
        $path = str_replace('\\', '/', $path);
        if (strpos($file, $path) === 0) {
          $shortenedName = $replaceString . substr($file, strlen($path));
          $replaced = true;
          break;
        }
      }
    }

    # fallback to find common path with Kint dir
    if (!$replaced) {
      $pathParts = explode('/', str_replace('\\', '/', KINT_DIR));
      $fileParts = explode('/', $file);
      $i = 0;
      foreach ($fileParts as $i => $filePart) {
        if (!isset($pathParts[$i]) || $pathParts[$i] !== $filePart) {
          break;
        }
      }
      $shortenedName = ($i ? '.../' : '') . implode('/', array_slice($fileParts, $i));
    }
    return $shortenedName;
  }
  public static function getIdeLink($file, $line) {
    return str_replace(array(
      '%f',
      '%l',
    ), array(
      $file,
      $line,
    ), self::$fileLinkFormat);
  }

  /**
   * trace helper, shows the place in code inline
   *
   * @param string $file full path to file
   * @param int    $lineNumber the line to display
   * @param int    $padding surrounding lines to show besides the main one
   *
   * @return bool|string
   */
  private static function _showSource($file, $lineNumber, $padding = 7) {
    if (!$file or !is_readable($file)) {

      # continuing will cause errors
      return false;
    }

    # open the file and set the line position
    $file = fopen($file, 'r');
    $line = 0;

    # Set the reading range
    $range = array(
      'start' => $lineNumber - $padding,
      'end' => $lineNumber + $padding,
    );

    # set the zero-padding amount for line numbers
    $format = '% ' . strlen($range['end']) . 'd';
    $source = '';
    while (($row = fgets($file)) !== false) {

      # increment the line number
      if (++$line > $range['end']) {
        break;
      }
      if ($line >= $range['start']) {

        # make the row safe for output
        $row = htmlspecialchars($row, ENT_NOQUOTES, 'UTF-8');

        # trim whitespace and sanitize the row
        $row = '<span>' . sprintf($format, $line) . '</span> ' . $row;
        if ($line === $lineNumber) {

          # apply highlighting to this row
          $row = '<div class="kint-highlight">' . $row . '</div>';
        }
        else {
          $row = '<div>' . $row . '</div>';
        }

        # add to the captured source
        $source .= $row;
      }
    }

    # close the file
    fclose($file);
    return $source;
  }

  /**
   * returns parameter names that the function was passed, as well as any predefined symbols before function
   * call (modifiers)
   *
   * @param array $trace
   *
   * @return array( $parameters, $modifier, $callee, $previousCaller )
   */
  private static function _getCalleeInfo($trace) {
    $previousCaller = array();
    $miniTrace = array();
    $prevStep = array();

    # go from back of trace to find first occurrence of call to Kint or its wrappers
    while ($step = array_pop($trace)) {
      if (self::_stepIsInternal($step)) {
        $previousCaller = $prevStep;
        break;
      }
      elseif (isset($step['file'], $step['line'])) {
        unset($step['object'], $step['args']);
        array_unshift($miniTrace, $step);
      }
      $prevStep = $step;
    }
    $callee = $step;
    if (!isset($callee['file']) || !is_readable($callee['file'])) {
      return false;
    }

    # open the file and read it up to the position where the function call expression ended
    $file = fopen($callee['file'], 'r');
    $line = 0;
    $source = '';
    while (($row = fgets($file)) !== false) {
      if (++$line > $callee['line']) {
        break;
      }
      $source .= $row;
    }
    fclose($file);
    $source = self::_removeAllButCode($source);
    if (empty($callee['class'])) {
      $codePattern = $callee['function'];
    }
    else {
      if ($callee['type'] === '::') {
        $codePattern = $callee['class'] . "\7*" . $callee['type'] . "\7*" . $callee['function'];
      }
      else {

        /*if ( $callee['type'] === '->' )*/
        $codePattern = ".*\7*" . $callee['type'] . "\7*" . $callee['function'];
      }
    }

    // todo if more than one call in one line - not possible to determine variable names
    // todo does not recognize string concat

    # get the position of the last call to the function
    preg_match_all("\n            [\n            # beginning of statement\n            [\7{(]\n\n            # search for modifiers (group 1)\n            ([-+!@~]*)?\n\n            # spaces\n            \7*\n\n            # check if output is assigned to a variable (group 2) todo: does not detect concat\n            (\n                \\\$[a-z0-9_]+ # variable\n                \7*\\.?=\7*  # assignment\n            )?\n\n            # possibly a namespace symbol\n            \\\\?\n\n\t\t\t# spaces again\n            \7*\n\n            # main call to Kint\n            {$codePattern}\n\n\t\t\t# spaces everywhere\n            \7*\n\n            # find the character where kint's opening bracket resides (group 3)\n            (\\()\n\n            ]ix", $source, $matches, PREG_OFFSET_CAPTURE);
    $modifiers = end($matches[1]);
    $assignment = end($matches[2]);
    $bracket = end($matches[3]);
    $modifiers = $modifiers[0];
    if ($assignment[1] !== -1) {
      $modifiers .= '@';
    }
    $paramsString = preg_replace("[\7+]", ' ', substr($source, $bracket[1] + 1));

    # we now have a string like this:

    # <parameters passed>); <the rest of the last read line>

    # remove everything in brackets and quotes, we don't need nested statements nor literal strings which would

    # only complicate separating individual arguments
    $c = strlen($paramsString);
    $inString = $escaped = $openedBracket = $closingBracket = false;
    $i = 0;
    $inBrackets = 0;
    $openedBrackets = array();
    while ($i < $c) {
      $letter = $paramsString[$i];
      if (!$inString) {
        if ($letter === '\'' || $letter === '"') {
          $inString = $letter;
        }
        elseif ($letter === '(' || $letter === '[') {
          $inBrackets++;
          $openedBrackets[] = $openedBracket = $letter;
          $closingBracket = $openedBracket === '(' ? ')' : ']';
        }
        elseif ($inBrackets && $letter === $closingBracket) {
          $inBrackets--;
          array_pop($openedBrackets);
          $openedBracket = end($openedBrackets);
          $closingBracket = $openedBracket === '(' ? ')' : ']';
        }
        elseif (!$inBrackets && $letter === ')') {
          $paramsString = substr($paramsString, 0, $i);
          break;
        }
      }
      elseif ($letter === $inString && !$escaped) {
        $inString = false;
      }

      # replace whatever was inside quotes or brackets with untypeable characters, we don't

      # need that info. We'll later replace the whole string with '...'
      if ($inBrackets > 0) {
        if ($inBrackets > 1 || $letter !== $openedBracket) {
          $paramsString[$i] = "\7";
        }
      }
      if ($inString) {
        if ($letter !== $inString || $escaped) {
          $paramsString[$i] = "\7";
        }
      }
      $escaped = !$escaped && $letter === '\\';
      $i++;
    }

    # by now we have an un-nested arguments list, lets make it to an array for processing further
    $arguments = explode(',', preg_replace("[\7+]", '...', $paramsString));

    # test each argument whether it was passed literary or was it an expression or a variable name
    $parameters = array();
    $blacklist = array(
      'null',
      'true',
      'false',
      'array(...)',
      'array()',
      '"..."',
      '[...]',
      'b"..."',
    );
    foreach ($arguments as $argument) {
      $argument = trim($argument);
      if (is_numeric($argument) || in_array(str_replace("'", '"', strtolower($argument)), $blacklist, true)) {
        $parameters[] = null;
      }
      else {
        $parameters[] = $argument;
      }
    }
    return array(
      $parameters,
      $modifiers,
      $callee,
      $previousCaller,
      $miniTrace,
    );
  }

  /**
   * removes comments and zaps whitespace & <?php tags from php code, makes for easier further parsing
   *
   * @param string $source
   *
   * @return string
   */
  private static function _removeAllButCode($source) {
    $commentTokens = array(
      T_COMMENT => true,
      T_INLINE_HTML => true,
      T_DOC_COMMENT => true,
    );
    $whiteSpaceTokens = array(
      T_WHITESPACE => true,
      T_CLOSE_TAG => true,
      T_OPEN_TAG => true,
      T_OPEN_TAG_WITH_ECHO => true,
    );
    $cleanedSource = '';
    foreach (token_get_all($source) as $token) {
      if (is_array($token)) {
        if (isset($commentTokens[$token[0]])) {
          continue;
        }
        if (isset($whiteSpaceTokens[$token[0]])) {
          $token = "\7";
        }
        else {
          $token = $token[1];
        }
      }
      elseif ($token === ';') {
        $token = "\7";
      }
      $cleanedSource .= $token;
    }
    return $cleanedSource;
  }

  /**
   * returns whether current trace step belongs to Kint or its wrappers
   *
   * @param $step
   *
   * @return array
   */
  private static function _stepIsInternal($step) {
    if (isset($step['class'])) {
      foreach (self::$aliases['methods'] as $alias) {
        if ($alias[0] === strtolower($step['class']) && $alias[1] === strtolower($step['function'])) {
          return true;
        }
      }
      return false;
    }
    else {
      return in_array(strtolower($step['function']), self::$aliases['functions'], true);
    }
  }
  private static function _parseTrace(array $data) {
    $trace = array();
    $traceFields = array(
      'file',
      'line',
      'args',
      'class',
    );
    $fileFound = false;

    # file element must exist in one of the steps

    # validate whether a trace was indeed passed
    while ($step = array_pop($data)) {
      if (!is_array($step) || !isset($step['function'])) {
        return false;
      }
      if (!$fileFound && isset($step['file']) && file_exists($step['file'])) {
        $fileFound = true;
      }
      $valid = false;
      foreach ($traceFields as $element) {
        if (isset($step[$element])) {
          $valid = true;
          break;
        }
      }
      if (!$valid) {
        return false;
      }
      if (self::_stepIsInternal($step)) {
        $step = array(
          'file' => $step['file'],
          'line' => $step['line'],
          'function' => '',
        );
        array_unshift($trace, $step);
        break;
      }
      if ($step['function'] !== 'spl_autoload_call') {

        # meaningless
        array_unshift($trace, $step);
      }
    }
    if (!$fileFound) {
      return false;
    }
    $output = array();
    foreach ($trace as $step) {
      if (isset($step['file'])) {
        $file = $step['file'];
        if (isset($step['line'])) {
          $line = $step['line'];

          # include the source of this step
          if (self::enabled() === self::MODE_RICH) {
            $source = self::_showSource($file, $line);
          }
        }
      }
      $function = $step['function'];
      if (in_array($function, array(
        'include',
        'include_once',
        'require',
        'require_once',
      ))) {
        if (empty($step['args'])) {

          # no arguments
          $args = array();
        }
        else {

          # sanitize the included file path
          $args = array(
            'file' => self::shortenPath($step['args'][0]),
          );
        }
      }
      elseif (isset($step['args'])) {
        if (empty($step['class']) && !function_exists($function)) {

          # introspection on closures or language constructs in a stack trace is impossible before PHP 5.3
          $params = null;
        }
        else {
          try {
            if (isset($step['class'])) {
              if (method_exists($step['class'], $function)) {
                $reflection = new ReflectionMethod($step['class'], $function);
              }
              else {
                if (isset($step['type']) && $step['type'] == '::') {
                  $reflection = new ReflectionMethod($step['class'], '__callStatic');
                }
                else {
                  $reflection = new ReflectionMethod($step['class'], '__call');
                }
              }
            }
            else {
              $reflection = new ReflectionFunction($function);
            }

            # get the function parameters
            $params = $reflection
              ->getParameters();
          } catch (Exception $e) {

            # avoid various PHP version incompatibilities
            $params = null;
          }
        }
        $args = array();
        foreach ($step['args'] as $i => $arg) {
          if (isset($params[$i])) {

            # assign the argument by the parameter name
            $args[$params[$i]->name] = $arg;
          }
          else {

            # assign the argument by number
            $args['#' . ($i + 1)] = $arg;
          }
        }
      }
      if (isset($step['class'])) {

        # Class->method() or Class::method()
        $function = $step['class'] . $step['type'] . $function;
      }

      // todo it's possible to parse the object name out from the source!
      $output[] = array(
        'function' => $function,
        'args' => isset($args) ? $args : null,
        'file' => isset($file) ? $file : null,
        'line' => isset($line) ? $line : null,
        'source' => isset($source) ? $source : null,
        'object' => isset($step['object']) ? $step['object'] : null,
      );
      unset($function, $args, $file, $line, $source);
    }
    return $output;
  }

}
if (!function_exists('d')) {

  /**
   * Alias of Kint::dump()
   *
   * @return string
   */
  function d() {
    if (!Kint::enabled()) {
      return '';
    }
    $_ = func_get_args();
    return call_user_func_array(array(
      'Kint',
      'dump',
    ), $_);
  }
}
if (!function_exists('dd')) {

  /**
   * Alias of Kint::dump()
   * [!!!] IMPORTANT: execution will halt after call to this function
   *
   * @return string
   * @deprecated
   */
  function dd() {
    if (!Kint::enabled()) {
      return '';
    }
    echo "<pre>Kint: dd() is being deprecated, please use ddd() instead</pre>\n";
    $_ = func_get_args();
    call_user_func_array(array(
      'Kint',
      'dump',
    ), $_);
    die;
  }
}
if (!function_exists('ddd')) {

  /**
   * Alias of Kint::dump()
   * [!!!] IMPORTANT: execution will halt after call to this function
   *
   * @return string
   */
  function ddd() {
    if (!Kint::enabled()) {
      return '';
    }
    $_ = func_get_args();
    call_user_func_array(array(
      'Kint',
      'dump',
    ), $_);
    die;
  }
}
if (!function_exists('s')) {

  /**
   * Alias of Kint::dump(), however the output is in plain htmlescaped text and some minor visibility enhancements
   * added. If run in CLI mode, output is pure whitespace.
   *
   * To force rendering mode without autodetecting anything:
   *
   *  Kint::enabled( Kint::MODE_PLAIN );
   *  Kint::dump( $variable );
   *
   * [!!!] IMPORTANT: execution will halt after call to this function
   *
   * @return string
   */
  function s() {
    $enabled = Kint::enabled();
    if (!$enabled) {
      return '';
    }
    if ($enabled === Kint::MODE_WHITESPACE) {

      # if already in whitespace, don't elevate to plain
      $restoreMode = Kint::MODE_WHITESPACE;
    }
    else {
      $restoreMode = Kint::enabled(PHP_SAPI === 'cli' ? Kint::MODE_WHITESPACE : Kint::MODE_PLAIN);
    }
    $params = func_get_args();
    $dump = call_user_func_array(array(
      'Kint',
      'dump',
    ), $params);
    Kint::enabled($restoreMode);
    return $dump;
  }
}
if (!function_exists('sd')) {

  /**
   * @see s()
   *
   * [!!!] IMPORTANT: execution will halt after call to this function
   *
   * @return string
   */
  function sd() {
    $enabled = Kint::enabled();
    if (!$enabled) {
      return '';
    }
    if ($enabled !== Kint::MODE_WHITESPACE) {
      Kint::enabled(PHP_SAPI === 'cli' ? Kint::MODE_WHITESPACE : Kint::MODE_PLAIN);
    }
    $params = func_get_args();
    call_user_func_array(array(
      'Kint',
      'dump',
    ), $params);
    die;
  }
}

Constants

Namesort descending Description
KINT_DIR
KINT_PHP53

Classes

Namesort descending Description
Kint