yf_debug classDebug console (DEBUG_MODE only): the panel at the end of the page, built from the debug() records of the request.
Every `_debug_<name>()` method is a tab; it returns the tab's HTML and may set `$params['count']`, `$params['time']` (seconds)
and `$params['bad']` for the tab label. Listeners of the `debug.render` event may return `[name => html]` to add tabs, and
modules with a `_hook_debug($debug)` method feed the "hooks" tab. The console renders into its own shadow root with its own
stylesheet (assets/yf/debug_console/), so a site theme cannot change it and it cannot change the site.
| File | classes/yf_debug.class.php |
|---|---|
| Use | _class('debug') |
A site sets them in its config with $PROJECT_CONF['debug']['NAME'] = ...;
| Property | Default | Description |
|---|---|---|
$ADD_ADMIN_LINKS |
true |
|
$ADMIN_PATHS |
|
|
$DEBUG_CONSOLE_HIDDEN |
null |
|
$DEBUG_CONSOLE_LIGHT |
null |
|
$MAX_EXPLAINint |
100 |
EXPLAIN runs for at most this many SELECT queries per connection |
$MAX_TRACE_FRAMESint |
25 |
Frames of a stack trace shown, the rest is counted |
$MAX_VALUE_LENint |
20000 |
A value longer than this (bytes) is cut in the console |
$SHOW_APC_INFO |
true |
|
$SHOW_COOKIE_DATA |
true |
|
$SHOW_CORE_CACHE |
true |
|
$SHOW_CURL_REQUESTS |
true |
|
$SHOW_DASHBOARD_INFO |
true |
|
$SHOW_DB_EXPLAIN_QUERY |
true |
|
$SHOW_DB_QUERY_LOG |
true |
|
$SHOW_DB_STATS |
false |
|
$SHOW_DD_TABLE |
true |
|
$SHOW_ENV_DATA |
true |
|
$SHOW_FILES_DATA |
true |
|
$SHOW_FORM2 |
true |
|
$SHOW_GET_DATA |
true |
|
$SHOW_GLOBALS |
true |
|
$SHOW_HTMXbool |
true |
htmx requests the page makes after load (recorded in the browser) |
$SHOW_I18N_VARS |
true |
|
$SHOW_INCLUDED_FILES |
true |
|
$SHOW_INPUT_DATA |
true |
|
$SHOW_LOADED_MODULES |
true |
|
$SHOW_LOGGERbool |
true |
logger() records of the request |
$SHOW_MAIN_EXECUTE |
true |
|
$SHOW_MAIN_GET_DATA |
true |
|
$SHOW_MEMCACHED_INFO |
true |
|
$SHOW_NOT_TRANSLATED |
true |
|
$SHOW_OUTPUT_CACHE_INFO |
true |
|
$SHOW_PHP_ERRORSbool |
true |
PHP warnings / notices / deprecations of the request (yf_core_errors) |
$SHOW_POST_DATA |
true |
|
$SHOW_REDIS_INFO |
true |
|
$SHOW_RESIZED_IMAGES_LOG |
true |
|
$SHOW_REWRITE_INFO |
true |
|
$SHOW_SEARCHbool |
true |
HTTP requests of the elasticsearch / opensearch search drivers |
$SHOW_SERVER_DATA |
true |
|
$SHOW_SESSION_DATA |
true |
|
$SHOW_SETTINGS |
true |
|
$SHOW_SPHINX |
true |
|
$SHOW_SSH |
true |
|
$SHOW_STPLS |
true |
|
$SHOW_TABLE2 |
true |
|
$SORT_TEMPLATES_BY_NAME |
true |
|
$TAB_LABELSarray |
['DEBUG_YF' => 'yf', '_session' => 'session', 'sphinxsearch' => 'sphinx', 'php_errors' => 'errors', 'curl_requests' => 'curl', 'resize_images' => 'images'] |
Tab label per panel name, the name itself otherwise |
$_NOT_TRANSLATED_FILE |
null |
|
$_dataarray |
['b' => [], 'f' => [], 't' => []] |
@conf_skip lazy data: texts, trace frames, traces (frame ids) |
$_data_idsarray |
['b' => [], 'f' => [], 't' => []] |
@conf_skip id of each text / frame / trace stored in the lazy data |
$_lazybool |
false |
@conf_skip go() is rendering: hidden values and traces go into the lazy data of the console, deduplicated |
$_timingsarray |
[] |
@conf_skip render time of each panel, seconds |
$_used_debug_datas |
[] |
|
$backup_debug_data |
[] |
public function go()
The console: host element, its declarative shadow root (style, bar, tabs, one inert <template> per pane) and its script.
public function go()
{
if ($this->DEBUG_CONSOLE_HIDDEN) {
return false;
}
// Do hide console if needed
if (($_SESSION['hide_debug_console'] ?? null) || ($_GET['hide_debug_console'] ?? null)) {
return '';
}
$ts = microtime(true);
$exec_time = microtime(true) - main()->_time_start;
// what _show_execution_time() set, without its translated text (t() calls would land in the i18n tab)
main()->_time_end = common()->_format_time_value($exec_time);
$summary = $this->_summary($exec_time);
$panels = [];
$meta = [];
$this->_timings = [];
$this->_data = ['b' => [], 'f' => [], 't' => []];
$this->_data_ids = ['b' => [], 'f' => [], 't' => []];
$this->_lazy = true;
$class_name = get_class($this);
foreach ((array) get_class_methods($class_name) as $method) {
if (substr($method, 0, strlen('_debug_')) != '_debug_' || $method == $class_name || $method == __FUNCTION__) {
continue;
}
$name = substr($method, strlen('_debug_'));
if ($this->DEBUG_CONSOLE_LIGHT && $name !== 'DEBUG_YF') {
continue;
}
$ts2 = microtime(true);
$method_params = [];
$content = $this->$method($method_params);
$this->_timings[$method] = microtime(true) - $ts2;
$panels[$name] = $content;
$meta[$name] = is_array($method_params) ? $method_params : [];
}
// listeners return [name => html]: the payload is passed by value, so the return value is the only way back
$debug_add = [];
$responses = _class('core_events')->fire('debug.render', $debug_add);
foreach ((array) $responses as $response) {
is_array($response) && $debug_add = array_merge($debug_add, $response);
}
foreach ((array) $debug_add as $k => $v) {
$panels[$k] = $v;
}
$this->_lazy = false;
$debug_time = microtime(true) - $ts;
$summary['console'] = $debug_time;
if (isset($panels['DEBUG_YF']) && is_string($panels['DEBUG_YF']) && $panels['DEBUG_YF'] !== '') {
$timings = [];
foreach ($this->_timings as $method => $time) {
$timings[$method] = $this->_ms($time);
}
$panels['DEBUG_YF'] .= $this->_heading('console render: ' . $this->_ms($debug_time))
. '<div class="yfd-grid">' . $this->_show_key_val_table($timings, ['no_sort' => 1]) . '</div>';
}
return $this->_render_console($panels, $meta, $summary);
}
_ hooks and helpers; not routable as page actionspublic function __call($name, $args)
Catch missing method call.
public function __call($name, $args)
{
return main()->extend_call($this, $name, $args);
}
public function _admin_link($type, $text = '', $just_link = false, $replace = [])
Process through admin link or just return text if links disabled. The text is escaped.
public function _admin_link($type, $text = '', $just_link = false, $replace = [])
{
if ( ! $this->ADD_ADMIN_LINKS || ! isset($this->ADMIN_PATHS[$type])) {
return $just_link ? $text : $this->_e($text);
}
if ($type == 'link') {
// http(s) and relative URLs only: a javascript: or data: URL stays text
if ( ! preg_match('~^(https?://|//|/|\?|[\w.-]+(/|\?|$))~i', (string) $text)) {
return $this->_e($text);
}
return '<a href="' . $this->_e($text) . '" rel="nofollow noindex" target="_blank">' . $this->_e($text) . '</a>';
}
$id = (string) $text;
$replace += [
'{ID}' => urlencode(str_replace('\\', '/', $id)),
'{THEME}' => urlencode((string) conf('theme')),
];
$url_tpl = $this->ADMIN_PATHS[$type];
if ($type == 'show_db_table') {
$url_tpl = 'adminer.php?server=' . urlencode(DB_HOST) . '&username=' . urlencode(DB_USER) . '&db=' . urlencode(DB_NAME) . '&table={ID}';
} elseif ($type == 'sql_query') {
$url_tpl = 'adminer.php?server=' . urlencode(DB_HOST) . '&username=' . urlencode(DB_USER) . '&db=' . urlencode(DB_NAME) . '&sql={ID}';
}
$url = str_replace(array_keys($replace), array_map('strval', array_values($replace)), $url_tpl);
$link = ADMIN_WEB_PATH . $url;
if ($just_link) {
return $link;
}
return '<a href="' . $this->_e($link) . '" rel="nofollow noindex" target="_blank">' . $this->_e($text) . '</a>';
}
public function _asset_items($items)
Rows of the assets_out / assets_add logs: a one-line preview, the length and the content (opened from the preview).
public function _asset_items($items)
{
$items = (array) $items;
$i = 0;
foreach ($items as $k => $v) {
$v = (array) $v;
$content = is_string($v['content'] ?? null) ? $v['content'] : $this->_var_export($v['content'] ?? '');
unset($v['content'], $v['is_added']);
$preview = mb_strimwidth((string) preg_replace('~\s+~u', ' ', $content), 0, 120, '...', 'UTF-8');
$items[$k] = ['id' => ++$i] + $v + ['preview' => $preview, 'strlen' => strlen($content), 'content' => $content];
}
return $items;
}
public function _bytes($bytes)
public function _bytes($bytes)
{
$bytes = (float) $bytes;
if ($bytes >= 1048576) {
return number_format($bytes / 1048576, 1, '.', '') . ' MB';
}
return $bytes >= 1024 ? number_format($bytes / 1024, 1, '.', '') . ' KB' : (int) $bytes . ' B';
}
public function _cache_panel($name, &$params, $hidden_map)
$name mixed cache_get, cache_set ... public function _cache_panel($name, &$params, $hidden_map)
{
if ( ! $this->SHOW_CORE_CACHE) {
return '';
}
$items = (array) $this->_get_debug_data($name);
$time = 0.0;
foreach ($items as $k => $v) {
$items[$k] = $this->_data_item($v);
$time += (float) ($v['time'] ?? 0);
}
$items = $this->_time_count_changes($items);
$params['count'] = count($items);
$params['time'] = $time;
return $this->_show_auto_table($items, ['hidden_map' => $hidden_map, 'caption' => ['driver' => cache()->DRIVER, 'ns' => cache()->CACHE_NS]]);
}
public function _cap($text, $max = 0)
$text cut to $max bytes (MAX_VALUE_LEN by default) on a UTF-8 boundary, with a note of the full size.
public function _cap($text, $max = 0)
{
$text = (string) $text;
$max = (int) ($max ?: $this->MAX_VALUE_LEN);
if ($max <= 0 || strlen($text) <= $max) {
return $text;
}
return mb_strcut($text, 0, $max, 'UTF-8') . PHP_EOL . '... cut, ' . strlen($text) . ' bytes in all';
}
public function _cell($value, $raw = false, $col = '')
A cell value: short text escaped inline, long text in a <pre>, arrays dumped; $raw passes trusted HTML through.
public function _cell($value, $raw = false, $col = '')
{
if ($value === null || $value === false || $value === [] || $value === '') {
return '';
}
if (is_array($value) || is_object($value)) {
return $this->_pre($this->_var_export($value));
}
if ($value === true) {
return '1';
}
if ($raw) {
return (string) $value;
}
if ($col === 'time' && is_numeric($value)) {
return $this->_ms($value);
}
$value = (string) $value;
if (strlen($value) > 160 || strpos($value, "\n") !== false) {
return $this->_pre($value);
}
return $this->_e($value);
}
public function _console_file($name)
Stylesheet / script of the console, inlined: the framework dir is not served by the web server.
public function _console_file($name)
{
static $files = [];
if ( ! isset($files[$name])) {
$path = YF_PATH . 'assets/yf/debug_console/' . basename((string) $name);
$text = is_file($path) ? (string) file_get_contents($path) : '';
// the inline <style> / <script> must not be closed early by the file's own text
$files[$name] = str_ireplace(['</style', '</script'], ['<\/style', '<\/script'], $text);
}
return $files[$name];
}
public function _data_item($v)
A get_data / cache row: `data` dumped and cut, its size next to it.
public function _data_item($v)
{
$v = (array) $v;
if (array_key_exists('data', $v)) {
$data = $v['data'];
$dump = is_string($data) ? $data : $this->_var_export($data);
$v['data_size'] = $data === null ? 'NULL' : strlen($dump);
$v['data'] = $data === null ? '' : $this->_cap($dump, 2000);
}
return $v;
}
public function _debug_DEBUG_YF(&$params = [])
public function _debug_DEBUG_YF(&$params = [])
{
if ( ! $this->SHOW_SETTINGS) {
return '';
}
$cache_use = ((main()->USE_SYSTEM_CACHE || conf('USE_CACHE')) && ! cache()->NO_CACHE);
$locale_debug = (array) $this->_get_debug_data('locale');
$lang_selected = $locale_debug['lang_selected'] ?? '';
$data['yf'] = [
'MAIN_TYPE' => MAIN_TYPE,
'LANG' => conf('language'),
'IP' => common()->get_ip(),
'COUNTRY' => conf('country') ?: ($_SERVER['GEOIP_COUNTRY_CODE'] ?? null),
'TIMEZONE' => date_default_timezone_get() . (conf('timezone') ? ', conf: ' . conf('timezone') : ''),
'DEBUG_MODE' => (int) DEBUG_MODE,
'DEV_MODE' => (int) conf('DEV_MODE'),
'REWRITE_MODE' => (int) tpl()->REWRITE_MODE,
'DEBUG_CONSOLE_POPUP' => (int) conf('DEBUG_CONSOLE_POPUP'),
'CACHE_USE' => (int) $cache_use,
'CACHE_NO_CACHE' => (int) cache()->NO_CACHE,
'CACHE_NO_WHY' => cache()->_NO_CACHE_WHY,
'CACHE_DRIVER' => cache()->DRIVER,
'CACHE_NS' => cache()->CACHE_NS,
'CACHE_TTL' => (int) cache()->TTL,
'YF_PATH' => YF_PATH,
'YF_VERSION' => $this->_get_yf_version(),
'APP_PATH' => APP_PATH,
'APP_VERSION' => $this->_get_app_version(),
'PROJECT_PATH' => PROJECT_PATH,
'SITE_PATH' => SITE_PATH,
'ADMIN_SITE_PATH' => ADMIN_SITE_PATH,
'CONFIG_PATH' => CONFIG_PATH,
'STORAGE_PATH' => STORAGE_PATH,
'LOGS_PATH' => LOGS_PATH,
'UPLOADS_PATH' => UPLOADS_PATH,
'WEB_PATH' => WEB_PATH,
'MEDIA_PATH' => MEDIA_PATH,
'ADMIN_WEB_PATH' => ADMIN_WEB_PATH,
'CSS_FRAMEWORK' => conf('css_framework') ?: 'bs3',
'BOOTSTRAP_THEME' => common()->bs_current_theme() ?: ($_COOKIE['yf_theme'] ?? null) ?: (conf('DEF_BOOTSTRAP_THEME_' . strtoupper(MAIN_TYPE)) ?: conf('DEF_BOOTSTRAP_THEME')),
'TPL_DRIVER' => tpl()->DRIVER_NAME,
'TPL_COMPILE' => (int) tpl()->COMPILE_TEMPLATES,
'TPL_THEMES_PATH' => tpl()->_THEMES_PATH,
'TPL_PATH' => tpl()->TPL_PATH,
'TPL_SKIN' => conf('theme'),
'TPL_INHERIT_SKIN' => (string) tpl()->INHERIT_SKIN,
'TPL_INHERIT_SKIN2' => (string) tpl()->INHERIT_SKIN2,
'MAIN_HOSTNAME' => main()->HOSTNAME,
'SITE_ID' => (int) conf('SITE_ID'),
'SERVER_ID' => (int) conf('SERVER_ID'),
'SERVER_ROLE' => conf('SERVER_ROLE'),
'SERVER_ADDR' => ($_SERVER['SERVER_ADDR'] ?? null),
'SERVER_SELF_IPS' => implode(', ', (array) main()->_server_self_ips),
'USER_ID' => (int) main()->USER_ID,
'USER_GROUP' => (int) main()->USER_GROUP,
'USER_ROLE' => main()->USER_ROLE,
'IS_POST' => (int) main()->is_post(),
'IS_AJAX' => (int) main()->is_ajax(),
'IS_HTTPS' => (int) main()->is_https(),
'IS_CONSOLE' => (int) main()->is_console(),
'IS_REDIRECT' => (int) main()->is_redirect(),
'IS_COMMON_PAGE' => (int) main()->is_common_page(),
'IS_UNIT_TEST' => (int) main()->is_unit_test(),
'IS_SPIDER' => (int) conf('IS_SPIDER'),
'NO_GRAPHICS' => (int) main()->NO_GRAPHICS,
'HTTP_HOST' => ($_SERVER['HTTP_HOST'] ?? null),
'SERVER_PORT' => ($_SERVER['SERVER_PORT'] ?? null),
'REWRITE_DEF_HOST' => _class('rewrite')->DEFAULT_HOST,
'REWRITE_DEF_PORT' => _class('rewrite')->DEFAULT_PORT,
'WEB_DOMAIN' => WEB_DOMAIN,
'REQUEST_URI' => ($_SERVER['REQUEST_URI'] ?? null),
'REQUEST_METHOD' => ($_SERVER['REQUEST_METHOD'] ?? null),
'OUTPUT_CACHING' => (int) main()->OUTPUT_CACHING,
'NO_CACHE_HEADERS' => (int) main()->NO_CACHE_HEADERS,
'LANG_SELECTED_FROM' => $lang_selected . ' (' . ($locale_debug['lang_variants'][$lang_selected] ?? '') . ')',
'LANG_PRIORITIES' => implode(', ', (array) ($locale_debug['lang_priorities'] ?? [])),
'LANG_VARIANTS' => $locale_debug['lang_variants'] ?? null,
'LOCALE_CURRENT' => $locale_debug['current'] ?? null,
'LOCALE_DEFAULT' => $locale_debug['default'] ?? null,
'LOCALE_LC_VARIANTS' => implode(', ', (array) ($locale_debug['lc_variants']['LC_ALL'] ?? [])),
'LOCALE_SYSTEM' => implode(', ', (array) ($locale_debug['system'] ?? [])),
'HTTP_IN_HEADERS' => $this->_get_request_headers(),
'HTTP_OUT_HEADERS' => headers_list(),
];
foreach ((array) $this->_get_debug_data('_DEBUG_META') as $k => $v) {
$data['yf']['META_' . strtoupper($k)] = $v;
}
$ini_all = ini_get_all(null, false);
foreach (['memory_limit', 'max_execution_time', 'default_socket_timeout', 'max_input_time', 'post_max_size', 'upload_max_filesize',
'file_uploads', 'allow_url_fopen', 'error_reporting', 'display_errors', 'opcache.enable', 'opcache.jit'] as $name) {
isset($ini_all[$name]) && $data['php']['ini: ' . $name] = $ini_all[$name];
}
if (tpl()->COMPRESS_OUTPUT && ! main()->NO_GRAPHICS) {
$c_info = (array) $this->_get_debug_data('compress_output');
$original = (int) ($c_info['size_original'] ?? 0);
$compressed = (int) ($c_info['size_compressed'] ?? 0);
$data['php'] += [
'compress: size original' => $original . ' bytes',
'compress: size compressed' => $compressed . ' bytes',
'compress: ratio' => ($compressed ? round($original / $compressed * 100, 0) : 0) . '%',
];
}
$loaded_exts = array_map('strtolower', get_loaded_extensions());
sort($loaded_exts);
$data['php'] += [
'memory_usage' => $this->_bytes(memory_get_usage()),
'memory_peak_usage' => $this->_bytes(memory_get_peak_usage()),
'sys_loadavg' => function_exists('sys_getloadavg') ? implode(' | ', (array) sys_getloadavg()) : '',
'db_server_version' => db()->get_server_version(),
'db_host_info' => db()->get_host_info(),
'php_version' => phpversion(),
'php_sapi_name' => php_sapi_name(),
'php_current_user' => get_current_user(),
'php_uname' => php_uname(),
'php_include_path' => get_include_path(),
'php_loaded_extensions' => implode(', ', $loaded_exts),
'php_ini_scanned_files' => function_exists('php_ini_scanned_files') ? php_ini_scanned_files() : '',
];
// the session tab has the same, unless it is off
if ( ! $this->SHOW_SESSION_DATA) {
$data['session']['session_id'] = session_id();
foreach ((array) ini_get_all('session', false) as $k => $v) {
$data['session'][$k] = $v;
}
}
// the switches of test::change_debug (session values), as before
$form = '<form class="yfd-form" method="post" action="' . $this->_e(url('/test/change_debug')) . '">'
. '<input type="hidden" name="locale_edit" value="0"><label><input type="checkbox" name="locale_edit" value="1"'
. (($_SESSION['locale_vars_edit'] ?? null) ? ' checked' : '') . '> locale edit</label>'
. '<input type="hidden" name="debug_console_light" value="0"><label><input type="checkbox" name="debug_console_light" value="1"'
. (($_SESSION['debug_console_light'] ?? null) ? ' checked' : '') . '> console light (this tab only)</label>'
. '<button type="submit">save</button></form>';
$body = '';
foreach ($data as $name => $_data) {
$body .= '<div>' . $this->_heading($name) . $this->_show_key_val_table($_data, ['no_sort' => 1, 'raw_keys' => $name == 'yf' ? ['YF_VERSION', 'APP_VERSION'] : []]) . '</div>';
}
return $form . '<div class="yfd-grid">' . $body . '</div>';
}
public function _debug__session(&$params = [])
public function _debug__session(&$params = [])
{
if ( ! $this->SHOW_SESSION_DATA) {
return '';
}
$out = '<div>' . $this->_heading('$_SESSION') . $this->_show_key_val_table((array) ($_SESSION ?? [])) . '</div>';
$data = ['session_id' => session_id()];
foreach ((array) ini_get_all('session', false) as $k => $v) {
$data[$k] = $v;
}
$out .= '<div>' . $this->_heading('session ini') . $this->_show_key_val_table($data, ['no_sort' => 1]) . '</div>';
$items = $this->_get_debug_data('input_session');
$items && $out .= '<div>' . $this->_show_auto_table($items, ['hidden_map' => ['trace' => 'name']]) . '</div>';
$params['count'] = count((array) ($_SESSION ?? []));
return '<div class="yfd-grid">' . $out . '</div>';
}
public function _debug_apc(&$params = [])
public function _debug_apc(&$params = [])
{
if ( ! $this->SHOW_APC_INFO || ! function_exists('apcu_cache_info') || ! function_exists('apcu_enabled') || ! apcu_enabled()) {
return '';
}
$data = (array) apcu_cache_info(true);
foreach ((array) ini_get_all('apcu', false) as $_k => $_v) {
$data[$_k] = $_v;
}
return $this->_show_key_val_table($data);
}
public function _debug_assets(&$params = [])
public function _debug_assets(&$params = [])
{
$body = [];
$items = $this->_asset_items($this->_get_debug_data('assets_out'));
$params['count'] = count($items);
$body['out: ' . count($items)] = $this->_show_auto_table($items, ['hidden_map' => ['trace' => 'name', 'content' => 'preview', 'params' => 'name']]);
$items = $this->_asset_items($this->_get_debug_data('assets_add'));
$body['add log: ' . count($items)] = $this->_show_auto_table($items, ['hidden_map' => ['trace' => 'name', 'content' => 'preview', 'params' => 'name']]);
$items = (array) $this->_get_debug_data('assets_names');
$i = 0;
foreach ($items as $k => $v) {
$items[$k] = ['id' => ++$i] + (array) $v;
}
$body['names: ' . count($items)] = $this->_show_auto_table($items, ['hidden_map' => ['content' => 'path']]);
return $this->_tabs($body);
}
public function _debug_cache_del(&$params = [])
public function _debug_cache_del(&$params = [])
{
return $this->_cache_panel('cache_del', $params, ['trace' => 'name']);
}
public function _debug_cache_get(&$params = [])
public function _debug_cache_get(&$params = [])
{
return $this->_cache_panel('cache_get', $params, ['trace' => 'params', 'data' => 'name']);
}
public function _debug_cache_other(&$params = [])
The other cache calls: flush, multi_*, list_keys, del_by_prefix ...
public function _debug_cache_other(&$params = [])
{
if ( ! $this->SHOW_CORE_CACHE) {
return '';
}
$items = [];
$time = 0.0;
foreach ((array) debug() as $key => $rows) {
if (strpos((string) $key, 'cache_') !== 0 || in_array($key, ['cache_get', 'cache_set', 'cache_del'], true) || ! is_array($rows)) {
continue;
}
foreach ((array) $this->_get_debug_data($key) as $v) {
$items[] = ['call' => substr($key, strlen('cache_'))] + $this->_data_item($v);
$time += (float) ($v['time'] ?? 0);
}
}
$params['count'] = count($items);
$params['time'] = $time;
return $this->_show_auto_table($items, ['hidden_map' => ['trace' => 'call', 'data' => 'call']]);
}
public function _debug_cache_set(&$params = [])
public function _debug_cache_set(&$params = [])
{
return $this->_cache_panel('cache_set', $params, ['trace' => 'name', 'data' => 'name']);
}
public function _debug_curl_requests(&$params = [])
public function _debug_curl_requests(&$params = [])
{
if ( ! $this->SHOW_CURL_REQUESTS) {
return '';
}
$items = (array) $this->_get_debug_data('curl_get_remote_page');
foreach ($items as $k => $v) {
$info = (array) ($v['info'] ?? []);
$items[$k] = [
'id' => $k + 1,
'url' => $info['url'] ?? '',
'status' => $info['http_code'] ?? '',
'time' => (float) ($info['total_time'] ?? 0),
'info' => $info,
'trace' => $v['trace'] ?? '',
];
}
$items = $this->_time_count_changes($items);
$params['count'] = count($items);
return $this->_show_auto_table($items, ['hidden_map' => ['trace' => 'url', 'info' => 'url']]);
}
public function _debug_dashboard()
public function _debug_dashboard()
{
if ( ! $this->SHOW_DB_STATS) {
return '';
}
$items = $this->_get_debug_data('dashboard');
if ( ! isset($items) || ! isset($items['widgets'])) {
return false;
}
$loaded_modules = (array) ($this->backup_debug_data['main_load_class'] ?? []);
$_items = [];
foreach ((array) $items['widgets'] as $key => $value) {
$_items[$key]['class_name'] = $value['class_name'];
$_items[$key]['action'] = $value['action'];
foreach ($loaded_modules as $v) {
if (($value['class_name'] == $v['class_name'])) {
$_items[$key]['loaded_class_name'] = $v['loaded_class_name'];
$_items[$key]['storage'] = $v['storage'];
$_items[$key]['loaded_path'] = $v['loaded_path'];
break;
}
}
$_items[$key]['time'] = $value['time'];
}
$_items = $this->_time_count_changes($_items);
return $this->_show_auto_table($_items, ['caption' => ['dashboard' => $items['name'] ?? '', 'total_time' => $items['total_time'] ?? '']]);
}
public function _debug_db(&$params = [])
public function _debug_db(&$params = [])
{
if ( ! $this->SHOW_DB_QUERY_LOG) {
return false;
}
$items = [];
$count = 0;
$time = 0.0;
$instances_trace = (array) $this->_get_debug_data('db_instances_trace');
foreach ((array) $this->_get_debug_data('db_instances') as $k => $db) {
if ( ! is_object($db)) {
continue;
}
foreach ((array) ($db->_LOG ?? []) as $log) {
$count++;
$time += (float) ($log['time'] ?? 0);
}
$connect_trace = $instances_trace[$k] ?? [];
$name = $db->DB_TYPE . ' | ' . $db->DB_USER . ' | ' . $db->DB_HOST . ($db->DB_PORT ? ':' . $db->DB_PORT : '') . ' | ' . $db->DB_NAME;
$items[$name] = $this->_do_debug_db_connection_queries($db, $connect_trace);
$items['shutdown queries: ' . $name] = $this->_show_db_shutdown_queries($db);
$items['stats: ' . $name] = $this->_show_db_stats($db);
}
$params['count'] = $count;
$params['time'] = $time;
// one connection: its DSN heads the table already
return $this->_tabs($items, ['no_single_heading' => 1]);
}
public function _debug_dd_table(&$params = [])
public function _debug_dd_table(&$params = [])
{
if ( ! $this->SHOW_DD_TABLE) {
return '';
}
$i = 0;
$items = (array) $this->_get_debug_data('dd_table');
foreach ($items as $k => $v) {
$items[$k] = ['id' => ++$i] + (array) $v;
}
$items = $this->_time_count_changes($items);
$params['count'] = count($items);
return $this->_show_auto_table($items, ['hidden_map' => ['trace' => 'fields']]);
}
public function _debug_events(&$params = [])
public function _debug_events(&$params = [])
{
$main_ts = main()->_time_start;
$items = [];
$count = 0;
foreach (['listen', 'fire', 'queue'] as $name) {
$data = $this->_get_debug_data('events_' . $name);
if ( ! $data) {
continue;
}
foreach ((array) $data as $k => $v) {
$data[$k]['time_offset'] = $this->_ms(($v['time_offset'] ?? $main_ts) - $main_ts);
}
$name === 'fire' && $count = count($data);
$items[$name . ': ' . count($data)] = $this->_show_auto_table($data, ['no_total_time' => 1, 'hidden_map' => ['trace' => 'name']]);
}
$params['count'] = $count;
return $this->_tabs($items);
}
public function _debug_execute(&$params = [])
public function _debug_execute(&$params = [])
{
if ( ! $this->SHOW_MAIN_EXECUTE) {
return '';
}
$items = (array) $this->_get_debug_data('main_execute_block_time');
$items = $this->_time_count_changes($items);
$params['count'] = count($items);
return $this->_show_auto_table($items, ['hidden_map' => ['trace' => 'params']]);
}
public function _debug_form2(&$params = [])
public function _debug_form2(&$params = [])
{
if ( ! $this->SHOW_FORM2) {
return '';
}
$i = 0;
$items = (array) $this->_get_debug_data('form2');
foreach ($items as $k => $v) {
$items[$k] = ['id' => ++$i] + (array) $v;
}
$items = $this->_time_count_changes($items);
$params['count'] = count($items);
return $this->_show_auto_table($items, ['hidden_map' => ['trace' => 'params', 'fields' => 'params']]);
}
public function _debug_get_data(&$params = [])
public function _debug_get_data(&$params = [])
{
if ( ! $this->SHOW_MAIN_GET_DATA) {
return '';
}
$items = (array) $this->_get_debug_data('main_get_data');
foreach ($items as $k => $v) {
$items[$k] = $this->_data_item($v);
}
$items = $this->_time_count_changes($items);
$params['count'] = count($items);
return $this->_show_auto_table($items, ['hidden_map' => ['trace' => 'params', 'data' => 'name']]);
}
public function _debug_globals(&$params = [])
public function _debug_globals(&$params = [])
{
if ( ! $this->SHOW_GLOBALS) {
return '';
}
$classes_builtin = [];
$classes_custom = [];
foreach (get_declared_classes() as $name) {
$r = new ReflectionClass($name);
$file_name = $r->getFileName();
if ( ! $file_name) {
$classes_builtin[$name] = 'built-in';
} else {
$classes_custom[$name] = $this->_short_paths($file_name) . ':' . $r->getStartLine();
}
}
ksort($classes_custom);
ksort($classes_builtin);
$data['classes'] = $classes_custom + $classes_builtin;
$data['functions'] = [];
$funcs = get_defined_functions();
foreach ((array) $funcs['user'] as $name) {
$r = new ReflectionFunction($name);
$data['functions'][$name] = $this->_short_paths($r->getFileName()) . ':' . $r->getStartLine();
}
ksort($data['functions']);
// names only: the values hold credentials (DB_PSWD ...)
$constants = get_defined_constants(true);
$data['constants'] = array_keys((array) ($constants['user'] ?? []));
sort($data['constants']);
$data['globals'] = array_values(array_filter(array_keys($GLOBALS), function ($v) {
return $v[0] != '_';
}));
sort($data['globals']);
$body = '';
foreach ($data as $name => $_data) {
$table = array_is_list($_data) ? $this->_pre(implode("\n", $_data)) : $this->_show_key_val_table($_data, ['no_sort' => 1]);
$body .= '<div>' . $this->_heading($name . ' (' . count($_data) . ')') . $table . '</div>';
}
return '<div class="yfd-grid">' . $body . '</div>';
}
public function _debug_hooks(&$params = [])
public function _debug_hooks(&$params = [])
{
$items = [];
$hook_name = '_hook_debug';
foreach (main()->modules as $module_name => $module_obj) {
if ( ! is_object($module_obj) || ! method_exists($module_obj, $hook_name)) {
continue;
}
$items[$module_name] = $module_obj->$hook_name($this);
}
return $this->_show_key_val_table($items);
}
public function _debug_htmx(&$params = [])
htmx requests of the page after load: the browser fills this tab (console.js), the tab shows once htmx is on the page. Each row has the X-YF-Debug header (time, db queries, memory) when the answer was made with DEBUG_MODE on.
public function _debug_htmx(&$params = [])
{
if ( ! $this->SHOW_HTMX) {
return '';
}
$params['hidden'] = true;
$head = '';
foreach (['id', 'verb', 'url', 'status', 'ms', 'bytes', 'target', 'X-YF-Debug', 'HX-Trigger'] as $col) {
$head .= '<th>' . $this->_e($col) . '</th>';
}
return '<div class="yfd-note">htmx requests made by this page since it loaded (recorded in the browser).</div>'
. '<table class="yfd-t"><thead><tr>' . $head . '</tr></thead><tbody></tbody></table>';
}
public function _debug_i18n(&$params = [])
public function _debug_i18n(&$params = [])
{
if ( ! $this->SHOW_I18N_VARS) {
return '';
}
$i = 0;
$calls = (array) (_class('i18n')->_calls ?? []);
$items = (array) $this->_get_debug_data('i18n');
foreach ($items as $k => $v) {
$v = (array) $v;
$v['calls'] = (int) ($calls[$v['name_orig'] ?? ''] ?? 0);
$v['name'] = $this->_admin_link('edit_i18n', (string) ($v['name'] ?? ''));
$items[$k] = ['id' => ++$i] + $v;
}
$items = $this->_time_count_changes($items);
$params['count'] = count($items);
return $this->_show_auto_table($items, ['raw' => ['name'], 'hidden_map' => ['trace' => 'name', 'data' => 'name']]);
}
public function _debug_included(&$params = [])
public function _debug_included(&$params = [])
{
if ( ! $this->SHOW_INCLUDED_FILES) {
return '';
}
$total_size = 0;
$i = 0;
$items = (array) $this->_get_debug_data('included_files');
foreach ($items as $k => $v) {
if (empty($v['exists'])) {
unset($items[$k]);
continue;
}
$v['path'] = $this->_admin_link('edit_file', (string) ($v['path'] ?? ''));
$items[$k] = ['id' => ++$i] + $v;
$total_size += (int) ($v['size'] ?? 0);
}
$items = $this->_time_count_changes($items);
$params['count'] = count($items);
return $this->_show_auto_table($items, ['raw' => ['path'], 'caption' => ['total_size' => $this->_bytes($total_size)], 'hidden_map' => ['trace' => 'path']]);
}
public function _debug_input(&$params = [])
public function _debug_input(&$params = [])
{
if ( ! $this->SHOW_INPUT_DATA) {
return '';
}
$body = [];
foreach (['GET', 'POST', 'FILES', 'COOKIE', 'SERVER', 'ENV'] as $name) {
$enabled_var = 'SHOW_' . $name . '_DATA';
if ($this->$enabled_var) {
$out = (string) $this->_show_key_val_table($GLOBALS['_' . $name] ?? null);
$items = $this->_get_debug_data('input_' . strtolower($name));
$items && $out .= $this->_show_auto_table($items, ['hidden_map' => ['trace' => 'name']]);
$out !== '' && $body[] = $this->_heading('$_' . $name) . $out;
}
}
return implode(PHP_EOL, $body);
}
public function _debug_logger(&$params = [])
logger() records of the request (yf_logger::_debug_record()), as the handlers got them.
public function _debug_logger(&$params = [])
{
if ( ! $this->SHOW_LOGGER) {
return '';
}
$items = [];
$i = 0;
$bad = false;
foreach ((array) $this->_get_debug_data('logger') as $v) {
$level = (string) ($v['level'] ?? '');
$bad = $bad || in_array($level, ['emergency', 'alert', 'critical', 'error'], true);
$items[] = [
'id' => ++$i,
'level' => '<span class="yfd-lvl-' . preg_replace('~[^a-z]~', '', $level) . '">' . $this->_e($level) . '</span>',
'message' => $v['message'] ?? '',
'context' => $v['context'] ?? [],
'handlers' => implode(', ', (array) ($v['handlers'] ?? [])),
'logged' => ! empty($v['done']) ? 'yes' : 'no',
'time_offset' => isset($v['time']) ? $this->_ms($v['time'] - main()->_time_start) : '',
'trace' => $v['trace'] ?? '',
];
}
$params['count'] = count($items);
$params['bad'] = $bad;
return $this->_show_auto_table($items, ['raw' => ['level'], 'hidden_map' => ['trace' => 'message', 'context' => 'message']]);
}
public function _debug_memcached(&$params = [])
public function _debug_memcached(&$params = [])
{
if ( ! $this->SHOW_MEMCACHED_INFO) {
return '';
}
if (strpos(strtolower((string) cache()->DRIVER), 'memcache') === false) {
return '';
}
$mc_obj = cache_memcached_connect();
if ( ! is_object($mc_obj)) {
return '';
}
$data = [];
$ext = '';
if (method_exists($mc_obj, 'getExtendedStats')) {
$ext = 'memcache (old)';
$data = $mc_obj->getExtendedStats();
} elseif (method_exists($mc_obj, 'getStats')) {
$ext = 'memcached (new)';
$data = $mc_obj->getStats();
}
if ( ! $data) {
return '';
}
$body = '';
foreach ((array) $data as $name => $_data) {
$body .= '<div>' . $this->_heading($name) . $this->_show_key_val_table($_data, ['skip_empty_values' => 1]) . '</div>';
}
return '<div class="yfd-note">PHP extension: ' . $this->_e($ext) . '</div><div class="yfd-grid">' . $body . '</div>';
}
public function _debug_modules(&$params = [])
public function _debug_modules(&$params = [])
{
if ( ! $this->SHOW_LOADED_MODULES) {
return '';
}
$counter = 0;
$items = [];
$time = 0.0;
foreach ((array) $this->_get_debug_data('main_load_class') as $data) {
$path = (string) ($data['loaded_path'] ?? '');
$items[] = [
'id' => ++$counter,
'module' => $data['class_name'] ?? '',
'loaded_class' => $data['loaded_class_name'] ?? '',
'path' => $this->_admin_link('edit_file', $path),
'size' => $path !== '' && file_exists($path) ? filesize($path) : '',
'storage' => $data['storage'] ?? '',
'time' => (float) ($data['time'] ?? 0),
'trace' => $data['trace'] ?? '',
'storages' => $data['storages'] ?? '',
];
$time += (float) ($data['time'] ?? 0);
}
$items = $this->_time_count_changes($items);
$params['count'] = count($items);
$params['time'] = $time;
return $this->_show_auto_table($items, ['raw' => ['path'], 'hidden_map' => ['trace' => 'path', 'storages' => 'path']]);
}
public function _debug_other(&$params = [])
public function _debug_other(&$params = [])
{
$items = [];
foreach ((array) debug() as $k => $v) {
if (isset($this->_used_debug_datas[$k]) || $v === false) {
continue;
}
$items[$k] = $v;
}
$params['count'] = count($items);
return $this->_show_key_val_table($items);
}
public function _debug_php_errors(&$params = [])
PHP warnings, notices and deprecations the request raised (yf_core_errors::_debug_record()).
public function _debug_php_errors(&$params = [])
{
if ( ! $this->SHOW_PHP_ERRORS) {
return '';
}
$items = [];
$i = 0;
foreach ((array) $this->_get_debug_data('php_errors') as $v) {
$items[] = [
'id' => ++$i,
'type' => $v['type'] ?? '',
'message' => $v['message'] ?? '',
'file' => $this->_short_paths(($v['file'] ?? '') . ':' . ($v['line'] ?? '')),
'time_offset' => isset($v['time']) ? $this->_ms($v['time'] - main()->_time_start) : '',
'trace' => $v['trace'] ?? '',
];
}
$params['count'] = count($items);
$params['bad'] = (bool) $items;
return $this->_show_auto_table($items, ['hidden_map' => ['trace' => 'message'], 'tr' => function ($row) {
return preg_match('~ERROR|WARNING~', (string) $row['type']) ? ['class' => 'error'] : [];
}]);
}
public function _debug_profiling(&$params = [])
public function _debug_profiling(&$params = [])
{
$all_timings = main()->_timing;
if ( ! $all_timings) {
return false;
}
$ts = main()->_time_start;
$_last_item = end($all_timings);
$time_all = $_last_item[0] - $ts;
$items = [];
foreach ((array) $all_timings as $i => $v) {
$time_offset = $v[0] - $ts;
$time_change = 0;
$time_change_p = 0;
if (isset($all_timings[$i + 1])) {
$time_change = $all_timings[$i + 1][0] - $v[0];
}
if ($time_change > 0.001 && $time_all > 0) {
$time_change_p = round(100 - (($time_all - $time_change) / $time_all * 100), 1);
}
$items[] = [
'i' => $i,
'time_offset' => $this->_ms($time_offset),
'time_change' => $time_change > 0.0001 ? $this->_ms($time_change) : '',
'timep' => $time_change_p ? '<span class="yfd-pct' . ($time_change_p >= 5 ? ' yfd-warn' : '') . '" style="--p:' . min(100, $time_change_p) . '%">' . $time_change_p . '%</span>' : '',
'class' => $v[1],
'method' => $v[2],
'trace' => $v[3],
'args' => $v[4] ?: '',
];
}
$params['count'] = count($items);
return $this->_show_auto_table($items, ['hidden_map' => ['trace' => 'method', 'args' => 'method']]);
}
public function _debug_redis(&$params = [])
public function _debug_redis(&$params = [])
{
if ( ! $this->SHOW_REDIS_INFO) {
return '';
}
$instances = [
'redis_default' => redis(),
'redis_cache' => strpos(strtolower((string) cache()->DRIVER), 'redis') !== false ? (cache()->_driver->_connection ?? null) : null,
];
$counter = 0;
$tabs = [];
$time_all = 0.0;
foreach ((array) $instances as $iname => $instance) {
if ( ! is_object($instance) || empty($instance->log) || ($iname != 'redis_default' && $instance === $instances['redis_default'])) {
continue;
}
$items = [];
foreach ((array) $instance->log as $v) {
$func = (string) ($v['func'] ?? '');
$items[] = [
'id' => ++$counter,
'func' => '<a href="https://redis.io/commands/' . rawurlencode(strtolower($func)) . '" rel="noopener" target="_blank">' . $this->_e($func) . '</a>',
'args' => ! empty($v['args']) ? $this->_cap(implode(PHP_EOL, array_map(function ($a) {
return is_scalar($a) ? (string) $a : $this->_var_export($a);
}, (array) $v['args'])), 1000) : '',
'result' => $v['result'] ?? '',
'time' => (float) ($v['exec_time'] ?? 0),
'trace' => $v['trace'] ?? '',
];
$time_all += (float) ($v['exec_time'] ?? 0);
}
$items = $this->_time_count_changes($items);
$logs = $this->_show_auto_table($items, [
'raw' => ['func'],
'hidden_map' => ['trace' => 'args', 'result' => 'func'],
'caption' => $iname,
]);
$info = is_callable([$instance, 'info']) ? $instance->info() : [];
$tabs[$iname] = '<div class="yfd-grid"><div>' . $logs . '</div><div>' . $this->_show_key_val_table($info, ['skip_empty_values' => 1]) . '</div></div>';
}
$params['count'] = $counter;
$params['time'] = $time_all;
return $this->_tabs($tabs);
}
public function _debug_resize_images(&$params = [])
public function _debug_resize_images(&$params = [])
{
if ( ! $this->SHOW_RESIZED_IMAGES_LOG || empty($GLOBALS['_RESIZED_IMAGES_LOG'])) {
return '';
}
$params['count'] = count((array) $GLOBALS['_RESIZED_IMAGES_LOG']);
return $this->_show_auto_table($GLOBALS['_RESIZED_IMAGES_LOG'] ?? null);
}
public function _debug_rewrite(&$params = [])
public function _debug_rewrite(&$params = [])
{
if ( ! $this->SHOW_REWRITE_INFO) {
return '';
}
$data = $this->_get_debug_data('rewrite');
if (empty($data)) {
return '';
}
$items = [];
foreach ((array) $data as $k => $v) {
$items[] = [
'id' => $k + 1,
'source' => (string) ($v['source'] ?? ''),
'rewrited' => $this->_admin_link('link', (string) ($v['rewrited'] ?? '')),
'time' => (float) ($v['exec_time'] ?? 0),
'trace' => $v['trace'] ?? '',
];
}
$items = $this->_time_count_changes($items);
$params['count'] = count($items);
$params['time'] = (float) $this->_get_debug_data('rewrite_exec_time');
return $this->_show_auto_table($items, [
'raw' => ['rewrited'],
'caption' => [
'rewrite processing time' => $this->_ms($params['time']),
],
'hidden_map' => [
'trace' => 'source',
],
]);
}
public function _debug_search(&$params = [])
HTTP requests of the elasticsearch / opensearch search drivers (yf_search_driver_elasticsearch::_debug_record()).
public function _debug_search(&$params = [])
{
if ( ! $this->SHOW_SEARCH) {
return '';
}
$items = [];
$time = 0.0;
$bad = false;
foreach ((array) $this->_get_debug_data('search_requests') as $i => $v) {
$v = (array) $v;
$time += (float) ($v['time'] ?? 0);
$bad = $bad || ($v['error'] ?? '') !== '';
$items[] = [
'id' => $i + 1,
'driver' => $v['driver'] ?? '',
'request' => ($v['method'] ?? '') . ' ' . ($v['path'] ?? ''),
'status' => $v['status'] ?? '',
'hits' => $v['hits'] ?? '',
'took' => isset($v['took']) ? $v['took'] . ' ms' : '',
'time' => (float) ($v['time'] ?? 0),
'error' => $v['error'] ?? '',
'body' => $v['body'] ?? '',
'trace' => $v['trace'] ?? '',
];
}
$items = $this->_time_count_changes($items);
$params['count'] = count($items);
$params['time'] = $time;
$params['bad'] = $bad;
return $this->_show_auto_table($items, ['hidden_map' => ['body' => 'request', 'trace' => 'request'], 'tr' => function ($row) {
return $row['error'] !== '' ? ['class' => 'error'] : [];
}]);
}
public function _debug_sphinxsearch(&$params = [])
SphinxQL statements (Sphinx / Manticore search drivers and the legacy yf_sphinxsearch client). The server status is read only through a connection the request opened already: the console does not connect to a search server itself.
public function _debug_sphinxsearch(&$params = [])
{
if ( ! $this->SHOW_SPHINX) {
return '';
}
$items = $this->_get_debug_data('sphinxsearch');
if ( ! $items) {
return '';
}
$sphinx_connect_debug = [];
$time = 0.0;
$hosts = [];
foreach ((array) $items as $id => $item) {
if (($item['query'] ?? '') == 'sphinx connect') {
$sphinx_connect_debug = $item;
unset($items[$id]);
continue;
}
$time += (float) ($item['time'] ?? 0);
isset($item['host']) && $hosts[(string) $item['host']] = true;
$items[$id] = ['id' => $id + 1] + (array) $item;
}
$items = $this->_time_count_changes($items);
$params['count'] = count($items);
$params['time'] = $time;
$sphinx = _class('sphinxsearch');
$caption = ['host' => implode(', ', array_keys($hosts)) ?: $sphinx->_get_host()];
$status = [];
if ( ! empty($sphinx->sphinx_connection)) {
$caption['version'] = $sphinx->_get_server_version();
$status = $sphinx->_get_server_status();
}
$body = $this->_show_auto_table($items, ['caption' => $caption, 'hidden_map' => ['trace' => 'query', 'meta' => 'count', 'describe' => 'count', 'results' => 'count']]);
$body .= $sphinx_connect_debug ? $this->_heading('connect') . $this->_pre($this->_var_export($sphinx_connect_debug)) : '';
$body .= $status ? $this->_heading('SHOW STATUS') . $this->_show_key_val_table($status) : '';
return $body;
}
public function _debug_ssh(&$params = [])
public function _debug_ssh(&$params = [])
{
if ( ! $this->SHOW_SSH) {
return '';
}
// Need to enable only when ssh was used
if ( ! isset(main()->modules['ssh'])) {
return '';
}
return $this->_show_key_val_table(_class('ssh')->_debug);
}
public function _debug_stpls(&$params = [])
public function _debug_stpls(&$params = [])
{
if ( ! $this->SHOW_STPLS) {
return '';
}
$data = _class('tpl')->driver->CACHE ?? [];
$debug = _class('tpl')->driver->debug ?? [];
if ($this->SORT_TEMPLATES_BY_NAME && ! empty($data)) {
ksort($data);
}
$stpl_vars = (array) $this->_get_debug_data('STPL_REPLACE_VARS');
$stpl_traces = (array) $this->_get_debug_data('STPL_TRACES');
$total_size = 0;
$total_stpls_exec_time = 0;
$counter = 0;
$items = [];
foreach ((array) $data as $k => $v) {
if (empty($v['calls'])) {
continue;
}
$cur_size = strlen((string) ($v['string'] ?? ''));
$total_size += $cur_size;
$total_stpls_exec_time += (float) ($v['exec_time'] ?? 0);
$items[$counter] = [
'id' => ++$counter,
'name' => $this->_admin_link('edit_stpl', $k, false, ['{LOCATION}' => urlencode((string) ($debug[$k]['storage'] ?? ''))]),
'calls' => (string) ($v['calls']),
'driver' => (string) ($v['driver'] ?? ''),
'compiled' => (int) ($v['is_compiled'] ?? 0),
'storage' => (string) ($debug[$k]['storage'] ?? ''),
'storages' => $debug[$k]['storages'] ?? '',
'size' => (string) $cur_size,
'time' => (float) ($v['exec_time'] ?? 0),
'trace' => $stpl_traces[$k] ?? '',
];
if (isset($stpl_vars[$counter])) {
$items[$counter]['vars'] = $stpl_vars[$counter];
}
}
$items = $this->_time_count_changes($items);
$params['count'] = count($items);
$params['time'] = $total_stpls_exec_time;
return $this->_show_auto_table($items, [
'raw' => ['name'],
'caption' => [
'tpl_driver' => tpl()->DRIVER_NAME,
'compile_mode' => (int) tpl()->COMPILE_TEMPLATES,
'templates_size' => $this->_bytes($total_size),
],
'hidden_map' => [
'trace' => 'name',
'vars' => 'name',
'storages' => 'name',
],
]);
}
public function _debug_table2(&$params = [])
public function _debug_table2(&$params = [])
{
if ( ! $this->SHOW_TABLE2) {
return '';
}
$i = 0;
$items = (array) $this->_get_debug_data('table2');
foreach ($items as $k => $v) {
$items[$k] = ['id' => ++$i] + (array) $v;
}
$items = $this->_time_count_changes($items);
$params['count'] = count($items);
return $this->_show_auto_table($items, ['hidden_map' => ['trace' => 'header_links', 'fields' => 'header_links', 'buttons' => 'header_links']]);
}
public function _debug_url(&$params = [])
public function _debug_url(&$params = [])
{
if ( ! $this->SHOW_REWRITE_INFO) {
return '';
}
$items = (array) $this->_get_debug_data('_url');
foreach ($items as $k => $v) {
$items[$k]['time'] = (float) ($v['time'] ?? 0);
$items[$k]['rewrited_link'] = $this->_admin_link('link', (string) ($v['rewrited_link'] ?? ''));
}
$items = $this->_time_count_changes($items);
$params['count'] = count($items);
return $this->_show_auto_table($items, ['raw' => ['rewrited_link'], 'hidden_map' => ['trace' => 'params']]);
}
public function _do_debug_db_connection_queries($db, $connect_trace = [])
public function _do_debug_db_connection_queries($db, $connect_trace = [])
{
if ( ! $this->SHOW_DB_QUERY_LOG) {
return '';
}
if ( ! is_object($db) || ! is_array($db->_LOG) || ! $db->_tried_to_connect) {
return false;
}
$items = [];
$db_queries_list = $db->_LOG;
$db_explain_results = [];
if ($this->SHOW_DB_EXPLAIN_QUERY && ! empty($db_queries_list) && substr((string) $db->DB_TYPE, 0, 5) == 'mysql') {
$explained = 0;
foreach ((array) $db_queries_list as $id => $log) {
if ( ! empty($log['error']) || $explained >= $this->MAX_EXPLAIN) {
continue;
}
$sql = trim((string) $log['sql']);
// Cut comment
if (substr($sql, 0, 2) == '--') {
$sql = substr($sql, strpos($sql, "\n"));
}
$sql = preg_replace('/[\s]{2,}/ims', ' ', str_replace("\t", ' ', trim($sql)));
if (preg_match('/^[\(]*select/ims', $sql)) {
$db_explain_results[$id] = $db->get_all('EXPLAIN ' . $sql, -1);
$explained++;
}
}
}
$total_queries_exec_time = 0;
$dsn = ($db->DB_SSL ? 'SSL ' : '') . $db->DB_TYPE . '://' . $db->DB_USER . '@' . $db->DB_HOST . ($db->DB_PORT ? ':' . $db->DB_PORT : '')
. '/' . $db->DB_NAME . ($db->DB_CHARSET ? '?charset=' . $db->DB_CHARSET : '') . ($db->DB_SOCKET ? '?socket=' . $db->DB_SOCKET : '');
$body = '<div class="yfd-cap"><strong>' . $this->_e($dsn) . '</strong>';
if ($connect_trace) {
$body .= '<span class="yfd-tgs"><button type="button" class="yfd-tg" data-t="connect" aria-pressed="false">connect trace</button></span>'
. '<div class="yfd-hid" data-h="connect" hidden>' . $this->_format_trace($connect_trace) . '</div>';
}
$body .= '</div>';
$prefix = (string) $db->DB_PREFIX;
foreach ((array) $db_queries_list as $id => $log) {
$sql = trim((string) $log['sql']);
// Cut comment
if (substr($sql, 0, 2) == '--') {
$sql = trim(substr($sql, strpos($sql, "\n")));
if ( ! strlen($sql)) {
continue;
}
}
$total_queries_exec_time += $log['time'];
$admin_link = $this->_admin_link('sql_query', $sql, true);
$sql_html = $this->_e($this->_cap($sql, 5000));
if ($prefix !== '' && $this->ADD_ADMIN_LINKS) {
$sql_html = preg_replace_callback('/(\s`?)(' . preg_quote($this->_e($prefix), '/') . '[a-z0-9_]+)(`?)/ims', function ($m) {
return $m[1] . $this->_admin_link('show_db_table', $m[2]) . $m[3];
}, $sql_html);
}
$exec_time = $this->_ms($log['time']);
if ($admin_link && $this->ADD_ADMIN_LINKS && $admin_link !== $sql) {
$exec_time = '<a href="' . $this->_e($admin_link) . '" rel="nofollow noindex" target="_blank">' . $exec_time . '</a>';
}
$items[] = [
'id' => $id + 1,
'sql' => '<span class="yfd-sql">' . $sql_html . '</span>',
'rows' => (string) ($log['rows'] ?? ''),
'insert_id' => (string) ($log['insert_id'] ?? ''),
'error' => $log['error'] ?? '',
'warning' => $log['warning'] ?? '',
'info' => $log['info'] ?? '',
'exec_time' => $exec_time,
'time' => (float) $log['time'],
'trace' => $log['trace'] ?? '',
'explain' => isset($db_explain_results[$id]) ? $this->_format_db_explain_result($db_explain_results[$id]) : '',
];
}
$items = $this->_time_count_changes($items);
foreach ((array) $items as $k => $v) {
unset($items[$k]['time']);
}
return $body . $this->_show_auto_table($items, [
'raw' => ['sql', 'exec_time', 'explain'],
'tr' => function ($row, $id) {
return $row['error'] ? ' class="error"' : '';
},
'caption' => [
'total_exec_time' => $this->_ms($total_queries_exec_time),
'connect_time' => $this->_ms($db->_connection_time),
],
'hidden_map' => [
'explain' => 'sql',
'trace' => 'sql',
'error' => 'sql',
'warning' => 'sql',
'info' => 'sql',
],
]);
}
public function _dump($var, $depth, &$budget, array &$seen)
$depth int$budget int bytes left$seen array ids of the objects on the current path public function _dump($var, $depth, &$budget, array &$seen)
{
if ($budget <= 0) {
return '...';
}
if (is_array($var) || is_object($var)) {
$id = is_object($var) ? spl_object_id($var) : null;
if ($var instanceof Closure) {
return 'Closure';
}
if ($id !== null && isset($seen[$id])) {
return get_class($var) . ' {*recursion*}';
}
if ($depth >= 6) {
return $id !== null ? get_class($var) . ' {...}' : '[...]';
}
$data = $id !== null ? get_object_vars($var) : $var;
$open = $id !== null ? get_class($var) . ' {' : '[';
$close = $id !== null ? '}' : ']';
if ( ! $data) {
return $open . $close;
}
$id !== null && $seen[$id] = true;
$pad = str_repeat(' ', $depth + 1);
$list = $id === null && array_is_list($data);
$out = $open . "\n";
$i = 0;
foreach ($data as $k => $v) {
if ($budget <= 0 || ++$i > 200) {
$out .= $pad . '... ' . (count($data) - $i + 1) . " more\n";
break;
}
$line = $pad . ($list ? '' : var_export($k, true) . ' => ') . $this->_dump($v, $depth + 1, $budget, $seen) . ",\n";
$budget -= strlen($line);
$out .= $line;
}
if ($id !== null) {
unset($seen[$id]);
}
return $out . str_repeat(' ', $depth) . $close;
}
if (is_string($var)) {
$var = strlen($var) > $budget ? mb_strcut($var, 0, max(0, $budget), 'UTF-8') . '...' : $var;
return var_export($var, true);
}
if (is_resource($var)) {
return 'resource(' . get_resource_type($var) . ')';
}
return var_export($var, true);
}
public function _e($value)
HTML-escaped text; arrays and objects are dumped first.
public function _e($value)
{
if (is_array($value) || is_object($value)) {
$value = $this->_var_export($value);
}
// NUL is in the names of anonymous classes
return htmlspecialchars(str_replace("\0", '\\0', (string) $value), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
public function _format_db_explain_result($explain_result = [])
public function _format_db_explain_result($explain_result = [])
{
if (empty($explain_result) || ! is_array($explain_result)) {
return false;
}
$first = (array) reset($explain_result);
$body = '<table class="yfd-t"><thead><tr>';
foreach ($first as $k => $v) {
$body .= '<th>' . $this->_e($k) . '</th>';
}
$body .= '</tr></thead><tbody>';
foreach ($explain_result as $_data) {
$body .= '<tr>';
foreach ((array) $_data as $v) {
$body .= '<td>' . $this->_e($v ?? '') . '</td>';
}
$body .= '</tr>';
}
return $body . '</tbody></table>';
}
public function _format_trace($trace)
Escaped stack trace (string or frames), paths shortened, at most MAX_TRACE_FRAMES frames.
public function _format_trace($trace)
{
if (is_array($trace)) {
$trace = implode(PHP_EOL, array_map(function ($v) {
return is_scalar($v) ? (string) $v : $this->_var_export($v);
}, $trace));
}
$lines = explode("\n", $this->_short_paths(trim((string) $trace)));
$max = max(1, (int) $this->MAX_TRACE_FRAMES);
$more = max(0, count($lines) - $max);
$lines = array_slice($lines, 0, $max);
if ( ! $this->_lazy) {
$more && $lines[] = '... ' . $more . ' more';
return $this->_pre(implode("\n", $lines), 'yfd-trace');
}
// frames are stored once and numbered again by console.js: the traces of a request share most of their frames
$ids = [];
foreach ($lines as $line) {
$line = preg_replace('~^#\d+\s+~', '', $line);
if ( ! isset($this->_data_ids['f'][$line])) {
$this->_data_ids['f'][$line] = count($this->_data['f']);
$this->_data['f'][] = $line;
}
$ids[] = $this->_data_ids['f'][$line];
}
$more && $ids[] = -$more;
$key = implode(',', $ids);
if ( ! isset($this->_data_ids['t'][$key])) {
$this->_data_ids['t'][$key] = count($this->_data['t']);
$this->_data['t'][] = $ids;
}
return '<pre class="yfd-pre yfd-trace' . (count($ids) > 6 ? ' yfd-clip' : '') . '" data-tr="' . $this->_data_ids['t'][$key] . '"></pre>';
}
public function _format_trace_in_items($items)
public function _format_trace_in_items($items)
{
foreach ((array) $items as $k => $v) {
// raw traces only: _show_auto_table() formats them once, as it always did
if (is_array($v) && isset($v['trace']) && $v['trace'] !== '' && $v['trace'] !== []) {
$items[$k]['trace'] = $this->_format_trace($v['trace']);
}
}
return $items;
}
public function _get_app_version()
public function _get_app_version()
{
return $this->_version_links(APP_PATH, '.app_version');
}
public function _get_debug_data($name)
public function _get_debug_data($name)
{
$this->_used_debug_datas[$name] = ($this->_used_debug_datas[$name] ?? 0) + 1;
$data = debug($name);
$this->backup_debug_data[$name] = $data;
debug($name, false);
return $data;
}
public function _get_git_details($FS_PATH, $as_submodule = false)
public function _get_git_details($FS_PATH, $as_submodule = false)
{
$git_base_path = $FS_PATH . '.git';
if ( ! file_exists($git_base_path)) {
return [];
}
$git_head_path = $git_base_path . '/HEAD';
$git_branch = '';
$git_hash = '';
$git_date = 0;
if ( ! file_exists($git_head_path) && file_exists($git_base_path) && is_file($git_base_path)) {
// gitdir: ../.git/modules/yf
list(, $git_base_path) = explode('gitdir:', file_get_contents($git_base_path)) + [1 => ''];
$git_base_path = trim((string) $git_base_path);
// a worktree names its git dir with an absolute path
$git_base_path = realpath(str_starts_with($git_base_path, '/') ? $git_base_path : $FS_PATH . $git_base_path);
$git_head_path = $git_base_path && file_exists($git_base_path) ? $git_base_path . '/HEAD' : '';
}
if ($git_head_path && file_exists($git_head_path)) {
// ref: refs/heads/master (a detached HEAD holds the hash itself)
$head = trim((string) file_get_contents($git_head_path));
list(, $git_subhead_path) = explode('ref:', $head) + [1 => ''];
$git_subhead_path = trim((string) $git_subhead_path);
$git_branch = basename($git_subhead_path);
$git_hash_file = $git_subhead_path ? $git_base_path . '/' . $git_subhead_path : '';
$git_hash = $git_hash_file && file_exists($git_hash_file) ? trim(file_get_contents($git_hash_file)) : '';
if ( ! $git_hash && $git_subhead_path) {
$git_hash_file = $git_base_path . '/refs/remotes/origin/' . $git_branch;
$git_hash = file_exists($git_hash_file) ? trim(file_get_contents($git_hash_file)) : '';
}
if ( ! $git_hash && ! $git_subhead_path && preg_match('~^[0-9a-f]{40,64}$~', $head)) {
$git_hash = $head;
}
}
$git_log_path = $git_base_path . '/logs/HEAD';
if ($git_hash && file_exists($git_log_path)) {
// ad814593f2844620e37ae220aea8a746d6a53efa 54b490f737cb00c701b3d7439be6d8032d398e96 Some author <[email protected]> 1412937971 +0300 commit: db real mysql tests up
foreach ((array) file($git_log_path) as $line) {
if (strpos($line, $git_hash) === false) {
continue;
}
$line = trim($line);
$hash = trim(substr($line, 41, 40));
if ($hash === $git_hash) {
list($tmp) = explode("\t", $line);
$git_date = (int) substr($tmp, -strlen('1412937971 +0300'), -strlen(' +0300'));
break;
}
}
}
$git_config_path = $git_base_path . '/config';
$url_part = '';
if (file_exists($git_config_path)) {
$config = file_get_contents($git_config_path);
$pattern1 = '/origin"\].+?url\s+=\s+(git@|https:\/\/)(?P<url_part>.+?).git/ims';
$pattern2 = '/submodule\s+"yf"\].+?url\s+=\s+(git@|https:\/\/)(?P<url_part>.+?).git/ims';
if (preg_match($pattern1, $config, $m) || preg_match($pattern2, $config, $m)) {
$url_part = str_replace(':', '/', $m['url_part']);
}
}
return [
'hash' => $git_hash,
'date' => $git_date,
'url' => $url_part !== '' ? 'https://' . $url_part . '/tree/' : '',
'branch' => $git_branch,
];
}
public function _get_request_headers()
public function _get_request_headers()
{
$arh = [];
$rx_http = '/(\AHTTP_)/';
foreach ((array) $_SERVER as $key => $val) {
if ( ! preg_match($rx_http, $key, $m)) {
continue;
}
$arh_key = str_replace($m[1], '', $key);
$rx_matches = explode('_', $arh_key);
if (count((array) $rx_matches) > 0 && strlen($arh_key) > 2) {
foreach ($rx_matches as $ak_key => $ak_val) {
$rx_matches[$ak_key] = ucwords(strtolower($ak_val));
}
$arh_key = implode('-', $rx_matches);
}
$arh[$arh_key] = $val;
}
return $arh;
}
public function _get_yf_version()
public function _get_yf_version()
{
return $this->_version_links(YF_PATH, '.yf_version');
}
public function _heading($text)
public function _heading($text)
{
return '<div class="yfd-h">' . $this->_e($text) . '</div>';
}
public function _init()
Constructor.
public function _init()
{
$this->_NOT_TRANSLATED_FILE = PROJECT_PATH . 'logs/not_translated_' . conf('language') . '.php';
$name = 'debug_console_light';
$this->DEBUG_CONSOLE_LIGHT = (int) ((bool) (isset($_SESSION[$name]) ? ($_SESSION[$name] ?? null) : ($_GET[$name] ?? null)));
$name = 'debug_console_hidden';
$this->DEBUG_CONSOLE_HIDDEN = (int) ((bool) (isset($_SESSION[$name]) ? ($_SESSION[$name] ?? null) : ($_GET[$name] ?? null)));
if ($this->DEBUG_CONSOLE_HIDDEN) {
$this->DEBUG_CONSOLE_LIGHT = true;
}
}
public function _lazy_json()
The lazy data as JSON safe inside <script> (no "<", ">" or "&" left), read by console.js when a value is opened.
public function _lazy_json()
{
$json = json_encode($this->_data, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE);
return $json === false ? '{}' : $json;
}
public function _lazy_pre($text, $class = '')
An empty <pre> console.js fills (textContent) from the lazy data when it is shown; the same text is stored once.
public function _lazy_pre($text, $class = '')
{
$text = $this->_cap($text);
$key = strlen($text) > 64 ? md5($text) : $text;
if ( ! isset($this->_data_ids['b'][$key])) {
$this->_data_ids['b'][$key] = count($this->_data['b']);
$this->_data['b'][] = $text;
}
$clip = substr_count($text, "\n") > 5 || strlen($text) > 600;
return '<pre class="yfd-pre' . ($clip ? ' yfd-clip' : '') . ($class ? ' ' . $class : '') . '" data-b="' . $this->_data_ids['b'][$key] . '"></pre>';
}
public function _ms($seconds)
Milliseconds of a time in seconds.
public function _ms($seconds)
{
$ms = (float) $seconds * 1000;
if ($ms >= 100) {
return round($ms) . ' ms';
}
return number_format($ms, $ms >= 10 ? 1 : 2, '.', '') . ' ms';
}
public function _pre($text, $class = '')
Escaped <pre> of a long or multi-line text; clipped to a few lines until clicked.
public function _pre($text, $class = '')
{
$text = $this->_cap($text);
$clip = substr_count($text, "\n") > 5 || strlen($text) > 600;
return '<pre class="yfd-pre' . ($clip ? ' yfd-clip' : '') . ($class ? ' ' . $class : '') . '">' . $this->_e($text) . '</pre>';
}
public function _render_console(array $panels, array $meta = [], array $summary = [])
$panels array name => html$meta array name => [count, time, bad, hidden]$summary array public function _render_console(array $panels, array $meta = [], array $summary = [])
{
$tabs = [];
$panes = [];
foreach ($panels as $name => $content) {
if (is_array($content)) {
$content = $content['content'] ?? '';
}
$content = trim((string) $content);
if ($content === '') {
continue;
}
$m = (array) ($meta[$name] ?? []);
$id = strtolower(preg_replace('~[^a-z0-9_-]+~i', '', ltrim((string) $name, '_')));
if ($id === '' || isset($panes[$id])) {
$id .= '_' . count($panes);
}
$label = $this->_e($this->TAB_LABELS[$name] ?? ltrim((string) $name, '_'));
if (isset($m['count'])) {
$label .= '<b>' . (int) $m['count'] . '</b>';
}
if (isset($m['time']) && (float) $m['time'] > 0) {
$label .= '<i>' . $this->_ms($m['time']) . '</i>';
}
$tabs[$id] = '<button type="button" role="tab" data-tab="' . $id . '" aria-selected="false"'
. ( ! empty($m['bad']) ? ' class="yfd-bad"' : '') . ( ! empty($m['hidden']) ? ' hidden' : '') . '>' . $label . '</button>';
$panes[$id] = '<section class="yfd-pane" data-pane="' . $id . '" id="debug_item_' . $id . '" role="tabpanel" hidden><template>'
. $content . '</template></section>';
}
$chips = [];
if (($summary['route'] ?? '') !== '') {
$chips[] = '<span class="yfd-chip" title="object::action">' . $this->_e($summary['route']) . '</span>';
}
if (isset($summary['time'])) {
$chips[] = '<span class="yfd-chip" title="Page generation time"><b>' . $this->_ms($summary['time']) . '</b></span>';
}
if (isset($summary['db'])) {
$chips[] = '<span class="yfd-chip" title="Database queries, their time">db <b>' . (int) $summary['db'] . '</b>'
. ( ! empty($summary['db_time']) ? ' ' . $this->_ms($summary['db_time']) : '') . '</span>';
}
if (isset($summary['memory'])) {
$chips[] = '<span class="yfd-chip" title="Peak memory">' . $this->_bytes($summary['memory']) . '</span>';
}
if ( ! empty($summary['errors'])) {
$chips[] = '<span class="yfd-chip yfd-bad" title="PHP warnings, notices, deprecations">php <b>' . (int) $summary['errors'] . '</b></span>';
}
if (isset($summary['console'])) {
$chips[] = '<span class="yfd-chip" title="Debug console render time">D ' . $this->_ms($summary['console']) . '</span>';
}
$bar = '<div class="yfd-bar"><span class="yfd-logo">yf</span>' . implode('', $chips) . '<span class="yfd-sp"></span>'
. '<input class="yfd-q" type="search" placeholder="filter rows" aria-label="Filter the rows of the open tab">'
. '<button type="button" data-act="dock" title="Dock to the bottom of the window">dock</button>'
. '<button type="button" data-act="theme" title="Colours: system, light, dark">theme</button>'
. '<button type="button" data-act="popup" title="Open in a window">popup</button>'
. '<button type="button" data-act="min" title="Collapse / expand">–</button></div>';
return '<yf-debug-console id="debug_console" data-popup="' . (int) (bool) conf('DEBUG_CONSOLE_POPUP') . '">'
. '<template shadowrootmode="open"><style>' . $this->_console_file('console.css') . '</style>'
. '<div class="yfd" role="region" aria-label="yf debug console">' . $bar
. '<nav class="yfd-tabs" role="tablist">' . implode('', $tabs) . '</nav>'
. '<div class="yfd-panes">' . implode('', $panes) . '</div></div>'
. '<script type="application/json" class="yfd-data">' . $this->_lazy_json() . '</script></template></yf-debug-console>'
. '<script>' . $this->_console_file('console.js') . '</script>' . PHP_EOL;
}
public function _short_paths($text)
File paths of the framework and the app shortened to [yf]/ and [app]/.
public function _short_paths($text)
{
static $replace = null;
if ($replace === null) {
$replace = [];
foreach (['[app]/' => defined('APP_PATH') ? APP_PATH : '', '[yf]/' => defined('YF_PATH') ? YF_PATH : ''] as $label => $path) {
strlen((string) $path) > 1 && $replace[(string) $path] = $label;
}
// the longer (more specific) path first: the app may live inside the framework dir
uksort($replace, function ($a, $b) {
return strlen($b) <=> strlen($a);
});
}
return $replace ? str_replace(array_keys($replace), array_values($replace), (string) $text) : (string) $text;
}
public function _show_auto_table($items = [], $params = [], $name = '')
Table of a list of rows. Cells are escaped (arrays dumped, long text clipped) except the columns in `raw` (trusted HTML), all of them with `no_escape`, and the formatted `trace` / `timep`. hidden_map [column => target column]: the column is not shown, a toggle in the target cell opens it.
$params mixed raw, no_escape, hidden_map, tr, caption, header, no_total, no_total_time public function _show_auto_table($items = [], $params = [], $name = '')
{
if ( ! is_array($items) || ! $items) {
return false;
}
$params = (array) $params;
$items = $this->_format_trace_in_items($items);
$no_escape = ! empty($params['no_escape']);
$raw = array_flip(array_merge(['trace', 'timep'], (array) ($params['raw'] ?? [])));
$hidden_map = (array) ($params['hidden_map'] ?? []);
$cols = [];
foreach ($items as $row) {
foreach ((array) $row as $k => $v) {
$cols[$k] = true;
}
}
foreach ($hidden_map as $h => $to) {
unset($cols[$h]);
}
if ( ! $cols) {
return false;
}
$first = array_key_first($cols);
$total_time = 0.0;
$rows = '';
foreach ($items as $id => $row) {
$row = (array) $row;
if (isset($row['time']) && is_numeric($row['time'])) {
$total_time += (float) $row['time'];
}
$tr = isset($params['tr']) && is_callable($params['tr']) ? $this->_tr_attrs($params['tr']($row, $id, $id)) : '';
$cells = '';
foreach ($cols as $col => $_) {
$v = $row[$col] ?? '';
$toggles = '';
$hidden = '';
foreach ($hidden_map as $h => $to) {
$to = isset($cols[$to]) ? $to : $first;
if ($to !== $col || ! isset($row[$h]) || $row[$h] === '' || $row[$h] === [] || $row[$h] === false) {
continue;
}
$toggles .= '<button type="button" class="yfd-tg" data-t="' . $this->_e($h) . '" aria-pressed="false">' . $this->_e($h) . '</button>';
$hidden .= '<div class="yfd-hid" data-h="' . $this->_e($h) . '" hidden>' . $this->_hidden_cell($row[$h], $no_escape || isset($raw[$h])) . '</div>';
}
$is_num = is_int($v) || is_float($v) || (is_string($v) && $v !== '' && strlen($v) < 16 && is_numeric($v));
$class = $is_num || $col === 'time' || $col === 'timep' || $col === 'id' ? ' class="yfd-n"' : '';
if ($class === '' && is_string($v) && $v !== '' && strlen($v) <= 24 && ! isset($raw[$col]) && strpos($v, ' ') === false) {
$class = ' class="yfd-nw"';
}
$cells .= '<td' . $class . '>' . $this->_cell($v, $no_escape || isset($raw[$col]), $col)
. ($toggles !== '' ? '<span class="yfd-tgs">' . $toggles . '</span>' : '') . $hidden . '</td>';
}
$rows .= '<tr' . $tr . '>' . $cells . '</tr>';
}
$head = '';
foreach ($cols as $col => $_) {
$head .= '<th>' . $this->_e($col) . '</th>';
}
$caption = isset($params['header']) && $params['header'] !== '' ? $this->_heading($params['header']) : '';
if (empty($params['no_total'])) {
$chips = is_array($params['caption'] ?? null) ? $params['caption'] : [];
$chips['items'] = count($items);
if ($total_time > 0 && empty($params['no_total_time'])) {
$chips['total_time'] = $this->_ms($total_time);
}
$caption .= '<div class="yfd-cap">';
if (isset($params['caption']) && is_string($params['caption']) && $params['caption'] !== '') {
$caption .= '<strong>' . $this->_e($params['caption']) . '</strong>';
}
foreach ($chips as $k => $v) {
$caption .= '<span>' . $this->_e($k) . ': ' . $this->_e($v) . '</span>';
}
$caption .= '</div>';
}
return $caption . '<table class="yfd-t"><thead><tr>' . $head . '</tr></thead><tbody>' . $rows . '</tbody></table>';
}
public function _show_db_shutdown_queries($db)
public function _show_db_shutdown_queries($db)
{
if ( ! $this->SHOW_DB_QUERY_LOG) {
return '';
}
return $this->_show_key_val_table($db->_SHUTDOWN_QUERIES ?? []);
}
public function _show_db_stats($db)
public function _show_db_stats($db)
{
if ( ! $this->SHOW_DB_STATS) {
return '';
}
$data['stats'] = $db->get_2d('SHOW SESSION STATUS');
$data['vars'] = $db->get_2d('SHOW VARIABLES');
$body = '';
foreach ($data as $name => $_data) {
$body .= '<div>' . $this->_heading($name) . $this->_show_key_val_table($_data, ['no_total' => 1, 'skip_empty_values' => 1]) . '</div>';
}
return '<div class="yfd-grid">' . $body . '</div>';
}
public function _show_key_val_table($a, $params = [], $name = '')
Two-column table of $a. Keys and values are escaped (arrays dumped) unless no_escape; raw_keys: keys whose value is HTML.
$params mixed no_sort, no_escape, raw_keys, skip_empty_values, header public function _show_key_val_table($a, $params = [], $name = '')
{
if ( ! $a) {
return false;
}
$params = (array) $params;
if (is_array($a) && empty($params['no_sort'])) {
ksort($a);
}
$no_escape = ! empty($params['no_escape']);
$raw_keys = array_flip((array) ($params['raw_keys'] ?? []));
$rows = '';
foreach ((array) $a as $k => $v) {
if ( ! empty($params['skip_empty_values']) && ! $v) {
continue;
}
$rows .= '<tr><td class="yfd-k">' . ($no_escape ? $k : $this->_e($k)) . '</td><td>'
. $this->_cell(is_int($v) || is_float($v) ? (string) $v : $v, $no_escape || isset($raw_keys[$k])) . '</td></tr>';
}
if ($rows === '') {
return false;
}
return (isset($params['header']) ? $this->_heading($params['header']) : '') . '<table class="yfd-t yfd-kv"><tbody>' . $rows . '</tbody></table>';
}
public function _summary($exec_time)
Figures of the bar: page time, db queries, memory, PHP messages.
public function _summary($exec_time)
{
$db_time = 0.0;
$db_count = 0;
foreach ((array) debug('db_instances') as $db) {
if (is_object($db) && is_array($db->_LOG ?? null)) {
$db_count += count($db->_LOG);
foreach ($db->_LOG as $log) {
$db_time += (float) ($log['time'] ?? 0);
}
}
}
if ( ! $db_count && is_object(db())) {
$db_count = (int) db()->NUM_QUERIES;
}
return [
'time' => $exec_time,
'db' => $db_count,
'db_time' => $db_time,
'memory' => memory_get_peak_usage(),
'errors' => count((array) debug('php_errors')),
'route' => ($_GET['object'] ?? '') . (($_GET['action'] ?? '') !== '' ? '::' . $_GET['action'] : ''),
];
}
public function _tabs($items, $params = [])
Sub tabs inside a pane (connections, asset lists); empty items are skipped.
$items array name => html$params array show_all: every item under its heading, no tabs; no_single_heading: a lone item without its heading public function _tabs($items, $params = [])
{
$items = array_filter((array) $items, function ($v) {
return trim((string) $v) !== '';
});
if ( ! $items) {
return '';
}
if (count($items) === 1 || ! empty($params['show_all'])) {
$out = '';
foreach ($items as $name => $html) {
$out .= (empty($params['no_single_heading']) || count($items) > 1 ? $this->_heading($name) : '') . $html;
}
return $out;
}
$buttons = '';
$panes = '';
$i = 0;
foreach ($items as $name => $html) {
$buttons .= '<button type="button" data-sub="' . $i . '" aria-selected="false">' . $this->_e($name) . '</button>';
$panes .= '<div data-subpane="' . $i . '"' . ($i ? ' hidden' : '') . '>' . $html . '</div>';
$i++;
}
return '<div class="yfd-sub"><div class="yfd-subtabs">' . $buttons . '</div><div class="yfd-subpanes">' . $panes . '</div></div>';
}
public function _time_count_changes($items = [], $field = 'time')
Adds `timep`: the share of each row in the summed $field, highlighted when it stands out.
public function _time_count_changes($items = [], $field = 'time')
{
$time_all = 0;
$time_max = 0;
foreach ((array) $items as $i => $v) {
$time = (float) ($v[$field] ?? 0);
$time_all += $time;
if ($time > $time_max) {
$time_max = $time;
}
}
if ( ! $time_all) {
return $items;
}
$warn_limit = $time_max / $time_all * 100 / 2;
if ($warn_limit < 20) {
$warn_limit = 20;
}
foreach ((array) $items as $i => $v) {
$time = (float) ($v[$field] ?? 0);
$timep = round($time / $time_all * 100, 1);
$items[$i]['timep'] = $timep ? '<span class="yfd-pct' . ($timep > $warn_limit ? ' yfd-warn' : '') . '" style="--p:' . min(100, $timep) . '%">' . $timep . '%</span>' : '';
}
return $items;
}
public function _tr_attrs($attrs)
Row attributes from a `tr` callback: ' class="error"' (the old Bootstrap names) or ['class' => ...].
public function _tr_attrs($attrs)
{
$class = is_array($attrs) ? (string) ($attrs['class'] ?? '') : (preg_match('~class=["\']?([\w\s-]+)~', (string) $attrs, $m) ? $m[1] : '');
$map = ['error' => 'yfd-err', 'danger' => 'yfd-err', 'warning' => 'yfd-err', 'active' => 'yfd-tot', 'info' => 'yfd-tot'];
$out = [];
foreach (preg_split('~\s+~', trim($class)) as $c) {
isset($map[$c]) && $out[] = $map[$c];
}
return $out ? ' class="' . implode(' ', array_unique($out)) . '"' : '';
}
public function _var_export($var)
PHP-like dump of a value for the console: short arrays, objects by their properties, depth and size limited, no warnings on recursion (var_export() has them) and no changed parentheses (the global _var_export() turns ")" into "]").
public function _var_export($var)
{
$budget = max(1000, (int) $this->MAX_VALUE_LEN);
$seen = [];
return $this->_dump($var, 0, $budget, $seen);
}
public function _version_links($path, $version_file)
Version file, git hash, branch and commit date of a checkout (HTML), linked to the remote when it is known.
public function _version_links($path, $version_file)
{
$out = [];
$git = $this->_get_git_details($path) + ['hash' => '', 'date' => 0, 'url' => '', 'branch' => ''];
$file = $path . $version_file;
$version = file_exists($file) ? trim((string) file_get_contents($file)) : '';
$link = function ($ref, $text) use ($git) {
if ($git['url'] === '') {
return $this->_e($text);
}
return '<a href="' . $this->_e($git['url'] . rawurlencode($ref)) . '" rel="nofollow noopener" target="_blank">' . $this->_e($text) . '</a>';
};
$version !== '' && $out[] = $link($version, $version);
$git['hash'] && $out[] = $link($git['hash'], substr($git['hash'], 0, 8));
$git['branch'] && $out[] = $link($git['branch'], $git['branch']);
$git['date'] && $out[] = $this->_e(date('Y-m-d H:i', (int) $git['date']));
return implode(' | ', $out);
}