├── README.md └── app ├── code ├── community │ └── Bubble │ │ └── Debug │ │ ├── Model │ │ └── Observer.php │ │ └── etc │ │ └── config.xml └── local │ └── Zend │ └── Db │ └── Adapter │ └── Pdo │ └── Abstract.php └── etc └── modules └── Bubble_Debug.xml /README.md: -------------------------------------------------------------------------------- 1 | ### Magento module to list rendered blocks tree and SQL queries 2 | 3 | ![Rendered blocks](http://i.imgur.com/6spPPyQ.png) 4 | 5 | ![SQL queries](http://i.imgur.com/yWswA7c.png) 6 | 7 | ### Installation instructions 8 | 9 | Install with [modgit](https://github.com/jreinke/modgit): 10 | 11 | $ cd /path/to/magento 12 | $ modgit init 13 | $ modgit clone debug https://github.com/jreinke/magento-debug.git 14 | 15 | or download package manually [here](https://github.com/jreinke/magento-debug/archive/master.zip) and unzip in Magento root folder. 16 | 17 | Finally, clear cache. 18 | 19 | ### .gitignore (optional) 20 | 21 | Is is recommended to ignore this module files. Add this to your .gitignore file: 22 | 23 | app/code/community/Bubble/Debug/* 24 | app/code/local/Zend/Db/Adapter/Pdo/Abstract.php 25 | app/etc/modules/Bubble_Debug.xml 26 | 27 | ### Usage 28 | 29 | ##### Enable debugging for current page 30 | 31 | shop.example.com/apparel.html`?debug=1` 32 | 33 | ##### Enable debugging permanently 34 | 35 | shop.example.com/apparel.html`?debug=perm` 36 | 37 | ##### Disable permament debugging 38 | 39 | shop.example.com/apparel.html`?debug=0` 40 | -------------------------------------------------------------------------------- /app/code/community/Bubble/Debug/Model/Observer.php: -------------------------------------------------------------------------------- 1 | array(), // rendered blocks start time 16 | 'blocks' => array(), // rendered blocks 17 | 'current_block' => null, // current rendered block 18 | 'sql' => array(), // sql queries 19 | ); 20 | 21 | public function __construct() 22 | { 23 | $this->_request = Mage::app()->getRequest(); 24 | if (($this->_request->getQuery('debug') || $this->_request->getCookie('debug')) 25 | && !$this->_request->isAjax() && !$this->_request->getPost()) 26 | { 27 | $this->_debugEnabled = true; 28 | } 29 | } 30 | 31 | public function onSendResponseBefore(Varien_Event_Observer $observer) 32 | { 33 | // Reset permanent debugging if needed 34 | if ($this->_request->getQuery('debug') === '0') { 35 | Mage::getSingleton('core/cookie')->delete('debug'); 36 | return; 37 | } 38 | 39 | if (!$this->isDebugEnabled()) { 40 | return; 41 | } 42 | 43 | // handling permanent debugging 44 | if ($this->_request->getQuery('debug') === 'perm') { 45 | Mage::getSingleton('core/cookie')->set('debug', 1); 46 | } 47 | 48 | $front = $observer->getEvent()->getFront(); 49 | $html = $this->_getDebugHtml(); 50 | $front->getResponse()->appendBody($html); 51 | } 52 | 53 | public function onBlockToHtmlBefore(Varien_Event_Observer $observer) 54 | { 55 | if (!$this->isDebugEnabled()) { 56 | return; 57 | } 58 | 59 | /** @var $block Mage_Core_Block_Abstract */ 60 | $block = $observer->getEvent()->getBlock(); 61 | // Saving block rendering time, used in onBlockToHtmlAfter() 62 | $this->_debug['start'][$block->getNameInLayout()] = microtime(true); 63 | $this->_debug['current_block'] = $block->setDebugId(uniqid(mt_rand())); 64 | } 65 | 66 | public function onBlockToHtmlAfter(Varien_Event_Observer $observer) 67 | { 68 | if (!$this->isDebugEnabled()) { 69 | return; 70 | } 71 | 72 | $block = $observer->getEvent()->getBlock(); 73 | $this->_debug['current_block'] = null; 74 | 75 | // Block rendering duration 76 | $start = $this->_debug['start'][$block->getNameInLayout()]; 77 | if ($start) { 78 | $blocks =& $this->_debug['blocks']; 79 | $parents = array(); 80 | $parentBlock = $block->getParentBlock(); 81 | while ($parentBlock) { 82 | $parents[] = $parentBlock->getNameInLayout(); 83 | $parentBlock = $parentBlock->getParentBlock(); 84 | } 85 | foreach (array_reverse($parents) as $parent) { 86 | $blocks =& $blocks[$parent]['children']; 87 | } 88 | $tpl = false; 89 | if ($block->getTemplateFile() && pathinfo($block->getTemplateFile(), PATHINFO_EXTENSION) == 'phtml') { 90 | $tpl = 'app' . DS . 'design' . DS . $block->getTemplateFile(); 91 | } 92 | $blockInfo = array( 93 | 'debug_id' => $block->getDebugId(), 94 | 'name' => $block->getNameInLayout(), 95 | 'class' => get_class($block), 96 | 'tpl' => $tpl, 97 | 'took' => microtime(true) - $start, 98 | 'cached' => !is_null($block->getCacheLifetime()), 99 | ); 100 | if (isset($blocks[$blockInfo['name']])) { 101 | $blocks[$blockInfo['name']] = array_merge($blocks[$blockInfo['name']], $blockInfo); 102 | } else { 103 | $blocks[] = $blockInfo; 104 | } 105 | } 106 | } 107 | 108 | public function onSqlQueryBefore(Varien_Event_Observer $observer) 109 | { 110 | if (!$this->isDebugEnabled()) { 111 | return; 112 | } 113 | 114 | /** @var $adapter Zend_Db_Adapter_Pdo_Abstract */ 115 | $adapter = $observer->getEvent()->getAdapter(); 116 | $sql = $observer->getEvent()->getQuery(); 117 | $bind = $observer->getEvent()->getBind(); 118 | $took = $observer->getEvent()->getTook(); 119 | if ($adapter && $sql) { 120 | $debug = array( 121 | 'query' => $sql, 122 | 'took' => $took, 123 | 'stack' => array(), 124 | ); 125 | $debug['query'] = $sql; 126 | if (is_string(key($bind))) { 127 | foreach ($bind as $field => $value) { 128 | $debug['query'] = str_replace($field, $adapter->quote($value), $debug['query']); 129 | } 130 | } else if (is_numeric(key($bind))) { 131 | $offset = 0; 132 | foreach ($bind as $value) { 133 | $pos = strpos($debug['query'], '?', $offset); 134 | if (null === $value) { 135 | $value = 'NULL'; 136 | } else if (is_string($value)) { 137 | $value = $adapter->quote($value); 138 | } 139 | $debug['query'] = substr_replace($debug['query'], $value, $pos, 1); 140 | $offset = $pos + strlen($value); 141 | } 142 | } 143 | $debug['query'] .= ';'; 144 | $backtrace = array_slice(debug_backtrace(false), 4); 145 | foreach ($backtrace as $data) { 146 | $file = false; 147 | if (isset($data['file'])) { 148 | $file = ltrim(str_replace(dirname($_SERVER['SCRIPT_FILENAME']), '', $data['file']), DS); 149 | } 150 | $function = $data['function'] . '()'; 151 | if (isset($data['class'])) { 152 | $function = $data['class'] . $data['type'] . $function; 153 | } 154 | $debug['stack'][] = array( 155 | 'function' => $function, 156 | 'file' => $file, 157 | 'line' => isset($data['line']) ? $data['line'] : false, 158 | ); 159 | } 160 | if (isset($this->_debug['current_block'])) { 161 | $this->_debug['sql']['blocks'][$this->_debug['current_block']->getDebugId()][] = $debug; 162 | } 163 | $this->_debug['sql']['queries'][] = $debug; 164 | } 165 | } 166 | 167 | public function getDebug() 168 | { 169 | return $this->_debug; 170 | } 171 | 172 | public function isDebugEnabled() 173 | { 174 | return $this->_debugEnabled; 175 | } 176 | 177 | protected function _getBlockInfoHtml(&$html, $block, $level = 0) 178 | { 179 | $indent = $level * 4; 180 | if (!empty($block['name'])) { 181 | $blockId = $block['debug_id']; 182 | $html .= '
'; 183 | $html .= sprintf( 184 | '%s %s %s (%s)', 185 | str_repeat(' ', $indent), 186 | str_pad(round($block['took'], 4), 6, STR_PAD_LEFT), 187 | $block['name'], 188 | $block['cached'] ? 'limegreen' : 'red', 189 | $block['cached'] ? 'cached' : 'not cached' 190 | ); 191 | $html .= '
'; 192 | $html .= str_repeat(' ', $indent + 7) . ' ' . str_pad($block['class'], 6, STR_PAD_LEFT); 193 | if (!empty($block['tpl'])) { 194 | $html .= '
'; 195 | $html .= str_repeat(' ', $indent + 7) . ' ' . str_pad($block['tpl'], 6, STR_PAD_LEFT); 196 | } 197 | if (isset($this->_debug['sql']['blocks'][$blockId])) { 198 | $id = uniqid(mt_rand()); 199 | $html .= '
'; 200 | $onclick = "var el = document.getElementById('$id'); 201 | el.style.display = el.style.display == 'none' ? 'block' : 'none';return false;"; 202 | $html .= sprintf( 203 | '%s SQL Queries (%d)', 204 | str_repeat(' ', $indent + 7), 205 | $onclick, 206 | count($this->_debug['sql']['blocks'][$blockId]) 207 | ); 208 | $html .= '
'; 209 | $html .= ''; 217 | } 218 | $html .= '
'; 219 | } 220 | if (isset($block['children'])) { 221 | foreach ($block['children'] as $child) { 222 | $this->_getBlockInfoHtml($html, $child, $level + 1); 223 | } 224 | } 225 | 226 | return $this; 227 | } 228 | 229 | protected function _getSqlDebugHtml() 230 | { 231 | $html = ''; 232 | if (isset($this->_debug['sql']['queries'])) { 233 | $count = count($this->_debug['sql']['queries']); 234 | $html = '

