Show:

File: platform/classes/Q.php

<?php

/**
 * Contains core Qbix functionality.
 * @module Q
 * @main Q
 */
/**
 * Core Qbix platform functionality
 * @class Q
 * @static
 */

class Q
{
	/**
	 * Returns the id of the currently loaded app, found in the config under "Q"/"app"
	 * @return {string}
	 */
	static function app()
	{
		return Q_Config::expect('Q', 'app');
	}
	
	/**
	 * Used for shorthand for avoiding when you don't want to write
	 * (isset($some_long_expression) ? $some_long_expression: null)
	 * when you want to avoid possible "undefined variable" errors.
	 * @method ifset
	 * @param {&mixed} $ref
	 *  The reference to test. Only lvalues can be passed.
	 *  If $ref is an array or object, it can be followed by one or more
	 *  strings or numbers, which will be used to index deeper into
	 *  the contained arrays or objects.
	 *  You can also pass arrays instead of the strings and numbers,
	 *  which will then widen the search to try all combinations
	 *  of the strings and numbers in all the arrays, before returning
	 *  the default.
	 * @param {mixed} $def=null
	 *  The default, if the reference isn't set
	 * @return {mixed}
	 */
	static function ifset(& $ref, $def = null)
	{
		$count = func_num_args();
		if ($count <= 2) {
			return isset($ref) ? $ref : $def;
		}
		$args = func_get_args();
		$ref2 = $ref;
		$def = end($args);
		for ($i=1; $i<$count-1; ++$i) {
			$key = $args[$i];
			if (!is_array($key)) {
				$key = array($key);
			}
			if (is_array($ref2)) {
				foreach ($key as $k) {
					if (array_key_exists($k, $ref2)) {
						$ref2 = $ref2[$k];
						continue 2;
					}
				}
				return $def;
			} else if (is_object($ref2)) {
				foreach ($key as $k) {
					if (isset($ref2->$k)) {
						$ref2 = $ref2->$k;
						continue 2;
					}
				}
				return $def;
			} else {
				return $def;
			}
		}
		return $ref2;
	}


	/**
	 * Returns the number of milliseconds since the
	 * first call to this function (i.e. since script started).
	 * @method milliseconds
	 * @param {Boolean} $sinceEpoch
	 *  Defaults to false. If true, just returns the number of milliseconds in the UNIX timestamp.
	 * @return {float}
	 *  The number of milliseconds, with fractional part
	 */
	static function milliseconds ($sinceEpoch = false)
	{
		$result = microtime(true)*1000;
		if ($sinceEpoch) {
			return $result;
		}

		static $microtime_start;
		if (empty($microtime_start)) {
			$microtime_start = $result;
		}
		return $result - $microtime_start;
	}

	/**
	 * Default exception handler for Q
	 * @method exceptionHandler
	 * @param {Exception} $exception
	 */
	static function exceptionHandler (
	 $exception)
	{
		try {
			/**
			 * @event Q/exception
			 * @param {Exception} $exception
			 */
			self::event('Q/exception', compact('exception'));
		} catch (Exception $e) {
			/**
			 * @event Q/exception/native
			 * @param {Exception} $exception
			 */
			// Looks like the app's custom Q/exception handler threw
			// an exception itself. Just show Q's native exception handler.
			self::event('Q/exception/native', compact('exception'));
		}
	}

	/**
	 * Error handler
	 * @method errorHandler
	 * @param {integer} $errno
	 * @param {string} $errstr
	 * @param {string} $errfile
	 * @param {integer} $errline
	 * @param {array} $errcontext
	 */
	static function errorHandler (
		$errno,
		$errstr,
		$errfile,
		$errline,
		$errcontext)
	{
	    if (!(error_reporting()  & $errno)) {
	        // This error code is not included in error_reporting
			// just continue on with execution, if possible.
			// this situation can also happen when
			// someone has used the @ operator.
	        return;
	    }
		switch ($errno) {
			case E_WARNING:
			case E_NOTICE:
			case E_USER_WARNING:
			case E_USER_NOTICE:
				$context = Q::var_dump($errcontext, 4, '$', 'text');
				$dump = Q_Exception::coloredString(
					$errstr, $errfile, $errline, $context
				);
				Q::log("PHP Error($errno): \n\n$dump", null, null, array(
					'maxLength' => 10000
				));
				$type = 'warning';
				break;
			default:
				$type = 'error';
				break;
		}
		/**
		 * @event Q/error
		 * @param {integer} errno
		 * @param {string} errstr
		 * @param {string} errfile
		 * @param {integer} errline
		 * @param {array} errcontext
		 */
		self::event("Q/error", compact(
			'type','errno','errstr','errfile','errline','errcontext'
		));
	}

	/**
	 * Test whether $text is prefixed by $prefix
	 * @method startsWith
	 * @static
	 * @param {string|array} $text The string or array of strings to check
	 * @param {string} $prefix
	 * @return {boolean}
	 */
	static function startsWith($text, $prefix)
	{
		if (is_string($text)) {
			return substr($text, 0, strlen($prefix)) === $prefix;	
		}
		if (is_array($text)) {
			foreach ($text as $t) {
				if (!self::startsWith($t, $prefix)) {
					return false;
				}
			}
			return true;
		}
	}

	/**
	 * Goes through the params and replaces any references
	 * to their names in the string with their value.
	 * References are expected to be of the form {{varname}} or $varname.
	 * However, dollar signs prefixed with backslashes will not be replaced.
	 * @method interpolate
	 * @static
	 * @param {string} $expression
	 *  The string containing possible references to interpolate values for.
	 * @param {array|string} $params=array()
	 *  An array of parameters to the expression.
	 *  Variable names in the expression can refer to them.
	 *  You can also pass an indexed array, in which case the expression's
	 *  placeholders of the form {{0}}, {{1}}, or $0, $1 will be replaced by the
	 *  corresponding strings.
	 *  If the expression is missing {{0}} and $0, then {{1}} or $1 is replaced
	 *  by the first string, {{2}} or $2 by the second string, and so on.
	 * @return {string}
	 *  The result of the interpolation
	 */
	static function interpolate(
		$expression,
		$params = array())
	{
		$a = (
			strpos($expression, '{{0}}') === false
			and strpos($expression, '$0') === false
		) ? 1 : 0;
		$keys = array_keys($params);
		usort($keys, array(__CLASS__, 'reverseLengthCompare'));
		$expression = str_replace('\\$', '\\REAL_DOLLAR_SIGN\\', $expression);
		foreach ($keys as $key) {
			$p = (is_array($params[$key]) or is_object($params[$key]))
				? substr(Q::json_encode($params[$key]), 0, 100)
				: (string)$params[$key];
			if (is_numeric($key) and floor($key) == ceil($key)) {
				$key = $key + $a;
			}
			$expression = str_replace('$'.$key, $p, $expression);
			$expression = str_replace('{{'.$key.'}}', $p, $expression);
		}
		$expression = str_replace('\\REAL_DOLLAR_SIGN\\', '\\$', $expression);
		return $expression;
	}
	
