log = $this->getLogger(get_class($this)); $this->log->debug('Starting up...'); $this->templates_path = $this->getConfig('dirs/templates', 'templates'); $this->actions_path = $this->getConfig('dirs/actions', 'actions'); $this->block_stack = array(); $this->block_capture = array(); $this->extend_stack = array(); $this->tmpl_output = ''; $this->url_path = ''; $this->route_path = ''; $this->action_name = ''; $this->route_opts = array(); $this->route_params = array(); $this->route_matches = array(); $this->base_url = $this->getConfig('base_url', dirname($_SERVER[SCRIPT_NAME])); $this->global_ns = array( 'base_url' => $this->base_url ); } /** * Dispatch an incoming web request. Match current request URI against * defined routes, attempt to find an appropriate action, execute it. */ function dispatch() { // Get the incoming method and path. $this->url_path = $_SERVER['REQUEST_METHOD'].' '.$_SERVER['PATH_INFO']; $this->log->debug("URL path: {$this->url_path}"); // Attempt to match the request to a route. list($this->route_path, $this->action_name, $this->route_opts, $this->route_params, $this->route_matches) = $this->matchRoute($this->url_path); // Then, try executing the action. $output = $this->executeAction($this->action_name); // TODO: Caching? echo $output; } /** * Given a path, attempt to match it against configured paths. * @param string URL path * @return array array containing route, action, options, params, and raw matches. */ function matchRoute($path) { $routes = $this->getConfig('routes', array()); $defaults = array('action' => 'default'); foreach ($routes as $route=>$opts) { // Magically include delimiters to make the route pattern simpler, // but allow them to be overridden per-route. $delim_pre = isset($opts['delim_pre']) ? $opts['delim_pre'] : '#^'; $delim_post = isset($opts['delim_post']) ? $opts['delim_post'] : '$#U'; // Get opts for the route with defaults, start with empty matches. $opts = array_merge($defaults, $opts); $matches = array(); if ($route=='default' || preg_match($delim_pre.$route.$delim_post, $path, $matches)) { // Turn the regex matches into named parameters if possible. $params = array(); if (array_key_exists('params', $opts)) { $full_path = array_shift($matches); foreach ($opts['params'] as $key => $value) { if ($value === NULL) { // If the value is NULL, then grab it from the route. $params[$key] = array_shift($matches); } else { // Otherwise, use the value present in the params. $params[$key] = $value; } } } // Grab the action from the route opts, with default $action = array_key_exists('action', $opts) ? $opts['action'] : 'default'; $this->log->debug("Matched route $route"); $this->log->debug("Route opts: ".var_export($opts, true)); $this->log->debug("Route params: ".var_export($params, true)); $this->log->debug("Route raw matches: ".var_export($matches, true)); // Return the whole mess for matched route. return array( $route, $action, $opts, $params, $matches ); } } return array(NULL, NULL, NULL, NULL, NULL); } /** * Find the path to an action. * @param string Name of the action to locate. */ function findAction($action_name) { $action_path = $this->actions_path."/$action_name.action.php"; $this->log->debug("Trying action $action_path"); if (!is_file($action_path)) $action_path = $this->actions_path.'/default.action.php'; $this->log->debug("Found action $action_path"); return $action_path; } /** * Execute an action, either in a module class or a separate action file. * @param string name of the action or path to the PHP file implementing the action */ function executeAction($action_name) { // First, let's see if we can execute this action in a module. $module_name = @$this->route_opts['module']; if ($module_name) { $module = new $module_name($this); return $module->execute($action_name); } // If the module failed, locate the path to the action to execute. $action_path = $this->findAction($action_name); // Execute the action by including it in the context of this method, // capturing the output so we can possibly cache it. ob_start(); $app = $this; include $action_path; $output = ob_get_contents(); ob_end_clean(); return $output; } /** * Fetch a named route parameter extracted by matchRoute() * @param string name of the parameter * @param string default value if not set */ function getRouteParameter($name, $default=NULL) { return isset($this->route_params[$name]) ? $this->route_params[$name] : $default; } /** * Find the full path to a template file. * @param string name of the template to find. */ function findTemplate($template_name) { $template_path = $this->templates_path."/$template_name.tmpl.php"; if (!is_file($template_path)) $template_path = $this->templates_path.'/default.tmpl.php'; return $template_path; } /** * Render a template with a namespace, return the results. * @param $name - name of the template to find and render * @param $ns - namespace of variables to use in template. * @return results of the rendered template output */ function renderTemplate($name, $ns=FALSE) { // Find the full path to selected template. $template_path = $this->findTemplate($name); // Import all the namespace variables into this function's scope. $ns = array_merge($this->global_ns, ($ns) ? $ns : array()); extract($ns); // Start buffering, include the template source, return the buffered // output. ob_start(); $app = $this; include $template_path; $output = $this->tmpl_output ? $this->tmpl_output : ob_get_contents(); ob_end_clean(); return $output; } /** * Render a template with the name based on the current action. * @param array Namespace of variables for use by the template. * @return results of the rendered template output */ function renderTemplateForAction($ns=FALSE) { $path = $this->action_name; $module_name = @$this->route_opts['module']; if ($module_name) { $path = $module_name.'/'.$path; } $this->log->debug("Rendering template $path"); return $this->renderTemplate($path, $ns); } /** * Start a template as an extension of another. * @param string name of the template to extend. */ function extendTemplate($name='') { array_push($this->extend_stack, $name); ob_start(); } /** * Start a template as a base template. */ function startTemplate() { return $this->extendTemplate(FALSE); } /** * Finish the template out. If it was started as an extension of another, * use that base template for output. Otherwise, use the captured output * of this template for output. */ function endTemplate() { $name = array_pop($this->extend_stack); $output = ob_get_contents(); ob_end_clean(); return $this->tmpl_output = ($name) ? $this->renderTemplate($name) : $output; } /** * Begin capturing template output, for storage with given key. * @param string key used to identify output capture */ function startBlock($key) { array_push($this->block_stack, $key); ob_start(); } /** * Stop capturing template output, storing the result with given key. * @return string the key identifying captured output. */ function endBlock() { $key = array_pop($this->block_stack); if ($key == NULL) { return FALSE; } else { $output = ob_get_contents(); ob_end_clean(); $this->block_capture[$key] = $output; return $key; } } /** * Get the content captured for a block by key. * @return string the captured output, or default. */ public function getBlock($key, $default='') { return array_key_exists($key, $this->block_capture) ? $this->block_capture[$key] : $default; } /** * HTML escape values in an associative array, or a single string. * @param $mixed - An associative array or a single string. * @return An associative array with escaped values, or an escaped string. */ function htmlescape($mixed) { if (is_array($mixed)) { $safe = array(); foreach ($mixed as $n=>$v) { if (is_string($v)) $safe[$n] = htmlentities($v); } return $safe; } else { return htmlentities($mixed); } } /** * Echo some output, performing HTML escaping first. * @param $out - String to echo */ function _($out) { echo htmlentities($out); } /** * Return a logger using the given identity and details from config. * @return object A Log instance. */ public function getLogger($context='main') { return Log::singleton( $this->getConfig('log/type', 'file'), $this->getConfig('log/name', 'logs/main.log'), $context, $this->getConfig('log/options', array()) ); } /** * Get a shared instance of the application, creating a new instance if * necessary. * @todo Find a better way to do this. */ public static function getInstance() { if (!self::$_instance) { $app_class = APP_NAME.'_App'; self::$_instance = new $app_class(); } return self::$_instance; } /** * Fetch a configuration setting. * @param string Slash-separated path to config value. * @param string Default value for config setting. * @return mixed Config setting value */ public function getConfig($name, $default=NULL) { global $CONFIG; $curr = $CONFIG; $keys = split('/', $name); while ($key = array_shift($keys)) { if (is_array($curr) && array_key_exists($key, $curr)) { $curr = $curr[$key]; } else { $curr = NULL; } } return ($curr) ? $curr : $default; } }