Just for fun——PHP框架之簡單的路由器(2)

Salamander發表於2019-02-16

改進

緊接上一篇文章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;
    }
}

相關文章