<?php
namespace ROF;
class Router {
/** a protected construct function to prevent instantiation of the Router class. Handler, instead, should be instantiated by calling getInstance()
*/
protected function __construct(){}
protected $content;
/** This processes the request from beginning to end.
* Configurations must be set prior to calling this. Else it will throw an instructive error.
*
*/
public function go(){
$this->confirmConfigs();
$this->sessionToPost();
$this->willDeliverSite();
$this->registerDefaultUrlHandler();
$this->routeUrl();
$this->registerDefaultContentHandler();
$this->processContent();
}
public function sessionToPost(){
if (session_status()!==PHP_SESSION_ACTIVE)session_start();
if (isset($_SESSION['POST'])){
$_POST = $_SESSION['POST'];
unset($_SESSION['POST']);
}
}
/** redirects to the specified URL.
* Goal: Improve this so $_POST values are forwarded to the new URL as well
*
*/
public function redirectTo($url){
if (session_status()!==PHP_SESSION_ACTIVE){
session_start();
$_SESSION['POST'] = $_POST;
}
header("Location: ".$url);
exit;
}
public function registerDefaultUrlHandler(){
if ($this->getConfig('defaultUrlHandler')!==TRUE)return;
$this->registerUrlHandler(
function($url){
return TRUE;
},
array(\ROF\Handler::getInstance(),'handleUrl'),
'default'
);
}
public function registerDefaultContentHandler(){
if ($this->getConfig('defaultContentHandler')!==TRUE)return;
$this->registerContentHandler(
function($content){
return TRUE;
},
function($content){
echo $content;
},
'default'
);
}
/** This is where all the setup happens. In your willDeliver file, you'll register to handle URLs, to process content, and any other events TBD.
* You'll setup database handles, possibly authenticate users (but not relative to the URL or content?), configure libraries, and other setup.
*
* This is optional. In devMode, however, an exception will be thrown if the file is not found.
* The included file will be determined by the saved configurations: siteSource/eventsDir/willDeliver.
* The defaults: eventsDir = 'Events' & willDeliver = 'willDeliver.php'
*
* @throws an exception when devMode is turned on, if the file cannot be found.
*/
public function willDeliverSite(){
$file = $this->getConfig('siteSource').'/'.$this->getConfig('eventsDir').'/'.$this->getConfig('willDeliver');
if (file_exists($file)){
include($file);
} else if (!$this->getConfig('devMode')){
//do nothing. don't bother the user with this, as willDeliver is an optional event.
} else if (is_dir($eventsDir=dirname($file))){
throw new \Exception("You must create a file '".$this->getConfig('willDeliver')."' in your Events Directory located at:".$eventsDir);
} else {
throw new \Exception("Your Events directory must exist. "
."\nThe current path is '".dirname($file)."' which is your 'eventsDir' being appended to your 'siteSource'. "
."\nCall `\ROF\Handler::getInstance()->setConfig('eventsDir','SomeDir');` to change this. "
."\nYou may set 'eventsDir' to an empty string ('') in order to use your 'siteSource' as your 'eventsDir'");
}
}
/** An array of URL Handlers. See registerUrlHandler for a description of the data. The data for each handler is stored as an array inside $urlHandlers with indices, but no string keys.
*
*/
protected $urlHandlers = [];
/** Loops through the registered URL Handlers, calling them in the order they were registered. Breaks the loop if a handler handles the request.
*
*/
public function routeUrl(){
$url = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH); //rather than REDIRECT_URL, because REQUEST_URI more consistently will provide what the user sees in their browser.
foreach ($this->urlHandlers as $index=>$handler){
$name = $handler['name'];
if (call_user_func($handler['willRespond'],$url)){
$resp = $handler['responder'];
if (is_callable($resp)){
ob_start();
call_user_func($resp,$url);
$this->content = ob_get_clean();
break;
} else if (!is_object($resp)&&is_string($resp)&&is_file($resp)){
ob_start();
include($resp);
$this->content = ob_get_clean();
break;
} else {
throw new \Exception("Error registering URL Handler named '{$name}'. "
."\nThe second paramater passed to `registerUrlHandler` must be a callable or a string pointing to a file.");
}
}
}
}
/** Registers a file or a callable to handle a given request. If a given handler will respond to a request, then no other handler will respond to that request.
*
* @param callable $willRespond - A callable, invokable by call_user_func. Accepts the requested URL as it's only paramater. Must return TRUE or FALSE
* @param mixed $responder - A file to be included or a callable, invokable by call_user_func. Accepts the requested URL as it's only paramater (for callable). Does not return, but instead outputs content.
* @param string $name - an identifier for debugging purposes. Defaults to a 'un-named'. Has no effect upon funcitonality.
*
*/
public function registerUrlHandler($willRespond, $responder, $name='un-named'){
if (!is_callable($willRespond)){
throw new \Exception("Error registering '{$name}'. "
."\nThe first paramater of `registerUrlHandler` must be a callable. "
."\nIt will return TRUE if it handles the URL or FALSE otherwise.");
} else if (!is_callable($responder)&&(!is_object($responder)&&is_string($responder)&&!is_file($resonder))){
throw new \Exception("Error registering '{$name}'. "
."\nThe second paramater of 'registerUrlHandler' must be a callable or a valid file path."
."\nIt will output content and need not return a value");
}
$this->urlHandlers[] = ["willRespond" => $willRespond, "responder" => $responder,"name"=>$name];
}
/** An array of Content Handlers. See registerContentHandler for a description of the data. The data for each handler is stored as an array inside $urlHandlers with indices, but no string keys.
*
*/
protected $contentHandlers = [];
/** Loops through the registered Content Handlers, calling them in the order they were registered. Breaks the loop if a handler handles the content.
* Passes the previously echo'd content into the callable that process it.
*/
public function processContent(){
foreach ($this->contentHandlers as $index=>$handler){
$name = $handler['name'];
if (call_user_func($handler['willRespond'],$_SERVER['REQUEST_URI'])){
$resp = $handler['responder'];
if (is_callable($resp)){
call_user_func($resp,$this->content);
break;
} else {
throw new \Exception("ContentHandler named '{$name}' failed to process the content because the second paramater passed to registerContentHandler was NOT a callable. ");
}
}
}
}
/** Registers a file or a callable to handle a given request. If a given handler will respond to a request, then no other handler will respond to that request.
*
* @param callable $willRespond - A callable, invokable by call_user_func. Accepts the requested URL as it's only paramater. Must return TRUE or FALSE
* @param mixed $responder - A file to be included or a callable, invokable by call_user_func. Accepts the requested URL as it's only paramater (for callable). Does not return, but instead outputs content.
* @param string $name - an identifier for debugging purposes. Defaults to a 'un-named'. Has no effect upon funcitonality.
*
*/
public function registerContentHandler($willRespond, $responder, $name='un-named'){
if (!is_callable($willRespond)){
throw new \Exception("Error registering '{$name}'. "
."\nThe first paramater of `registerContentHandler` must be a callable. "
."\nIt will return TRUE if it handles the content or FALSE otherwise.");
} else if (!is_callable($responder)){
throw new \Exception("Error registering '{$name}'. "
."\nThe second paramater of 'registerContentHandler' must be a callable which accepts a single paramater."
."\nIt will be passed the content from the handle url event and will output that (or a modified version of) that content");
}
$this->contentHandlers[] = ["willRespond" => $willRespond, "responder" => $responder,"name"=>$name];
}
/*
*
*
*
* BEGIN CONFIGURATION CODE
*
*
*
*/
/** a key=>value array where the key is the Configuration Name and the value is one of: required, optional, or recommended.
* optional configurations do not need to be in this array. Configs within $availableConfigs are by default 'optional'
*/
protected $configLevel = ["devMode" => "required",
"serverSource"=>"required",
"siteSource" => "required",
];
/** The array of set configurations. When a developer sets a configuration it is added to this array. Previously set values are overwritten.
* This array contains default values for optional configurations.
*/
protected $config = [
"publicDir" => "Public",
"styleDir" => "Style",
"scriptDir" => "Script",
"resDir" => "Res",
"defaultUrlHandler" => TRUE,
"defaultContentHandler" => TRUE,
"willDeliver" => "willDeliver.php",
"eventsDir" => "Events",
"indexFile" => "index.php",
];
/** An array of all configurations which are available to be set.
* The key is the configuration name. The value is a message, which will be printed when showing relevant errors or warnings.
*
*/
protected $availableConfigs = ["devMode" => "Set to FALSE when you're going to production. Having as TRUE will output warnings & suggestions.",
"serverSource"=>"The root of your server. Generally \$_SERVER['DOCUMENT_ROOT'];",
"siteSource" => "The source of your website. This folder will contain the folders: 'Public', 'Style', 'Script', and 'Res', unless you change this setting.",
"publicDir" => "The name of the directory for public .php files inside your siteSource. Default is 'Public'",
"styleDir" => "The name of the direcotry for .css files inside your siteSource. Default is 'Style'",
"scriptDir" => "The name of the directory for .js files inside your siteSource. Default is 'Script'",
"resDir" => "The name of the directory inside your siteSource for all files which are not .php, .css, or .jss. Default is 'Res'",
"eventsDir" => "The name of the directory inside your siteSource for all files which are called as part of event processing.",
"indexFile" => "The name of the file which should be called when a directory is pointed to",
"defaultUrlHandler" => "TRUE to enable the default handler. FALSE to disable it. Runs only if all registered handlers fail to handle the url",
"defaultContentHandler" => "TRUE to enable the default handler, which merely echo's the content from the URL handling. FALSE to disable it. Runs only if all registered handlers fail to handle the content."
];
/** returns an array of all available configurations of the given type.
* 'all' will return all configurations listed in `availableConfigs`
* 'required' will return all configurations in $configLevel which have their type set to `required`
* `recommended` will return all configurations in $configLevel which have their type set to 'recommended'
* `optional' will return all configurations in $configLevel which have their type set to 'optional' AND it will return any congfigurations which do not have their type set in $configLevel, as long as they exist in `$availableConfigs`
*
* @throws if the submitted type is not one of the above mentioned
*/
public function getAvailableConfigs($type='all'){
$types = ['all','required','recommended','optional'];
if (!in_array($type,$types)){
throw new \Exception("'{$type}' is not a valid type of configuration. You must call getAvailableConfigs with one of ".implode('-',$types));
}
if ($type=='all')return $this->availableConfigs;
$confs = [];
foreach ($this->availableConfigs as $key=>$message){
if (TRUE||!isset($this->configLevel[$key])&&$type=='optional'||$this->configLevel[$key]==$type){
$confs[$key] = $this->availableConfigs[$key];
}
}
return $confs;
}
/** Returns TRUE if the $configName is required to be set. FALSE otherwise.
*
*/
public function isRequired($configName){
return isset($this->configLevel[$configName])&&$this->configLevel[$configName]=='required';
}
/** Returns TRUE if the $configName is required to be set. FALSE otherwise.
*
*/
public function isOptional($configName){
return isset($this->configLevel[$configName])&&$this->configLevel[$configName]=='optional'||isset($this->availableConfigs[$configName])&&!isset($this->configLevel[$configName]);
}
/** Returns TRUE if the config name is recommended to be set. FALSE otherwise
*/
public function isRecommended($configName){
return isset($this->configLevel[$configName])&&$this->configLevel[$configName]=='recommended';
}
/** This checks the set configurations.
* SHOULD log an error if recommended settings are NOT set and dev mode is turned on. (but doesn't yet)
* SHOULD check configured directories to ensure they exists.
*
* @throws an error if required configurations are not set. The error is very instructive.
*/
public function confirmConfigs(){
$configs = $this->getAvailableConfigs('required');
foreach ($configs as $confName => $message){
if (!isset($configs[$confName])){
throw new \Exception("You must set the config '{$confName}' for your requests to be handled. \nTo do this, call `".get_class($this)."::getInstance()->setConfig('{$confName}',CONFIGURATION_VALUE);` \nOr you may pass several configs as an array `...()->setMultiConfig([CONFIG_NAME => SETTING, CONFIG_2 => SETTING2]);`."
." \nInformation about '{$confName}': ".$message);
}
}
}
/** Returns an array of all configurations which have been set, including default configurations.
*/
public function getConfigArray(){
return $this->config;
}
/** Returns the set value for the given $configName.
*
* @throws if the config name has not been set. A different message is given if the config name CANNOT be set.
*/
public function getConfig($configName){
if (isset($this->config[$configName])){
return $this->config[$configName];
} else {
if (isset($this->availableConfigs[$configName])){
throw new \Exception("The configuration '{$configName}' has not been set. To set this configuration, try "
."\ROF\Handler::getInstance()->setConfig('{$configName}',VALUE)");
} else {
throw new \Exception("The configuration '{$configName}' does not exist and cannot be set. Do you have a typo? A capitalization problem?"
."\nTo get a list of available configuration options, try `print_r(\ROF\Handler::getInstance()->getAvailableConfigs('all'));`");
}
}
}
/** Sets the value for the given configuration name
*
*/
public function setConfig($configName,$configValue){
if (isset($this->availableConfigs[$configName])){
$this->config[$configName] = $configValue;
} else {
throw new \Exception("You set a config for key '{$configName}' but that is not a valid configuration. "
."\nTo get a list of available configuration options, try `print_r(\ROF\Handler::getInstance()->getAvailableConfigs('all'));`"
);
}
}
/** sets an array of values to their given names.
* The supplied $configArray should be a key=>value array containing configuration names for the keys and configuration settings for the values.
*
*/
public function setMultiConfig($configArray){
foreach ($configArray as $key=>$value){
$this->setConfig($key,$value);
}
}
/*
*
*
*
* END CONFIGURATION CODE
*
*
*
*
*/
}
?>