Just for fun——PHP框架之简单的路由器(2)

改进

紧接上一篇文章Just for fun——PHP框架之简单的路由器(1)
代码下载

效率不高原因

对于以下合并的正则

~^(?:
    /user/([^/]+)/(\d+)
    | /user/(\d+)
    | /user/([^/]+)
)$~x

最终匹配的是分组中的某一个,我们需要的子匹配也是那个分组中的,然而从结果看

preg_match($regex, '/user/nikic', $matches);
=> [
    "/user/nikic",   # 完全匹配
    "", "",          # 第一个(空)
    "",              # 第二个(空)
    "nikic",         # 第三个(被使用)
]

这里是最后一个路由被匹配了,但是其他分组的子匹配也被填充了,这是多余的。

解决思路

PCRE正则里?|也是非捕获分组,那么?|?:有什么区别呢??
区别在于?|组号重置,看以下几个例子就懂了

preg_match('~(?:(Sat)ur|(Sun))day~', 'Saturday', $matches)
=> ["Saturday", "Sat", ""]   # 最后一个""其实是不存在的,写在这里是为了阐释概念

preg_match('~(?:(Sat)ur|(Sun))day~', 'Sunday', $matches)
=> ["Sunday", "", "Sun"]

preg_match('~(?|(Sat)ur|(Sun))day~', 'Saturday', $matches)
=> ["Saturday", "Sat"]

preg_match('~(?|(Sat)ur|(Sun))day~', 'Sunday', $matches)
=> ["Sunday", "Sun"]

所有我们可以用?|来代替?:来减少多余的子匹配填充,但是这样一来的话,如何判断哪个分组被匹配了呢??(因为之前的判断技巧就失效了)
我们可以这样,添加一些多余子匹配

~^(?|
    /user/([^/]+)/(\d+)
  | /user/(\d+)()()
  | /user/([^/]+)()()()
)$~x

实现

dispatcher.php
<?php
/**
 * User: salamander
 * Date: 2017/11/12
 * Time: 13:43
 */

namespace SalamanderRoute;

class Dispatcher {
    /** @var mixed[][] */
    protected $staticRoutes = [];

    /** @var Route[][] */
    private $methodToRegexToRoutesMap = [];

    const NOT_FOUND = 0;
    const FOUND = 1;
    const METHOD_NOT_ALLOWED = 2;

    /**
     * 提取占位符
     * @param $route
     * @return array
     */
    private function parse($route) {
        $regex = '~^(?:/[a-zA-Z0-9_]*|/\{([a-zA-Z0-9_]+?)\})+/?$~';
        if(preg_match($regex, $route, $matches)) {
            // 区分静态路由和动态路由
            if(count($matches) > 1) {
                preg_match_all('~\{([a-zA-Z0-9_]+?)\}~', $route, $matchesVariables);
                return [
                    preg_replace('~{[a-zA-Z0-9_]+?}~', '([a-zA-Z0-9_]+)', $route),
                    $matchesVariables[1],
                ];
            } else {
                return [
                    $route,
                    [],
                ];
            }
        }
        throw new \LogicException('register route failed, pattern is illegal');
    }

    /**
     * 注册路由
     * @param $httpMethod string | string[]
     * @param $route
     * @param $handler
     */
    public function addRoute($httpMethod, $route, $handler) {
        $routeData = $this->parse($route);
        foreach ((array) $httpMethod as $method) {
            if ($this->isStaticRoute($routeData)) {
                $this->addStaticRoute($method, $routeData, $handler);
            } else {
                $this->addVariableRoute($method, $routeData, $handler);
            }
        }
    }


    private function isStaticRoute($routeData) {
        return count($routeData[1]) === 0;
    }

    private function addStaticRoute($httpMethod, $routeData, $handler) {
        $routeStr = $routeData[0];

        if (isset($this->staticRoutes[$httpMethod][$routeStr])) {
            throw new \LogicException(sprintf(
                'Cannot register two routes matching "%s" for method "%s"',
                $routeStr, $httpMethod
            ));
        }

        if (isset($this->methodToRegexToRoutesMap[$httpMethod])) {
            foreach ($this->methodToRegexToRoutesMap[$httpMethod] as $route) {
                if ($route->matches($routeStr)) {
                    throw new \LogicException(sprintf(
                        'Static route "%s" is shadowed by previously defined variable route "%s" for method "%s"',
                        $routeStr, $route->regex, $httpMethod
                    ));
                }
            }
        }

        $this->staticRoutes[$httpMethod][$routeStr] = $handler;
    }


    private function addVariableRoute($httpMethod, $routeData, $handler) {
        list($regex, $variables) = $routeData;

        if (isset($this->methodToRegexToRoutesMap[$httpMethod][$regex])) {
            throw new \LogicException(sprintf(
                'Cannot register two routes matching "%s" for method "%s"',
                $regex, $httpMethod
            ));
        }

        $this->methodToRegexToRoutesMap[$httpMethod][$regex] = new Route(
            $httpMethod, $handler, $regex, $variables
        );
    }


    public function get($route, $handler) {
        $this->addRoute('GET', $route, $handler);
    }