'; 235 | $html .= 'All SQL Queries ('. $count .')'; 236 | $html .= '

'; 237 | $html .= '
    '; 238 | foreach ($this->_debug['sql']['queries'] as $sql) { 239 | $html .= '
  1. '; 240 | $html .= $sql['query'] . '
    '; 241 | $id = uniqid(mt_rand()); 242 | $onclick = "var el = document.getElementById('$id'); 243 | el.style.display = el.style.display == 'none' ? 'block' : 'none';return false;"; 244 | $html .= 'Stack Trace ' . round($sql['took'], 4); 245 | $html .= ''; 254 | $html .= '
  2. '; 255 | } 256 | $html .= '
'; 257 | } 258 | 259 | return $html; 260 | } 261 | 262 | protected function _getDebugHtml() 263 | { 264 | $html = '
';
265 |         $html .= '

'; 266 | $html .= 'Design'; 267 | $html .= '

'; 268 | $html .= sprintf('

Package: %s

', Mage::getDesign()->getPackageName()); 269 | $html .= sprintf('

Theme: %s

', Mage::getDesign()->getTheme('default')); 270 | $html .= '

'; 271 | $html .= 'Layout Updates'; 272 | $html .= '

'; 273 | $html .= implode('
', Mage::app()->getLayout()->getUpdate()->getHandles()); 274 | $html .= '

