init() gets called. $this->enabled = true; $debugbar = new DebugBar(); $debugbar->addCollector(new PhpInfoCollector()); $debugbar->addCollector(new MessagesCollector()); $debugbar->addCollector(new RequestDataCollector()); $debugbar->addCollector(new TimeDataCollector($_SERVER['REQUEST_TIME_FLOAT'] ?? GRAV_REQUEST_TIME)); $debugbar['time']->addMeasure('Server', $debugbar['time']->getRequestStartTime(), GRAV_REQUEST_TIME); $debugbar['time']->addMeasure('Loading', GRAV_REQUEST_TIME, $currentTime); $debugbar['time']->addMeasure('Debugger', $currentTime, microtime(true)); $this->debugbar = $debugbar; // Set deprecation collector. $this->setErrorHandler(); } /** * Initialize the debugger * * @return $this * @throws \DebugBar\DebugBarException */ public function init() { if ($this->initialized) { return $this; } $this->grav = Grav::instance(); $this->config = $this->grav['config']; // Enable/disable debugger based on configuration. $this->enabled = (bool)$this->config->get('system.debugger.enabled'); if ($this->enabled()) { $this->initialized = true; $plugins_config = (array)$this->config->get('plugins'); ksort($plugins_config); $debugbar = $this->debugbar; $debugbar->addCollector(new MemoryCollector()); $debugbar->addCollector(new ExceptionsCollector()); $debugbar->addCollector(new ConfigCollector((array)$this->config->get('system'), 'Config')); $debugbar->addCollector(new ConfigCollector($plugins_config, 'Plugins')); $this->addMessage('Grav v' . GRAV_VERSION); } return $this; } /** * Set/get the enabled state of the debugger * * @param bool $state If null, the method returns the enabled value. If set, the method sets the enabled state * * @return bool */ public function enabled($state = null) { if ($state !== null) { $this->enabled = (bool)$state; } return $this->enabled; } /** * Add the debugger assets to the Grav Assets * * @return $this */ public function addAssets() { if ($this->enabled()) { // Only add assets if Page is HTML $page = $this->grav['page']; if ($page->templateFormat() !== 'html') { return $this; } /** @var Assets $assets */ $assets = $this->grav['assets']; // Add jquery library $assets->add('jquery', 101); $this->renderer = $this->debugbar->getJavascriptRenderer(); $this->renderer->setIncludeVendors(false); // Get the required CSS files list($css_files, $js_files) = $this->renderer->getAssets(null, JavascriptRenderer::RELATIVE_URL); foreach ((array)$css_files as $css) { $assets->addCss($css); } $assets->addCss('/system/assets/debugger.css'); foreach ((array)$js_files as $js) { $assets->addJs($js); } } return $this; } public function getCaller($limit = 2) { $trace = debug_backtrace(false, $limit); return array_pop($trace); } /** * Adds a data collector * * @param DataCollectorInterface $collector * * @return $this * @throws \DebugBar\DebugBarException */ public function addCollector($collector) { $this->debugbar->addCollector($collector); return $this; } /** * Returns a data collector * * @param DataCollectorInterface $collector * * @return DataCollectorInterface * @throws \DebugBar\DebugBarException */ public function getCollector($collector) { return $this->debugbar->getCollector($collector); } /** * Displays the debug bar * * @return $this */ public function render() { if ($this->enabled()) { // Only add assets if Page is HTML $page = $this->grav['page']; if (!$this->renderer || $page->templateFormat() !== 'html') { return $this; } $this->addDeprecations(); echo $this->renderer->render(); } return $this; } /** * Sends the data through the HTTP headers * * @return $this */ public function sendDataInHeaders() { if ($this->enabled()) { $this->addDeprecations(); $this->debugbar->sendDataInHeaders(); } return $this; } /** * Returns collected debugger data. * * @return array */ public function getData() { if (!$this->enabled()) { return null; } $this->addDeprecations(); $this->timers = []; return $this->debugbar->getData(); } /** * Start a timer with an associated name and description * * @param string $name * @param string|null $description * * @return $this */ public function startTimer($name, $description = null) { if (strpos($name, '_') === 0 || $this->enabled()) { $this->debugbar['time']->startMeasure($name, $description); $this->timers[] = $name; } return $this; } /** * Stop the named timer * * @param string $name * * @return $this */ public function stopTimer($name) { if (\in_array($name, $this->timers, true) && (strpos($name, '_') === 0 || $this->enabled())) { $this->debugbar['time']->stopMeasure($name); } return $this; } /** * Dump variables into the Messages tab of the Debug Bar * * @param mixed $message * @param string $label * @param bool $isString * * @return $this */ public function addMessage($message, $label = 'info', $isString = true) { if ($this->enabled()) { $this->debugbar['messages']->addMessage($message, $label, $isString); } return $this; } /** * Dump exception into the Messages tab of the Debug Bar * * @param \Exception $e * @return Debugger */ public function addException(\Exception $e) { if ($this->initialized && $this->enabled()) { $this->debugbar['exceptions']->addException($e); } return $this; } public function setErrorHandler() { $this->errorHandler = set_error_handler( [$this, 'deprecatedErrorHandler'] ); } /** * @param int $errno * @param string $errstr * @param string $errfile * @param int $errline * @return bool */ public function deprecatedErrorHandler($errno, $errstr, $errfile, $errline) { if ($errno !== E_USER_DEPRECATED && $errno !== E_DEPRECATED) { if ($this->errorHandler) { return \call_user_func($this->errorHandler, $errno, $errstr, $errfile, $errline); } return true; } if (!$this->enabled()) { return true; } // Figure out error scope from the error. $scope = 'unknown'; if (stripos($errstr, 'grav') !== false) { $scope = 'grav'; } elseif (strpos($errfile, '/twig/') !== false) { $scope = 'twig'; } elseif (stripos($errfile, '/yaml/') !== false) { $scope = 'yaml'; } elseif (strpos($errfile, '/vendor/') !== false) { $scope = 'vendor'; } // Clean up backtrace to make it more useful. $backtrace = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT); // Skip current call. array_shift($backtrace); // Find yaml file where the error happened. if ($scope === 'yaml') { foreach ($backtrace as $current) { if (isset($current['args'])) { foreach ($current['args'] as $arg) { if ($arg instanceof \SplFileInfo) { $arg = $arg->getPathname(); } if (\is_string($arg) && preg_match('/.+\.(yaml|md)$/i', $arg)) { $errfile = $arg; $errline = 0; break 2; } } } } } // Filter arguments. $cut = 0; $previous = null; foreach ($backtrace as $i => &$current) { if (isset($current['args'])) { $args = []; foreach ($current['args'] as $arg) { if (\is_string($arg)) { $arg = "'" . $arg . "'"; if (mb_strlen($arg) > 100) { $arg = 'string'; } } elseif (\is_bool($arg)) { $arg = $arg ? 'true' : 'false'; } elseif (\is_scalar($arg)) { $arg = $arg; } elseif (\is_object($arg)) { $arg = get_class($arg) . ' $object'; } elseif (\is_array($arg)) { $arg = '$array'; } else { $arg = '$object'; } $args[] = $arg; } $current['args'] = $args; } $object = $current['object'] ?? null; unset($current['object']); $reflection = null; if ($object instanceof TemplateWrapper) { $reflection = new \ReflectionObject($object); $property = $reflection->getProperty('template'); $property->setAccessible(true); $object = $property->getValue($object); } if ($object instanceof Template) { $file = $current['file'] ?? null; if (preg_match('`(Template.php|TemplateWrapper.php)$`', $file)) { $current = null; continue; } $debugInfo = $object->getDebugInfo(); $line = 1; if (!$reflection) { foreach ($debugInfo as $codeLine => $templateLine) { if ($codeLine <= $current['line']) { $line = $templateLine; break; } } } $src = $object->getSourceContext(); //$code = preg_split('/\r\n|\r|\n/', $src->getCode()); //$current['twig']['twig'] = trim($code[$line - 1]); $current['twig']['file'] = $src->getPath(); $current['twig']['line'] = $line; $prevFile = $previous['file'] ?? null; if ($prevFile && $file === $prevFile) { $prevLine = $previous['line']; $line = 1; foreach ($debugInfo as $codeLine => $templateLine) { if ($codeLine <= $prevLine) { $line = $templateLine; break; } } //$previous['twig']['twig'] = trim($code[$line - 1]); $previous['twig']['file'] = $src->getPath(); $previous['twig']['line'] = $line; } $cut = $i; } elseif ($object instanceof ProcessorInterface) { $cut = $cut ?: $i; break; } $previous = &$backtrace[$i]; } unset($current); if ($cut) { $backtrace = array_slice($backtrace, 0, $cut + 1); } $backtrace = array_values(array_filter($backtrace)); // Skip vendor libraries and the method where error was triggered. foreach ($backtrace as $i => $current) { if (!isset($current['file'])) { continue; } if (strpos($current['file'], '/vendor/') !== false) { $cut = $i + 1; continue; } if (isset($current['function']) && ($current['function'] === 'user_error' || $current['function'] === 'trigger_error')) { $cut = $i + 1; continue; } break; } if ($cut) { $backtrace = array_slice($backtrace, $cut); } $backtrace = array_values(array_filter($backtrace)); $current = reset($backtrace); // If the issue happened inside twig file, change the file and line to match that file. $file = $current['twig']['file'] ?? ''; if ($file) { $errfile = $file; $errline = $current['twig']['line'] ?? 0; } $deprecation = [ 'scope' => $scope, 'message' => $errstr, 'file' => $errfile, 'line' => $errline, 'trace' => $backtrace, 'count' => 1 ]; $this->deprecations[] = $deprecation; // Do not pass forward. return true; } protected function addDeprecations() { if (!$this->deprecations) { return; } $collector = new MessagesCollector('deprecated'); $this->addCollector($collector); $collector->addMessage('Your site is using following deprecated features:'); /** @var array $deprecated */ foreach ($this->deprecations as $deprecated) { list($message, $scope) = $this->getDepracatedMessage($deprecated); $collector->addMessage($message, $scope); } } protected function getDepracatedMessage($deprecated) { $scope = $deprecated['scope']; $trace = []; if (isset($deprecated['trace'])) { foreach ($deprecated['trace'] as $current) { $class = $current['class'] ?? ''; $type = $current['type'] ?? ''; $function = $this->getFunction($current); if (isset($current['file'])) { $current['file'] = str_replace(GRAV_ROOT . '/', '', $current['file']); } unset($current['class'], $current['type'], $current['function'], $current['args']); if (isset($current['twig'])) { $trace[] = $current['twig']; } else { $trace[] = ['call' => $class . $type . $function] + $current; } } } $array = [ 'message' => $deprecated['message'], 'file' => $deprecated['file'], 'line' => $deprecated['line'], 'trace' => $trace ]; return [ array_filter($array), $scope ]; } protected function getFunction($trace) { if (!isset($trace['function'])) { return ''; } return $trace['function'] . '(' . implode(', ', $trace['args'] ?? []) . ')'; } }