    public function post($route, $handler) {
        $this->addRoute('POST', $route, $handler);
    }

    public function put($route, $handler) {
        $this->addRoute('PUT', $route, $handler);
    }

    public function delete($route, $handler) {
        $this->addRoute('DELETE', $route, $handler);
    }

    public function patch($route, $handler) {
        $this->addRoute('PATCH', $route, $handler);
    }

    public function head($route, $handler) {
        $this->addRoute('HEAD', $route, $handler);
    }

    /**
     * 分发
     * @param $httpMethod
     * @param $uri
     */
    public function dispatch($httpMethod, $uri) {
        $staticRoutes = array_keys($this->staticRoutes[$httpMethod]);
        foreach ($staticRoutes as $staticRoute) {
            if($staticRoute === $uri) {
                return [self::FOUND, $this->staticRoutes[$httpMethod][$staticRoute], []];
            }
        }

        $routeLookup = [];
        $regexes = [];
        foreach ($this->methodToRegexToRoutesMap[$httpMethod] as $regex => $route) {
            $index = count($route->variables);
            if(array_key_exists($index, $routeLookup)) {
                $indexNear = $this->getArrNearEmptyEntry($routeLookup, $index);
                array_push($regexes, $regex . str_repeat('()', $indexNear - $index));
                $routeLookup[$indexNear] = [
                    $this->methodToRegexToRoutesMap[$httpMethod][$regex]->handler,
                    $this->methodToRegexToRoutesMap[$httpMethod][$regex]->variables,
                ];
            } else {
                $routeLookup[$index] = [
                    $this->methodToRegexToRoutesMap[$httpMethod][$regex]->handler,
                    $this->methodToRegexToRoutesMap[$httpMethod][$regex]->variables,
                ];
                array_push($regexes, $regex);
            }
        }
        $regexCombined = '~^(?|' . implode('|', $regexes) . ')$~';
        if(!preg_match($regexCombined, $uri, $matches)) {
            return [self::NOT_FOUND];
        }
        list($handler, $varNames) = $routeLookup[count($matches) - 1];
        $vars = [];
        $i = 0;
        foreach ($varNames as $varName) {
            $vars[$varName] = $matches[++$i];
        }
        return [self::FOUND, $handler, $vars];
    }

    private function getArrNearEmptyEntry(&$arr, $index) {
        while (array_key_exists(++$index, $arr));
        return $index;
    }
}

Salamander
上帝在我很小的时候送给我了两个苹果,一个红苹果,一个蓝苹果。红苹果代表疯狂,蓝苹果代表思考
6.7k 声望
407 粉丝
0 条评论
推荐阅读
Java AtomicInteger类使用
这个问题发生的原因是++counter不是一个原子性操作。当要对一个变量进行计算的时候,CPU需要先从内存中将该变量的值读取到高速缓存中,再去计算,计算完毕后再将变量同步到主内存中。这在多线程环境中就会遇到问...

pigLoveRabbit2阅读 2.3k

怎样用 PHP 来实现枚举?
在数学和计算机科学理论中,一个集的枚举是列出某些有穷序列集的所有成员的程序,或者是一种特定类型对象的计数。这两种类型经常(但不总是)重叠。枚举是一个被命名的整型常数的集合,枚举在日常生活中很常见,...

唯一丶25阅读 6.4k评论 4

PHP转Go实践:xjson解析神器「开源工具集」
我和劲仔都是PHP转Go,身边越来越多做PHP的朋友也逐渐在用Go进行重构,重构过程中,会发现php的json解析操作(系列化与反序列化)是真的香,弱类型语言的各种隐式类型转换,很大程度的减低了程序的复杂度。

王中阳Go10阅读 2k评论 3

封面图
图片防盗链破解 解决图片防盗链问题 反向代理
当客户端(浏览器)向服务器请求内容的时候,会提交一个header,这个header中包含了如:浏览器信息、cookie等内容,那么有一个叫referer的东东,也包含在这里面。

TANKING7阅读 11.3k评论 5

Git操作不规范,战友提刀来相见!
年终奖都没了,还要扣我绩效,门都没有,哈哈。这波骚Git操作我也是第一次用,担心闪了腰,所以不仅做了备份,也做了笔记,分享给大家。问题描述小A和我在同时开发一个功能模块,他在优化之前的代码逻辑,我在开...

王中阳Go5阅读 2.3k评论 2

封面图
Hyperf 3.0 发布,PHP 新时代
在过去的一年半时间里,Hyperf 2.2 共发布了 35 个小版本,使 Hyperf 达到了一个前所未有的高度,这里也获得了一些不错的数据反馈。

huangzhhui4阅读 1.1k评论 1

封面图
微信公众号开发:自动回复文本/图片/图文消息/关键词回复/上传素材/自定义菜单
对接流程1、申请微信公众号测试账号URL:[链接]2、登录,配置开发者服务器URL和Token开发者服务器配置代码:config.php {代码...} URL是config.php在你服务器的URLToken是上面代码自己设置的Token搞定之后,就能完...

TANKING2阅读 10.1k

6.7k 声望
407 粉丝
宣传栏