| @copyright 2007-2026 (c) AVE.cms | @link https://ave-cms.ru | @version 3.3 */ namespace App\Helpers; defined("BASEPATH") || die('Direct access to this location is not allowed.'); use App\Common\DebugChannel; class Debug { protected static $time = []; protected static $memory = []; public static $max_nesting_level = 5; public static $js_toggle_open = false; public static $_document_content = ''; protected static $js_displayed = false; protected static $css_displayed = false; protected static $files = []; protected function __construct() {} protected function __clone() {} public static function _echo($var, $exit = false, $bg = null, $echo = true) { return self::echo($var, (bool) $exit, $bg, (bool) $echo); } public static function _print($var, $exit = false, $bg = null, $echo = true) { return self::print($var, (bool) $exit, $bg, (bool) $echo); } public static function _dump($var, $exit = false, $bg = null, $append = true, $fileName = '') { self::dump($var, (bool) $exit, $bg, (bool) $append); } public static function _($var, $bg = null, $from = '') { return self::echo($var, false, $bg, true); } public static function formatSize($size) { return Number::formatSize((float) $size); } public static function getStatistic($type = null) { $queries = method_exists('DB', 'getQueries') ? (array) \DB::getQueries() : array(); $queryTime = 0.0; foreach ($queries as $query) { $queryTime += isset($query['time']) ? (float) $query['time'] : 0.0; } switch ($type) { case 'time': $start = defined('START_MICROTIME') ? self::timestamp(START_MICROTIME) : microtime(true); return number_format(microtime(true) - $start, 3, ',', ' '); case 'memory': return self::formatSize(memory_get_usage() - (defined('START_MEMORY') ? START_MEMORY : 0)); case 'peak': return self::formatSize(memory_get_peak_usage()); case 'sqlcount': case 'sqltrace': return count($queries); case 'sqltime': return $queryTime; } return null; } public static function elapsed($start) { return max(0.0, microtime(true) - self::timestamp($start)); } protected static function timestamp($value) { $parts = explode(' ', trim((string) $value), 2); return count($parts) === 2 ? (float) $parts[1] + (float) $parts[0] : (float) $value; } // ── Assets ────────────────────────────────────────────────────────────── public static function addCss(): void { if (self::$css_displayed) { return; } self::$css_displayed = true; echo ''; } public static function addJs(): void { if (self::$js_displayed) { return; } self::$js_displayed = true; echo ''; } private static function ensureAssets(): void { if (!self::$css_displayed) { self::addCss(); } if (!self::$js_displayed) { self::addJs(); } } // ── Private helpers ────────────────────────────────────────────────────── /** * Возвращает файл и строку места вызова Debug::method(). */ private static function callerInfo(): array { $bt = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 4); $caller = $bt[2] ?? $bt[1] ?? []; $file = $caller['file'] ?? ''; $line = (int) ($caller['line'] ?? 0); if ($file && preg_match('/([^\(]*)\(/', $file, $m)) { $file = $m[1]; } return ['file' => $file, 'line' => $line]; } /** * Пытается извлечь имя переменной из исходной строки файла. */ private static function varName(string $file, int $line, string $funcName): string { if (!$file || !is_readable($file)) { return 'UNKNOWN'; } $fh = fopen($file, 'rb'); if ($fh === false) { return 'UNKNOWN'; } $code = ''; $i = 0; while (++$i <= $line) { $code = fgets($fh); } fclose($fh); preg_match('/' . preg_quote($funcName, '/') . '\s*\((.*)\)\s*;/u', $code, $m); if (!empty($m[1])) { return trim(explode(',', $m[1])[0]); } return 'EVAL'; } /** * Захватывает var_dump / print_r / var_export и возвращает отформатированный HTML. */ private static function captureVar($var, string $func): string { ob_start(); switch ($func) { case 'print_r': print_r($var); break; case 'var_export': var_export($var); break; default: var_dump($var); break; } $out = ob_get_clean(); $out = preg_replace('/=>(\s+|\s$)/', ' => ', $out); $out = htmlspecialchars($out); $out = preg_replace('/(=>)/', '$1', $out); return $out; } // ── Template ───────────────────────────────────────────────────────────── public static function _debug_tpl(array $arg): string { $color = ltrim($arg['DEBUG_COLOR'] ?? '43648c', '#'); $html = '
'; $html .= '
'; $html .= '
' . ($arg['DEBUG_HEADER'] ?? '') . '
'; $html .= '
'; if (!empty($arg['DEBUG_MSG_BOX'])) { $html .= '
' . $arg['DEBUG_MSG_BOX'] . '
'; } if (!empty($arg['DEBUG_MSG_TEXT'])) { $html .= '
' . $arg['DEBUG_MSG_TEXT'] . '
'; } if (!empty($arg['DEBUG_SQL_QUERY'])) { $html .= '
SQL:
'; $html .= '
' . htmlspecialchars($arg['DEBUG_SQL_QUERY']) . '
'; } if (!empty($arg['DEBUG_CLASS_LINE'])) { $html .= $arg['DEBUG_CLASS_LINE']; } if (!empty($arg['DEBUG_CODE_LINE'])) { $html .= '
' . $arg['DEBUG_CODE_LINE'] . '
'; } if (!empty($arg['DEBUG_CALLER'])) { $html .= '
' . $arg['DEBUG_CALLER'] . '
'; } $html .= '
'; return $html; } // ── Public dump methods ─────────────────────────────────────────────────── /** * Сохраняет значение в защищённой панели публичной отладки без вывода в HTML. */ public static function panel($var, $label = '') { $info = self::callerInfo(); if (trim((string) $label) === '') { $label = self::varName($info['file'], $info['line'], __FUNCTION__); } DebugChannel::push($var, $label, $info['file'], $info['line']); return $var; } /** * var_dump переменной прямо в страницу. */ public static function echo($var, bool $exit = false, $bg = null, bool $echo = true) { self::ensureAssets(); $info = self::callerInfo(); $varName = self::varName($info['file'], $info['line'], __FUNCTION__); $dump = self::captureVar($var, 'var_dump'); $html = self::_debug_tpl([ 'DEBUG_COLOR' => $bg ?: '43648c', 'DEBUG_HEADER' => 'Debug: var_dump(' . $varName . ')', 'DEBUG_CLASS_LINE' => self::dumpTrace(), 'DEBUG_CODE_LINE' => self::fileLines($info['file'], $info['line']), 'DEBUG_MSG_BOX' => $dump, ]); if (!$echo) { return $html; } echo $html; if ($exit) { exit; } return false; } /** * print_r переменной прямо в страницу. */ public static function print($var, bool $exit = false, $bg = null, bool $echo = true) { self::ensureAssets(); $info = self::callerInfo(); $varName = self::varName($info['file'], $info['line'], __FUNCTION__); $dump = self::captureVar($var, 'print_r'); $html = self::_debug_tpl([ 'DEBUG_COLOR' => $bg ?: '4e5665', 'DEBUG_HEADER' => 'Debug: print_r(' . $varName . ')', 'DEBUG_CLASS_LINE' => self::dumpTrace(), 'DEBUG_CODE_LINE' => self::fileLines($info['file'], $info['line']), 'DEBUG_MSG_BOX' => $dump, ]); if (!$echo) { return $html; } echo $html; if ($exit) { exit; } return false; } /** * var_export переменной прямо в страницу. */ public static function exp($var, bool $exit = false, $bg = null, bool $echo = true) { self::ensureAssets(); $info = self::callerInfo(); $varName = self::varName($info['file'], $info['line'], __FUNCTION__); $dump = self::captureVar($var, 'var_export'); $html = self::_debug_tpl([ 'DEBUG_COLOR' => $bg ?: '333a4d', 'DEBUG_HEADER' => 'Debug: var_export(' . $varName . ')', 'DEBUG_CLASS_LINE' => self::dumpTrace(), 'DEBUG_CODE_LINE' => self::fileLines($info['file'], $info['line']), 'DEBUG_MSG_BOX' => $dump, ]); if (!$echo) { return $html; } echo $html; if ($exit) { exit; } return false; } /** * Записывает var_dump в debug.html (не в страницу). */ public static function dump($var, bool $exit = false, $bg = null, bool $append = true): void { self::ensureAssets(); $info = self::callerInfo(); $varName = self::varName($info['file'], $info['line'], __FUNCTION__); $dump = self::captureVar($var, 'var_dump'); $html = self::_debug_tpl([ 'DEBUG_COLOR' => $bg ?: '43648c', 'DEBUG_HEADER' => 'Debug::dump(' . $varName . ')', 'DEBUG_CLASS_LINE' => self::dumpTrace(), 'DEBUG_CODE_LINE' => self::fileLines($info['file'], $info['line']), 'DEBUG_MSG_BOX' => $dump, ]); $flag = $append ? FILE_APPEND : 0; file_put_contents(BASEPATH . '/debug.html', $html, $flag); if ($exit) { exit; } } /** * Произвольное сообщение в страницу. */ public static function errorMsg(string $msg): void { self::ensureAssets(); $info = self::callerInfo(); $html = self::_debug_tpl([ 'DEBUG_COLOR' => '7350e6', 'DEBUG_HEADER' => 'Message', 'DEBUG_CLASS_LINE' => self::dumpTrace(), 'DEBUG_CODE_LINE' => self::fileLines($info['file'], $info['line']), 'DEBUG_MSG_BOX' => htmlspecialchars($msg), ]); echo $html; } /** * Вывод ошибки MySQL: текст, SQL-запрос, стек вызовов. * Вызывается из Database при ошибке запроса. */ public static function errorSql(string $header, string $sql, $caller = null, bool $exit = false): void { self::ensureAssets(); $html = self::_debug_tpl([ 'DEBUG_COLOR' => 'cc0000', 'DEBUG_HEADER' => 'MySQL Error', 'DEBUG_MSG_TEXT' => htmlspecialchars(preg_replace('/\s+/', ' ', $header)), 'DEBUG_SQL_QUERY' => $sql, 'DEBUG_CALLER' => $caller !== null ? self::prePrint($caller) : '', ]); echo $html; if ($exit) { exit; } } // ── Trace helpers ───────────────────────────────────────────────────────── public static function dumpTrace(): string { $bt = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 4); $trace = $bt[2] ?? $bt[1] ?? []; $upper = $bt[3] ?? []; $file = isset($trace['file']) ? File::cutRootPath($trace['file']) : '—'; $line = $trace['line'] ?? '—'; $class = $upper['class'] ?? 'None'; $type = $upper['type'] ?? '::'; $function = $upper['function'] ?? 'None'; return sprintf( '
Class: %s | Type: %s | Function: %s
' . '
File: %s line %s
', htmlspecialchars($class), htmlspecialchars($type), htmlspecialchars($function), htmlspecialchars($file), $line ); } public static function debugBacktrace(): array { $_trace = []; $_backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); array_shift($_backtrace); foreach ($_backtrace as $history) { if (isset($history['file'], $history['line'])) { $_trace[] = [ 'file' => File::cutRootPath($history['file']), 'line' => $history['line'], 'function' => (isset($history['class'], $history['type']) ? $history['class'] . $history['type'] : '') . $history['function'], ]; } } return $_trace; } public static function prePrint($var): string { ob_start(); print_r($var); $out = htmlspecialchars(ob_get_clean()); $out = preg_replace('/(=>)/', '$1', $out); return $out; } // ── File source viewer ──────────────────────────────────────────────────── public static function fileLines(string $filepath, int $line_num, bool $highlight = true, int $padding = 3) { if (!$filepath || strpos($filepath, "eval()'d code") !== false || strpos($filepath, 'runtime-created function') !== false ) { return ''; } if (!isset(static::$files[$filepath])) { if (!File::exists($filepath)) { return false; } static::$files[$filepath] = file($filepath, FILE_IGNORE_NEW_LINES); array_unshift(static::$files[$filepath], ''); } $start = max(0, $line_num - $padding); $length = ($line_num - $start) + $padding + 1; if (($start + $length) > count(static::$files[$filepath]) - 1) { $length = null; } $debug_lines = array_slice(static::$files[$filepath], $start, $length, true); if ($highlight) { foreach ($debug_lines as $key => $line) { $isActive = ($key === $line_num); $rowStyle = $isActive ? 'display:block;color:#000;background:#fffbeb;border-left:3px solid #f59e0b;margin-left:-4px;padding-left:4px;' : 'display:block;color:#000;background:#fff;border-left:3px solid transparent;margin-left:-4px;padding-left:4px;'; $lineNumStyle = 'display:inline-block;min-width:32px;padding-right:10px;margin-right:8px;' . 'text-align:right;color:#94a3b8;border-right:2px solid #e2e8f0;' . 'user-select:none;font-style:normal;' . ($isActive ? 'color:#b45309;font-weight:700;' : ''); $lineNum = '' . $key . ''; $line = str_replace( ['', '', '<?php ', "\n", ''], ['', '', '' . $lineNum, '', ''], highlight_string(' sprintf('%1.6f', $after - $before), 'result' => $result, ]; } }