Router.php

<?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
    *
    *
    *
    *
    */
    
    

    
    
    
    
    
    
}

?>