You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1082 lines
43 KiB
PHP

<?php
/*
* Sage is a zero-setup PHP debugging assistant. It provides insightful data about variables and program flow.
*
* https://github.com/php-sage/sage
*
* The MIT License (MIT)
*
* Copyright (c) 2013 Rokas Sleinius (raveren@gmail.com) and contributors:
* (https://github.com/php-sage/sage/contributors)
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
if (defined('SAGE_DIR')) {
return;
}
define('SAGE_DIR', dirname(__FILE__) . '/');
require SAGE_DIR . 'inc/SageVariableData.php';
require SAGE_DIR . 'inc/SageTraceStep.php';
require SAGE_DIR . 'inc/SageParser.php';
require SAGE_DIR . 'inc/SageHelper.php';
require SAGE_DIR . 'inc/shorthands.inc.php';
require SAGE_DIR . 'decorators/SageDecoratorsInterface.php';
require SAGE_DIR . 'decorators/SageDecoratorsRich.php';
require SAGE_DIR . 'decorators/SageDecoratorsPlain.php';
require SAGE_DIR . 'parsers/SageParserInterface.php';
class Sage
{
private static $_initialized = false;
private static $_enabledMode = true;
private static $_openedOutput;
/*
* ██████╗ ██████╗ ███╗ ██╗███████╗██╗ ██████╗ ██╗ ██╗██████╗ █████╗ ████████╗██╗ ██████╗ ███╗ ██╗
* ██╔════╝██╔═══██╗████╗ ██║██╔════╝██║██╔════╝ ██║ ██║██╔══██╗██╔══██╗╚══██╔══╝██║██╔═══██╗████╗ ██║
* ██║ ██║ ██║██╔██╗ ██║█████╗ ██║██║ ███╗██║ ██║██████╔╝███████║ ██║ ██║██║ ██║██╔██╗ ██║
* ██║ ██║ ██║██║╚██╗██║██╔══╝ ██║██║ ██║██║ ██║██╔══██╗██╔══██║ ██║ ██║██║ ██║██║╚██╗██║
* ╚██████╗╚██████╔╝██║ ╚████║██║ ██║╚██████╔╝╚██████╔╝██║ ██║██║ ██║ ██║ ██║╚██████╔╝██║ ╚████║
* ╚═════╝ ╚═════╝ ╚═╝ ╚═══╝╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝
*
* ASCII ART: patorjk.com/software/taag/#p=display&h=1&v=2&c=c&f=ANSI Shadow&t=
*/
/**
* @var string makes visible source file paths clickable to open your editor.
*
* Pre-defined values:
* 'sublime' => 'subl://open?url=file://%file&line=%line',
* 'textmate' => 'txmt://open?url=file://%file&line=%line',
* 'emacs' => 'emacs://open?url=file://%file&line=%line',
* 'macvim' => 'mvim://open/?url=file://%file&line=%line',
* 'phpstorm' => 'phpstorm://open?file=%file&line=%line',
* 'phpstorm-remote' => 'http://localhost:63342/api/file/%file:%line',
* 'idea' => 'idea://open?file=%file&line=%line',
* 'vscode' => 'vscode://file/%file:%line',
* 'vscode-insiders' => 'vscode-insiders://file/%file:%line',
* 'vscode-remote' => 'vscode://vscode-remote/%file:%line',
* 'vscode-insiders-remote' => 'vscode-insiders://vscode-remote/%file:%line',
* 'vscodium' => 'vscodium://file/%file:%line',
* 'atom' => 'atom://core/open/file?filename=%file&line=%line',
* 'nova' => 'nova://core/open/file?filename=%file&line=%line',
* 'netbeans' => 'netbeans://open/?f=%file:%line',
* 'xdebug' => 'xdebug://%file@%line'
*
* Or pass a custom string where %fileileileileileile should be replaced with full file path, %line with line number
* to create a custom link. Set to null to disable linking.
*
* Example:
* // works with for PHPStorm and IDE Remote Control Plugin
* Sage::$editor = 'phpstorm-remote';
* Example:
* // same result as above, but explicitly defined
* Sage::$editor = 'http://localhost:63342/api/file/f:%line';
*
* Default:
* ini_get('xdebug.file_link_format') ?: 'phpstorm-remote'
*
*/
public static $editor;
/**
* @var string the full path (not URL) to your project folder on your remote dev server, be this Homestead, Docker,
* or in the cloud.
*
* Default:
* null
*/
public static $fileLinkServerPath;
/**
* @var string the full path (not URL) to your project on your local machine, the way your IDE or editor accesses
* the files.
*
* Default:
* null
*/
public static $fileLinkLocalPath;
/**
* @var bool whether to display where Sage was called from
*
* Default:
* true
*/
public static $displayCalledFrom;
/**
* @var int max array/object levels to go deep, set to zero/false to disable
*
* Default:
* 7
*/
public static $maxLevels;
/**
* @var string theme for rich view
*
* Example:
* Sage::$theme = Sage::THEME_ORIGINAL;
* Sage::$theme = Sage::THEME_LIGHT;
* Sage::$theme = Sage::THEME_SOLARIZED;
* Sage::$theme = Sage::THEME_SOLARIZED_DARK;
*
* Default:
* Sage::THEME_ORIGINAL
*/
public static $theme;
/**
* @var bool draw rich output already expanded without having to click
*
* Default:
* false
*/
public static $expandedByDefault;
/**
* @var bool enable detection when running in command line and adjust output format accordingly.
*
* Default:
* true
*/
public static $cliDetection;
/**
* @var bool in addition to above setting, enable detection when Sage is run in *UNIX* command line.
* Attempts to add coloring, but if seen as plain text, the color information is visible as gibberish
*
* Default:
* true
*/
public static $cliColors;
/**
* @var array possible alternative char encodings in order of probability,
*
* Default:
* array(
* 'UTF-8',
* 'Windows-1252', // Western; includes iso-8859-1, replace this with windows-1251 if you use Russian
* 'euc-jp', // Japanese
* );
*/
public static $charEncodings;
/**
* @var bool|string Sage returns output instead of echo.
*
* If true, the return has scripts+css always included, if set to a string, only first time per "group".
*
* Default:
* false
*/
public static $returnOutput;
/**
* @var string Write output to this file instead of echoing it. If it ends in `.html` forces output in html mode.
*
* Default:
* false
*/
public static $outputFile;
/**
* @var array Add new custom Sage wrapper names. Needed for nice backtraces, variable name detection and modifiers.
*
* [!] Use notation `Class::method` for methods.
*
* Example:
* function doom_dump($args)
* {
* echo "DOOOM!";
* d(...func_get_args());
* }
* Sage::$aliases = 'doom_dump';
*
* Default:
* array()
*/
public static $aliases = array();
/*
* ██╗ ██╗██╗██████╗
* ██║ ██║██║██╔══██╗
* ██║ █╗ ██║██║██████╔╝
* ██║███╗██║██║██╔═══╝
* ╚███╔███╔╝██║██║
* ╚══╝╚══╝ ╚═╝╚═╝
*/
/**
* @var string[] keys don't matter, but you can use them to unset a particular entry.
*/
public static $traceBlacklist = array(
'vendor' => '#\/vendor\/#',
'middleware' => '#\/Middleware\/#'
);
public static $classNameBlacklist = array(
'illuminate' => '/^Illuminate(?!.*(?:Exception|Collection))/'
// 'symfony' => '/^Symfony/'
);
public static $keysBlacklist = array();
public static $minimumTraceStepsToShowFull = 1;
/** @var class-string<SageParser>[] */
public static $enabledParsers = array(
'SageParsersSmarty' => true,
'SageParsersSplFileInfo' => true,
'SageParsersClosure' => true,
'SageParsersEloquent' => true,
'SageParsersDateTime' => true,
'SageParsersSplObjectStorage' => true,
'SageParsersTimestamp' => true,
'SageParsersFilePath' => true,
// above this line are only those parsers that $replacesAllOtherParsers
// now we run the blacklist
'SageParsersBlacklist' => true,
// all the rest
'SageParsersXml' => true,
'SageParsersObjectIterateable' => true,
'SageParsersClassStatics' => true,
'SageParsersColor' => true,
'SageParsersJson' => true,
'SageParsersClassName' => true,
'SageParsersMicrotime' => true,
);
public static function saveState($state = array())
{
$rich = new SageDecoratorsRich();
$plain = new SageDecoratorsPlain();
if (func_num_args()) {
self::$_enabledMode = $state['enabled'];
self::$editor = $state['editor'];
self::$fileLinkServerPath = $state['fileLinkServerPath'];
self::$fileLinkLocalPath = $state['fileLinkLocalPath'];
self::$displayCalledFrom = $state['displayCalledFrom'];
self::$maxLevels = $state['maxLevels'];
self::$theme = $state['theme'];
self::$expandedByDefault = $state['expandedByDefault'];
self::$cliDetection = $state['cliDetection'];
self::$cliColors = $state['cliColors'];
self::$charEncodings = $state['charEncodings'];
self::$returnOutput = $state['returnOutput'];
self::$outputFile = $state['outputFile'];
self::$aliases = $state['aliases'];
self::$traceBlacklist = $state['traceBlacklist'];
self::$classNameBlacklist = $state['classNameBlacklist'];
self::$enabledParsers = $state['enabledParsers'];
$rich->setAssetsNeeded($state['SageDecoratorsRich::firstRun']);
$plain->setAssetsNeeded($state['SageDecoratorsPlain::firstRun']);
return;
}
return array(
'enabled' => self::$_enabledMode,
'editor' => self::$editor,
'fileLinkServerPath' => self::$fileLinkServerPath,
'fileLinkLocalPath' => self::$fileLinkLocalPath,
'displayCalledFrom' => self::$displayCalledFrom,
'maxLevels' => self::$maxLevels,
'theme' => self::$theme,
'expandedByDefault' => self::$expandedByDefault,
'cliDetection' => self::$cliDetection,
'cliColors' => self::$cliColors,
'charEncodings' => self::$charEncodings,
'returnOutput' => self::$returnOutput,
'outputFile' => self::$outputFile,
'aliases' => self::$aliases,
'traceBlacklist' => self::$traceBlacklist,
'classNameBlacklist' => self::$classNameBlacklist,
'enabledParsers' => self::$enabledParsers,
'SageDecoratorsRich::firstRun' => $rich->areAssetsNeeded(),
'SageDecoratorsPlain::firstRun' => $plain->areAssetsNeeded()
);
}
/**
* @var bool there are multiple ways to direct sage to display "simpler" view than current mode (e.g. Rich -> PLain)
* todo must be private
*/
public static $simplifyDisplay = false;
/*
* ██████╗ ██████╗ ███╗ ██╗███████╗████████╗ █████╗ ███╗ ██╗████████╗███████╗
* ██╔════╝██╔═══██╗████╗ ██║██╔════╝╚══██╔══╝██╔══██╗████╗ ██║╚══██╔══╝██╔════╝
* ██║ ██║ ██║██╔██╗ ██║███████╗ ██║ ███████║██╔██╗ ██║ ██║ ███████╗
* ██║ ██║ ██║██║╚██╗██║╚════██║ ██║ ██╔══██║██║╚██╗██║ ██║ ╚════██║
* ╚██████╗╚██████╔╝██║ ╚████║███████║ ██║ ██║ ██║██║ ╚████║ ██║ ███████║
* ╚═════╝ ╚═════╝ ╚═╝ ╚═══╝╚══════╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═══╝ ╚═╝ ╚══════╝
*
*/
const MODE_RICH = 'r';
const MODE_TEXT_ONLY = 'w';
const MODE_CLI = 'c';
const MODE_PLAIN = 'p';
const THEME_ORIGINAL = 'original';
const THEME_LIGHT = 'aante-light';
const THEME_ORIGINAL_LIGHT = 'original-light';
const THEME_SOLARIZED_DARK = 'solarized-dark';
const THEME_SOLARIZED = 'solarized';
/*
* ███████╗███╗ ██╗ █████╗ ██████╗ ██╗ ███████╗██████╗
* ██╔════╝████╗ ██║██╔══██╗██╔══██╗██║ ██╔════╝██╔══██╗
* █████╗ ██╔██╗ ██║███████║██████╔╝██║ █████╗ ██║ ██║
* ██╔══╝ ██║╚██╗██║██╔══██║██╔══██╗██║ ██╔══╝ ██║ ██║
* ███████╗██║ ╚████║██║ ██║██████╔╝███████╗███████╗██████╔╝
* ╚══════╝╚═╝ ╚═══╝╚═╝ ╚═╝╚═════╝ ╚══════╝╚══════╝╚═════╝
*/
/**
* Enables or disables Sage, and forces display mode. Also returns currently active mode.
*
* @param mixed $forceMode
* null or void - return current mode
* false - disable Sage
* true - enable Sage and allow it to auto-detect the best formatting
* Sage::MODE_* - enable and force selected mode:
* - Sage::MODE_RICH Rich Text HTML
* - Sage::MODE_PLAIN Plain-view, HTML formatted output
* - Sage::MODE_CLI Console-formatted colored output
* - Sage::MODE_TEXT_ONLY Non-escaped plain text mode
*
* @return mixed previously set value
*/
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 `sage(1)`.
*
* Skip trace arguments and only see the paths - `sage(2)`
*
* @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 ($trace === null) {
$trace = SageHelper::php53orLater() ? debug_backtrace(true) : debug_backtrace();
}
return self::dump($trace);
}
/**
* Dump information about variables, accepts any number of parameters, supports prefix-modifiers:
*
* ```
* |-------|----------------------------------------------|
* | | Example: `+ saged('magic');` |
* |-------|----------------------------------------------|
* | ! | Dump ignoring depth limits for large objects |
* | print | Puts output into current DIR as sage.html |
* | ~ | Simplifies sage output (rich->html->plain) |
* | - | Clean up any output before dumping |
* | + | Expand all nodes (in rich view) |
* | @ | Return output instead of displaying it |
* |-------|----------------------------------------------|
* ```
*
* Modifiers are supported by all dump wrapper functions, including Sage::trace(). Combinations possible.
*
* -----
* Shorthand to display debug_backtrace():
* Sage::dump( 1 );
* Sage::dump( debug_backtrace() ); // must be single parameter!
*
* @param mixed $data
*
* @return string|int returns 5463 (Sage in l33tspeak) if disabled
*
* Explanation for the magic number in return:
* The return value has to be an int otherwise modifiers throw typesafe warinings, eg if we return null:
*
* ~d(); // TypeError: Cannot perform bitwise not on null
*
* It's not zero because it doesn't matter and if you find this somewhere in your logs or something - you know who
* to blame :))
*/
public static function dump($data = null)
{
try {
$params = func_get_args();
return call_user_func_array(array('Sage', 'doDump'), $params);
} catch (Throwable $e) {
} catch (Exception $e) {
}
return 5463;
}
public static function doDump($data = null)
{
$enabledMode = self::enabled();
if (! $enabledMode) {
return 5463;
}
self::_init();
list($names, $modifiers, $callee, $previousCaller, $miniTrace) = self::_getCalleeInfo();
// auto-detect mode if not explicitly set
if ($enabledMode === true) {
if (! empty($modifiers) && strpos($modifiers, 'print') !== false && isset($callee['file'])) {
$newMode = self::MODE_RICH;
} elseif (self::$outputFile && substr(self::$outputFile, -5) === '.html') {
$newMode = self::MODE_RICH;
} else {
$newMode = PHP_SAPI === 'cli' && self::$cliDetection === true
? self::MODE_CLI
: self::MODE_RICH;
}
if (self::$simplifyDisplay) {
switch ($newMode) {
case self::MODE_RICH:
$newMode = self::MODE_PLAIN;
break;
case self::MODE_CLI:
$newMode = self::MODE_TEXT_ONLY;
break;
}
}
if (! empty($modifiers) && strpos($modifiers, '~') !== false) {
switch ($newMode) {
case self::MODE_RICH:
$newMode = self::MODE_PLAIN;
break;
case self::MODE_PLAIN:
case self::MODE_CLI:
$newMode = self::MODE_TEXT_ONLY;
break;
}
}
self::enabled($newMode);
}
$decoratorClass = self::enabled() === self::MODE_RICH ? 'SageDecoratorsRich' : 'SageDecoratorsPlain';
/** @var SageDecoratorsPlain|SageDecoratorsRich $decorator */
$decorator = new $decoratorClass();
$firstRunOldValue = $decorator->areAssetsNeeded();
// process modifiers: @, +, !, ~ and -
if (! empty($modifiers) && strpos($modifiers, '-') !== false) {
$decorator->setAssetsNeeded(true);
while (ob_get_level()) {
ob_end_clean();
}
}
if (! empty($modifiers) && strpos($modifiers, '+') !== false) {
$expandedByDefaultOldValue = self::$expandedByDefault;
self::$expandedByDefault = true;
}
if (! empty($modifiers) && strpos($modifiers, '!') !== false) {
/*if (strpos($modifiers, '!!') !== false) {
$oldClassNameBlacklist = self::$classNameBlacklist = array();
$oldTraceBlacklist = self::$traceBlacklist = array();
$oldEnabledParsers = self::$enabledParsers;
self::$classNameBlacklist = array();
self::$traceBlacklist = array();
if (($key = array_search('SageParsersEloquent', self::$enabledParsers)) !== false) {
unset(self::$enabledParsers[$key]);
}
} else {*/
$maxLevelsOldValue = self::$maxLevels;
self::$maxLevels = false;
/*}*/
}
if (! empty($modifiers) && strpos($modifiers, '@') !== false) {
$returnOldValue = self::$returnOutput;
self::$returnOutput = true;
}
if (self::$returnOutput) {
if (self::$returnOutput === true) {
$decorator->setAssetsNeeded(true);
} elseif (! isset(self::$_openedOutput[self::$returnOutput])) {
$decorator->setAssetsNeeded(true);
self::$_openedOutput[self::$returnOutput] = true;
}
}
if (! empty($modifiers) && strpos($modifiers, 'print') !== false && isset($callee['file'])) {
$outputFileOldValue = self::$outputFile;
self::$outputFile = dirname($callee['file']) . '/sage.html';
}
if (self::$outputFile && ! isset(self::$_openedOutput[self::$outputFile])) {
$firstRunOldValue = $decorator->areAssetsNeeded();
$decorator->setAssetsNeeded(true);
}
$trace = false;
$lightTrace = false;
if (func_num_args() === 1) {
if ($names === array('1') && $data === 1) {
// Sage::dump(1) shorthand
$trace = SageHelper::php53orLater() ? debug_backtrace(true) : debug_backtrace();
} elseif ($names === array('2') && $data === 2) {
// Sage::dump(2) shorthand todo: create Sage::traceWithoutArgs()
$lightTrace = true;
$trace = debug_backtrace();
} elseif (is_array($data)) {
$trace = $data; // test if the single parameter is result of debug_backtrace()
}
}
if ($trace) {
$trace = self::_parseTrace($trace);
}
$output = '';
if ($decorator->areAssetsNeeded()) {
$output .= $decorator->init();
}
$output .= $decorator->wrapStart();
if ($trace) {
$output .= $decorator->decorateTrace($trace, $lightTrace);
} else {
if (func_num_args() === 0) {
SageParser::reset();
$tmp = microtime();
$varData = SageParser::process($tmp, '');
$varData->type = null;
$varData->name = 'Sage called with no arguments';
$varData->value = null;
$varData->size = null;
if (! empty($callee['function'])) {
if (! empty($callee['class']) && ! empty($callee['type'])) {
$name = $callee['class'] . $callee['type'] . $callee['function'];
} else {
$name = $callee['function'];
}
$varData->name = $name . '( no parameters )';
}
$output .= $decorator->decorate($varData);
} else {
foreach (func_get_args() as $k => $argument) {
SageParser::reset();
// when the dump arguments take long to generate output, user might have changed the file and
// Sage 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 .= $decorator->decorate(
SageParser::process($argument, empty($names[$k]) ? '???' : $names[$k])
);
}
}
}
$output .= $decorator->wrapEnd($callee, $miniTrace, $previousCaller);
// now restore all on-the-fly settings and return
if (self::$outputFile) {
try {
if (! isset(self::$_openedOutput[self::$outputFile])) {
self::$_openedOutput[self::$outputFile] = fopen(self::$outputFile, 'w');
$decorator->setAssetsNeeded($firstRunOldValue);
}
fwrite(self::$_openedOutput[self::$outputFile], $output);
echo 'Sage -> ' . self::$outputFile . PHP_EOL;
} catch (Throwable $e) {
self::$outputFile = null;
$output .= "Error: Sage can't write file to " . self::$outputFile;
} catch (Exception $e) {
self::$outputFile = null;
$output .= "Error: Sage can't write file to " . self::$outputFile;
}
}
self::enabled($enabledMode);
$decorator->setAssetsNeeded(false);
if (! empty($modifiers)) {
if (strpos($modifiers, '~') !== false) {
$decorator->setAssetsNeeded($firstRunOldValue);
}
if (strpos($modifiers, '+') !== false) {
self::$expandedByDefault = $expandedByDefaultOldValue;
}
if (isset($maxLevelsOldValue)) {
self::$maxLevels = $maxLevelsOldValue;
}
if (! empty($modifiers) && strpos($modifiers, 'print') !== false && isset($callee['file'])) {
self::$outputFile = $outputFileOldValue;
return 5463;
}
if (strpos($modifiers, '@') !== false) {
self::$returnOutput = $returnOldValue;
$decorator->setAssetsNeeded($firstRunOldValue);
return $output;
}
}
if (self::$returnOutput) {
return $output;
}
if (self::$outputFile) {
return 5463;
}
echo $output;
return 5463;
}
/*
* ██████╗ ██████╗ ██╗██╗ ██╗ █████╗ ████████╗███████╗
* ██╔══██╗██╔══██╗██║██║ ██║██╔══██╗╚══██╔══╝██╔════╝
* ██████╔╝██████╔╝██║██║ ██║███████║ ██║ █████╗
* ██╔═══╝ ██╔══██╗██║╚██╗ ██╔╝██╔══██║ ██║ ██╔══╝
* ██║ ██║ ██║██║ ╚████╔╝ ██║ ██║ ██║ ███████╗
* ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═══╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝
*
*/
/**
* returns parameter names that the function was passed, as well as any predefined symbols before function
* call (modifiers)
*
* @return array{$parameters, $modifier, $callee, $previousCaller}
*/
private static function _getCalleeInfo()
{
$trace = debug_backtrace();
$previousCaller = array();
$miniTrace = array();
$prevStep = array();
$insideTemplateDetected = null;
// go from back of trace forward to find first occurrence of call to Sage or its wrappers
while ($step = array_pop($trace)) {
if (SageHelper::stepIsInternal($step)) {
$previousCaller = $prevStep;
break;
}
if (
isset($step['args'][0])
&& is_string($step['args'][0])
&& substr($step['args'][0], -strlen('.blade.php')) === '.blade.php'
) {
$insideTemplateDetected = $step['args'][0];
}
if (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 array(null, null, $callee, $previousCaller, $miniTrace);
}
SageHelper::detectProjectRoot($callee['file']);
// open the file and read it up to the position where the function call expression ended
// TODO since PHP 8.2 backtrace reports the lineno of the function/method name!
// https://github.com/php/php-src/pull/8818
// $file = new SplFileObject($callee['file']);
// do {
// $file->seek($callee['line']);
// $contents = $file->current(); // $contents would hold the data from line x
//
// } while (! $file->eof());
$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 {
$codePattern = "\w+\x07*" . $callee['type'] . "\x07*" . $callee['function'];
}
// get the position of the last call to the function
preg_match_all(
"
/
# beginning of statement
[\x07{(]
# search for modifiers (group 1)
([print\x07-+!@~]*)?
# spaces
\x07*
# possibly a namespace symbol
\\\\?
# spaces again
\x07*
# main call to Sage
({$codePattern})
# spaces everywhere
\x07*
# find the character where Sage's opening bracket resides (group 3)
(\\()
/ix",
$source,
$matches,
PREG_OFFSET_CAPTURE
);
$modifiers = end($matches[1]);
$callToSage = end($matches[2]);
$bracket = end($matches[3]);
if (empty($callToSage)) {
// if a wrapper is misconfigured, don't display the whole file as variable name
return array(array(), $modifiers, $callee, $previousCaller, $miniTrace);
}
$modifiers = str_replace("\x07", '', $modifiers[0]);
$paramsString = preg_replace("[\x07+]", ' ', 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
// complicate separating individual arguments
$c = strlen($paramsString);
$inString = $escaped = $openedBracket = $closingBracket = false;
$i = 0;
$inBrackets = 0;
$openedBrackets = array();
$bracketPairs = array('(' => ')', '[' => ']', '{' => '}');
while ($i < $c) {
$letter = $paramsString[$i];
if (! $inString) {
if ($letter === '\'' || $letter === '"') {
$inString = $letter;
} elseif ($letter === '(' || $letter === '[' || $letter === '{') {
$inBrackets++;
$openedBrackets[] = $openedBracket = $letter;
$closingBracket = $bracketPairs[$letter];
} elseif ($inBrackets && $letter === $closingBracket) {
$inBrackets--;
array_pop($openedBrackets);
$openedBracket = end($openedBrackets);
if ($openedBracket) {
$closingBracket = $bracketPairs[$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.
if ($inBrackets > 0) {
if ($inBrackets > 1 || $letter !== $openedBracket) {
$paramsString[$i] = "\x07";
}
}
if ($inString) {
if ($letter !== $inString || $escaped) {
$paramsString[$i] = "\x07";
}
}
$escaped = ! $escaped && ($letter === '\\');
$i++;
}
$names = explode(',', preg_replace("[\x07+]", '...', $paramsString));
$names = array_map('trim', $names);
if ($insideTemplateDetected) {
$callee['file'] = $insideTemplateDetected;
$callee['line'] = null;
}
return array($names, $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 = "\x07";
} else {
$token = $token[1];
}
} elseif ($token === ';') {
$token = "\x07";
}
$cleanedSource .= $token;
}
return $cleanedSource;
}
private static function _parseTrace($data)
{
$trace = array();
$traceFields = array('file', 'line', 'args', 'class');
$fileFound = false; // file element must exist in one of the steps
$lastStep = array();
// validate whether a trace was indeed passed
foreach ($data as $step) {
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 ($step['function'] === 'spl_autoload_call') { // meaningless
continue;
}
// also modify it in the same go
if (SageHelper::stepIsInternal($step)) {
// take first step from the top that is not inside Sage already
if (isset($step['file'], $step['line'])) {
$lastStep = array(
'file' => $step['file'],
'line' => $step['line'],
'function' => '',
);
}
continue;
}
$trace[] = $step;
}
if (! $fileFound) {
return false;
}
if ($lastStep) {
array_unshift($trace, $lastStep);
}
// now parse the trace into a usable format
$output = array();
foreach ($trace as $i => $step) {
$output[] = new SageTraceStep($step, $i);
}
return $output;
}
/*
* ██╗███╗ ██╗██╗████████╗
* ██║████╗ ██║██║╚══██╔══╝
* ██║██╔██╗ ██║██║ ██║
* ██║██║╚██╗██║██║ ██║
* ██║██║ ╚████║██║ ██║
* ╚═╝╚═╝ ╚═══╝╚═╝ ╚═╝
*
*/
private static function _initSetting($name, $default)
{
if (! isset(self::$$name)) {
$value = get_cfg_var('sage.' . $name);
if (! $value) {
$value = $default;
}
self::$$name = $value;
}
}
private static $loadedParsers = 0;
/** Called before each invocation */
private static function _init()
{
SageHelper::buildAliases();
$parsersCount = 0;
foreach (Sage::$enabledParsers as $enabled) {
if ($enabled) {
$parsersCount++;
}
}
if (self::$loadedParsers !== $parsersCount) {
self::$loadedParsers = $parsersCount;
foreach (Sage::$enabledParsers as $className => $enabled) {
if ($enabled && file_exists($f = SAGE_DIR . 'parsers/' . $className . '.php')) {
require_once $f;
}
}
}
if (self::$_initialized) {
return;
}
// first load defaults for configuration. In this order:
// 1. If value is set, it means user explicitly set it
// 2. TODO: composer.json
// 3. If present in get_cfg_var means user put it into his php.ini
// 4. Load default from Sage
self::_initSetting(
'editor',
ini_get('xdebug.file_link_format') ? ini_get('xdebug.file_link_format') : 'phpstorm-remote'
);
self::_initSetting('fileLinkServerPath', null);
self::_initSetting('fileLinkLocalPath', null);
self::_initSetting('displayCalledFrom', true);
self::_initSetting('maxLevels', 7);
self::_initSetting('theme', self::THEME_ORIGINAL);
self::_initSetting('expandedByDefault', false);
self::_initSetting('cliDetection', true);
self::_initSetting('cliColors', true);
self::_initSetting(
'charEncodings',
array(
'UTF-8',
'Windows-1252', // Western; includes iso-8859-1, replace this with windows-1251 if you have Russian code
'euc-jp', // Japanese
)
);
self::_initSetting('returnOutput', false);
self::_initSetting('aliases', array());
}
}
if (get_cfg_var('sage.enabled') !== false) {
Sage::enabled(get_cfg_var('sage.enabled'));
}