'; 275 | $html .= 'Rendered Blocks'; 276 | $html .= '

'; 277 | foreach ($this->_debug['blocks'] as $block) { 278 | $this->_getBlockInfoHtml($html, $block); 279 | } 280 | $html .= $this->_getSqlDebugHtml(); 281 | $html .= '
'; 282 | 283 | return $html; 284 | } 285 | } -------------------------------------------------------------------------------- /app/code/community/Bubble/Debug/etc/config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 1.1.0 6 | 7 | 8 | 9 | 10 | 11 | Bubble_Debug_Model 12 | 13 | 14 | 15 | 16 | 17 | 18 | singleton 19 | bubble_debug/observer 20 | onSendResponseBefore 21 | 22 | 23 | 24 | 25 | 26 | 27 | singleton 28 | bubble_debug/observer 29 | onBlockToHtmlBefore 30 | 31 | 32 | 33 | 34 | 35 | 36 | singleton 37 | bubble_debug/observer 38 | onBlockToHtmlAfter 39 | 40 | 41 | 42 | 43 | 44 | 45 | singleton 46 | bubble_debug/observer 47 | onSqlQueryBefore 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /app/code/local/Zend/Db/Adapter/Pdo/Abstract.php: -------------------------------------------------------------------------------- 1 | _config settings. 57 | * 58 | * @return string 59 | */ 60 | protected function _dsn() 61 | { 62 | // baseline of DSN parts 63 | $dsn = $this->_config; 64 | 65 | // don't pass the username, password, charset, persistent and driver_options in the DSN 66 | unset($dsn['username']); 67 | unset($dsn['password']); 68 | unset($dsn['options']); 69 | unset($dsn['charset']); 70 | unset($dsn['persistent']); 71 | unset($dsn['driver_options']); 72 | 73 | // use all remaining parts in the DSN 74 | foreach ($dsn as $key => $val) { 75 | $dsn[$key] = "$key=$val"; 76 | } 77 | 78 | return $this->_pdoType . ':' . implode(';', $dsn); 79 | } 80 | 81 | /** 82 | * Creates a PDO object and connects to the database. 83 | * 84 | * @return void 85 | * @throws Zend_Db_Adapter_Exception 86 | */ 87 | protected function _connect() 88 | { 89 | // if we already have a PDO object, no need to re-connect. 90 | if ($this->_connection) { 91 | return; 92 | } 93 | 94 | // get the dsn first, because some adapters alter the $_pdoType 95 | $dsn = $this->_dsn(); 96 | 97 | // check for PDO extension 98 | if (!extension_loaded('pdo')) { 99 | /** 100 | * @see Zend_Db_Adapter_Exception 101 | */ 102 | #require_once 'Zend/Db/Adapter/Exception.php'; 103 | throw new Zend_Db_Adapter_Exception('The PDO extension is required for this adapter but the extension is not loaded'); 104 | } 105 | 106 | // check the PDO driver is available 107 | if (!in_array($this->_pdoType, PDO::getAvailableDrivers())) { 108 | /** 109 | * @see Zend_Db_Adapter_Exception 110 | */ 111 | #require_once 'Zend/Db/Adapter/Exception.php'; 112 | throw new Zend_Db_Adapter_Exception('The ' . $this->_pdoType . ' driver is not currently installed'); 113 | } 114 | 115 | // create PDO connection 116 | $q = $this->_profiler->queryStart('connect', Zend_Db_Profiler::CONNECT); 117 | 118 | // add the persistence flag if we find it in our config array 119 | if (isset($this->_config['persistent']) && ($this->_config['persistent'] == true)) { 120 | $this->_config['driver_options'][PDO::ATTR_PERSISTENT] = true; 121 | } 122 | 123 | try { 124 | $this->_connection = new PDO( 125 | $dsn, 126 | $this->_config['username'], 127 | $this->_config['password'], 128 | $this->_config['driver_options'] 129 | ); 130 | 131 | $this->_profiler->queryEnd($q); 132 | 133 | // set the PDO connection to perform case-folding on array keys, or not 134 | $this->_connection->setAttribute(PDO::ATTR_CASE, $this->_caseFolding); 135 | 136 | // always use exceptions. 137 | $this->_connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); 138 | 139 | } catch (PDOException $e) { 140 | /** 141 | * @see Zend_Db_Adapter_Exception 142 | */ 143 | #require_once 'Zend/Db/Adapter/Exception.php'; 144 | throw new Zend_Db_Adapter_Exception($e->getMessage(), $e->getCode(), $e); 145 | } 146 | 147 | } 148 | 149 | /** 150 | * Test if a connection is active 151 | * 152 | * @return boolean 153 | */ 154 | public function isConnected() 155 | { 156 | return ((bool) ($this->_connection instanceof PDO)); 157 | } 158 | 159 | /** 160 | * Force the connection to close. 161 | * 162 | * @return void 163 | */ 164 | public function closeConnection() 165 | { 166 | $this->_connection = null; 167 | } 168 | 169 | /** 170 | * Prepares an SQL statement. 171 | * 172 | * @param string $sql The SQL statement with placeholders. 173 | * @param array $bind An array of data to bind to the placeholders. 174 | * @return PDOStatement 175 | */ 176 | public function prepare($sql) 177 | { 178 | $this->_connect(); 179 | $stmtClass = $this->_defaultStmtClass; 180 | if (!class_exists($stmtClass)) { 181 | #require_once 'Zend/Loader.php'; 182 | Zend_Loader::loadClass($stmtClass); 183 | } 184 | $stmt = new $stmtClass($this, $sql); 185 | $stmt->setFetchMode($this->_fetchMode); 186 | return $stmt; 187 | } 188 | 189 | /** 190 | * Gets the last ID generated automatically by an IDENTITY/AUTOINCREMENT column. 191 | * 192 | * As a convention, on RDBMS brands that support sequences 193 | * (e.g. Oracle, PostgreSQL, DB2), this method forms the name of a sequence 194 | * from the arguments and returns the last id generated by that sequence. 195 | * On RDBMS brands that support IDENTITY/AUTOINCREMENT columns, this method 196 | * returns the last value generated for such a column, and the table name 197 | * argument is disregarded. 198 | * 199 | * On RDBMS brands that don't support sequences, $tableName and $primaryKey 200 | * are ignored. 201 | * 202 | * @param string $tableName OPTIONAL Name of table. 203 | * @param string $primaryKey OPTIONAL Name of primary key column. 204 | * @return string 205 | */ 206 | public function lastInsertId($tableName = null, $primaryKey = null) 207 | { 208 | $this->_connect(); 209 | return $this->_connection->lastInsertId(); 210 | } 211 | 212 | /** 213 | * Special handling for PDO query(). 214 | * All bind parameter names must begin with ':' 215 | * 216 | * @param string|Zend_Db_Select $sql The SQL statement with placeholders. 217 | * @param array $bind An array of data to bind to the placeholders. 218 | * @return Zend_Db_Statement_Pdo 219 | * @throws Zend_Db_Adapter_Exception To re-throw PDOException. 220 | */ 221 | public function query($sql, $bind = array()) 222 | { 223 | if (empty($bind) && $sql instanceof Zend_Db_Select) { 224 | $bind = $sql->getBind(); 225 | } 226 | 227 | if (is_array($bind)) { 228 | foreach ($bind as $name => $value) { 229 | if (!is_int($name) && !preg_match('/^:/', $name)) { 230 | $newName = ":$name"; 231 | unset($bind[$name]); 232 | $bind[$newName] = $value; 233 | } 234 | } 235 | } 236 | 237 | // Bubble_Debug start 238 | try { 239 | $start = microtime(true); 240 | $result = parent::query($sql, $bind); 241 | 242 | if (class_exists('Mage')) { 243 | Mage::dispatchEvent('bubble_debug_sql_query_before', array( 244 | 'adapter' => $this, 245 | 'query' => $sql, 246 | 'bind' => $bind, 247 | 'took' => microtime(true) - $start, 248 | )); 249 | } 250 | 251 | return $result; 252 | } catch (PDOException $e) { 253 | /** 254 | * @see Zend_Db_Statement_Exception 255 | */ 256 | #require_once 'Zend/Db/Statement/Exception.php'; 257 | throw new Zend_Db_Statement_Exception($e->getMessage(), $e->getCode(), $e); 258 | } 259 | // Bubble_Debug end 260 | } 261 | 262 | /** 263 | * Executes an SQL statement and return the number of affected rows 264 | * 265 | * @param mixed $sql The SQL statement with placeholders. 266 | * May be a string or Zend_Db_Select. 267 | * @return integer Number of rows that were modified 268 | * or deleted by the SQL statement 269 | */ 270 | public function exec($sql) 271 | { 272 | if ($sql instanceof Zend_Db_Select) { 273 | $sql = $sql->assemble(); 274 | } 275 | 276 | try { 277 | $affected = $this->getConnection()->exec($sql); 278 | 279 | if ($affected === false) { 280 | $errorInfo = $this->getConnection()->errorInfo(); 281 | /** 282 | * @see Zend_Db_Adapter_Exception 283 | */ 284 | #require_once 'Zend/Db/Adapter/Exception.php'; 285 | throw new Zend_Db_Adapter_Exception($errorInfo[2]); 286 | } 287 | 288 | return $affected; 289 | } catch (PDOException $e) { 290 | /** 291 | * @see Zend_Db_Adapter_Exception 292 | */ 293 | #require_once 'Zend/Db/Adapter/Exception.php'; 294 | throw new Zend_Db_Adapter_Exception($e->getMessage(), $e->getCode(), $e); 295 | } 296 | } 297 | 298 | /** 299 | * Quote a raw string. 300 | * 301 | * @param string $value Raw string 302 | * @return string Quoted string 303 | */ 304 | protected function _quote($value) 305 | { 306 | if (is_int($value) || is_float($value)) { 307 | return $value; 308 | } 309 | $this->_connect(); 310 | return $this->_connection->quote($value); 311 | } 312 | 313 | /** 314 | * Begin a transaction. 315 | */ 316 | protected function _beginTransaction() 317 | { 318 | $this->_connect(); 319 | $this->_connection->beginTransaction(); 320 | } 321 | 322 | /** 323 | * Commit a transaction. 324 | */ 325 | protected function _commit() 326 | { 327 | $this->_connect(); 328 | $this->_connection->commit(); 329 | } 330 | 331 | /** 332 | * Roll-back a transaction. 333 | */ 334 | protected function _rollBack() { 335 | $this->_connect(); 336 | $this->_connection->rollBack(); 337 | } 338 | 339 | /** 340 | * Set the PDO fetch mode. 341 | * 342 | * @todo Support FETCH_CLASS and FETCH_INTO. 343 | * 344 | * @param int $mode A PDO fetch mode. 345 | * @return void 346 | * @throws Zend_Db_Adapter_Exception 347 | */ 348 | public function setFetchMode($mode) 349 | { 350 | //check for PDO extension 351 | if (!extension_loaded('pdo')) { 352 | /** 353 | * @see Zend_Db_Adapter_Exception 354 | */ 355 | #require_once 'Zend/Db/Adapter/Exception.php'; 356 | throw new Zend_Db_Adapter_Exception('The PDO extension is required for this adapter but the extension is not loaded'); 357 | } 358 | switch ($mode) { 359 | case PDO::FETCH_LAZY: 360 | case PDO::FETCH_ASSOC: 361 | case PDO::FETCH_NUM: 362 | case PDO::FETCH_BOTH: 363 | case PDO::FETCH_NAMED: 364 | case PDO::FETCH_OBJ: 365 | $this->_fetchMode = $mode; 366 | break; 367 | default: 368 | /** 369 | * @see Zend_Db_Adapter_Exception 370 | */ 371 | #require_once 'Zend/Db/Adapter/Exception.php'; 372 | throw new Zend_Db_Adapter_Exception("Invalid fetch mode '$mode' specified"); 373 | break; 374 | } 375 | } 376 | 377 | /** 378 | * Check if the adapter supports real SQL parameters. 379 | * 380 | * @param string $type 'positional' or 'named' 381 | * @return bool 382 | */ 383 | public function supportsParameters($type) 384 | { 385 | switch ($type) { 386 | case 'positional': 387 | case 'named': 388 | default: 389 | return true; 390 | } 391 | } 392 | 393 | /** 394 | * Retrieve server version in PHP style 395 | * 396 | * @return string 397 | */ 398 | public function getServerVersion() 399 | { 400 | $this->_connect(); 401 | try { 402 | $version = $this->_connection->getAttribute(PDO::ATTR_SERVER_VERSION); 403 | } catch (PDOException $e) { 404 | // In case of the driver doesn't support getting attributes 405 | return null; 406 | } 407 | $matches = null; 408 | if (preg_match('/((?:[0-9]{1,2}\.){1,3}[0-9]{1,2})/', $version, $matches)) { 409 | return $matches[1]; 410 | } else { 411 | return null; 412 | } 413 | } 414 | } 415 | 416 | -------------------------------------------------------------------------------- /app/etc/modules/Bubble_Debug.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | true 6 | community 7 | 8 | 9 | 10 | --------------------------------------------------------------------------------