	/**
	 * Evaluates a string containing an expression,
	 * with possible references to parameters.
	 * CAUTION: make sure the expression is safe!!
	 * @method evalExpression
	 * @static
	 * @param {string} $expression
	 *  The code to eval.
	 * @param {array} $params=array()
	 *  Optional. An array of parameters to the expression.
	 *  Variable names in the expression can refer to them.
	 * @return {mixed}
	 *  The result of the expression
	 */
	static function evalExpression(
	 $expression,
	 $params = array())
	{
		if (is_array($params)) {
			extract($params);
		}
		@eval('$value = ' . $expression . ';');
		extract($params);
		/**
		 * @var $value
		 */
		return $value;
	}

	/**
	 * Use for surrounding text, so it can later be processed throughout.
	 * @method t
	 * @static
	 * @param {string} $text
	 * @return {string}
	 */
	static function t($text)
	{
		/**
		 * @event Q/t {before}
		 * @return {string}
		 */
		$text = Q::event('Q/t', array(), 'before', false, $text);
		return $text;
	}
	
	/**
	 * A convenience method to use in your PHP templates.
	 * It is short for Q_Html::text(Q::interpolate($expression, ...)).
	 * In Handlebars templates, you just use {{interpolate expression ...}}
	 * @method text
	 * @static
	 * @param {string} $expression Same as in Q::interpolate()
	 * @param {array} $params Same as in Q::interpolate()
	 * @param {string} [$convert=array()] Same as in Q_Html::text().
	 * @param {string} [$unconvert=array()] Same as in Q_Html::text().
	 */
	static function text($expression, $params = array(), $convert = array(), $unconvert = array())
	{
		return Q_Html::text(Q::interpolate($expression, $params), $convert, $unconvert);
	}

	/**
	 * Check if a file exists in the include path
	 * And if it does, return the absolute path.
	 * @method realPath
	 * @static
	 * @param {string} $filename
	 *  Name of the file to look for
	 * @param {boolean} $ignoreCache=false
	 *  Defaults to false. If true, then this function ignores
	 *  the cached value, if any, and attempts to search
	 *  for the file. It will cache the new value.
	 * @return {string|false}
	 *  The absolute path if file exists, false if it does not
	 */
	static function realPath (
		$filename,
		$ignoreCache = false)
	{
		$filename = str_replace('/', DS, $filename);
		if (!$ignoreCache) {
			// Try the extended cache mechanism, if any
			$result = Q::event('Q/realPath', array(), 'before');
			if (isset($result)) {
				return $result;
			}
			// Try the native cache mechanism
			$result = Q_Cache::get("Q::realPath\t$filename");
			if (isset($result)) {
				return $result;
			}
		}

		// Do a search for the file
	    $paths = explode(PS, get_include_path());
		array_unshift($paths, "");
		$result = false;
	    foreach ($paths as $path) {
			if (substr($path, -1) == DS) {
	        	$fullpath = $path.$filename;
			} else {
				$fullpath = ($path ? $path . DS : "") . $filename;
			}
			// Note: the following call to the OS may take some time:
			$realpath = realpath($fullpath);
			if ($realpath && file_exists($realpath)) {
	            $result = $realpath;
				break;
	        }
	    }

		// Notify the cache mechanism, if any
		Q_Cache::set("Q::realPath\t$filename", $result);
		/**
		 * @event Q/realPath {after}
		 * @param {string} $result
		 */
		Q::event('Q/realPath', compact('result'), 'after');

	    return $result;

	}

	/**
	 * Includes a file and evaluates code from it. Uses Q::realPath().
	 * @method includeFile
	 * @static
	 * @param {string} $filename
	 *  The filename to include
	 * @param {array} $params=array()
	 *  Optional. Extracts this array before including the file.
	 * @param {boolean} $once=false
	 *  Optional. Whether to use include_once instead of include.
	 * @param {boolean} $get_vars=false
	 *  Optional. If true, returns the result of get_defined_vars() at the end.
	 *  Otherwise, returns whatever the file returned.
	 * @return {mixed}
	 *  Depends on $get_vars
	 * @throws {Q_Exception_MissingFile}
	 *  May throw a Q_Exception_MissingFile exception.
	 */
	static function includeFile(
		$filename,
		array $params = array(),
		$once = false,
		$get_vars = false)
	{
		/**
		 * Skips includes to prevent recursion
		 * @event Q/includeFile {before}
		 * @param {string} filename
		 *  The filename to include
		 * @param {array} params
		 *  Optional. Extracts this array before including the file.
		 * @param {boolean} once
		 *  Optional. Whether to use include_once instead of include.
		 * @param {boolean} get_vars
		 *  Optional. Set to true to return result of get_defined_vars()
		 *  at the end.
		 * @return {mixed}
		 *  Optional. If set, override method return
		 */
		$result = self::event(
			'Q/includeFile',
			compact('filename', 'params', 'once', 'get_vars'),
			'before',
			true
		);
		if (isset($result)) {
			// return this result instead
			return $result;
		}

		$abs_filename = self::realPath($filename);

		if (!$abs_filename or is_dir($abs_filename)) {
			$include_path = get_include_path();
			require_once(Q_CLASSES_DIR.DS.'Q'.DS.'Exception'.DS.'MissingFile.php');
			throw new Q_Exception_MissingFile(compact('filename', 'include_path'));
		}

		extract($params);
		if ($get_vars === true) {
			if ($once) {
				if (!isset(self::$included_files[$filename])) {
					self::$included_files[$filename] = true;
					include_once($abs_filename);
				}
			} else {
				include($abs_filename);
			}
			return get_defined_vars();
		} else {
			if ($once) {
				if (!isset(self::$included_files[$filename])) {
					self::$included_files[$filename] = true;
					return include_once($abs_filename);
				}
			} else {
				return include($abs_filename);
			}
		}
	}

	/**
	 * Reads a file and caches it for a time period. Uses Q::realPath().
	 * @method readFile
	 * @static
	 * @param {string} $filename The name of the file to get the content of
	 * @param {array} $options
	 * @param {integer} [$options.duration=0] Number of seconds to cache it for
	 * @param {boolean} [$options.dontCache=false] whether to skip caching it
	 * @param {boolean} [$options.ignoreCache=false] whether to ignore already cached result
	 * @return {string} the content of the file
	 * @throws {Q_Exception_MissingFile}
	 *  May throw a Q_Exception_MissingFile exception.
	 */
	static function readFile(
		$filename,
		$options = array())
	{
		/**
		 * Skips includes to prevent recursion
		 * @event Q/readFile {before}
		 * @param {string} $filename The name of the file to get the content of
		 * @param {array} $options
		 * @param {integer} $options.duration Number of seconds to cache it for
		 * @param {boolean} $options.dontCache whether to skip caching it
		 * @param {boolean} $options.ignoreCache whether to ignore already cached result
		 * @return {string} Optional. If set, override method return
		 */
		$result = self::event(
			'Q/readFile',
			compact('filename', 'params', 'once', 'get_vars'),
			'before',
			true
		);
		if (isset($result)) {
			// return this result instead
			return $result;
		}
		
		$namespace = "Q::readFile\t$filename";
		if (empty($options['ignoreCache'])) {
			$result = Q_Cache::get('content', null, $namespace);
		}

		$abs_filename = self::realPath($filename);
		if (!$abs_filename or is_dir($abs_filename)) {
			$include_path = get_include_path();
			require_once(Q_CLASSES_DIR.DS.'Q'.DS.'Exception'.DS.'MissingFile.php');
			throw new Q_Exception_MissingFile(compact('filename', 'include_path'));
		}
		
		if (!isset($result)) {
			$result = file_get_contents($abs_filename);
		}
		if (empty($options['dontCache'])) {
			$duration = Q::ifset($options, 'duration', 0);
			Q_Cache::set('content', $result, $namespace);
			Q_Cache::setDuration($duration, $namespace);
		}
		return $result;
	}

