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