All pastes #613539 Raw Edit

Untitled

public text v1 · immutable
·published 2007-07-11 01:04 UTC
rendered paste body
<?php

/**
 * $header
 * @copyright Copyright 2007, Peter Goodman
 * @package valkyrie
 * @subpackage Controller
 * @author Peter Goodman
 */

if(!defined('IN_VALKYRIE')) {
	exit;
}

/**
 * Deal with errors that happen in the ApplicationController
 * @author Peter Goodman
 * @see ApplicationController
 */
class ControllerException extends ValkyrieException {

}

/**
 * Deal with incoming action/event requests and initialize the Request,
 * Response (template), DatabaseDriver and other main classes.
 * @author Peter Goodman
 */
class ApplicationController extends Registry {
	
	/**
	 * A string holding the action name. The action is the function to be called
	 * in the appropriate PageController. The default action is 'index'.
	 * @internal
	 */
	private $action = 'index';
	
	/**
	 * An array holding all of the path parts. The path is passed via index.php?path=
	 * and--when using mod_rewrite--looks like a clean url to a sub-folder of the site.
	 * @internal
	 */
	private $path = array();
	
	/**
	 * The name of the page controller to load.
	 * @internal
	 */
	private $page;
	
	/**
	 * An array of special variables passed in through the path
	 * @see ApplicationController::parseRoutes()
	 * @internal
	 */
	private $vars = array();
	
	/**
	 * A reference to the database layer
	 * @see ApplicationController::connectToDatabase()
	 * @see Database
	 * @internal
	 */
	//protected $dba;
	
	/**
	 * A reference to the page controller object for this action.
	 * @see ApplicationController::loadPageController()
	 * @see PageController
	 * @internal
	 */
	private $controller;
	
	/**
	 * The directory to look for the page controller in. It is stored in this variable
	 * because page controllers can be location in sub-directories of the VK_CONTROLLERS_DIR.
	 * @see ApplicationControll::parsePath()
	 * @internal
	 */
	private $controller_dir = VK_CONTROLLERS_DIR;
	
	/**
	 * ApplicationController constructor.
	 * class.
	 */
	public function __construct() {
	}
	
	/**
	 * ApplicationController destructor. Unset the database, response, and request
	 * reference objects. Also unset the reference variable holding an instance of
	 * the current page controller.
	 * @see Database, PageController, Response, Request
	 * @internal
	 */
	public function __destruct() {
		unset($this->controller, $this->dba, $this->request, $this->response, $this->models);
		$this->destroyRegistry();
	}
	
	/**
	 * Parse the path request variable and derive the
	 * page controller from it, then return an array
	 * of all of the elements in the path (without the
	 * leading controller name). The Path request variable  (index.php?path=) will
	 * normally look like a folder path because of .htaccess mod_rewrite
	 * @internal
	 */
	private function parsePath() 
	{
		// Get the path and then split it up by the /'s
		// P.S. explode() does not work in this scenario
		// because explode() will get empty elements.
		
		$path = $this->request['path'];
		$path = preg_split('~/~', $path, -1, PREG_SPLIT_NO_EMPTY);
		
		// Shift off elements of the array if directories
		// in the controller dir exist for this path.
				
		$i = -1;
		while(isset($path[++$i])) {
			if(is_dir($this->controller_dir . $path[$i])) {
				$this->controller_dir .= array_shift($path) . '/';
			} else {
				break;
			}
		}
		
		// Figure out what controller function should be 
		// used.
		
		$page = empty($path) ? 'index' : array_shift($path);
		
		// Set the info.
		
		$this->page = $page;
		$this->path = $path;
		
		if(!empty($path)) {
			$this->action = $path[0];
		}
	}
	
	/**
	 * Create a connection to a database. This will first find the
	 * database abstraction layer (eg: MySQLi, SQLite) using the Layers
	 * class in the /layers/database/ directory, then using config data
	 * try to connect to it.
	 * @see Layers, Database
	 * @internal
	 */
	private function connectToDatabase() {
		
		if(DBA_DRIVER != '') {
			
			$driver = strtolower(DBA_DRIVER);
			
			try {
				lazy_load(VK_DIR .'/database/database.php');
				lazy_load(VK_DIR .'/database/drivers/'. $driver .'/driver.php');
				lazy_load(VK_DIR .'/database/drivers/'. $driver .'/result.php');
				lazy_load(VK_DIR .'/database/drivers/'. $driver .'/transaction.php');
				lazy_load(VK_DIR .'/database/drivers/'. $driver .'/table.php');
			} catch(LazyLoadException $e) {
				
				throw new ControllerException("Failed to load the database driver. Please check that it exists.");
			}
			
			$driver = ucfirst($driver) .'Driver';
			
			// load the database into the registry
			$this->dba = new $driver(DBA_HOST, DBA_DATABASE, DBA_USER, DBA_PASS);
		}
	}
		