	/**
	 * Default autoloader for Q
	 * @method autoload
	 * @static
	 * @param {string} $className
	 * @throws {Q_Exception_MissingClass}
	 *	If requested class is missing
	 */
	static function autoload(
	 $className)
	{
		if (class_exists($className, false)) {
			return;
		}
		try {
			$parts = array();
			$className_parts = explode('\\', $className);
			foreach ($className_parts as $part) {
				$parts = array_merge($parts, explode('_', $part));
			}
			$filename = 'classes'.DS.implode(DS, $parts).'.php';
			
			/**
			 * @event Q/autoload {before}
			 * @param {string} $className
			 * @return {string} the filename of the file to load
			 */
			$filename = self::event(
				'Q/autoload', compact('className'), 
				'before', false, $filename
			);

			// Now we can include the file
			try {
				self::includeFile($filename);
			} catch (Q_Exception_MissingFile $e) {
				// the file doesn't exist
				// and you will get an error if you try to use the class
			}

			// if (!class_exists($className) && !interface_exists($className)) {
			// 	require_once(Q_CLASSES_DIR.DS.'Q'.DS.'Exception'.DS.'MissingClass.php');
			// 	throw new Q_Exception_MissingClass(compact('className'));
			// }

			/**
			 * @event Q/autoload {after}
			 * @param {string} className
			 * @param {string} filename
			 */
			self::event('Q/autoload', compact('className', 'filename'), 'after');

		} catch (Exception $exception) {
			/**
			 * @event Q/exception
			 * @param {Exception} exception
			 */
			self::event('Q/exception', compact('exception'));
		}
	}

	/**
	 * Renders a particular view
	 * @method view
	 * @static
	 * @param {string} $viewName
	 *  The full name of the view
	 * @param {array} $params=array()
	 *  Parameters to pass to the view
	 * @param {array} $options Some options
	 * @param {string|null} $options.language Preferred language
	 * @return {string}
	 *  The rendered content of the view
	 * @throws {Q_Exception_MissingFile}
	 */
	static function view(
	 $viewName,
	 $params = array(),
	 $options = array())
	{
		require_once(Q_CLASSES_DIR.DS.'Q'.DS.'Exception'.DS.'MissingFile.php');

		if (empty($params)) {
			$params = array();
		}

		$parts = explode('/', $viewName);
		$viewPath = implode(DS, $parts);
		if ($fields = Q_Config::get('Q', 'views', 'fields', null)) {
			$params = array_merge($fields, $params);
		}

		// set options
		$options['language'] = isset($options['language']) ? $options['language'] : null;

		$params = array_merge(Q_Text::params($parts, array('language' => $options['language'])), $params);

		/**
		 * @event {before} Q/view
		 * @param {string} viewName
		 * @param {string} viewPath
		 * @param {string} params
		 * @return {string}
		 *  Optional. If set, override method return
		 */
		$result = self::event('Q/view', compact(
			'viewName', 'viewPath', 'params'
		), 'before');
		if (isset($result)) {
			return $result;
		}

		try {
			$ob = new Q_OutputBuffer();
			self::includeFile('views'.DS.$viewPath, $params);
			return $ob->getClean();
		} catch (Q_Exception_MissingFile $e) {
			if (basename($e->params['filename']) != basename($viewPath)) {
				throw $e;
			}
			$ob->flushHigherBuffers();
			/**
			 * Renders 'Missing View' page
			 * @event Q/missingView
			 * @param {string} viewName
			 * @param {string} viewpath
			 * @return {string}
			 */
			return self::event('Q/missingView', compact('viewName', 'viewPath', 'params'));
		}
	}

	/**
	 * Instantiates a particular tool.
	 * Also generates javascript around it.
	 * @method tool
	 * @static
	 * @param {string} $name
	 *  The name of the tool, of the form "$moduleName/$toolName"
	 *  The handler is found in handlers/$moduleName/tool/$toolName
	 *  Also can be an array of $toolName => $toolOptions, in which case the
	 *  following parameter, $options, is skipped.
	 * @param {array} $options=array()
	 *  The options passed to the tool (or array of options arrays passed to the tools).
	 * @param {array} [$extra=array()] Options used by Qbix when rendering the tool.
	 * @param {string} [$extra.id]
	 *    An additional ID to distinguish tools instantiated
	 *    side-by-side from each other, within the same parent HTMLElement.
	 * @param {string} [$extra.prefix]
	 *    Set a custom prefix for the tool's id
	 * @param {boolean} [$extra.cache=false]
	 *    If true, then the Qbix front end will not replace existing tools with same id
	 *    during Q.loadUrl when this tool appears in the rendered HTML
	 * @param {boolean} [$extra.merge=false]
	 *    If true, the element for this tool is merged with the element of the tool
	 *    already being rendered (if any), producing one element with markup
	 *    for both tools and their options. This can be used more than once, merging
	 *    multiple tools in one element.
	 *    As part of the mege, the content this tool (if any) is prepended
	 *    to the content of the tool which is already being rendered.
	 * @throws {Q_Exception_WrongType}
	 * @throws {Q_Exception_MissingFile}
	 */
	static function tool(
	 $name,
	 $options = array(),
	 $extra = array())
	{
		if (is_string($name)) {
			$info = array($name => $options);
		} else {
			$info = $name;
			$extra = $options;
		}
		
		$oldToolName = self::$toolName;
		
		/**
		 * @event Q/tool/render {before}
		 * @param {string} info
		 *  An array of $toolName => $options pairs
		 * @param {array} extra
		 *  Options used by Qbix when rendering the tool. Can include:<br/>
		 *  "id" =>
		 *    an additional ID to distinguish tools instantiated
		 *    side-by-side from each other. Usually numeric.<br/>
		 *  "cache" =>
		 *    if true, then the Qbix front end will not replace existing tools with same id
		 *    during Q.loadUrl when this tool appears in the rendered HTML
		 * @return {string|null}
		 *  If set, override the method return
		 */
		$returned = Q::event(
			'Q/tool/render',
			array('info' => $info, 'extra' => &$extra),
			'before'
		);
		$result = '';
		$exception = null;
		foreach ($info as $name => $options) {
			Q::$toolName = $name;
			$toolHandler = "$name/tool";
			$options = is_array($options) ? $options : array();
			if (is_array($returned)) {
				$options = array_merge($returned, $options);
			}
			try {
				/**
				 * Renders the tool
				 * @event $toolHandler
				 * @param {array} $options
				 *  The options passed to the tool
				 * @return {string}
				 *	The rendered tool content
				 */
				$result .= Q::event($toolHandler, $options); // render the tool
			} catch (Q_Exception_MissingFile $e) {
				/**
				 * Renders the 'Missing Tool' content
				 * @event Q/missingTool
				 * @param {array} name
				 *  The name of the tool
				 * @return {string}
				 *	The rendered content
				 */
				$params = $e->params();
				if ($params['filename'] === str_replace('/', DS, "handlers/$toolHandler.php")) {
					$result .= self::event('Q/missingTool', compact('name', 'options'));
				} else {
					$exception = $e;
				}
			} catch (Exception $e) {
				$exception = $e;
			}
			if ($exception) {
				Q::log($exception);
				$result .= $exception->getMessage();
			}
			Q::$toolName = $name; // restore the current tool name
		}
		// Even if the tool rendering throws an exception,
		// it is important to run the "after" handlers
		/**
		 * @event Q/tool/render {after}
		 * @param {string} info
		 *  An array of $toolName => $options pairs
		 * @param {array} 'extra'
		 *  Options used by Qbix when rendering the tool. Can include:<br/>
		 *  "id" =>
		 *    an additional ID to distinguish tools instantiated
		 *    side-by-side from each other. Usually numeric.<br/>
		 *  "cache" =>
		 *    if true, then the Qbix front end will not replace existing tools with same id
		 *    during Q.loadUrl when this tool appears in the rendered HTML
		 */
		Q::event(
			'Q/tool/render',
			compact('info', 'extra'),
			'after',
			false,
			$result
		);
		Q::$toolName = $oldToolName;
		return $result;
	}

