Source for file DibiProfiler.php

Documentation is available at DibiProfiler.php

  1. 1: <?php
  2. 2:  
  3. 3: /**
  4. 4:  * dibi - tiny'n'smart database abstraction layer
  5. 5:  * ----------------------------------------------
  6. 6:  *
  7. 7:  * @copyright  Copyright (c) 2005, 2010 David Grudl
  8. 8:  * @license    http://dibiphp.com/license  dibi license
  9. 9:  * @link       http://dibiphp.com
  10. 10:  * @package    dibi
  11. 11:  */
  12. 12:  
  13. 13:  
  14. 14:  
  15. 15: /**
  16. 16:  * dibi basic logger & profiler (experimental).
  17. 17:  *
  18. 18:  * Profiler options:
  19. 19:  *   - 'explain' - explain SELECT queries?
  20. 20:  *   - 'filter' - which queries to log?
  21. 21:  *
  22. 22:  * @copyright  Copyright (c) 2005, 2010 David Grudl
  23. 23:  * @package    dibi
  24. 24:  */
  25. 25: class DibiProfiler extends DibiObject implements IDibiProfilerIDebugPanel
  26. 26: {
  27. 27:     /** maximum number of rows */
  28. 28:     static public $maxQueries = 30;
  29. 29:  
  30. 30:     /** maximum SQL length */
  31. 31:     static public $maxLength = 1000;
  32. 32:  
  33. 33:     /** @var string  Name of the file where SQL errors should be logged */
  34. 34:     private $file;
  35. 35:  
  36. 36:     /** @var bool  log to firebug? */
  37. 37:     public $useFirebug;
  38. 38:  
  39. 39:     /** @var bool  explain queries? */
  40. 40:     public $explainQuery = TRUE;
  41. 41:  
  42. 42:     /** @var int */
  43. 43:     private $filter self::ALL;
  44. 44:  
  45. 45:     /** @var array */
  46. 46:     public static $tickets array();
  47. 47:  
  48. 48:     /** @var array */
  49. 49:     public static $fireTable array(array('Time''SQL Statement''Rows''Connection'));
  50. 50:  
  51. 51:  
  52. 52:  
  53. 53:     public function __construct(array $config)
  54. 54:     {
  55. 55:         if (is_callable('Nette\Debug::addPanel')) {
  56. 56:             call_user_func('Nette\Debug::addPanel'$this);
  57. 57:         elseif (is_callable('NDebug::addPanel')) {
  58. 58:             NDebug::addPanel($this);
  59. 59:         elseif (is_callable('Debug::addPanel')) {
  60. 60:             Debug::addPanel($this);
  61. 61:         }
  62. 62:  
  63. 63:         $this->useFirebug isset($_SERVER['HTTP_USER_AGENT']&& strpos($_SERVER['HTTP_USER_AGENT']'FirePHP/');
  64. 64:  
  65. 65:         if (isset($config['filter'])) {
  66. 66:             $this->setFilter($config['filter']);
  67. 67:         }
  68. 68:  
  69. 69:         if (isset($config['explain'])) {
  70. 70:             $this->explainQuery = (bool) $config['explain'];
  71. 71:         }
  72. 72:     }
  73. 73:  
  74. 74:  
  75. 75:  
  76. 76:     /**
  77. 77:      * @param  string  filename
  78. 78:      * @return DibiProfiler  provides a fluent interface
  79. 79:      */
  80. 80:     public function setFile($file)
  81. 81:     {
  82. 82:         $this->file = $file;
  83. 83:         return $this;
  84. 84:     }
  85. 85:  
  86. 86:  
  87. 87:  
  88. 88:     /**
  89. 89:      * @param  int 
  90. 90:      * @return DibiProfiler  provides a fluent interface
  91. 91:      */
  92. 92:     public function setFilter($filter)
  93. 93:     {
  94. 94:         $this->filter = (int) $filter;
  95. 95:         return $this;
  96. 96:     }
  97. 97:  
  98. 98:  
  99. 99:  
  100. 100:     /**
  101. 101:      * Before event notification.
  102. 102:      * @param  DibiConnection 
  103. 103:      * @param  int     event name
  104. 104:      * @param  string  sql
  105. 105:      * @return int 
  106. 106:      */
  107. 107:     public function before(DibiConnection $connection$event$sql NULL)
  108. 108:     {
  109. 109:         if ($event self::QUERYdibi::$numOfQueries++;
  110. 110:         dibi::$elapsedTime FALSE;
  111. 111:         self::$tickets[array($connection$eventtrim($sql)-microtime(TRUE)NULLNULL);
  112. 112:         end(self::$tickets);
  113. 113:         return key(self::$tickets);
  114. 114:     }
  115. 115:  
  116. 116:  
  117. 117:  
  118. 118:     /**
  119. 119:      * After event notification.
  120. 120:      * @param  int 
  121. 121:      * @param  DibiResult 
  122. 122:      * @return void 
  123. 123:      */
  124. 124:     public function after($ticket$res NULL)
  125. 125:     {
  126. 126:         if (!isset(self::$tickets[$ticket])) {
  127. 127:             throw new InvalidArgumentException('Bad ticket number.');
  128. 128:         }
  129. 129:  
  130. 130:         $ticket self::$tickets[$ticket];
  131. 131:         $ticket[3+= microtime(TRUE);
  132. 132:         list($connection$event$sql$time$ticket;
  133. 133:  
  134. 134:         dibi::$elapsedTime $time;
  135. 135:         dibi::$totalTime += $time;
  136. 136:  
  137. 137:         if (($event $this->filter=== 0return;
  138. 138:  
  139. 139:         if ($event self::QUERY{
  140. 140:             try {
  141. 141:                 $ticket[4$count $res instanceof DibiResult count($res'-';
  142. 142:             catch (Exception $e{
  143. 143:                 $count '?';
  144. 144:             }
  145. 145:  
  146. 146:             if (count(self::$fireTableself::$maxQueries{
  147. 147:                 self::$fireTable[array(
  148. 148:                     sprintf('%0.3f'$time 1000),
  149. 149:                     strlen($sqlself::$maxLength substr($sql0self::$maxLength'...' $sql,
  150. 150:                     $count,
  151. 151:                     $connection->getConfig('driver''/' $connection->getConfig('name')
  152. 152:                 );
  153. 153:  
  154. 154:                 if ($this->explainQuery && $event === self::SELECT{
  155. 155:                     $tmpSql dibi::$sql;
  156. 156:                     try {
  157. 157:                         $ticket[5dibi::dump($connection->setProfiler(NULL)->nativeQuery('EXPLAIN ' $sql)TRUE);
  158. 158:                     catch (DibiException $e{}
  159. 159:                     $connection->setProfiler($this);
  160. 160:                     dibi::$sql $tmpSql;
  161. 161:                 }
  162. 162:  
  163. 163:                 if ($this->useFirebug && !headers_sent()) {
  164. 164:                     header('X-Wf-Protocol-dibi: http://meta.wildfirehq.org/Protocol/JsonStream/0.2');
  165. 165:                     header('X-Wf-dibi-Plugin-1: http://meta.firephp.org/Wildfire/Plugin/FirePHP/Library-FirePHPCore/0.2.0');
  166. 166:                     header('X-Wf-dibi-Structure-1: http://meta.firephp.org/Wildfire/Structure/FirePHP/FirebugConsole/0.1');
  167. 167:  
  168. 168:                     $payload json_encode(array(
  169. 169:                         array(
  170. 170:                             'Type' => 'TABLE',
  171. 171:                             'Label' => 'dibi profiler (' dibi::$numOfQueries ' SQL queries took ' sprintf('%0.3f'dibi::$totalTime 1000' ms)',
  172. 172:                         ),
  173. 173:                         self::$fireTable,
  174. 174:                     ));
  175. 175:                     foreach (str_split($payload4990as $num => $s{
  176. 176:                         $num++;
  177. 177:                         header("X-Wf-dibi-1-1-d$num: |$s|\\")// protocol-, structure-, plugin-, message-index
  178. 178:                     }
  179. 179:                     header("X-Wf-dibi-1-1-d$num: |$s|");
  180. 180:                 }
  181. 181:             }
  182. 182:  
  183. 183:             if ($this->file{
  184. 184:                 $this->writeFile(
  185. 185:                     "OK: " $sql
  186. 186:                     . ($res instanceof DibiResult ";\n-- rows: " $count '')
  187. 187:                     . "\n-- takes: " sprintf('%0.3f'$time 1000' ms'
  188. 188:                     . "\n-- driver: " $connection->getConfig('driver''/' $connection->getConfig('name')
  189. 189:                     . "\n-- " date('Y-m-d H:i:s')
  190. 190:                     . "\n\n"
  191. 191:                 );
  192. 192:             }
  193. 193:         }
  194. 194:     }
  195. 195:  
  196. 196:  
  197. 197:  
  198. 198:     /**
  199. 199:      * After exception notification.
  200. 200:      * @param  DibiDriverException 
  201. 201:      * @return void 
  202. 202:      */
  203. 203:     public function exception(DibiDriverException $exception)
  204. 204:     {
  205. 205:         if ((self::EXCEPTION $this->filter=== 0return;
  206. 206:  
  207. 207:         if ($this->useFirebug{
  208. 208:             // TODO: implement
  209. 209:         }
  210. 210:  
  211. 211:         if ($this->file{
  212. 212:             $message $exception->getMessage();
  213. 213:             $code $exception->getCode();
  214. 214:             if ($code{
  215. 215:                 $message "[$code$message";
  216. 216:             }
  217. 217:             $this->writeFile(
  218. 218:                 "ERROR: $message"
  219. 219:                 . "\n-- SQL: " dibi::$sql
  220. 220:                 . "\n-- driver: " //. $connection->getConfig('driver')
  221. 221:                 . ";\n-- " date('Y-m-d H:i:s')
  222. 222:                 . "\n\n"
  223. 223:             );
  224. 224:         }
  225. 225:     }
  226. 226:  
  227. 227:  
  228. 228:  
  229. 229:     private function writeFile($message)
  230. 230:     {
  231. 231:         $handle fopen($this->file'a');
  232. 232:         if (!$handlereturn// or throw exception?
  233. 233:         flock($handleLOCK_EX);
  234. 234:         fwrite($handle$message);
  235. 235:         fclose($handle);
  236. 236:     }
  237. 237:  
  238. 238:  
  239. 239:  
  240. 240:     /********************* interface Nette\IDebugPanel ****************d*g**/
  241. 241:  
  242. 242:  
  243. 243:  
  244. 244:     /**
  245. 245:      * Returns HTML code for custom tab.
  246. 246:      * @return mixed 
  247. 247:      */
  248. 248:     public function getTab()
  249. 249:     {
  250. 250:         return '<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAEYSURBVBgZBcHPio5hGAfg6/2+R980k6wmJgsJ5U/ZOAqbSc2GnXOwUg7BESgLUeIQ1GSjLFnMwsKGGg1qxJRmPM97/1zXFAAAAEADdlfZzr26miup2svnelq7d2aYgt3rebl585wN6+K3I1/9fJe7O/uIePP2SypJkiRJ0vMhr55FLCA3zgIAOK9uQ4MS361ZOSX+OrTvkgINSjS/HIvhjxNNFGgQsbSmabohKDNoUGLohsls6BaiQIMSs2FYmnXdUsygQYmumy3Nhi6igwalDEOJEjPKP7CA2aFNK8Bkyy3fdNCg7r9/fW3jgpVJbDmy5+PB2IYp4MXFelQ7izPrhkPHB+P5/PjhD5gCgCenx+VR/dODEwD+A3T7nqbxwf1HAAAAAElFTkSuQmCC">'
  251. 251:             . dibi::$numOfQueries ' queries';
  252. 252:     }
  253. 253:  
  254. 254:  
  255. 255:  
  256. 256:     /**
  257. 257:      * Returns HTML code for custom panel.
  258. 258:      * @return mixed 
  259. 259:      */
  260. 260:     public function getPanel()
  261. 261:     {
  262. 262:         if (!dibi::$numOfQueriesreturn;
  263. 263:  
  264. 264:         $content "
  265. 265: <h1>Queries: " dibi::$numOfQueries (dibi::$totalTime === NULL '' ', time: ' sprintf('%0.3f'dibi::$totalTime 1000' ms'"</h1>
  266. 266:  
  267. 267: <style>
  268. 268:     #nette-debug-DibiProfiler td.dibi-sql { background: white }
  269. 269:     #nette-debug-DibiProfiler .nette-alt td.dibi-sql { background: #F5F5F5 }
  270. 270:     #nette-debug-DibiProfiler .dibi-sql div { display: none; margin-top: 10px; max-height: 150px; overflow:auto }
  271. 271: </style>
  272. 272:  
  273. 273: <div class='nette-inner'>
  274. 274: <table>
  275. 275: <tr>
  276. 276:     <th>Time</th><th>SQL Statement</th><th>Rows</th><th>Connection</th>
  277. 277: </tr>
  278. 278: ";
  279. 279:         $i 0$classes array('class="nette-alt"''');
  280. 280:         foreach (self::$tickets as $ticket{
  281. 281:             list($connection$event$sql$time$count$explain$ticket;
  282. 282:             if (!($event self::QUERY)) continue;
  283. 283:             $content .= "
  284. 284: <tr {$classes[++$i%2]}>
  285. 285:     <td>sprintf('%0.3f'$time 1000($explain "
  286. 286:     <br><a href='#' class='nette-toggler' rel='#nette-debug-DibiProfiler-row-$i'>explain&nbsp;&#x25ba;</a>''"</td>
  287. 287:     <td class='dibi-sql'>" dibi::dump(strlen($sqlself::$maxLength substr($sql0self::$maxLength'...' $sqlTRUE($explain "
  288. 288:     <div id='nette-debug-DibiProfiler-row-$i'>{$explain}</div>''"</td>
  289. 289:     <td>{$count}</td>
  290. 290:     <td>htmlSpecialChars($connection->getConfig('driver''/' $connection->getConfig('name')) "</td>
  291. 291: </tr>
  292. 292: ";
  293. 293:         }
  294. 294:         $content .= '</table></div>';
  295. 295:         return $content;
  296. 296:     }
  297. 297:  
  298. 298:  
  299. 299:  
  300. 300:     /**
  301. 301:      * Returns panel ID.
  302. 302:      * @return string 
  303. 303:      */
  304. 304:     public function getId()
  305. 305:     {
  306. 306:         return get_class($this);
  307. 307:     }
  308. 308: