includes/debugger/debugConsole.class.php
author Dan
Wed, 21 Nov 2007 21:03:48 -0500
changeset 279 8acd77a6c19d
parent 1 fe660c52c48f
permissions -rw-r--r--
Merging in updates and improvements from stable

<?php
/**
 * debugConsole class
 *
 * This class allows opening an external JavaScript
 * window for debugging purposes.
 *
 * @author Andreas Demmer <info@debugconsole.de>
 * @see <http://www.debugconsole.de>
 * @version 1.2.1
 * @package debugConsole_1.2.1
 */
class debugConsole {
	/**
	 * events which are shown in debug console
	 *
	 * @var array
	 */
	protected $filters;
	
	/**
	 * all watched variables with their current content
	 *
	 * @var array
	 */
	protected $watches;
	
	/**
	 * debugConsole configuration values
	 *
	 * @var array
	 */
	protected $config;
	
	/**
	 * URL where template can be found
	 *
	 * @var string
	 */
	protected $template;

	/**
	 * javascripts to control popup
	 *
	 * @var array
	 */
	protected $javascripts;

	/**
	 * html for popup
	 *
	 * @var array
	 */
	protected $html;

	/**
	 * time of debugrun start in milliseconds
	 *
	 * @var string
	 */
	protected $starttime;

	/**
	 * time of timer start in milliseconds
	 *
	 * @var array
	 */
	protected $timers;

	/**
	 * constructor, opens popup window
	 */
	public function __construct () {
		/* initialize class vars */
		$this->starttime = $this->getMicrotime();
		$this->watches = array ();
		$this->config = $GLOBALS['_debugConsoleConfig'];
		$this->html = $this->config['html'];
		$this->html['header'] = str_replace("\n\r", NULL, $this->html['header']);
		$this->html['header'] = str_replace("\n", NULL, $this->html['header']);
		$this->javascripts = $this->config['javascripts'];
		
		/* replace PHP's errorhandler */
		$errorhandler = array (
			$this,
			'errorHandlerCallback'
		);
		
		set_error_handler($errorhandler);
		
		/* open popup */
		$popupOptions = "', 'debugConsole', 'width=" . $this->config['dimensions']['width'] . ",height=" . $this->config['dimensions']['height'] . ',scrollbars=yes';
		
		$this->sendCommand('openPopup', $popupOptions);
		$this->sendCommand('write', $this->html['header']);
		
		$this->startDebugRun();
	}
	
	/**
	 * destructor, shows runtime and finishes html document in popup window
	 */
	public function __destruct () {
		$runtime = $this->getMicrotime() - $this->starttime;
		$runtime = number_format((float)$runtime, 4, '.', NULL);
		
		$info = '<p class="runtime">This debug-run took ' . $runtime . ' seconds to complete.</p>';

		$this->sendCommand('write', $info);
		$this->sendCommand('write', '</div>');
		$this->sendCommand('scroll', "0','100000");
		$this->sendCommand('write', $this->html['footer']);
		
		if ($this->config['focus']) {
			$this->sendCommand('focus');
		}
	}
	
	/**
	 * show new debug run header in console
	 */
	
	protected function startDebugRun () {
		$info = '<h1>new debug-run (' . date('H:i') . ' hours)</h1>';
		$this->sendCommand('write', '<div>');
		$this->sendCommand('write', $info);
	}

	/**
	 * adds a variable to the watchlist
	 * 
	 * Watched variables must be in a declare(ticks=n)
	 * block so that every n ticks the watched variables
	 * are checked for changes. If any changes were made,
	 * the new value of the variable is shown in the
	 * debugConsole with additional information where the
	 * changes happened.
	 *
	 * @param string $variableName
	 */
	public function watchVariable ($variableName) {
		if (count($this->watches) === 0) {
			$watchMethod = array (
				$this,
				'watchesCallback'
			);
			
			register_tick_function($watchMethod);
		}
		
		if (isset($GLOBALS[$variableName])) {
			$this->watches[$variableName] = $GLOBALS[$variableName];
		} else {
			$this->watches[$variableName] = NULL;
		}
	}
	
