debug_kit / branches / master / controllers / components / toolbar.php
history
<?php
/**
* DebugKit DebugToolbar Component
*
* PHP versions 4 and 5
*
* CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
* Copyright 2005-2009, Cake Software Foundation, Inc. (http://cakefoundation.org)
*
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright 2005-2009, Cake Software Foundation, Inc. (http://cakefoundation.org)
* @link http://cakephp.org
* @package debug_kit
* @subpackage debug_kit.controllers.components
* @since DebugKit 0.1
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
**/
class ToolbarComponent extends Object {
/**
* Settings for the Component
*
* - forceEnable - Force the toolbar to display even if debug == 0. Default = false
* - autoRun - Automatically display the toolbar. If set to false, toolbar display can be triggered by adding
* `?debug=true` to your URL.
*
* @var array
**/
var $settings = array(
'forceEnable' => false,
'autoRun' => true
);
/**
* Controller instance reference
*
* @var object
*/
var $controller;
/**
* Components used by DebugToolbar
*
* @var array
*/
var $components = array('RequestHandler');
/**
* The default panels the toolbar uses.
* which panels are used can be configured when attaching the component
*
* @var array
*/
var $_defaultPanels = array('history', 'session', 'request', 'sqlLog', 'timer', 'log', 'variables');
/**
* Loaded panel objects.
*
* @var array
*/
var $panels = array();
/**
* javascript files component will be using
*
* @var array
**/
var $javascript = array(
'behavior' => '/debug_kit/js/js_debug_toolbar'
);
/**
* CacheKey used for the cache file.
*
* @var string
**/
var $cacheKey = 'toolbar_cache';
/**
* Duration of the debug kit history cache
*
* @var string
**/
var $cacheDuration = '+4 hours';
/**
* initialize
*
* If debug is off the component will be disabled and not do any further time tracking
* or load the toolbar helper.
*
* @return bool
**/
function initialize(&$controller, $settings) {
$this->settings = am($this->settings, $settings);
if (!Configure::read('debug') && empty($this->settings['forceEnable'])) {
$this->enabled = false;
return false;
}
if ($this->settings['autoRun'] == false && !isset($controller->params['url']['debug'])) {
$this->enabled = false;
return false;
}
App::import('Vendor', 'DebugKit.DebugKitDebugger');
DebugKitDebugger::setMemoryPoint(__d('debug_kit', 'Component intitailization', true));
DebugKitDebugger::startTimer('componentInit', __d('debug_kit', 'Component initialization and startup', true));
$panels = $this->_defaultPanels;
if (isset($settings['panels'])) {
$panels = $this->_makePanelList($settings['panels']);
unset($settings['panels']);
}
$this->cacheKey .= $controller->Session->read('Config.userAgent');
if (!isset($settings['history']) || (isset($settings['history']) && $settings['history'] !== false)) {
$this->_createCacheConfig();
}
$this->_loadPanels($panels, $settings);
$this->_set($settings);
$this->controller =& $controller;
return false;
}
/**
* Go through user panels and remove default panels as indicated.
*
* @param array $userPanels The list of panels ther user has added removed.
* @return array Array of panels to use.
**/
function _makePanelList($userPanels) {
$panels = $this->_defaultPanels;
foreach ($userPanels as $key => $value) {
if (is_numeric($key)) {
$panels[] = $value;
}
if (is_string($key) && $value === false) {
$index = array_search($key, $panels);
if ($index !== false) {
unset($panels[$index]);
}
}
}
return $panels;
}
/**
* Component Startup
*
* @return bool
**/
function startup(&$controller) {
$currentViewClass = $controller->view;
$this->_makeViewClass($currentViewClass);
$controller->view = 'DebugKit.Debug';
$isHtml = (
!isset($controller->params['url']['ext']) ||
(isset($controller->params['url']['ext']) && $controller->params['url']['ext'] == 'html')
);
if (!$this->RequestHandler->isAjax() && $isHtml) {
$format = 'Html';
} else {
$format = 'FirePhp';
}
$controller->helpers['DebugKit.Toolbar'] = array(
'output' => sprintf('DebugKit.%sToolbar', $format),
'cacheKey' => $this->cacheKey,
'cacheConfig' => 'debug_kit',
'forceEnable' => isset($this->settings['forceEnable'])? true : null,
);
$panels = array_keys($this->panels);
foreach ($panels as $panelName) {
$this->panels[$panelName]->startup($controller);
}
DebugKitDebugger::stopTimer('componentInit');
DebugKitDebugger::startTimer('controllerAction', __d('debug_kit', 'Controller action', true));
DebugKitDebugger::setMemoryPoint(__d('debug_kit', 'Controller action start', true));
}
/**
* beforeRedirect callback
*
* @return void
**/
function beforeRedirect(&$controller) {
if (!class_exists('DebugKitDebugger')) {
return null;
}
DebugKitDebugger::stopTimer('controllerAction');
$vars = $this->_gatherVars($controller);
$this->_saveState($controller, $vars);
}
/**
* beforeRender callback
*
* Calls beforeRender on all the panels and set the aggregate to the controller.
*
* @return void
**/
function beforeRender(&$controller) {
DebugKitDebugger::stopTimer('controllerAction');
$vars = $this->_gatherVars($controller);
$this->_saveState($controller, $vars);
$controller->set(array('debugToolbarPanels' => $vars, 'debugToolbarJavascript' => $this->javascript));
DebugKitDebugger::startTimer('controllerRender', __d('debug_kit', 'Render Controller Action', true));
DebugKitDebugger::setMemoryPoint(__d('debug_kit', 'Controller render start', true));
}
/**
* Load a toolbar state from cache
*
* @param int $key
* @return array
**/
function loadState($key) {
$history = Cache::read($this->cacheKey, 'debug_kit');
if (isset($history[$key])) {
return $history[$key];
}
return array();
}
/**
* Create the cache config for the history
*
* @return void
* @access protected
**/
function _createCacheConfig() {
if (Configure::read('Cache.disable') !== true) {
Cache::config('debug_kit', array(
'duration' => $this->cacheDuration,
'engine' => 'File',
'path' => CACHE
));
}
}
/**
* collects the panel contents
*
* @return array Array of all panel beforeRender()
* @access protected
**/
function _gatherVars(&$controller) {
$vars = array();
$panels = array_keys($this->panels);
foreach ($panels as $panelName) {
$panel =& $this->panels[$panelName];
$panelName = Inflector::underscore($panelName);
$vars[$panelName]['content'] = $panel->beforeRender($controller);
$elementName = Inflector::underscore($panelName) . '_panel';
if (isset($panel->elementName)) {
$elementName = $panel->elementName;
}
$vars[$panelName]['elementName'] = $elementName;
$vars[$panelName]['plugin'] = $panel->plugin;
$vars[$panelName]['title'] = $panel->title;
$vars[$panelName]['disableTimer'] = true;
}
return $vars;
}
/**
* Load Panels used in the debug toolbar
*
* @return void
* @access protected
**/
function _loadPanels($panels, $settings) {
foreach ($panels as $panel) {
$className = $panel . 'Panel';
if (!class_exists($className) && !App::import('Vendor', $className)) {
trigger_error(sprintf(__d('debug_kit', 'Could not load DebugToolbar panel %s', true), $panel), E_USER_WARNING);
continue;
}
if (strpos($className, '.') !== false) {
list($plugin, $className) = explode('.', $className);
}
$panelObj =& new $className($settings);
if (is_subclass_of($panelObj, 'DebugPanel') || is_subclass_of($panelObj, 'debugpanel')) {
$this->panels[$panel] =& $panelObj;
}
}
}
/**
* Makes the DoppleGangerView class if it doesn't already exist.
* This allows DebugView to be compatible with all view classes.
*
* @param string $baseClassName
* @access protected
* @return void
*/
function _makeViewClass($baseClassName) {
if (!class_exists('DoppelGangerView')) {
App::import('View', $baseClassName);
if (strpos($baseClassName, '.') !== false) {
list($plugin, $baseClassName) = explode('.', $baseClassName);
}
if (strpos($baseClassName, 'View') === false) {
$baseClassName .= 'View';
}
$class = "class DoppelGangerView extends $baseClassName {}";
$this->_eval($class);
}
}
/**
* Method wrapper for eval() for testing uses.
*
* @return void
**/
function _eval($code) {
eval($code);
}
/**
* Save the current state of the toolbar varibles to the cache file.
*
* @param object $controller Controller instance
* @param array $vars Vars to save.
* @access protected
* @return void
**/
function _saveState(&$controller, $vars) {
$config = Cache::config('debug_kit');
if (empty($config) || !isset($this->panels['history'])) {
return;
}
$history = Cache::read($this->cacheKey, 'debug_kit');
if (empty($history)) {
$history = array();
}
if (count($history) == $this->panels['history']->history) {
array_pop($history);
}
unset($vars['history']);
array_unshift($history, $vars);
Cache::write($this->cacheKey, $history, 'debug_kit');
}
}
/**
* Debug Panel
*
* Abstract class for debug panels.
*
* @package cake.debug_kit
*/
class DebugPanel extends Object {
/**
* Defines which plugin this panel is from so the element can be located.
*
* @var string
*/
var $plugin = null;
/**
* Defines the title for displaying on the toolbar.
*
* @var string
*/
var $title = null;
/**
* startup the panel
*
* Pull information from the controller / request
*
* @param object $controller Controller reference.
* @return void
**/
function startup(&$controller) { }
/**
* Prepare output vars before Controller Rendering.
*
* @param object $controller Controller reference.
* @return void
**/
function beforeRender(&$controller) { }
}
/**
* History Panel
*
* Provides debug information on previous requests.
*
* @package cake.debug_kit.panels
**/
class HistoryPanel extends DebugPanel {
var $plugin = 'debug_kit';
/**
* Number of history elements to keep
*
* @var string
**/
var $history = 5;
/**
* Constructor
*
* @param array $settings Array of settings.
* @return void
**/
function __construct($settings) {
if (isset($settings['history'])) {
$this->history = $settings['history'];
}
}
/**
* beforeRender callback function
*
* @return array contents for panel
**/
function beforeRender(&$controller) {
$cacheKey = $controller->Toolbar->cacheKey;
$toolbarHistory = Cache::read($cacheKey, 'debug_kit');
$historyStates = array();
if (is_array($toolbarHistory) && !empty($toolbarHistory)) {
$prefix = array();
if (!empty($controller->params['prefix'])) {
$prefix[$controller->params['prefix']] = false;
}
foreach ($toolbarHistory as $i => $state) {
if (!isset($state['request']['content']['params']['url']['url'])) {
continue;
}
$historyStates[] = array(
'title' => $state['request']['content']['params']['url']['url'],
'url' => array_merge($prefix, array(
'plugin' => 'debug_kit',
'controller' => 'toolbar_access',
'action' => 'history_state',
$i + 1))
);
}
}
if (count($historyStates) >= $this->history) {
array_pop($historyStates);
}
return $historyStates;
}
}
/**
* Variables Panel
*
* Provides debug information on the View variables.
*
* @package cake.debug_kit.panels
**/
class VariablesPanel extends DebugPanel {
var $plugin = 'debug_kit';
/**
* beforeRender callback
*
* @return array
**/
function beforeRender(&$controller) {
return array_merge($controller->viewVars, array('$this->data' => $controller->data));
}
}
/**
* Session Panel
*
* Provides debug information on the Session contents.
*
* @package cake.debug_kit.panels
**/
class SessionPanel extends DebugPanel {
var $plugin = 'debug_kit';
/**
* beforeRender callback
*
* @param object $controller
* @access public
* @return array
*/
function beforeRender(&$controller) {
$sessions = $controller->Session->read();
return $sessions;
}
}
/**
* Request Panel
*
* Provides debug information on the Current request params.
*
* @package cake.debug_kit.panels
**/
class RequestPanel extends DebugPanel {
var $plugin = 'debug_kit';
/**
* beforeRender callback - grabs request params
*
* @return array
**/
function beforeRender(&$controller) {
$out = array();
$out['params'] = $controller->params;
if (isset($controller->Cookie)) {
$out['cookie'] = $controller->Cookie->read();
}
$out['get'] = $_GET;
$out['currentRoute'] = Router::currentRoute();
return $out;
}
}
/**
* Timer Panel
*
* Provides debug information on all timers used in a request.
*
* @package cake.debug_kit.panels
**/
class TimerPanel extends DebugPanel {
var $plugin = 'debug_kit';
/**
* startup - add in necessary helpers
*
* @return void
**/
function startup(&$controller) {
if (!in_array('Number', $controller->helpers)) {
$controller->helpers[] = 'Number';
}
if (!in_array('SimpleGraph', $controller->helpers)) {
$controller->helpers[] = 'DebugKit.SimpleGraph';
}
}
}
/**
* SqlLog Panel
*
* Provides debug information on the SQL logs and provides links to an ajax explain interface.
*
* @package cake.debug_kit.panels
**/
class SqlLogPanel extends DebugPanel {
var $plugin = 'debug_kit';
/**
* Minimum number of Rows Per Millisecond that must be returned by a query before an explain
* is done.
*
* @var int
**/
var $slowRate = 20;
/**
* Get Sql Logs for each DB config
*
* @param string $controller
* @access public
* @return void
*/
function beforeRender(&$controller) {
if (!class_exists('ConnectionManager')) {
return array();
}
App::import('Core', 'Xml');
$queryLogs = array();
$dbConfigs = ConnectionManager::sourceList();
foreach ($dbConfigs as $configName) {
$db =& ConnectionManager::getDataSource($configName);
if ($db->isInterfaceSupported('showLog')) {
ob_start();
$db->showLog();
$htmlBlob = ob_get_clean();
$Xml =& new Xml($htmlBlob);
$table = $Xml->children[0];
$tbody = $table->children('tbody');
$rows = $tbody[0]->children;
if (empty($rows) || empty($rows[0]->children)) {
continue;
}
$queries = $explained = array();
foreach ($rows as $row) {
$tds = $this->_getCells($row);
$queries[] = $tds;
$isSlow = (
$tds[5] > 0 &&
$tds[4] / $tds[5] != 1 &&
$tds[4] / $tds[5] <= $this->slowRate
);
if ($isSlow && preg_match('/^SELECT /', $tds[1])) {
$explain = $this->_explainQuery($db, $tds[1]);
if (!empty($explain)) {
$explained[] = $explain;
}
}
}
$queryLogs[$configName]['queries'] = $queries;
$queryLogs[$configName]['explains'] = $explained;
}
}
return $queryLogs;
}
/**
* get cell values from xml
*
* @param array of XmlElements.
* @return array Array of extracted values.
**/
function _getCells(&$rowXml) {
$tds = array();
foreach ($rowXml->children as $cell) {
if ($cell->hasChildren()) {
$tds[] = $cell->children[0]->value;
} else {
$tds[] = $cell->value;
}
}
return $tds;
}
/**
* Run an explain query for a slow query.
*
* @param object $db Dbo instance
* @param string $queryString The Query to explain
* @access public
* @return void
**/
function _explainQuery(&$db, $queryString) {
$driver = $db->config['driver'];
$return = null;
if ($driver === 'mysqli' || $driver === 'mysql' || $driver === 'postgres') {
$results = $db->query('EXPLAIN ' . $queryString);
if ($driver === 'postgres') {
$queryPlan = array();
foreach ($results as $postgreValue) {
$queryPlan[] = array($postgreValue[0]['QUERY PLAN']);
}
$results = array_merge(array(array('')), $queryPlan);
} else {
$keys = array_keys($results[0][0]);
foreach ($results as $mysqlValue) {
$queryPlan[] = array_values($mysqlValue[0]);
}
$results = array_merge(array($keys), $queryPlan);
}
$return['explain'] = $results;
$return['query'] = $queryString;
}
return $return;
}
}
/**
* Log Panel - Reads log entries made this request.
*
* @package cake.debug_kit.panels
*/
class LogPanel extends DebugPanel {
var $plugin = 'debug_kit';
/**
* Log files to scan
*
* @var array
*/
var $logFiles = array('error.log', 'debug.log');
/**
* startup
*
* @return void
**/
function startup(&$controller) {
if (!class_exists('CakeLog')) {
App::import('Core', 'Log');
}
}
/**
* beforeRender Callback
*
* @return array
**/
function beforeRender(&$controller) {
$this->startTime = DebugKitDebugger::requestStartTime();
$this->currentTime = DebugKitDebugger::requestTime();
$out = array();
foreach ($this->logFiles as $log) {
$file = LOGS . $log;
if (!file_exists($file)) {
continue;
}
$out[$log] = $this->_parseFile($file);
}
return $out;
}
/**
* parse a log file and find the relevant entries
*
* @param string $filename Name of file to read
* @access protected
* @return array
*/
function _parseFile($filename) {
$fh = fopen($filename, 'r');
$timePattern = '/^(\d{4}-\d{2}\-\d{2}\s\d{1,2}\:\d{1,2}\:\d{1,2})\s(.*)/';
$out = array();
$entry = '';
$done = false;
while (!feof($fh)) {
$line = fgets($fh);
if (preg_match($timePattern, $line, $matches)) {
if (strtotime($matches[1]) < $this->startTime) {
continue;
}
$out[] = $matches[1];
$out[] = $matches[2];
} elseif (count($out) - 1 > 0) {
$currentIndex = count($out) - 1;
while (!feof($fh)) {
$line = fgets($fh);
if (preg_match($timePattern, $line)) {
break;
}
$out[$currentIndex] .= $line;
}
}
}
fclose($fh);
return $out;
}
}
?>