	/**
	 * Parse the routes and figure out which one is the
	 * one we're on. Routes are defined in PageController::$routes
	 * in the form of /controller/function/(int)var/(str)var/(bool)var/(float)var
	 * where all of the 'var' elements are the different types of variables
	 * that can be passed in. Each 'var' is added as a key to the Request reference
	 * object as if it were a $_REQUEST variable, where its value is the corresponding
	 * value in the path (url) itself.
	 * @see Request, PageController
	 * @internal
	 */
	private function parseRoutes() {
		
		// Get the routes defined in the controllers and
		// setup some other variables while at it.
		$routes = $this->controller->routes;
		
		$matched = FALSE;
				
		// If there are parts to the path...		
		if(!empty($this->path) && isset($routes[$this->path[0]])) {
			
			$vars = array();
			$rparts = preg_split("~/~", $routes[$this->path[0]], -1, PREG_SPLIT_NO_EMPTY);
			$i = 0;
			
			while(isset($rparts[$i]) && isset($this->path[$i])) {
				
				$ppart = $this->path[$i];
				$rpart = $rparts[$i];
				
				if($rpart{0} == '(') {
					$type = '';

					if(FALSE === ($ii = strpos($rpart, ')'))) {
						throw new ControllerException("Invalid route: missing closing ')'");
					}

					$type = substr($rpart, 1, $ii - 1);
					$var = substr($rpart, $ii + 1);

					$callback = "rt_$type";

					$vars[$var] = $callback($ppart);
				}
				
				++$i;
			}
			
			$this->action = $this->path[0];
			$this->vars = $vars;
			$matched = TRUE;
		}
		
		
		
		// add the variables that we found to the Request object
		$this->request->setArray($this->vars);
	}
	
	/**
	 * Try to find, include, and instanciate a PageController class for this action.
	 * @see PageController
	 * @internal
	 */
	private function loadPageController($rec = FALSE) 
	{
		$filename = $this->controller_dir . $this->page . '.php';
		
		if(!file_exists($filename) || !is_readable($filename)) {
			
			// we'll try one last time with the index controller
			// and the $this->page being the action
			
			if(!$rec) {
								
				$this->action = $this->page;
				$this->page = 'index';
				array_unshift($this->path, $this->action);
				
				return $this->loadPageController(TRUE);
				
			} else {
				$this->setNotFound();
			}
		}
		
		require_once $filename;
		
		$class = camelize($this->page) . 'Controller';
		
		if(!class_exists($class, FALSE)) {
			$this->setNotFound();
		}
		
		$controller = new $class; //($this->dba, $this->request, $this->response);
		
		if(!$controller instanceof PageController) {
			$this->setNotFound();
		}
		
		// reset for the default action if it's different
		if($this->action === 'index') {
			$this->action = $controller->default_action;
		}
		
		// more advanced stuff, first check if the action is callable
		if(!is_callable(array($controller, $this->action))) {
			
			$this->setNotFound();
			
		// action is callable, does the method exists or are we using __call?
		} else {
			
			// callable, must be using __call, assume index route
			if(!method_exists($controller, $this->action)) {
				array_unshift($this->path, '*');
			}
		}
		
		$this->controller = $controller;
	}
	
	/**
	 * Now that the PageController has been loaded, run the appropriate action
	 * function in it. The default action is 'index'. Pass a reference to the
	 * Request and Response objects as the two action function arguments.
	 * @see Request, Response
	 * @internal
	 */
	private function runPageController() 
	{
		if(!is_callable(array($this->controller, $this->action))) {
			throw new ControllerException("No controller function present for the action: {$this->action}", E_USER_ERROR);
		}
		
		call_user_func_array(array($this->controller, $this->action), array($this->request, $this->response));		
	}
	
	/**
	 * Run any filters in the page controller.
	 * @internal
	 */
	private function runFilters($which) {
		
		// get the filters
		
		$filters = $this->controller->$which;
		
		// loop the filters
		foreach($filters as $f) {
			try {
				lazy_load(VK_APP_DIR .'/filters/'. $f .'.php');
				
				$class = camelize($f) .'Filter';
				
				// can depend on parent catch ;)
				if(!class_exists($class, FALSE)) {
					throw new LazyLoadException;
				}
				
				// run the filter
				$filter = new $class($this->dba, $this->request, $this->response);
				$filter->run($this->request, $this->response);
			
			// not a pre/post filter, lets see if we're trying to load a library as a filter	
			} catch(LazyLoadException $e) {
				
				// try to load up the library without cloning it
				try {
					$this->load($f, FALSE);
				
				// so, the library doesn't exist either
				} catch(LazyLoadException $e) {
					throw new ControllerException("Could not load filter/library [{$f}], please make sure that it exists.");
				}
			}
		}
	}
	