	/**
	 * Fires a particular event.
	 * Might result in several handlers being called.
	 * @method event
	 * @static
	 * @param {string} $eventName
	 *  The name of the event
	 * @param {array} $params=array()
	 *  Parameters to pass to the event
	 * @param {boolean} $pure=false
	 *  Defaults to false.
	 *  If true, the handler of the same name is not invoked.
	 *  Put true here if you just want to fire a pure event,
	 *  without any default behavior.
	 *  If 'before', only runs the "before" handlers, if any.
	 *  If 'after', only runs the "after" handlers, if any.
	 *  You'd want to signal events with 'before' and 'after'
	 *  before and after some "default behavior" happens.
	 *  Check for a non-null return value on "before",
	 *  and cancel the default behavior if it is present.
	 * @param {boolean} $skipIncludes=false
	 *  Defaults to false.
	 *  If true, no new files are loaded. Only handlers which have
	 *  already been defined as functions are run.
	 * @param {reference} $result=null
	 *  Defaults to null. You can pass here a reference to a variable.
	 *  It will be returned by this function when event handling
	 *  has finished, or has been aborted by an event handler.
	 *  It is passed to all the event handlers, which can modify it.
	 * @return {mixed}
	 *  Whatever the default event handler returned, or the final
	 *  value of $result if it is modified by any event handlers.
	 * @throws {Q_Exception_Recursion}
	 * @throws {Q_Exception_MissingFile}
	 * @throws {Q_Exception_MissingFunction}
	 */
	static function event(
	 $eventName,
	 $params = array(),
	 $pure = false,
	 $skipIncludes = false,
	 &$result = null)
	{
		// for now, handle only event names which are strings
		if (!is_string($eventName)) {
			return;
		}
		if (!is_array($params)) {
			$params = array();
		}

		static $event_stack_limit = null;
		if (!isset($event_stack_limit)) {
			$event_stack_limit = Q_Config::get('Q', 'eventStackLimit', 100);
		}
		self::$event_stack[] = compact('eventName', 'params', 'pure', 'skipIncludes');
		++self::$event_stack_length;
		if (self::$event_stack_length > $event_stack_limit) {
			if (!class_exists('Q_Exception_Recursion', false)) {
				include(dirname(__FILE__).DS.'Q'.DS.'Exception'.DS.'Recursion.php');
			}
			throw new Q_Exception_Recursion(array('function_name' => "Q::event($eventName)"));
		}

		try {
			if ($pure !== 'after') {
				// execute the "before" handlers
				$handlers = Q_Config::get('Q', 'handlersBeforeEvent', $eventName, array());
				if (is_string($handlers)) {
					$handlers = array($handlers); // be nice
				}
				if (is_array($handlers)) {
					foreach ($handlers as $handler) {
						if (false === self::handle($handler, $params, $skipIncludes, $result)) {
							// return this result instead
							return $result;
						}
					}
				}
			}

			// Execute the primary handler, wherever that is
			if (!$pure) {
				// If none of the "after" handlers return anything,
				// the following result will be returned:
				$result = self::handle($eventName, $params, $skipIncludes, $result);
			}

			if ($pure !== 'before') {
				// execute the "after" handlers
				$handlers = Q_Config::get('Q', 'handlersAfterEvent', $eventName, array());
				if (is_string($handlers)) {
					$handlers = array($handlers); // be nice
				}
				if (is_array($handlers)) {
					foreach ($handlers as $handler) {
						if (false === self::handle($handler, $params, $skipIncludes, $result)) {
							// return this result instead
							return $result;
						}
					}
				}
			}
			array_pop(self::$event_stack);
			--self::$event_stack_length;
		} catch (Exception $e) {
			array_pop(self::$event_stack);
			--self::$event_stack_length;
			throw $e;
		}

		// If no handlers ran, the $result is still unchanged.
		return $result;
	}

	/**
	 * Tests whether a particular handler exists
	 * @method canHandle
	 * @static
	 * @param {string} $handler_name
	 *  The name of the handler. The handler can be overridden
	 *  via the include path, but an exception is thrown if it is missing.
	 * @param {boolean} $skip_include=false
	 *  Defaults to false. If true, no file is loaded;
	 *  the handler is executed only if the function is already defined;
	 *  otherwise, null is returned.
	 * @return {boolean}
	 *  Whether the handler exists
	 * @throws {Q_Exception_MissingFile}
	 */
	static function canHandle(
	 $handler_name,
	 $skip_include = false)
	{
		if (!isset($handler_name) || isset(self::$event_empty[$handler_name])) {
			return false;
		}
		$handler_name_parts = explode('/', $handler_name);
		$function_name = str_replace('-', '_', implode('_', $handler_name_parts));
		if (function_exists($function_name))
		 	return true;
		if ($skip_include)
			return false;
		// try to load appropriate file using relative filename
		// (may search multiple include paths)
		$filename = 'handlers'.DS.implode(DS, $handler_name_parts).'.php';
		try {
			self::includeFile($filename, array(), true);
		} catch (Q_Exception_MissingFile $e) {
			self::$event_empty[$handler_name] = true;
			return false;
		}
		return function_exists($function_name);

	}
	/**
	 * Executes a particular handler
	 * @method handle
	 * @static
	 * @param {string} $handler_name
	 *  The name of the handler. The handler can be overridden
	 *  via the include path, but an exception is thrown if it is missing.
	 * @param {array} $params=array()
	 *  Parameters to pass to the handler.
	 *  They may be altered by the handler, if it accepts $params as a reference.
	 * @param {boolean} $skip_include=false
	 *  Defaults to false. If true, no file is loaded;
	 *  the handler is executed only if the function is already defined;
	 *  otherwise, null is returned.
	 * @param {&mixed} $result=null
	 *  Optional. Lets handlers modify return values of events.
	 * @return {mixed}
	 *  Whatever the particular handler returned, or null otherwise;
	 * @throws {Q_Exception_MissingFunction}
	 */
	protected static function handle(
	 $handler_name,
	 &$params = array(),
	 $skip_include = false,
	 &$result = null)
	{
		if (!isset($handler_name)) {
			return null;
		}
		$handler_name_parts = explode('/', $handler_name);
		$function_name = str_replace('-', '_', implode('_', $handler_name_parts));
		if (!is_array($params)) {
			$params = array();
		}
		if (!function_exists($function_name)) {
			if ($skip_include) {
				return null;
			}
			// try to load appropriate file using relative filename
			// (may search multiple include paths)
			$filename = 'handlers'.DS.implode(DS, $handler_name_parts).'.php';
			self::includeFile($filename, $params, true);
			if (!function_exists($function_name)) {
				require_once(Q_CLASSES_DIR.DS.'Q'.DS.'Exception'.DS.'MissingFunction.php');
				throw new Q_Exception_MissingFunction(compact('function_name'));
			}
		}
		// The following avoids the bug in PHP where
		// call_user_func doesn't work with references being passed
		$args = array(&$params, &$result);
		return call_user_func_array($function_name, $args);
	}

