<?php
/**
* _Core_FrontController Performs all the needed operations for PWF flow. Decides which Controller will be used and what method.
* It also builds a communication port with the Controller that instantiate providing its instance as constructor parameter.
* Also decides the view template and includes it.
* @copyright Copyright (c) 2012, PWF
* @license http://phpwebframework.com/License
* @version PWF_0.3.0
*/
class _Core_FrontController
{
/**
* Instance of _Core_Path with properties and methods relative to the url path
* @var _Core_Path
*/
public $path;
/**
* Instance of _Core_Server with properties and methods relative to the variables passed by the server
* @var _Core_Server
*/
public $server;
/**
* Instance of _Properties generated by the PROPERTIES_FILE constant difined in the index.php
* @var _Properties
*/
public $properties;
/**
* Instance of _Core_Post with properties and methods relative to data received by post
* @var _Core_Post
*/
public $post;
/**
* The unique id of the application
* @var string
*/
public $applicationId;
/**
* If the Controller decides to use an alternative template file than the one normally used
* from the properties (MAIN_TEMPLATE or SPECIAL_TEMPLATES) then it sets this variable and
* FrontController will use that instead.
* @var string
*/
public $specialTemplate;
/**
* The serverâs path of the directory where index.php is
* @var string
*/
private $publicPath;
/**
* The instance of the _Core_ClassLoader that is responsible for autoloading , caching public
* static Controller's properties and caching binds.
* @var _Core_ClassLoader
*/
private $loader;
/**
* The instance of the current Controller
* @var _Core_ControllerAbstract
*/
private $controller;
/**
* The reflection class of the current Controller
* @var ReflectionClass
*/
private $controllerReflection;
/**
* The properAfterRootUrl is the after root url with Capitalized the first letter after /
* Controllers paths that may used in properties (SPECIAL_TEMPLATES and PATHS_MAP) must fallow this rule
* @var string
*/
private $properAfterRootUrl;
/**
* Contructs the FrontController instance (called outside the class - near the end of this file)
* Initialize needed parameters and objects and dictates the flow.
* @param string $publicPath
*/
public function __construct($publicPath)
{
$this->loader = new _Core_ClassLoader();
session_start();
$this->publicPath = $publicPath;
$this->loadProperties();
$this->loader->init($this->properties);
$this->applicationId = _Core_ClassLoader::$applicationId;
$this->server = new _Core_Server();
$this->post = new _Core_Post();
$this->setController();
$this->startReflection();
$this->setPublicProperties();
$this->checkInternalPost();
$this->invokeMethods();
$this->includeTempate();
$this->caching();
}
/**
* Desides which Controller is going to be used
* @throws _Exception_FileSystem
*/
private function setController()
{
$rootUrl = _String::toLowerCase($this->server->getProtocol())."://".$this->server->getHttpHost() . "/" . _String::beforeLast($this->server->getPhpSelf(), "/");
$afterRootUrl = _String::afterFirst(_String::toLowerCase($this->server->getProtocol())."://".$this->server->getHttpHost() . "/" .$this->server->getRedirectUrl(),$rootUrl."/");
$controllerClass = null;
$controllerUrl = null;
$controllerExtension = "";
$pathVars = array();
if($afterRootUrl == "")
{
// Root Controller
$controllerCheck = $this->properties->get("CONTROLLERS_FOLDER")."/".$this->properties->get("ROOT_CONTROLLER");
if(_File::exists($controllerCheck.".php"))
{
$controllerClass = _String::replace($controllerCheck, "/", "_");
$controllerUrl = "";
$pathVars = _String::split($afterRootUrl, "/");
$this->properAfterRootUrl = "";
}
else
{
throw new _Exception_FileSystem($controllerCheck.".php "._Exception_Messages::$fileNotFound, 1234);
}
}
else
{
// Find Controller
$properPathVars = _String::split(_String::capitalizeWordsFirstLetter(_String::replace($afterRootUrl, "/", " ")), " ");
$this->properAfterRootUrl = _Array::joinValues($properPathVars, "/");
$properAfterRootUrl = $this->properAfterRootUrl;
// Search in paths map
$replacementFrom = null;
$replacementTo = null;
$disabledPath = false;
if($this->properties->containsKey("PATHS_MAP"))
{
foreach ($this->properties->get("PATHS_MAP") as $path => $controllerReplacement)
{
if(_String::startsWith($properAfterRootUrl, $path,false))
{
$disabledPath = false;
$properAfterRootUrl = trim($controllerReplacement._String::afterFirst($properAfterRootUrl, $path),"/");
$properPathVars = _String::split($properAfterRootUrl, "/");
$replacementFrom = $path;
$replacementTo = $controllerReplacement;
}
else if(_String::startsWith($properAfterRootUrl, $controllerReplacement,false))
{
$disabledPath = true;
}
}
}
// Search controller using proper path variables
while(!$disabledPath && count($properPathVars) > 0 && $controllerClass == null)
{
$controllerExtension = "";
if(_String::contains($properPathVars[count($properPathVars) - 1], "."))
{
$proper = $properPathVars[count($properPathVars) - 1];
$properPathVars[count($properPathVars) - 1] = _String::beforeFirst($proper, ".");
$controllerExtension = "."._String::afterFirst($proper, ".");
}
$properJoinValues = _Array::joinValues($properPathVars, "/");
$fileToSearch = _String::insesitiveCasePattern($this->properties->get("CONTROLLERS_FOLDER")."/".$properJoinValues);
$match = _File::match($fileToSearch.".php");
if(count($match) > 0)
{
$controllerClass = _String::replace(_String::beforeLast($match[0],"."), "/", "_");
if($replacementFrom == null)
{
$controllerUrl = _String::substring($afterRootUrl, 0,_String::length($properJoinValues.$controllerExtension));
}
else
{
$controllerUrl = $replacementFrom._String::substring($properJoinValues.$controllerExtension, _String::length($replacementTo));
}
$pathVars = _String::split(trim(_String::afterFirst($afterRootUrl, $controllerUrl),"/"), "/");
}
else
{
_Array::removeLastElement($properPathVars);
}
}
//If there isn't yet controller it will be the NOT_FOUND_CONTROLLER from properties
if($controllerClass == null || _String::endsWith($controllerClass, "Abstract",false) || _String::endsWith($controllerClass, "Interface",false))
{
$controllerCheck = $this->properties->get("CONTROLLERS_FOLDER")."/".$this->properties->get("NOT_FOUND_CONTROLLER");
if(_File::exists($controllerCheck.".php"))
{
$controllerClass = _String::replace($controllerCheck, "/", "_");
$controllerUrl = "";
$pathVars = _String::split($afterRootUrl, "/");
}
else
{
throw new _Exception_FileSystem($controllerCheck.".php "._Exception_Messages::$fileNotFound, 1234);
}
}
}
if(count($pathVars)==1 && $pathVars[0] == "")
{
$pathVars = array();
}
$controllerPath = _String::replace(_String::afterFirst($controllerClass, $this->properties->get("CONTROLLERS_FOLDER")."_"), "_", "/");
$this->path = new _Core_Path($this->publicPath, $rootUrl, $pathVars, $afterRootUrl, $controllerClass, $controllerPath, $controllerUrl,$controllerExtension);
}
/**
* Starts the reflection constructing the reflection class and the Controller class instances
*/
private function startReflection()
{
$this->controllerReflection = new ReflectionClass($this->path->getControllerClass());
$this->controller = $this->controllerReflection->newInstance($this);
}
/**
* Instantiate the properties object based in the PROPERTIES_FILE constant and adds some needed properties to it
* if not allready exists
*/
private function loadProperties()
{
$this->properties = new _Properties(PROPERTIES_FILE);
$this->addPropertyIfNotExists("VIEWS_FOLDER", "View");
$this->addPropertyIfNotExists("JAVASCRIPTS_FOLDER", "JavaScripts");
$this->addPropertyIfNotExists("MAIN_TEMPLATE", "template");
$this->addPropertyIfNotExists("CONTROLLERS_FOLDER", "Controller");
}
/**
* Sets public properties values of the Controller for visit scope caching
* using session if those properties have been already cached.
* (it excludes public properties starting with underscore _ )
*/
private function setPublicProperties()
{
$className = $this->controllerReflection->getName();
foreach($this->controllerReflection->getProperties() as $property)
{
$isSetable = $property->isPublic() && !$property->isStatic() && $property->getDeclaringClass()->getName() != "_Core_ControllerAbstract" && !_String::startsWith($property->getName(), "_");
if($isSetable)
{
if(isset($_SESSION[$this->applicationId."-".$className][$property->getName()]))
{
$property->setValue($this->controller,$_SESSION[$this->applicationId."-".$className][$property->getName()]);
}
}
}
}
/**
* Save public properties values of the Controller for visit scope caching
* using session.
* (it excludes public properties starting with underscore _ )
*/
private function savePublicProperties()
{
$className = $this->controllerReflection->getName();
foreach($this->controllerReflection->getProperties() as $property)
{
$isSetable = $property->isPublic() && !$property->isStatic() && $property->getDeclaringClass()->getName() != "_Core_Controller" && !_String::startsWith($property->getName(), "_");
if($isSetable)
{
$value = $property->getValue($this->controller);
if($value != null)
{
$_SESSION[$this->applicationId."-".$className][$property->getName()] = $value;
}
}
}
}
/**
* If the submit function is called by a Controller, then the post data are stored in session.
* This function checks if there is internal post (from a Controller) and if there is it feels
* the $_POST array.
*/
private function checkInternalPost()
{
if(isset($_SESSION[$this->applicationId]["POST"]))
{
unset($_POST);
$_POST = $_SESSION[$this->applicationId]["POST"];
unset($_SESSION[$this->applicationId]["POST"]);
}
}
/**
* The logic of deciding the methods that will be invoked of the current Controller instance and calls to invoke function to invoke them.
* If there is onUse then it will be invoked
*
* If there is Post AND (PWF)Get then first will invoke onPost and then onGet
* Else if there is Post will invoke onPost
* Else if there is (PWF)Get will invoke onGet
*
* If there weren't any invoke from the above logick then onEntry will be invoked
* (Meaning that it wans't Post or (PWF)Get or that the above check didn't invoked any method from
* current Controller class (not its parents).
*
* If there is onView then it will be invoked
*
*/
private function invokeMethods()
{
$this->invoke("onUse");
$invoked = false;
if(!$this->post->isEmpty() && $this->path->hasVars())
{
$invoked = $this->invoke("onPost") || $this->invoke("onGet");
}
else if(!$this->post->isEmpty())
{
$invoked = $this->invoke("onPost");
}
else if($this->path->hasVars())
{
$invoked = $this->invoke("onGet");
}
if(!$invoked)
{
$this->invoke("onEntry");
}
$this->invoke("onView");
}
/**
* It checks that there is a method needed to be invoked in the current Controller , invoke it and returns true if that method
* invoked AND was declared in the current Controller class (NOT ITS PARENTS)
* @param string $methodName
* @return boolean
*/
private function invoke($methodName)
{
$invoked = false;
if($this->controllerReflection->hasMethod($methodName))
{
$this->controllerReflection->getMethod($methodName)->invoke($this->controller,"");
$invoked = $this->controllerReflection->getMethod($methodName)->getDeclaringClass()->getName() == $this->controllerReflection->getName();
}
return $invoked;
}
/**
* It stops the flow order and redirect to the given Controller Path imitating the PWF.js submit function.
* It keeps post data to session variables , redirects and then FrontController will recognize them as post.
* @param string $submitData The submit data as defined also for a submit from a page
* (e.g. ControlerPath?action:actionData or action or action:actionData or ControlerPath?action if ControllerPath
* is not provided the current controller path will be used)
* @param boolean $clearPostData If true the current data from post will not be used.
*/
public function submit($submitData,$clearPostData)
{
if($clearPostData == true)
{
unset($_POST);
}
else
{
$this->post->removeList("pwf");
$_SESSION[$this->applicationId]["POST"] = $_POST;
}
$action = self::internalReplacement($submitData);
$toControllerPath = $this->path->getControllerPath();
if(_String::contains($action, "?"))
{
$toControllerPath = _String::beforeFirst($action, "?");
$action = _String::afterFirst($action, "?");
}
if(_String::contains($action, ":"))
{
$action = _String::beforeFirst($action, ":");
$actionData = _String::afterFirst($action, ":");
if(_String::contains($actionData, "="))
{
mb_parse_str($actionData,$result);
foreach ($result as $key => $value)
{
$_SESSION[$this->applicationId]["POST"]["pwf_ActionData_".self::internalRestoration($key)] = self::internalRestoration($value);
}
}
}
$_SESSION[$this->applicationId]["POST"]["pwf_Action"] = $action;
$this->link($toControllerPath);
}
/**
* It stops the flow order and redirect to the given url or path of the Controller relative to root()
* @param $link The url for the redirection or the path of the Controller to redirect to
*/
public function link($link)
{
$this->caching();
header("Location: ".(_String::startsWith($link, "http",false)?"":$this->path->getRootUrl()."/").$link);
exit;
}
/**
* Sets the special Template property that will be used for template instead of what would normally would
* @param string $template
*/
public function setSpecialTemplate($template)
{
$this->specialTemplate = $template;
}
/**
* Enter description here ...
* @param unknown_type $propertyName
* @param unknown_type $propertyValue
*/
private function addPropertyIfNotExists($propertyName,$propertyValue)
{
if(!$this->properties->containsKey($propertyName))
{
$this->properties->set($propertyName, $propertyValue);
}
}
/**
* Dicides which template file will be used and includes it
* @throws _Exception_FileSystem
*/
private function includeTempate()
{
if(!isset($this->specialTemplate))
{
if($this->properties->containsKey("SPECIAL_TEMPLATES"))
{
$properAfterRootUrl = $this->properAfterRootUrl;
foreach ($this->properties->get("SPECIAL_TEMPLATES") as $path => $template)
{
if(_String::startsWith($properAfterRootUrl, $path,false))
{
$file = $this->properties->get("VIEWS_FOLDER")."/".$template.".php";
}
}
}
if(!isset($file))
{
$file = $this->properties->get("VIEWS_FOLDER")."/".$this->properties->get("MAIN_TEMPLATE").".php";
}
}
else
{
$file = $this->properties->get("VIEWS_FOLDER")."/".$this->specialTemplate.".php";
}
if(_File::exists($file))
{
$c = $this->controller;
include $file;
}
else
{
throw new _Exception_FileSystem(_String::prepare(_Exception_Messages::$templateFileDontExists, $file), 1234);
}
}
/**
* It saves properties values for
* Visit Scope Caching (for current Controller public properties - not started with underscore) using session
* Application Scope Caching (for current Controller public static properties - not started with underscore) using file system
* Caching Binds (for current Controller public static properties - started with underscore and declared the binding from inside the Controller using an id) using file system
*/
private function caching()
{
$this->savePublicProperties();
$this->loader->cache();
}
/**
* Performs replacements in special characters in properties and in actionData.
* (e.g. That way if you want to have = or & in the value of a property then you just
* use the escape symbol prior to them \= and \&)
* @param $str The sting to make the internal replacement
* @return string
*/
public static function internalReplacement($str)
{
return _String::replace($str, array("\&","\=","\:","[","]","\?"), array("{PWF_AMPERSAND}","{PWF_EQUALS}","{PWF_COLON}","{PWF_LSB}","{PWF_RSB}","{PWF_QUESTION_MARK}"));
}
/**
* Takes a string that have been altered using internalReplacement and changes the special symbols without the
* escape character.
* @param $str The string where the special characters will be restored
* @return string
*/
public static function internalRestoration($str)
{
return _String::replace($str, array("{PWF_AMPERSAND}","{PWF_EQUALS}","{PWF_COLON}","{PWF_LSB}","{PWF_RSB}","{PWF_QUESTION_MARK}"), array("&","=",":","[","]","?"));
}
}
// Set the error handler function
set_error_handler("errorHandler");
// Set the encoding for multibyte string functions
mb_internal_encoding("UTF-8");
require_once(PWF_VERSION."/Core/ClassLoader.php");
// Sets the publicPath (the current directory)
$publicPath = getcwd();
// Changes the current directory with the SRC directory difined in the index.php
if(!@chdir(SRC))
{
handleException(new _Exception_FileSystem(SRC." "._Exception_Messages::$directoryNotFound, 1568));
}
// It defines the PROPERTIES_FILE as properties.pwf index.php didn't
if(!defined("PROPERTIES_FILE"))
{
define("PROPERTIES_FILE","properties.pwf");
}
// Instantiate the FrontController with the publicPath as constructor argument
try
{
$frontController = new _Core_FrontController($publicPath);
}
catch (Exception $e)
{
handleException($e);
}
exit;
/**
* If an exception is allready thrown then it knows it in order not to try to
* log the new exception (that could lead to infinite loop in a file system exception
* for example writing the log file)
*/
$exceptionOnProccess = false;
/**
* The Exception handler function of the project if there is ErrorHandler.php
* then it will loaded with the exception as $e. That file can also declare
* as $log (boolean) if the exception will be logged into a file and with $logFile the file
* (and path) of the error log file. If those two won't declared then it will log the error to
* the exceptions.pwf file)
* @param Exception $e
*/
function handleException(Exception $e)
{
global $exceptionOnProccess;
require_once(PWF_VERSION."/File.php");
$log = true;
$logFile = "exceptions.pwf";
if(!$exceptionOnProccess)
{
$exceptionOnProccess = true;
$filename = "ErrorHandler.php";
if(_File::exists($filename))
{
include $filename;
}
}
if($log)
{
_File::append($logFile, "\n".date("Y-m-d H:i:s")." ".getcwd()."\n".$e."\n");
}
exit;
}
/**
* The error handler function for errors generated by PHP
* @param $code
* @param $errstr
* @param $filename
* @param $lineno
*/
function errorHandler($code, $errstr, $filename, $lineno )
{
handleException(new _Exception_FatalError($errstr, $code, 99, $filename, $lineno));
}
?>