MVC;任意路由路径级别和参数

发布于 2024-10-21 07:10:54 字数 1296 浏览 9 评论 0原文

我正在使用 PHP 开发一个(哦,不,不是另一个)MVC 框架,主要用于教育目的,但也很有趣和有利可图。

无论如何,我的路由器遇到了一些问题,特别是使用正确的参数路由到正确的路径。现在,我正在研究一个路由器(使用__autoload())允许任意长的路由路径:

"path/to/controller/action"
"also/a/path/to/a/controller/action"

路由从application目录开始,路由路径是本质上与文件系统路径并行:

"/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(){}
    }

这将允许模块配置文件在应用程序文件系统的不同级别上可用。当然,问题是,当我们引入路由路径参数时,很难区分路由路径的结束位置和路径参数的开始位置:

"path/to/controller/action/key1/param1/key2/param2"

显然会寻找文件:

"/framework/application/path/to/controller/action/key1/param1/key2.class.php"
    => 'param2()'
//no class or method by this name can be found

这不好。当然,现在这听起来像是一个设计问题,但我确信必须有一种干净的方法来规避这个问题。

我最初的想法是测试路由路径的每个级别是否存在目录/文件。

  • 如果它命中 1+ 个目录,后跟一个文件,则附加路径组件是一个操作,后跟参数。
  • 如果它到达 1+ 个目录并且没有找到文件,则对其进行 404。

但是,这仍然容易错误地查找文件。当然,这可以通过更严格的命名约定和保留某些单词来缓解,但如果可能的话,我想避免这种情况。

我不知道这是否是最好的方法。有没有人以优雅的方式解决了这样的问题?

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.

  • If it hits 1+ directories followed by a file, additional path components are an action followed by parameters.
  • If it hits 1+ directories and no file is found, 404 it.

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?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

明天过后 2024-10-28 07:10:54

好吧,用我自己的建议来回答我自己的问题:

// 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);
}

我的自动加载器是最基本的:

function __autoload($className){
    require_once FLOOID_PATH_BASE . '/' . str_replace('_', '/', $className) . '.class.php';
}

它的效果出奇地好。我尚未实现某些检查,例如确保请求的控制器实际上是从我的 Flooid_System_ControllerAbstract 扩展的,但目前,这就是我正在运行的内容。

无论如何,我认为这种方法即使不是全面的改革,也可以从批评中受益。


此后我修改了这种方法,尽管它最终执行了相同的功能。它不实例化控制器,而是传回控制器类名、方法名和参数数组。这一切的核心都在 getVerifiedRouteData()verifyRouteParts()createParamArray() 中。 我想我想重构或修改这个类。我正在寻找有关在哪里可以优化可读性和可用性的见解。

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;
    }

}

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;
    }

}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文