	/**
	 * tick callback: process watches and show changes
	 */
	public function watchesCallback () {
		if ($this->config['filters']['watches']) {
			foreach ($this->watches as $variableName => $variableValue) {
				if ($GLOBALS[$variableName] !== $this->watches[$variableName]) {
					$info = '<p class="watch"><strong>$' . $variableName;
					$info .= '</strong> changed from "';
					$info .= $this->watches[$variableName];
					$info .= '" (' . gettype($this->watches[$variableName]) . ')';
					$info .= ' to "' . $GLOBALS[$variableName] . '" (';
					$info .= gettype($GLOBALS[$variableName]) . ')';
					$info .= $this->getTraceback() . '</p>';
					
					$this->watches[$variableName] = $GLOBALS[$variableName];
					$this->sendCommand('write', $info);
				}
			}
		}
	}
	
	/**
	 * sends a javascript command to browser
	 *
	 * @param string $command
	 * @param string $value
	 */
	protected function sendCommand ($command, $value = FALSE) {
    if($command == 'write') $value = '\'+unescape(\''.rawurlencode($value).'\')+\'';
		$value = str_replace('\\', '\\\\', $value);
    $value = nl2br($value);
		
		if ((bool)$value) { 
			/* write optionally logfile */
			$this->writeLogfileEntry($command, $value);
			
			$command = $this->javascripts[$command] . "('" . $value . "');";
		} else {
			$command = $this->javascripts[$command] . ';';
		}
		
		$command = str_replace("\n\r", NULL, $command);
		$command = str_replace("\n", NULL, $command);
		
		if (!$this->config['logfile']['disablePopup']) {
			echo $this->javascripts['openTag'], "\n";
			echo $command, "\n";
			echo $this->javascripts['closeTag'], "\n";
		}
		
		flush();
	}
	
	/**
	 * writes html output as text entry into logfile
	 *
	 * @param string $command
	 * @param string $value
	 */
	protected function writeLogfileEntry ($command, $value) {
		if ($this->config['logfile']['enable']) {
			$logfile = $this->config['logfile']['path'] . $this->config['logfile']['filename'];
			/* log only useful entries, no html header and footer */
			if (
				$command === 'write'
				&& !strpos($value, '<html>')
				&&  !strpos($value, '</html>')
			) {
				/* convert html to text */
				$value = html_entity_decode($value);
				$value = str_replace('>', '> ', $value);
				$value = strip_tags($value);
				
				$fp = fopen($logfile, 'a+');
				fputs($fp, $value . "\n\n");
				fclose($fp);
			} elseif (strpos($value, '</html>')) {
				$fp = fopen($logfile, 'a+');
				fputs($fp, "-----------\n");
				fclose($fp);
			}
		}
	}

	/**
	 * shows in console that a checkpoint has been passed,
	 * additional info is the file and line which triggered
	 * the output
	 *
	 * @param string $message
	 */	
	public function passedCheckpoint ($message = NULL) {
		if ($this->config['filters']['checkpoints']) {
			$message = (bool)$message ? $message : 'Checkpoint passed!';
	
			$info = '<p class="checkpoint"><strong>' . $message . '</strong>';
			$info .= $this->getTraceback() . '</p>';
			
			$this->sendCommand('write', $info);
		}
	}
	
	/**
	 * returns microtime as float value
	 *
	 * @return float
	 */
	protected function getMicrotime () {
		list($usec, $sec) = explode(' ', microtime()); 
    	return ((float)$usec + (float)$sec);
	}
	
	/**
	 * returns all possible filter events for debugConsole::setFilter() method
	 *
	 * @return array
	 */
	public function getFilters () {
		$filters = array_keys($this->config['filters']);
		
		ksort($filters);
		reset($filters);
		
		return $filters; 
	}
	
	/**
	 * shows or hides an event-type in debugConsole,
	 * returns previous setting of the given event-type
	 *
	 * @param string $event
	 * @param bool $isShown
	 * @return bool
	 */
	public function setFilter ($event, $isShown) {
		if (array_key_exists($event, $this->config['filters'])) {
			$oldValue = $this->config['filters'][$event];
			$this->config['filters'][$event] = $isShown;
		} else {
			throw new Exception ('debugConsole: unknown event "' . $event . '" in debugConsole::filter()');
		}
		
		return $oldValue;
	}
	