	/**
	 * Call most of the ApplicationControllers functions to bring everything together
	 * and run the action.
	 */
	public function run() {
		try {
			$this->connectToDatabase();
			echo $this->page .' '. $this->action .'<br>';
			$this->parsePath();
			echo $this->page .' '. $this->action .'<br>';
			$this->loadPageController();
			echo $this->page .' '. $this->action .'<br>';
			$this->parseRoutes();
			echo $this->page .' '. $this->action .'<br>';
			
			$this->runFilters('pre_filters');
			$this->runPageController();
			echo $this->page .' '. $this->action .'<br>';
			$this->runFilters('post_filters');
			
			$this->response->render($this->dba, $this->request);
			
		} catch(Exception $e) {
			fatal_error($e);
		}
	}
	
	/**
	 * If the action was not found in the PageController, then a custom
	 * 404 error page will be displayed.
	 * @see ApplicationController::loadPageController()
	 * @internal
	 */
	public function setNotFound() {
		header('Status: 404 Not Found');
		$this->response->getFile('/errors/error_404.html');
		exit;
	}
}

/**
 * The page controller represents a namespace of actions available
 * to users. The default action in a page controller will be PageController::index.
 * To pass variables to these actions, see PageController::$routes and 
 * ApplicationController::parseRoutes(). The PageController has a "registry" that is
 * simply PageController::$registry and can be accessed via object overloading
 * with Overload.
 * @see Overload
 * @author Peter Goodman
 */
abstract class PageController extends Registry {
	
	/**
	 * The datasource we are currently using.
	 */
	private $datasource = 'database';
	
	/**
	 * Get a PageControllers custom routes. If no custom routes exist (custom routes being
	 * ones that include either special variables and/or more path elements after the action
	 * name, eg: /controller_name/action_name/custom/custom/)
	 * @see ApplicationController::parseRoutes()
	 */
	public $routes = array();
	
	/**
	 * Get an array of filters to execute before a controller's action is called.
	 */
	public $pre_filters = array();
	
	/**
	 * Get an array of filters to execute after a controller's action is called.
	 */
	public $post_filters = array();
			
	/**
	 * The default action to call for a controller. Normally it's 'index' but if any page controller
	 * redefines this method, it can be changed to anything.
	 * @return string
	 */
	public $default_action = 'index';
	
	/**
	 * PageController constructor, take in the database, request, response (template) and
	 * layers reference objects.
	 * @param mixed $dba A reference to the DatabaseDriver object.
	 * @param mixed $request A reference to the Request object.
	 * @param mixed $response A reference to the Response object.
	 * @param mixed $layers A reference to the Layers object.
	 * @see Database, Request, Response, Layers
	 * @internal
	 */
	// DatabaseDriver $dba, Request $request, Response $response
	final public function __construct() {		
		/**
		 * @todo Find a nicer solution to loading different models.
		 */
		try {
			lazy_load(VK_DIR .'/models/database/model.php');
		} catch(LazyLoadException $e) {
			throw new ControllerException("Could not load database model. Please make sure it exists.");
		}
		
		$this->models = array();
		$this->models['database'] = new DatabaseModel;
	}
	
	/**
	 */
	final public function getFinder($name = '') {
		return $this->models[$this->datasource]->getFinder($name);
	}
	
	/**
	 * Change the datasource, eg: database->authorize.net/google/whatever.
	 */
	final public function setDatasource($ds = 'database') {
				
		if(!isset($this->models[$ds])) {
			lazy_load(VK_DIR .'/models/'. $ds .'/model.php');
			$class = camelize($ds) .'Model';
			
			$this->models[$ds] = new $class;
		}
		
		$this->datasource = $ds;
	}
	
	/**
	 * If the action was not found in the PageController, then a custom
	 * 404 error page will be displayed.
	 * @see ApplicationController::loadPageController()
	 * @internal
	 */
	public function setNotFound() {
		header('Status: 404 Not Found');
		$this->response->getFile('/errors/error_404.html');
		exit;
	}
}

/**
 * Controllers for components.
 */
abstract class ComponentController extends PageController {
	abstract public function run(Request $request, Response $response);
}

/**
 * Controllers for filters.
 */
abstract class FilterController extends PageController {
	abstract public function run(Request $request, Response $response);
}