	/**
	 * A replacement for call_user_func_array
	 * that implements some conveniences.
	 * @method call
	 * @static
	 * @param {callable} $callback
	 * @param {array} $params=array()
	 * @return {mixed}
	 *  Returns whatever the function returned.
	 * @throws {Q_Exception_MissingFunction}
	 */
	static function call(
		$callback,
		$params = array())
	{
		if ($callback === 'echo' or $callback === 'print') {
			foreach ($params as $p) {
				echo $p;
			}
			return;
		}
		if (!is_array($callback)) {
			$parts = explode('::', $callback);
			if (count($parts) > 1) {
				$callback = array($parts[0], $parts[1]);
			}
		}
		if (!is_callable($callback)) {
			$function_name = $callback;
			if (is_array($function_name)) {
				$function_name = implode('::', $function_name);
			}
			throw new Q_Exception_MissingFunction(compact('function_name'));
		}
		return call_user_func_array($callback, $params);
	}
	
	/**
	 * @method take
	 * @param {array|object} $source An array or object from which to take things.
	 * @param {array} $fields An array of fields to take or an associative array of fieldname => default pairs
	 * @param {array|object} &$dest Optional reference to an array or object in which we will set values.
	 *  Otherwise an empty array is used.
	 * @return {array|object} The $dest array or object, otherwise an array that has been filled with values.
	 */
	static function take($source, $fields, &$dest = null)
	{
		if (!is_array($fields)) {
			$fields = array($fields);
		}
		if (!isset($dest)) {
			$dest = array();
		}
		if (Q::isAssociative($fields)) {
			if (is_array($source)) {
				if (is_array($dest)) {
					foreach ($fields as $k => $v) {
						$dest[$k] = array_key_exists($k, $source) ? $source[$k] : $v;
					}
				} else {
					foreach ($fields as $k => $v) {
						$dest->$k = array_key_exists($k, $source) ? $source[$k] : $v;
					}
				}
			} else if (is_object($source)) {
				if (is_array($dest)) {
					foreach ($fields as $k => $v) {
						$dest[$k] = (property_exists($source, $k) or isset($source->$k)) ? $source->$k : $v;
				 	}
				} else {
					foreach ($fields as $k => $v) {
						$dest->$k = (property_exists($source, $k) or isset($source->$k)) ? $source->$k : $v;
				 	}
				}
			} else {
				if (is_array($dest)) {
					foreach ($fields as $k => $v) {
						$dest[$k] = $v;
					}
				} else {
					foreach ($fields as $k => $v) {
						$dest->$k = $v;
					}
				}
			}
		} else {
			if (is_array($source)) {
				if (is_array($dest)) {
					foreach ($fields as $k) {
						if (array_key_exists($k, $source)) {
							$dest[$k] = $source[$k];
						}
					}
				} else {
					foreach ($fields as $k) {
						if (array_key_exists($k, $source)) {
							$dest->$k = $source[$k];
						}
					}
				}
			} else if (is_object($source)) {
				if (is_array($dest)) {
					foreach ($fields as $k) {
						if (property_exists($source, $k) or isset($source->$k)) {
							$dest[$k] = $source->$k;
						}
					}
				} else {
					foreach ($fields as $k) {
						if (property_exists($source, $k) or isset($source->$k)) {
							$dest->$k = $source->$k;
						}
					}
				}
			} else {
				return $source;
			}
		}
		return $dest;
	}
	
	/**
	 * Determine whether a PHP array if associative or not
	 * Might be slow as it has to iterate through the array
	 * @param {array} $array
	 */
	static function isAssociative($array)
	{
		if (!is_array($array)) {
			return false;
		}
		
		// Keys of the array
		$keys = array_keys($array);

		// If the array keys of the keys match the keys, then the array must
		// not be associative (e.g. the keys array looked like {0:0, 1:1...}).
		return array_keys($keys) !== $keys;
	}
	
	/**
	 * If an array is not associative, then makes an associative array
	 * with the keys taken from the values of the regular array
	 * @param {array} $array
	 * @param {array} [$value=true] The value to assign to each item in the generated array
	 * @return {array}
	 */
	static function makeAssociative($array, $value = true)
	{
		if (Q::isAssociative($array)) {
			return $array;
		}
		$result = array();
		foreach ($array as $item) {
			$result[$item] = $value;
		}
		return $result;
	}

	/**
	 * Append a message to the main log
	 * @method log
	 * @static
	 * @param {mixed} $message
	 *  the message to append. Usually a string.
	 * @param {string} $key=null
	 *  The name of log file. Defaults to "$app_name.log"
	 * @param {bool} $timestamp=true
	 *  whether to prepend the current timestamp
	 * @param {array} $options
	 * @param {integer} [$options.maxLength=ini_get('log_errors_max_len')]
	 * @param {integer} [$options.maxDepth=3]
	 * @throws {Q_Exception_MissingFile}
	 *	If unable to create directory or file for the log
	 */
	static function log (
		$message,
		$key = null,
		$timestamp = true,
		$options = array())
	{
		if (is_array($timestamp)) {
			$options = $timestamp;
			$timestamp = true;
		}
		if (is_array($key)) {
			$options = $key;
			$key = null;
			$timestamp = true;
		}
		if (false === Q::event('Q/log', compact(
			'message', 'timestamp', 'error_log_arguments'
		), 'before')) {
			return;
		}

		if (!is_string($message)) {
			$maxDepth = Q::ifset($options, 'maxDepth', 3);
			if (!is_object($message)) {
				$message = Q::var_dump($message, $maxDepth, '$', 'text');
			} else if (!is_callable(array($message, '__toString'))) {
				$message = Q::var_dump($message, $maxDepth, '$', 'text');
			}
		}

		$app = Q_Config::get('Q', 'app', null);
		if (!isset($app)) {
			$app = defined('APP_DIR') ? basename(APP_DIR) : 'Q App';
		}
		$message = "(".(isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : "cli").") $app: $message";
		$max_len = Q::ifset($options, 'maxLength', 
			Q_Config::get('Q', 'log', 'maxLength', ini_get('log_errors_max_len'))
		);
		$path = (defined('APP_FILES_DIR') ? APP_FILES_DIR : Q_FILES_DIR)
			.DS.'Q'.DS.Q_Config::get('Q', 'internal', 'logDir', 'logs');

		$mask = umask(0000);
		if (!($realPath = Q::realPath($path))
		and !($realPath = Q::realPath($path, true))) {
			if (!@mkdir($path, 0777, true)) {
				throw new Q_Exception_FilePermissions(array('action' => 'create', 'filename' => $path, 'recommendation' => ' Please set the app files directory to be writable.'));
			}
			$realPath = Q::realPath($path, true);
		}
		$filename = (isset($key) ? $key : $app).'.log';
		$toSave = "\n".($timestamp ? '['.date('Y-m-d H:i:s') . '] ' : '') .substr($message, 0, $max_len);
		file_put_contents($realPath.DS.$filename, $toSave, FILE_APPEND);
		umask($mask);
	}

