Reputation: 20592
I'm working on an (oh no, not another) MVC framework in PHP, primarily for education, but also fun and profit.
Anyways, I'm having some trouble with my Router
, specifically routing to the correct paths, with the correct parameters. Right now, I'm looking at a router that (using __autoload()
) allows for arbitrarily long routing paths:
"path/to/controller/action"
"also/a/path/to/a/controller/action"
Routing starts at the application
directory, and the routing path is essentially parallel with the file system path:
"/framework/application/path/to/controller.class.php" => "action()"
class Path_To_Controller{
public function action(){}
}
"/framework/application/also/a/path/to/a/controller.class.php" => "action()"
class Also_A_Path_To_A_Controller{
public function action(){}
}
This will allow for module configuration files to be available at varying levels of the application file system. The problem is of course, when we introduce routing path parameters, it becomes difficult differentiating where the routing path ends and the path parameters begin:
"path/to/controller/action/key1/param1/key2/param2"
Will obviously be looking for the file:
"/framework/application/path/to/controller/action/key1/param1/key2.class.php"
=> 'param2()'
//no class or method by this name can be found
This ain't good. Now this smells like a design issue of course, but I'm certain there must be a clean way to circumvent this problem.
My initial thoughts were to test each level of the routing path for directory/file existence.
However, this is still susceptible to erroneously finding files. Sure this can be alleviated by stricter naming conventions and reserving certain words, but I'd like to avoid that if possible.
I don't know if this is the best approach. Has anyone solved such an issue in an elegant manner?
Upvotes: 1
Views: 762
Reputation: 20592
Well, to answer my own question with my own suggestion:
// split routePath and set base path
$routeParts = explode('/', $routePath);
$classPath = 'Flooid/Application';
do{
// append part to path and check if file exists
$classPath .= '/' . array_shift($routeParts);
if(is_file(FLOOID_PATH_BASE . '/' . $classPath . '.class.php')){
// transform to class name and check if method exists
$className = str_replace('/', '_', $classPath);
if(method_exists($className, $action = array_shift($routeParts))){
// build param key => value array
do{
$routeParams[current($routeParts)] = next($routeParts);
}while(next($routeParts));
// controller instance with params passed to __construct and break
$controller = new $className($routeParams);
break;
}
}
}while(!empty($routeParts));
// if controller exists call action else 404
if(isset($controller)){
$controller->{$action}();
}else{
throw new Flooid_System_ResponseException(404);
}
My autoloader is about as basic as it gets:
function __autoload($className){
require_once FLOOID_PATH_BASE . '/' . str_replace('_', '/', $className) . '.class.php';
}
This works, surprisingly well. I've yet to implement certain checks, like ensuring that the requested controller in fact extends from my Flooid_System_ControllerAbstract
, but for the time being, this is what I'm running with.
Regardless, I feel this approach could benefit from critique, if not a full blown overhaul.
I've since revised this approach, though it ultimately performs the same functionality. Instead of instantiating the controller, it passes back the controller class name, method name, and parameter array. The guts of it all are in getVerifiedRouteData()
, verifyRouteParts()
and createParamArray()
. I'm thinking I want to refactor or revamp this class though. I'm looking for insight on where I can optimize readability and usability.:
class Flooid_Core_Router {
protected $_routeTable = array();
public function getRouteTable() {
return !empty($this->_routeTable)
? $this->_routeTable
: null;
}
public function setRouteTable(Array $routeTable, $mergeTables = true) {
$this->_routeTable = $mergeTables
? array_merge($this->_routeTable, $routeTable)
: $routeTable;
return $this;
}
public function getRouteRule($routeFrom) {
return isset($this->_routeTable[$routeFrom])
? $this->_routeTable[$routeFrom]
: null;
}
public function setRouteRule($routeFrom, $routeTo, Array $routeParams = null) {
$this->_routeTable[$routeFrom] = is_null($routeParams)
? $routeTo
: array($routeTo, $routeParams);
return $this;
}
public function unsetRouteRule($routeFrom) {
if(isset($this->_routeTable[$routeFrom])){
unset($this->_routeTable[$routeFrom]);
}
return $this;
}
public function getResolvedRoutePath($routePath, $strict = false) {
// iterate table
foreach($this->_routeTable as $routeFrom => $routeData){
// if advanced rule
if(is_array($routeData)){
// build rule
list($routeTo, $routeParams) = each($routeData);
foreach($routeParams as $paramName => $paramRule){
$routeFrom = str_replace("{{$paramName}}", "(?<{$paramName}>{$paramRule})", $routeFrom);
}
// if !advanced rule
}else{
// set rule
$routeTo = $routeData;
}
// if path matches rule
if(preg_match("#^{$routeFrom}$#Di", $routePath, $paramMatch)){
// check for and iterate rule param matches
if(is_array($paramMatch)){
foreach($paramMatch as $paramKey => $paramValue){
$routeTo = str_replace("{{$paramName}}", $paramValue, $routeTo);
}
}
// return resolved path
return $routeTo;
}
}
// if !strict return original path
return !$strict
? $routePath
: false;
}
public function createParamArray(Array $routeParts) {
$params = array();
if(!empty($routeParts)){
// iterate indexed array, use odd elements as keys
do{
$params[current($routeParts)] = next($routeParts);
}while(next($routeParts));
}
return $params;
}
public function verifyRouteParts($className, $methodName) {
if(!is_subclass_of($className, 'Flooid_Core_Controller_Abstract')){
return false;
}
if(!method_exists($className, $methodName)){
return false;
}
return true;
}
public function getVerfiedRouteData($routePath) {
$classParts = $routeParts = explode('/', $routePath);
// iterate class parts
do{
// get parts
$classPath = 'Flooid/Application/' . implode('/', $classParts);
$className = str_replace('/', '_', $classPath);
$methodName = isset($routeParts[count($classParts)]);
// if verified parts
if(is_file(FLOOID_PATH_BASE . '/' . $classPath . '.class.php') && $this->verifyRouteParts($className, $methodName)){
// return data array on verified
return array(
'className'
=> $className,
'methodName'
=> $methodName,
'params'
=> $this->createParamArray(array_slice($routeParts, count($classParts) + 1)),
);
}
// if !verified parts, slide back class/method/params
$classParts = array_slice($classParts, 0, count($classParts) - 1);
}while(!empty($classParts));
// return false on not verified
return false;
}
}
Upvotes: 1