* @license GNU General Public License version 2 or later; see LICENSE.txt */ namespace Joomla\Plugin\System\Debug\DataCollector; use DebugBar\DataCollector\AssetProvider; use Joomla\CMS\Uri\Uri; use Joomla\Database\Monitor\DebugMonitor; use Joomla\Plugin\System\Debug\AbstractDataCollector; use Joomla\Registry\Registry; // phpcs:disable PSR1.Files.SideEffects \defined('_JEXEC') or die; // phpcs:enable PSR1.Files.SideEffects /** * QueryDataCollector * * @since 4.0.0 */ class QueryCollector extends AbstractDataCollector implements AssetProvider { /** * Collector name. * * @var string * @since 4.0.0 */ private $name = 'queries'; /** * The query monitor. * * @var DebugMonitor * @since 4.0.0 */ private $queryMonitor; /** * Profile data. * * @var array * @since 4.0.0 */ private $profiles; /** * Explain data. * * @var array * @since 4.0.0 */ private $explains; /** * Accumulated Duration. * * @var integer * @since 4.0.0 */ private $accumulatedDuration = 0; /** * Accumulated Memory. * * @var integer * @since 4.0.0 */ private $accumulatedMemory = 0; /** * Constructor. * * @param Registry $params Parameters. * @param DebugMonitor $queryMonitor Query monitor. * @param array $profiles Profile data. * @param array $explains Explain data * * @since 4.0.0 */ public function __construct(Registry $params, DebugMonitor $queryMonitor, array $profiles, array $explains) { $this->queryMonitor = $queryMonitor; parent::__construct($params); $this->profiles = $profiles; $this->explains = $explains; } /** * Called by the DebugBar when data needs to be collected * * @since 4.0.0 * * @return array Collected data */ public function collect(): array { $statements = $this->getStatements(); return [ 'data' => [ 'statements' => $statements, 'nb_statements' => \count($statements), 'accumulated_duration_str' => $this->getDataFormatter()->formatDuration($this->accumulatedDuration), 'memory_usage_str' => $this->getDataFormatter()->formatBytes($this->accumulatedMemory), 'xdebug_link' => $this->getXdebugLinkTemplate(), 'root_path' => JPATH_ROOT, ], 'count' => \count($this->queryMonitor->getLogs()), ]; } /** * Returns the unique name of the collector * * @since 4.0.0 * * @return string */ public function getName(): string { return $this->name; } /** * Returns a hash where keys are control names and their values * an array of options as defined in {@see \DebugBar\JavascriptRenderer::addControl()} * * @since 4.0.0 * * @return array */ public function getWidgets(): array { return [ 'queries' => [ 'icon' => 'database', 'widget' => 'PhpDebugBar.Widgets.SQLQueriesWidget', 'map' => $this->name . '.data', 'default' => '[]', ], 'queries:badge' => [ 'map' => $this->name . '.count', 'default' => 'null', ], ]; } /** * Assets for the collector. * * @since 4.0.0 * * @return array */ public function getAssets(): array { return [ 'css' => Uri::root(true) . '/media/plg_system_debug/widgets/sqlqueries/widget.min.css', 'js' => Uri::root(true) . '/media/plg_system_debug/widgets/sqlqueries/widget.min.js', ]; } /** * Prepare the executed statements data. * * @since 4.0.0 * * @return array */ private function getStatements(): array { $statements = []; $logs = $this->queryMonitor->getLogs(); $boundParams = $this->queryMonitor->getBoundParams(); $timings = $this->queryMonitor->getTimings(); $memoryLogs = $this->queryMonitor->getMemoryLogs(); $stacks = $this->queryMonitor->getCallStacks(); $collectStacks = $this->params->get('query_traces'); foreach ($logs as $id => $item) { $queryTime = 0; $queryMemory = 0; if ($timings && isset($timings[$id * 2 + 1])) { // Compute the query time. $queryTime = ($timings[$id * 2 + 1] - $timings[$id * 2]); $this->accumulatedDuration += $queryTime; } if ($memoryLogs && isset($memoryLogs[$id * 2 + 1])) { // Compute the query memory usage. $queryMemory = ($memoryLogs[$id * 2 + 1] - $memoryLogs[$id * 2]); $this->accumulatedMemory += $queryMemory; } $trace = []; $callerLocation = ''; if (isset($stacks[$id])) { $cnt = 0; foreach ($stacks[$id] as $i => $stack) { $class = $stack['class'] ?? ''; $file = $stack['file'] ?? ''; $line = $stack['line'] ?? ''; $caller = $this->formatCallerInfo($stack); $location = $file && $line ? "$file:$line" : 'same'; $isCaller = 0; if (\Joomla\Database\DatabaseDriver::class === $class && false === strpos($file, 'DatabaseDriver.php')) { $callerLocation = $location; $isCaller = 1; } if ($collectStacks) { $trace[] = [\count($stacks[$id]) - $cnt, $isCaller, $caller, $file, $line]; } $cnt++; } } $explain = $this->explains[$id] ?? []; $explainColumns = []; // Extract column labels for Explain table if ($explain) { $explainColumns = array_keys(reset($explain)); } $statements[] = [ 'sql' => $item, 'params' => $boundParams[$id] ?? [], 'duration_str' => $this->getDataFormatter()->formatDuration($queryTime), 'memory_str' => $this->getDataFormatter()->formatBytes($queryMemory), 'caller' => $callerLocation, 'callstack' => $trace, 'explain' => $explain, 'explain_col' => $explainColumns, 'profile' => $this->profiles[$id] ?? [], ]; } return $statements; } }