	/**
	 * Check if Qbix is ran as script
	 * @method textMode
	 * @static
	 * @return {boolean}
	 */
	static function textMode()
	{
		if (!isset($_SERVER['HTTP_HOST'])) {
			return true;
		}
		return false;
	}
	
	/**
	 * Helper function for cutting an array or object to a specific depth
	 * for stuff like json_encode in PHP versions < 5.5
	 * @param {mixed} $value to json encode
	 * @param {array} $options passed to json_encode
	 * @param {integer} $depth defaults to 10
	 * @param {mixed} $replace what to replace the cut off values with
	 */
	static function cutoff($value, $depth = 10, $replace = array())
	{
		if (!is_array($value) and !is_object($value)) {
			return $value;
		}
		$to_encode = array();
		foreach ($value as $k => $v) {
			$to_encode[$k] = ($depth > 0 ? self::cutoff($v, $depth-1) : $replace);
		}
		return $to_encode;
	}

	/**
	 * Dumps a variable.
	 * Note: cannot show protected or private members of classes.
	 * @method var_dump
	 * @static
	 * @param {mixed} $var
	 *  the variable to dump
	 * @param {integer} $max_levels=null
	 *  the maximum number of levels to recurse
	 * @param {string} $label='$'
	 *  optional - label of the dumped variable. Defaults to $.
	 * @param {boolean} $return_content=null
	 *  if true, returns the content instead of dumping it.
	 *  You can also set to "text" to return text instead of HTML
	 * @return {string|null}
	 */
	static function var_dump (
		$var,
		$max_levels = null,
		$label = '$',
		$return_content = null)
	{
		$scope = false;
		$prefix = 'unique';
		$suffix = 'value';
		$as_text = ($return_content === 'text') ? true : Q::textMode();

		if ($scope) {
			$vals = $scope;
		} else {
			$vals = $GLOBALS;
		}

		$old = $var;
		$var = $new = $prefix . rand() . $suffix;
		$vname = FALSE;
		foreach ($vals as $key => $val)
			if ($val === $new) // ingenious way of finding a global var :)
				$vname = $key;
		$var = $old;

		if ($return_content) {
			ob_start();
		}
		if ($as_text) {
			echo PHP_EOL;
		} else {
			echo "<pre style='margin: 0px 0px 10px 0px; display: block; background: white; color: black; font-family: Verdana; border: 1px solid #cccccc; padding: 5px; font-size: 10px; line-height: 13px;'>";
		}
		if (!isset(self::$var_dump_max_levels)) {
			self::$var_dump_max_levels = Q_Config::get('Q', 'var_dump_max_levels', 5);
		}
		$current_levels = self::$var_dump_max_levels;
		if (isset($max_levels)) {
			self::$var_dump_max_levels = $max_levels;
		}
		self::do_dump($var, $label . $vname, null, null, $as_text);
		if (isset($max_levels)) {
			self::$var_dump_max_levels = $current_levels;
		}
		if ($as_text) {
			echo PHP_EOL;
		} else {
			echo "</pre>";
		}

		if ($return_content) {
			return ob_get_clean();
		}
		return null;
	}
	
	private static function toArrays($value)
	{
		$result = (is_object($value) and method_exists($value, 'toArray'))
			? $value->toArray()
			: $value;
		if (is_array($result)) {
			foreach ($result as $k => &$v) {
				$v = self::toArrays($v);
			}
		}
		return $result;
	}
	
	/**
	 * A wrapper for json_encode
	 */
	static function json_encode($value, $options = 0, $depth = 512)
	{
		$args = func_get_args();
		$args[0] = self::toArrays($value);
		$result = call_user_func_array('json_encode', $args);
		if ($result === false) {
			if (is_callable('json_last_error')) {
				throw new Q_Exception_JsonEncode(array(
					'message' => json_last_error_msg()
				), null, json_last_error());
			}
			throw new Q_Exception_JsonEncode(array(
				'message' => 'Invalid JSON'
			), null, -1);
		}
		return str_replace("\\/", '/', $result);
	}


	/**
	 * A wrapper for json_decode
	 */
	static function json_decode()
	{
		$args = func_get_args();
		$result = call_user_func_array('json_decode', $args);
		if (is_callable('json_last_error')) {
			if ($code = json_last_error()) {
				throw new Q_Exception_JsonDecode(array(
					'message' => json_last_error_msg()
				), null, $code);
			}
		} else if (!isset($result) and strtolower(trim($args[0])) !== 'null') {
			throw new Q_Exception_JsonEncode(array(
				'message' => 'Invalid JSON'
			), null, -1);
		}
		return $result;
	}

	/**
	 * Exports a simple variable into something that looks nice, nothing fancy (for now)
	 * Does not preserve order of array keys.
	 * @method var_export
	 * @static
	 * @param {&mixed} $var
	 *  the variable to export
	 * @return {string}
	 */
	static function var_export (&$var)
	{
		if (is_string($var)) {
			$var_2 = addslashes($var);
			return "'$var_2'";
		} elseif (is_array($var)) {
			$len = 0;
			$indexed_values_quoted = array();
			$keyed_values_quoted = array();
			foreach ($var as $key => $value) {
				$value = self::var_export($value);
				if ($key === $len) {
					$indexed_values_quoted[] = $value;
				} else {
					$keyed_values_quoted[] = "'" . addslashes($key) . "' => $value";
				}
				++$len;
			}
			$parts = array();
			if (!empty($indexed_values_quoted)) {
				$parts['indexed'] = implode(', ', $indexed_values_quoted);
			}
			if (!empty($keyed_values_quoted)) {
				$parts['keyed'] = implode(', ', $keyed_values_quoted);
			}
			$exported = '[' . implode(", ".PHP_EOL, $parts) . ']';
			return $exported;
		} else {
			return var_export($var, true);
		}
	}