	/**
	 * show debug info for variable in debugConsole,
	 * added by custom text for documentation and hints
	 *
	 * @param mixed $variable
	 * @param string $text
	 */
	public function dump ($variable, $text) {
		if ($this->config['filters']['debug']) {
			@ob_start();
			
			/* grab current ob content */
			$obContents = ob_get_contents();
			ob_clean();
			
			/* grap var dump from ob */
			var_dump($variable);
			$variableDebug = ob_get_contents();
			ob_end_clean();
			
			/* restore previous ob content */
			if ((bool)$obContents) echo $obContents;
			
			/* render debug */
			$variableDebug = htmlspecialchars($variableDebug);			
			$infos = '<p class="dump">' . $text . '<br />';
			
			if (is_array($variable)) {
				$variableDebug = str_replace(' ', '&nbsp;', $variableDebug);
				$infos .= '<span class="source">' . $variableDebug . '</span>';
			} else {
				$infos .= '<strong>' . $variableDebug . '</strong>';
			}
			
			$infos .= $this->getTraceback() . '</p>';
			$this->sendCommand('write', $infos);
		}
	}
	
	/**
	 * callback method for PHP errorhandling
	 * 
	 * @todo implement more errorlevels
	 */
	public function errorHandlerCallback () {
		$details = func_get_args();
		$details[1] = str_replace("'", '"', $details[1]);
		$details[1] = str_replace('href="function.', 'target="_blank" href="http://www.php.net/', $details[1]);
		
		
		/* determine error level */
		switch ($details[0]) {
			case 2:
				if (!$this->config['filters']['php_warnings']) return;
				$errorlevel = 'warning';
				break;
			case 8:
				if (!$this->config['filters']['php_notices']) return;
				$errorlevel = 'notice';
				break;
			case 2048:
				if (!$this->config['filters']['php_suggestions']) return;
				$errorlevel = 'suggestion';
				break;
		}

		$file = $this->cropScriptPath($details[2]);
		
		$infos = '<p class="' . $errorlevel . '"><strong>';
		$infos .= 'PHP ' . strtoupper($errorlevel) . '</strong>';
		$infos .= $details[1] . '<span class="backtrace">';
		$infos .= $file . ' on line ';
		$infos .= $details[3] . '</span></p>';		
		
		$this->sendCommand('write', $infos);
	}
	
	/**
	 * start timer clock, returns timer handle
	 * 
	 * @return mixed
	 * @param string $comment
	 */
	public function startTimer ($comment) {
		if ($this->config['filters']['timers']) {
			$timerHandle = md5(microtime());
			
			$this->timers[$timerHandle] = array (
				'starttime' => $this->getMicrotime(),
				'comment' => $comment
			);
		} else {		
			$timerHandle = FALSE;		
		}
		
		return $timerHandle;
	}
	
	/**
	 * stop timer clock
	 * 
	 * @return bool
	 * @param string $timerHandle
	 */
	public function stopTimer ($timerHandle) {
		if ($this->config['filters']['timers']) {
			if (array_key_exists($timerHandle, $this->timers)) {
				$timerExists = TRUE;
				$timespan = $this->getMicrotime() - $this->timers[$timerHandle]['starttime'];
			
				$info = '<p class="timer"><strong>' . $this->timers[$timerHandle]['comment'];
				$info .= '</strong><br />The timer ran ';
				$info .= '<strong>' . number_format ($timespan, 4, '.', NULL) . '</strong>';
				$info .= ' seconds.' . $this->getTraceback() . '</p>';
			
				$this->sendCommand('write', $info);
			} else {
				$timerExists = FALSE;
			}
		} else {
			$timerExists = FALSE;
		}
		
		return $timerExists;
	}
	
	/**
	 * returns a formatted traceback string
	 *
	 * @return string
	 */
	public function getTraceback () {
		$callStack = debug_backtrace();

		$debugConsoleFiles = array(
			'debugConsole.class.php',
			'debugConsole.functions.php'
		);
		
		$call = array (
			'file' => 'debugConsole.class.php'
		);
		
		while(in_array(basename($call['file']), $debugConsoleFiles)) {
			$call = array_shift($callStack);
		}

		$call['file'] = $this->cropScriptPath($call['file']);
		
		$traceback = '<span class="backtrace">';
		$traceback .= $call['file'] . ' on line ';
		$traceback .= $call['line'] . '</span>';
		
		return $traceback;
	}
	
	/**
	 * crops long script path, shows only the last $maxLength chars
	 *
	 * @param string $path
	 * @param int $maxLength
	 * @return string
	 */
	protected function cropScriptPath ($path, $maxLength = 30) {
		if (strlen($path) > $maxLength) {
			$startPos = strlen($path) - $maxLength - 2;
			
			if ($startPos > 0) {
				$path = '...' . substr($path, $startPos);
			}
		}

		return $path;
	}
}
?>