YiiDebugToolbarPanelSql.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452
  1. <?php
  2. /**
  3. * YiiDebugToolbarPanelSql class file.
  4. *
  5. * @author Sergey Malyshev <malyshev.php@gmail.com>
  6. */
  7. /**
  8. * YiiDebugToolbarPanelSql class.
  9. *
  10. * Description of YiiDebugToolbarPanelSql
  11. *
  12. * @author Sergey Malyshev <malyshev.php@gmail.com>
  13. * @author Igor Golovanov <igor.golovanov@gmail.com>
  14. * @package YiiDebugToolbar
  15. * @since 1.1.7
  16. */
  17. class YiiDebugToolbarPanelSql extends YiiDebugToolbarPanel
  18. {
  19. public $i = 'i';
  20. /**
  21. * If true, the sql query in the list will use syntax highlighting.
  22. *
  23. * @var boolean
  24. */
  25. public $highlightSql = true;
  26. private $_countLimit = 1;
  27. private $_timeLimit = 0.01;
  28. private $_groupByToken = true;
  29. private $_dbConnections;
  30. private $_dbConnectionsCount;
  31. private $_textHighlighter;
  32. public function __construct($owner = null)
  33. {
  34. parent::__construct($owner);
  35. try {
  36. Yii::app()->db;
  37. } catch (Exception $e) {
  38. $this->_dbConnections = false;
  39. }
  40. }
  41. public function getCountLimit()
  42. {
  43. return $this->_countLimit;
  44. }
  45. public function setCountLimit($value)
  46. {
  47. $this->_countLimit = CPropertyValue::ensureInteger($value);
  48. }
  49. public function getTimeLimit()
  50. {
  51. return $this->_timeLimit;
  52. }
  53. public function setTimeLimit($value)
  54. {
  55. $this->_timeLimit = CPropertyValue::ensureFloat($value);
  56. }
  57. public function getDbConnectionsCount()
  58. {
  59. if (null === $this->_dbConnectionsCount)
  60. {
  61. $this->_dbConnectionsCount = count($this->getDbConnections());
  62. }
  63. return $this->_dbConnectionsCount;
  64. }
  65. public function getDbConnections()
  66. {
  67. if (null === $this->_dbConnections)
  68. {
  69. $this->_dbConnections = array();
  70. foreach (Yii::app()->components as $id=>$component)
  71. {
  72. if (false !== is_object($component)
  73. && false !== ($component instanceof CDbConnection))
  74. {
  75. $this->_dbConnections[$id] = $component;
  76. }
  77. }
  78. }
  79. return $this->_dbConnections;
  80. }
  81. /**
  82. * {@inheritdoc}
  83. */
  84. public function getMenuTitle()
  85. {
  86. return YiiDebug::t('SQL');
  87. }
  88. /**
  89. * {@inheritdoc}
  90. */
  91. public function getMenuSubTitle($f=4)
  92. {
  93. if (false !== $this->_dbConnections) {
  94. list ($count, $time) = Yii::app()->db->getStats();
  95. return $count . ($count > 0 ? ('/'. vsprintf('%0.'.$f.'F', $time) . 's') : '');
  96. }
  97. return YiiDebug::t('No active connections');
  98. }
  99. /**
  100. * {@inheritdoc}
  101. */
  102. public function getTitle()
  103. {
  104. if (false !== $this->_dbConnections)
  105. {
  106. $conn=$this->getDbConnectionsCount();
  107. return YiiDebug::t('SQL Queries from {n} connection|SQL Queries from {n} connections', array($conn));
  108. }
  109. return YiiDebug::t('No active connections');
  110. }
  111. /**
  112. * {@inheritdoc}
  113. */
  114. public function getSubTitle()
  115. {
  116. return false !== $this->_dbConnections
  117. ? ('(' . self::getMenuSubTitle(6) . ')')
  118. : null;
  119. }
  120. /**
  121. * Initialize panel
  122. */
  123. public function init()
  124. {
  125. }
  126. /**
  127. * {@inheritdoc}
  128. */
  129. public function run()
  130. {
  131. if (false === $this->_dbConnections) {
  132. return;
  133. }
  134. $logs = $this->filterLogs();
  135. $this->render('sql', array(
  136. 'connections' => $this->getDbConnections(),
  137. 'connectionsCount' => $this->getDbConnectionsCount(),
  138. 'summary' => $this->processSummary($logs),
  139. 'callstack' => $this->processCallstack($logs)
  140. ));
  141. }
  142. private function duration($secs)
  143. {
  144. $vals = array(
  145. 'w' => (int) ($secs / 86400 / 7),
  146. 'd' => $secs / 86400 % 7,
  147. 'h' => $secs / 3600 % 24,
  148. 'm' => $secs / 60 % 60,
  149. 's' => $secs % 60
  150. );
  151. $result = array();
  152. $added = false;
  153. foreach ($vals as $k => $v)
  154. {
  155. if ($v > 0 || false !== $added)
  156. {
  157. $added = true;
  158. $result[] = $v . $k;
  159. }
  160. }
  161. return implode(' ', $result);
  162. }
  163. /**
  164. * Returns the DB server info by connection ID.
  165. * @param string $connectionId
  166. * @return mixed
  167. */
  168. public function getServerInfo($connectionId)
  169. {
  170. if (null !== ($connection = Yii::app()->getComponent($connectionId))
  171. && false !== ($connection instanceof CDbConnection)
  172. && !in_array($connection->driverName, array('sqlite', 'oci', 'dblib'))
  173. && '' !== ($serverInfo = $connection->getServerInfo()))
  174. {
  175. $info = array(
  176. YiiDebug::t('Driver') => $connection->getDriverName(),
  177. YiiDebug::t('Server Version') => $connection->getServerVersion()
  178. );
  179. $lines = explode(' ', $serverInfo);
  180. foreach($lines as $line) {
  181. list($key, $value) = explode(': ', $line, 2);
  182. $info[YiiDebug::t($key)] = $value;
  183. }
  184. if(!empty($info[YiiDebug::t('Uptime')])) {
  185. $info[YiiDebug::t('Uptime')] = $this->duration($info[YiiDebug::t('Uptime')]);
  186. }
  187. return $info;
  188. }
  189. return null;
  190. }
  191. /**
  192. * Processing callstack.
  193. *
  194. * @param array $logs Logs.
  195. * @return array
  196. */
  197. protected function processCallstack(array $logs)
  198. {
  199. if (empty($logs))
  200. {
  201. return $logs;
  202. }
  203. $stack = array();
  204. $results = array();
  205. $n = 0;
  206. foreach ($logs as $log)
  207. {
  208. if(CLogger::LEVEL_PROFILE !== $log[1])
  209. continue;
  210. $message = $log[0];
  211. if (0 === strncasecmp($message,'begin:',6))
  212. {
  213. $log[0] = substr($message,6);
  214. $log[4] = $n;
  215. $stack[] = $log;
  216. $n++;
  217. }
  218. else if (0 === strncasecmp($message, 'end:', 4))
  219. {
  220. $token = substr($message,4);
  221. if(null !== ($last = array_pop($stack)) && $last[0] === $token)
  222. {
  223. $delta = $log[3] - $last[3];
  224. $results[$last[4]] = array($token, $delta, count($stack));
  225. }
  226. else
  227. throw new CException(Yii::t('yii-debug-toolbar',
  228. 'Mismatching code block "{token}". Make sure the calls to Yii::beginProfile() and Yii::endProfile() be properly nested.',
  229. array('{token}' => $token)
  230. ));
  231. }
  232. }
  233. // remaining entries should be closed here
  234. $now = microtime(true);
  235. while (null !== ($last = array_pop($stack))){
  236. $results[$last[4]] = array($last[0], $now - $last[3], count($stack));
  237. }
  238. ksort($results);
  239. return array_map(array($this, 'formatLogEntry'), $results);
  240. }
  241. /**
  242. * Processing summary.
  243. *
  244. * @param array $logs Logs.
  245. * @return array
  246. */
  247. protected function processSummary(array $logs)
  248. {
  249. if (empty($logs))
  250. {
  251. return $logs;
  252. }
  253. $stack = array();
  254. foreach($logs as $log)
  255. {
  256. $message = $log[0];
  257. if(0 === strncasecmp($message, 'begin:', 6))
  258. {
  259. $log[0] =substr($message, 6);
  260. $stack[] =$log;
  261. }
  262. else if(0 === strncasecmp($message,'end:',4))
  263. {
  264. $token = substr($message,4);
  265. if(null !== ($last = array_pop($stack)) && $last[0] === $token)
  266. {
  267. $delta = $log[3] - $last[3];
  268. if(isset($results[$token]))
  269. $results[$token] = $this->aggregateResult($results[$token], $delta);
  270. else{
  271. $results[$token] = array($token, 1, $delta, $delta, $delta);
  272. }
  273. }
  274. else
  275. throw new CException(Yii::t('yii-debug-toolbar',
  276. 'Mismatching code block "{token}". Make sure the calls to Yii::beginProfile() and Yii::endProfile() be properly nested.',
  277. array('{token}' => $token)));
  278. }
  279. }
  280. $now = microtime(true);
  281. while(null !== ($last = array_pop($stack)))
  282. {
  283. $delta = $now - $last[3];
  284. $token = $last[0];
  285. if(isset($results[$token]))
  286. $results[$token] = $this->aggregateResult($results[$token], $delta);
  287. else{
  288. $results[$token] = array($token, 1, $delta, $delta, $delta);
  289. }
  290. }
  291. $entries = array_values($results);
  292. $func = create_function('$a,$b','return $a[4]<$b[4]?1:0;');
  293. usort($entries, $func);
  294. return array_map(array($this, 'formatLogEntry'), $entries);
  295. }
  296. /**
  297. * Format log entry
  298. *
  299. * @param array $entry
  300. * @return array
  301. */
  302. public function formatLogEntry(array $entry)
  303. {
  304. // extract query from the entry
  305. $queryString = $entry[0];
  306. $sqlStart = strpos($queryString, '(') + 1;
  307. $sqlEnd = strrpos($queryString , ')');
  308. $sqlLength = $sqlEnd - $sqlStart;
  309. $queryString = substr($queryString, $sqlStart, $sqlLength);
  310. if (false !== strpos($queryString, '. Bound with '))
  311. {
  312. list($query, $params) = explode('. Bound with ', $queryString);
  313. $binds = array();
  314. $matchResult = preg_match_all("/(?<key>[a-z0-9\.\_\-\:]+)=(?<value>[\d\.e\-\+]+|''|'.+?(?<!\\\)')/ims", $params, $paramsMatched, PREG_SET_ORDER);
  315. if ($matchResult) {
  316. foreach ($paramsMatched as $paramsMatch)
  317. if (isset($paramsMatch['key'], $paramsMatch['value']))
  318. $binds[':' . trim($paramsMatch['key'],': ')] = trim($paramsMatch['value']);
  319. }
  320. $entry[0] = strtr($query, $binds);
  321. }
  322. else
  323. {
  324. $entry[0] = $queryString;
  325. }
  326. if(false !== CPropertyValue::ensureBoolean($this->highlightSql))
  327. {
  328. $entry[0] = $this->getTextHighlighter()->highlight($entry[0]);
  329. }
  330. $entry[0] = strip_tags($entry[0], '<div>,<span>');
  331. return $entry;
  332. }
  333. /**
  334. * @return CTextHighlighter the text highlighter
  335. */
  336. private function getTextHighlighter()
  337. {
  338. if (null === $this->_textHighlighter)
  339. {
  340. $this->_textHighlighter = Yii::createComponent(array(
  341. 'class' => 'CTextHighlighter',
  342. 'language' => 'sql',
  343. 'showLineNumbers' => false,
  344. ));
  345. }
  346. return $this->_textHighlighter;
  347. }
  348. /**
  349. * Aggregates the report result.
  350. *
  351. * @param array $result log result for this code block
  352. * @param float $delta time spent for this code block
  353. * @return array
  354. */
  355. protected function aggregateResult($result, $delta)
  356. {
  357. list($token, $calls, $min, $max, $total) = $result;
  358. switch (true)
  359. {
  360. case ($delta < $min):
  361. $min = $delta;
  362. break;
  363. case ($delta > $max):
  364. $max = $delta;
  365. break;
  366. default:
  367. // nothing
  368. break;
  369. }
  370. $calls++;
  371. $total += $delta;
  372. return array($token, $calls, $min, $max, $total);
  373. }
  374. /**
  375. * Get filter logs.
  376. *
  377. * @return array
  378. */
  379. protected function filterLogs()
  380. {
  381. $logs = array();
  382. foreach ($this->owner->getLogs() as $entry)
  383. {
  384. if (CLogger::LEVEL_PROFILE === $entry[1] && 0 === strpos($entry[2], 'system.db.CDbCommand'))
  385. {
  386. $logs[] = $entry;
  387. }
  388. }
  389. return $logs;
  390. }
  391. }