	/**
	 * Dumps as a table
	 * @method dump_table
	 * @static
	 * @param {array} $rows
	 */
	static function dump_table ($rows)
	{
		$first_row = true;
		$keys = array();
		$lengths = array();
		foreach ($rows as $row) {
			foreach ($row as $key => $value) {
				if ($first_row) {
					$keys[] = $key;
					$lengths[$key] = strlen($key);
				}
				$val_len = strlen((string)$value);
				if ($val_len > $lengths[$key])
					$lengths[$key] = $val_len;
			}
			$first_row = false;
		}
		foreach ($keys as $i => $key) {
			$key_len = strlen($key);
			if ($key_len < $lengths[$key]) {
				$keys[$i] .= str_repeat(' ', $lengths[$key] - $key_len);
			}
		}
		echo PHP_EOL;
		echo implode("\t", $keys);
		echo PHP_EOL;
		foreach ($rows as $i => $row) {
			foreach ($row as $key => $value) {
				$val_len = strlen((string)$value);
				if ($val_len < $lengths[$key]) {
					$row[$key] .= str_repeat(' ', $lengths[$key] - $val_len);
				}
			}
			echo implode("\t", $row);
			echo PHP_EOL;
		}
	}
	
	/**
	 * Parses a querystring like mb_parse_str but without converting some
	 * characters to underscores like PHP's version does
	 * @method parse_str
	 * @static
	 * @param {string} $str
	 * @param {reference} $arr reference to an array to fill, just like in parse_str
	 * @return {array} the resulting array of $field => $value pairs
	 */
	static function parse_str ($str, &$arr = null)
	{
		static $s = null, $r = null;
		if (!$s) {
			$s = array('.', ' ');
			$r = array('____DOT____', '____SPACE____');
			for ($i=128; $i<=159; ++$i) {
				$s[] = chr($i);
				$r[] = "____{$i}____";
			}
		}
		mb_parse_str(str_replace($s, $r, $str), $arr);
		return $arr = self::arrayReplace($r, $s, $arr);
	}
	
	/**
	 * Replaces strings in all keys and values of an array, and nested arrays
	 * @method parse_str
	 * @static
	 * @param {string} $search the first parameter to pass to str_replace
	 * @param {string} $replace the first parameter to pass to str_replace
	 * @param {array} $source the array in which the values are found
	 * @return {array} the resulting array
	 */
	static function arrayReplace($search, $replace, $source)
	{
		if (!is_array($source)) {
			return str_replace($search, $replace, $source);
		}
		$result = array();
		foreach ($source as $k => $v) {
			$k2 = str_replace($search, $replace, $k);
			$v2 = is_array($v)
				? self::arrayReplace($search, $replace, $v)
				: str_replace($search, $replace, $v);
			$result[$k2] = $v2;
		}
		return $result;
	}

	/**
	 * Returns stack of events currently being executed.
	 * @method eventStack
	 * @static
	 * @param {string} $eventName=null
	 *  Optional. If supplied, searches event stack for this event name.
	 *  If found, returns the latest call with this event name.
	 *  Otherwise, returns false
	 *
	 * @return {array|false}
	 */
	static function eventStack($eventName = null)
	{
		if (!isset($eventName)) {
			return self::$event_stack;
		}
		foreach (self::$event_stack as $key => $ei) {
			if ($ei['eventName'] === $eventName) {
				return $ei;
			}
		}
		return false;
	}

	/**
	 * Return backtrace
	 * @method backtrace
	 * @static
	 * @param {string} $pattern='$class::$function&#32;(from&#32;line&#32;$line)'
	 * @param {integer} $skip=2
	 */
	static function backtrace($pattern = '{{class}}::{{function}} (from line {{line}})', $skip = 2)
	{
		$result = array();
		$i = 0;
		foreach (debug_backtrace() as $entry) {
			if (++$i < $skip) {
				continue;
			}
			$entry['i'] = $i;
			foreach (array('class', 'line') as $k) {
				if (empty($entry[$k])) {
					$entry[$k] = '';
				}
			}
			$result[] = self::interpolate($pattern, $entry);
		}
		return $result;
	}

	/**
	 * Backtrace as html
	 * @method b
	 * @static
	 * @param {string} $separator=",&#32;<br>\n"
	 * @return {string}
	 */
	static function b($separator = ", <br>\n")
	{
		return implode($separator, Q::backtrace('{{i}}) {{class}}::{{function}} (from line {{line}})', 3));
	}

	/**
	 * @method test
	 * @param {string} $pattern
	 */
	static function test($pattern)
	{
		if (!is_string($pattern)) {
			return false;
		}
		Q::var_dump(glob($pattern));
		// TODO: implement
		exit;
	}

	/**
	 * Compares version strings in the format A.B.C...
	 * @method compareVersion
	 * @static
	 * @param {string} $a
	 * @param {string} $b
	 * @return {-1|0|1}
	 */
	static function compareVersion($a, $b)
	{
		if ($a && !$b) return 1;
		if ($b && !$a) return -1;
		if (!$a && !$b) return 0;
	    $a = explode(".", $a);
	    $b = explode(".", $b);
		$ca = count($a);
		for ($i = 0; $i < $ca; ++$i) {
			$ai = $a[$i];
			$bi = isset($b[$i]) ? intval($b[$i]) : 0;
            if ($ai > $bi) {
				return 1;
			}
			if ($ai < $bi) {
				return -1;
			}
		}
		$cb = count($b);
		for ($i = $ca; $i < $cb; ++$i) {
			if (intval($b[$i]) > 0) {
				return -1;
			}
		}
		return 0;
	}

