| 1 | <?php |
||
| 2 | |||
| 3 | namespace LeKoala\DebugBar\Collector; |
||
| 4 | |||
| 5 | use LeKoala\DebugBar\DebugBar; |
||
| 6 | use SilverStripe\Control\Director; |
||
| 7 | use DebugBar\DataCollector\Renderable; |
||
| 8 | use DebugBar\DataCollector\AssetProvider; |
||
| 9 | use DebugBar\DataCollector\DataCollector; |
||
| 10 | use LeKoala\DebugBar\Extension\ProxyDBExtension; |
||
| 11 | |||
| 12 | /** |
||
| 13 | * Collects data about SQL statements executed through the proxied behaviour |
||
| 14 | */ |
||
| 15 | class DatabaseCollector extends DataCollector implements Renderable, AssetProvider |
||
| 16 | { |
||
| 17 | /** |
||
| 18 | * @var TimeDataCollector |
||
| 19 | */ |
||
| 20 | protected $timeCollector; |
||
| 21 | |||
| 22 | /** |
||
| 23 | * @return array |
||
| 24 | */ |
||
| 25 | public function collect() |
||
| 26 | { |
||
| 27 | $this->timeCollector = DebugBar::getDebugBar()->getCollector('time'); |
||
| 28 | |||
| 29 | $data = $this->collectData($this->timeCollector); |
||
| 30 | |||
| 31 | // Check for excessive number of queries |
||
| 32 | $dbQueryWarningLevel = DebugBar::config()->get('warn_query_limit'); |
||
| 33 | if ($dbQueryWarningLevel && $data['nb_statements'] > $dbQueryWarningLevel) { |
||
| 34 | $helpLink = DebugBar::config()->get('performance_guide_link'); |
||
| 35 | $messages = DebugBar::getMessageCollector(); |
||
| 36 | if ($messages) { |
||
| 37 | $messages->addMessage( |
||
| 38 | "This page ran more than $dbQueryWarningLevel database queries." . |
||
| 39 | "\nYou could reduce this by implementing caching." . |
||
| 40 | "\nYou can find more info here: $helpLink", |
||
| 41 | 'warning', |
||
| 42 | true |
||
| 43 | ); |
||
| 44 | } |
||
| 45 | } |
||
| 46 | |||
| 47 | if (isset($data['show_db']) && $data['show_db']) { |
||
| 48 | $showDbWarning = DebugBar::config()->get('show_db_warning'); |
||
| 49 | $messages = DebugBar::getMessageCollector(); |
||
| 50 | if ($messages && $showDbWarning) { |
||
| 51 | $messages->addMessage( |
||
| 52 | "Consider enabling `optimistic_connect` to avoid an extra query. You can turn this warning off by setting show_db_warning = false", |
||
| 53 | 'warning', |
||
| 54 | true |
||
| 55 | ); |
||
| 56 | } |
||
| 57 | } |
||
| 58 | |||
| 59 | if (isset($data['has_unclosed_transaction']) && $data['has_unclosed_transaction']) { |
||
| 60 | $messages = DebugBar::getMessageCollector(); |
||
| 61 | if ($messages) { |
||
| 62 | $messages->addMessage( |
||
| 63 | "There is an unclosed transaction on your page and some statement may not be persisted to the database", |
||
| 64 | 'warning', |
||
| 65 | true |
||
| 66 | ); |
||
| 67 | } |
||
| 68 | } |
||
| 69 | |||
| 70 | return $data; |
||
| 71 | } |
||
| 72 | |||
| 73 | /** |
||
| 74 | * Explode comma separated elements not within parenthesis or quotes |
||
| 75 | * |
||
| 76 | * @param string $str |
||
| 77 | * @return array |
||
| 78 | */ |
||
| 79 | protected static function explodeFields($str) |
||
| 80 | { |
||
| 81 | return preg_split("/(?![^(]*\)),/", $str); |
||
| 82 | } |
||
| 83 | |||
| 84 | /** |
||
| 85 | * Collects data |
||
| 86 | * |
||
| 87 | * @param TimeDataCollector $timeCollector |
||
| 88 | * @return array |
||
| 89 | */ |
||
| 90 | protected function collectData(?TimeDataCollector $timeCollector = null) |
||
| 91 | { |
||
| 92 | $stmts = []; |
||
| 93 | |||
| 94 | $total_duration = 0; |
||
| 95 | $total_mem = 0; |
||
| 96 | |||
| 97 | $failed = 0; |
||
| 98 | |||
| 99 | $i = 0; |
||
| 100 | |||
| 101 | // Get queries gathered by proxy |
||
| 102 | $queries = ProxyDBExtension::getQueries(); |
||
| 103 | |||
| 104 | $limit = DebugBar::config()->get('query_limit'); |
||
| 105 | $warnDurationThreshold = DebugBar::config()->get('warn_dbqueries_threshold_seconds'); |
||
| 106 | |||
| 107 | // only show db if there is more than one database in use |
||
| 108 | $showDb = count(array_filter(array_unique(array_map(function ($stmt) { |
||
|
0 ignored issues
–
show
Unused Code
introduced
by
Loading history...
|
|||
| 109 | return $stmt['database']; |
||
| 110 | }, $queries)))) > 1; |
||
| 111 | |||
| 112 | $showDb = false; |
||
| 113 | $hasUnclosedTransaction = false; |
||
| 114 | foreach ($queries as $stmt) { |
||
| 115 | $i++; |
||
| 116 | |||
| 117 | $total_duration += $stmt['duration']; |
||
| 118 | $total_mem += $stmt['memory']; |
||
| 119 | |||
| 120 | if (!$stmt['success']) { |
||
| 121 | $failed++; |
||
| 122 | } |
||
| 123 | |||
| 124 | if (str_starts_with((string) $stmt['short_query'], 'SHOW DATABASES LIKE')) { |
||
| 125 | $showDb = true; |
||
| 126 | } |
||
| 127 | |||
| 128 | if (str_contains((string) $stmt['short_query'], 'START TRANSACTION')) { |
||
| 129 | $hasUnclosedTransaction = true; |
||
| 130 | } |
||
| 131 | if (str_contains((string) $stmt['short_query'], 'END TRANSACTION')) { |
||
| 132 | $hasUnclosedTransaction = false; |
||
| 133 | } |
||
| 134 | |||
| 135 | if ($limit && $i > $limit) { |
||
| 136 | $stmts[] = [ |
||
| 137 | 'sql' => "Only the first $limit queries are shown" |
||
| 138 | ]; |
||
| 139 | break; |
||
| 140 | } |
||
| 141 | |||
| 142 | $stmts[] = [ |
||
| 143 | 'sql' => $stmt['short_query'], |
||
| 144 | 'row_count' => $stmt['rows'], |
||
| 145 | 'params' => $stmt['select'] ? $stmt['select'] : null, |
||
| 146 | 'duration' => $stmt['duration'], |
||
| 147 | 'duration_str' => $this->getDataFormatter()->formatDuration($stmt['duration']), |
||
| 148 | 'memory' => $stmt['memory'], |
||
| 149 | 'memory_str' => $this->getDataFormatter()->formatBytes($stmt['memory']), |
||
| 150 | 'is_success' => $stmt['success'], |
||
| 151 | 'database' => $showDb ? $stmt['database'] : null, |
||
| 152 | 'source' => $stmt['source'], |
||
| 153 | 'warn' => $stmt['duration'] > $warnDurationThreshold |
||
| 154 | ]; |
||
| 155 | |||
| 156 | if ($timeCollector !== null) { |
||
| 157 | $timeCollector->addMeasure( |
||
| 158 | $stmt['short_query'], |
||
| 159 | $stmt['start_time'], |
||
| 160 | $stmt['end_time'] |
||
| 161 | ); |
||
| 162 | } |
||
| 163 | } |
||
| 164 | |||
| 165 | // Save as CSV |
||
| 166 | $db_save_csv = DebugBar::config()->get('db_save_csv'); |
||
| 167 | if ($db_save_csv && !empty($queries)) { |
||
| 168 | $filename = date('Ymd_His') . '_' . count($queries) . '_' . uniqid() . '.csv'; |
||
| 169 | $isOutput = false; |
||
| 170 | if (isset($_REQUEST['downloadqueries']) && Director::isDev()) { |
||
| 171 | $isOutput = true; |
||
| 172 | if (headers_sent()) { |
||
| 173 | die('Cannot download queries, headers are already sent'); |
||
|
0 ignored issues
–
show
|
|||
| 174 | } |
||
| 175 | header('Content-Type: text/csv'); |
||
| 176 | header('Content-Disposition: attachment;filename=' . $filename); |
||
| 177 | $fp = fopen('php://output', 'w'); |
||
| 178 | } else { |
||
| 179 | $tempFolder = \TEMP_PATH . '/debugbar/db'; |
||
| 180 | if (!is_dir($tempFolder)) { |
||
| 181 | mkdir($tempFolder, 0755, true); |
||
| 182 | } |
||
| 183 | $fp = fopen($tempFolder . '/' . $filename, 'w'); |
||
| 184 | } |
||
| 185 | $headers = array_keys($queries[0]); |
||
| 186 | fputcsv($fp, $headers); |
||
| 187 | foreach ($queries as $query) { |
||
| 188 | fputcsv($fp, $query); |
||
| 189 | } |
||
| 190 | fclose($fp); |
||
| 191 | |||
| 192 | if ($isOutput) { |
||
| 193 | die(); |
||
|
0 ignored issues
–
show
|
|||
| 194 | } |
||
| 195 | } |
||
| 196 | |||
| 197 | return [ |
||
| 198 | 'nb_statements' => count($queries), |
||
| 199 | 'nb_failed_statements' => $failed, |
||
| 200 | 'show_db' => $showDb, |
||
| 201 | 'has_unclosed_transaction' => $hasUnclosedTransaction, |
||
| 202 | 'statements' => $stmts, |
||
| 203 | 'accumulated_duration' => $total_duration, |
||
| 204 | 'accumulated_duration_str' => $this->getDataFormatter()->formatDuration($total_duration), |
||
| 205 | 'memory_usage' => $total_mem, |
||
| 206 | 'memory_usage_str' => $this->getDataFormatter()->formatBytes($total_mem), |
||
| 207 | ]; |
||
| 208 | } |
||
| 209 | |||
| 210 | /** |
||
| 211 | * @return string |
||
| 212 | */ |
||
| 213 | public function getName() |
||
| 214 | { |
||
| 215 | return 'db'; |
||
| 216 | } |
||
| 217 | |||
| 218 | /** |
||
| 219 | * @return array |
||
| 220 | */ |
||
| 221 | public function getWidgets() |
||
| 222 | { |
||
| 223 | return [ |
||
| 224 | "database" => [ |
||
| 225 | "icon" => "database", |
||
| 226 | "widget" => "PhpDebugBar.Widgets.SQLQueriesWidget", |
||
| 227 | "map" => "db", |
||
| 228 | "default" => "[]" |
||
| 229 | ], |
||
| 230 | "database:badge" => [ |
||
| 231 | "map" => "db.nb_statements", |
||
| 232 | "default" => 0 |
||
| 233 | ] |
||
| 234 | ]; |
||
| 235 | } |
||
| 236 | |||
| 237 | /** |
||
| 238 | * @return array |
||
| 239 | */ |
||
| 240 | public function getAssets() |
||
| 241 | { |
||
| 242 | return [ |
||
| 243 | 'base_path' => '/' . DebugBar::moduleResource('javascript')->getRelativePath(), |
||
| 244 | 'base_url' => Director::makeRelative(DebugBar::moduleResource('javascript')->getURL()), |
||
| 245 | 'css' => 'sqlqueries/widget.css', |
||
| 246 | 'js' => 'sqlqueries/widget.js' |
||
| 247 | ]; |
||
| 248 | } |
||
| 249 | } |
||
| 250 |