	/**
	 * @method do_dump
	 * @static
	 * @private
	 * @param {&mixed} $var
	 * @param {string} $var_name=null
	 * @param {string} $indent=null
	 * @param {string} $reference=null
	 * @param {boolean} $as_text=false
	 */
	static private function do_dump (
		&$var,
		$var_name = NULL,
		$indent = NULL,
		$reference = NULL,
		$as_text = false)
	{
		static $n = null;
		if (!isset($n)) {
			$n = Q_Config::get('Q', 'newline', "
");
		}
		$do_dump_indent = $as_text
			? "  "
			: "<span style='color:#eeeeee;'>|</span> &nbsp;&nbsp; ";
		$reference = $reference . $var_name;
		$keyvar = 'the_do_dump_recursion_protection_scheme';
		$keyname = 'referenced_object_name';

		$max_indent = self::$var_dump_max_levels;
		if (strlen($indent) >= strlen($do_dump_indent) * $max_indent) {
			echo $indent . $var_name . " (...)$n";
			return;
		}

		if (is_array($var) && isset($var[$keyvar])) {
			$real_var = &$var[$keyvar];
			$real_name = &$var[$keyname];
			$type = ucfirst(gettype($real_var));
			if ($as_text) {
				echo "$indent$var_name<$type> = $real_name$n";
			} else {
				echo "$indent$var_name <span style='color:#a2a2a2'>$type</span> = <span style='color:#e87800;'>&amp;$real_name</span><br>";
			}
		} else {
			$var = array($keyvar => $var, $keyname => $reference);
			$avar = &$var[$keyvar];

			$type = ucfirst(gettype($avar));
			if ($type == "String") {
				$type_color = "green";
			} elseif ($type == "Integer") {
				$type_color = "red";
			} elseif ($type == "Double") {
				$type_color = "#0099c5";
				$type = "Float";
			} elseif ($type == "Boolean") {
				$type_color = "#92008d";
			} elseif ($type == "NULL") {
				$type_color = "black";
			} else {
				$type_color = '#92008d';
			}

			if (is_array($avar)) {
				$count = count($avar);
				if ($as_text) {
					echo "$indent" . ($var_name ? "$var_name => " : "")
						. "<$type>($count)$n$indent($n";
				} else {
					echo "$indent" . ($var_name ? "$var_name => " : "")
						. "<span style='color:#a2a2a2'>$type ($count)</span><br>$indent(<br>";
				}
				$keys = array_keys($avar);
				foreach ($keys as $name) {
					$value = &$avar[$name];
					$displayName = is_string($name)
						? "['" . addslashes($name) . "']"
						: "[$name]";
					self::do_dump($value, $displayName,
						$indent . $do_dump_indent, $reference, $as_text);
				}
				if ($as_text) {
					echo "$indent)$n";
				} else {
					echo "$indent)<br>";
				}
			} elseif (is_object($avar)) {
				$class = get_class($avar);
				if ($as_text) {
					echo "$indent$var_name<$type>[$class]$n$indent($n";
				} else {
					echo "$indent$var_name <span style='color:$type_color'>$type [$class]</span><br>$indent(<br>";
				}
				if ($avar instanceof Exception) {
					$code = $avar->getCode();
					$message = addslashes($avar->getMessage());
					echo "$indent$do_dump_indent"."code: $code, message: \"$message\"";
					if ($avar instanceof Q_Exception) {
						echo " inputFields: " . implode(', ', $avar->inputFields());
					}
					echo ($as_text ? $n : "<br />");
				}

				if (class_exists('Q_Tree')
				 and $avar instanceof Q_Tree) {
						$getall = $avar->getAll();
						self::do_dump($getall, "",
						$indent . $do_dump_indent, $reference, $as_text);
				} else if ($avar instanceof Q_Uri) {
					$arr = $avar->toArray();
					self::do_dump($arr, 'fields',
						$indent . $do_dump_indent, $reference, $as_text);
					self::do_dump($route_pattern, 'route_pattern',
						$indent . $do_dump_indent, $reference, $as_text);
				}

				if ($avar instanceof Db_Row) {
					foreach ($avar as $name => $value) {
						$modified = $avar->wasModified($name) ? "<span style='color:blue'>*</span>:" : '';
						self::do_dump($value, "$name$modified",
							$indent . $do_dump_indent, $reference, $as_text);
					}
				} else {
					foreach ($avar as $name => $value) {
						self::do_dump($value, "$name",
							$indent . $do_dump_indent, $reference, $as_text);
					}
				}

				if ($as_text) {
					echo "$indent)$n";
				} else {
					echo "$indent)<br>";
				}
			} elseif (is_int($avar)) {
				$avar_len = strlen((string)$avar);
				if ($as_text) {
					echo sprintf("$indent$var_name = <$type(%d)>$avar$n", $avar_len);
				} else {
					echo sprintf(
						"$indent$var_name = <span style='color:#a2a2a2'>$type(%d)</span>"
						. " <span style='color:$type_color'>$avar</span><br>",
						$avar_len
					);
				}
			} elseif (is_string($avar)) {
				$avar_len = strlen($avar);
				if ($as_text) {
					echo sprintf("$indent$var_name = <$type(%d)> ", $avar_len),
						$avar, "$n";
				} else {
					echo sprintf("$indent$var_name = <span style='color:#a2a2a2'>$type(%d)</span>",
						$avar_len)
						. " <span style='color:$type_color'>"
						. Q_Html::text($avar)
						. "</span><br>";
				}
			} elseif (is_float($avar)) {
				$avar_len = strlen((string)$avar);
				if ($as_text) {
					echo sprintf("$indent$var_name = <$type(%d)>$avar$n", $avar_len);
				} else {
					echo sprintf(
						"$indent$var_name = <span style='color:#a2a2a2'>$type(%d)</span>"
						. " <span style='color:$type_color'>$avar</span><br>",
						$avar_len);
				}
			} elseif (is_bool($avar)) {
				$v = ($avar == 1 ? "TRUE" : "FALSE");
				if ($as_text) {
					echo "$indent$var_name = <$type>$v$n";
				} else {
					echo "$indent$var_name = <span style='color:#a2a2a2'>$type</span>"
						. " <span style='color:$type_color'>$v</span><br>";
				}
			} elseif (is_null($avar)) {
				if ($as_text) {
					echo "$indent$var_name = NULL$n";
				} else {
					echo "$indent$var_name = "
						. " <span style='color:$type_color'>NULL</span><br>";
				}
			} else {
				$avar_len = strlen((string)$avar);
				if ($as_text) {
					echo sprintf("$indent$var_name = <$type(%d)>$avar$n", $avar_len);
				} else {
					echo sprintf("$indent$var_name = <span style='color:#a2a2a2'>$type(%d)</span>",
						$avar_len)
						. " <span style='color:$type_color'>"
						. gettype($avar)
						. "</span><br>";
				}
			}

			$var = $var[$keyvar];
		}
	}
	
	/**
	 * @method reverseLengthCompare
	 * @private
	 * @return {integer}
	 */
	protected static function reverseLengthCompare($a, $b)
	{
		return strlen($b)-strlen($a);
	}

	/**
	 * @property $included_files
	 * @type array
	 * @static
	 * @protected
	 */
	protected static $included_files = array();
	/**
	 * @property $var_dump_max_levels
	 * @type integer
	 * @static
	 * @protected
	 */
	protected static $var_dump_max_levels;

	/**
	 * @property $event_stack
	 * @type array
	 * @static
	 * @protected
	 */
	protected static $event_stack = array();
	/**
	 * @property $event_stack_length
	 * @type integer
	 * @static
	 * @protected
	 */
	protected static $event_stack_length = 0;
	/**
	 * @property $event_empty
	 * @type array
	 * @static
	 * @protected
	 */
	protected static $event_empty = array();

	/**
	 * @property $controller
	 * @type array
	 * @static
	 */
	static $controller = null;

	/**
	 * @property $state
	 * @type array
	 * @static
	 */
	static $state = array();
	/**
	 * @property $cache
	 * @type array
	 * @static
	 */
	public static $cache = array();

	/**
	 * This is set to true when the bootstrapping process has completed successfully.
	 * @property $bootstrapped
	 * @type boolean
	 * @static
	 */
	public static $bootstrapped = false;

	/**
	 * @property $toolName
	 * @type string
	 * @static
	 */
	public static $toolName = null;
	
	/**
	 * @property $toolWasRendered
	 * @type array
	 * @static
	 */
	public static $toolWasRendered = array();
}

if (!function_exists('json_last_error_msg')) {
    function json_last_error_msg() {
        static $ERRORS = array(
            JSON_ERROR_NONE => 'No error',
            JSON_ERROR_DEPTH => 'Maximum stack depth exceeded',
            JSON_ERROR_STATE_MISMATCH => 'State mismatch (invalid or malformed JSON)',
            JSON_ERROR_CTRL_CHAR => 'Control character error, possibly incorrectly encoded',
            JSON_ERROR_SYNTAX => 'Syntax error',
            JSON_ERROR_UTF8 => 'Malformed UTF-8 characters, possibly incorrectly encoded'
        );

        $error = json_last_error();
        return isset($ERRORS[$error]) ? $ERRORS[$error] : 'Unknown error';
    }
}

/// { aggregate classes for production
/// Q/*.php
/// Q/Exception/MissingFile.php
/// }