timetoogo/rapid-route

该软件包已被放弃,不再维护。未建议替代软件包。

另一个快速路由库,用于PHP

2.0.0 2015-08-09 02:20 UTC

This package is auto-updated.

Last update: 2022-12-25 03:22:05 UTC


README

Build status Code quality Coverage Status Stable Release License

RapidRoute旨在成为另一个快速的PHP路由器。该库通过将路由器编译为优化后的PHP代码,采用不同的方法进行uri路由,最小化了传统正则表达式的需求。

由于此项目侧重于性能,因此该库的范围有限。总的来说,该库提供了一种匹配提供的HTTP请求(方法和方法)与一系列路由定义的能力。以下是一些使用示例。

基准测试

测试名称 RapidRoute (req/sec) FastRoute (req/sec) 变化
第一个静态路由 3385.28 2906.64 16.47%更快
最后一个静态路由 3419.56 2901.09 17.87%更快
第一个动态路由 3428.94 2829.18 21.20%更快
最后一个动态路由 3379.56 2890.18 16.93%更快
不存在的路由 3412.31 2823.27 20.86%更快
最长路由 3371.36 2853.40 18.15%更快
无效方法,静态路由 3125.81 2864.19 9.13%更快
无效方法,动态路由 3402.57 2847.55 19.49%更快

这些结果使用此基准测试套件在PHP 5.5上运行并启用opcache时生成。这些结果表明,与FastRoute相比,性能提高了10-20%,具体取决于输入uri和http方法。

安装

此项目与PHP 5.4+兼容。可以通过composer加载。

composer require timetoogo/rapid-route ~2.0

路由器使用

该库设计用于由另一个库/框架使用,或作为独立软件包。它为每个用例提供特定的API。

在框架中的使用

框架通常提供自己的包装API,因此在这种情况下,该库提供了一个更底层的API。以下是一个基本示例。

use RapidRoute\CompiledRouter;
use RapidRoute\RouteCollection;
use RapidRoute\MatchResult;

$compiledRouterPath = __DIR__ . '/path/to/compiled/router.php';

$router = CompiledRouter::generate(
    $compiledRouterPath,
    function (RouteCollection $routes) {
        // Route definitions...
    }
);

$result = $router($httpMethod, $uri);

switch ($result[0]) {
    case MatchResult::NOT_FOUND:
        // 404 Not Found...
        break;

    case MatchResult::HTTP_METHOD_NOT_ALLOWED:
        // 405 Method Not Allowed...
        $allowedMethods = $result[1];
        break;

    case MatchResult::FOUND:
        // Matched route, dispatch to associated handler...
        $routeData = $result[1];
        $parameters = $result[2];
        break;
}

路由器返回的结果是一个包含结果状态作为第一个元素的数组。数组的后续元素依赖于状态,并将采用以下三种格式之一

// Could not match route
[MatchResult::NOT_FOUND]

// Matched route but disallowed HTTP method
[MatchResult::HTTP_METHOD_NOT_ALLOWED, [<allowed HTTP methods>]]

// Found matching route
[MatchResult::FOUND, <associated route data>, [<matched route parameters>]]

作为独立软件包的使用

如果打算将此库用作独立软件包,则提供更干净、更全面的包装API。以下是一个展示此API的类似示例。

use RapidRoute\Router;
use RapidRoute\RouteCollection;
use RapidRoute\MatchResult;

$compiledRouterPath = __DIR__ . '/path/to/compiled/router.php';

$router = new Router(
    $compiledRouterPath,
    function (RouteCollection $routes) {
        // Route definitions...
    }
);

// If true the router will be recompiled every request
$router->setDevelopmentMode($developmentMode);
// Or you can manually call when appropriate
// $router->clearCompiled();

$result = $router->match($httpMethod, $uri);

if($result->isNotFound()) {
    // 404 Not Found...
} elseif ($result->isDisallowedHttpMethod()) {
    // 405 Method Not Allowed...
    $allowedMethods = $result->getAllowedHttpMethods();
} elseif ($result->isFound()) {
    // Matched route, dispatch to associated handler...
    $routeData = $result->getRouteData();
    $parameters = $result->getParameters();
}

// Or if preferred
switch ($result->getStatus()) {
    // case MatchResult::* as above
}

调用$router->match(...)的结果将是RapidRoute\MatchResult的实例。

路由定义

路由模式

要定义路由,使用熟悉的URL结构

// This is a static route, it will extactly match '/shop/product'
'/shop/product'

// A dynamic route can be defined using the {...} parameter syntax
// This will match urls such as '/shop/product/123' or '/shop/product/abcd'
'/shop/product/{id}'

// If a route parameter must match a specific format you can define it
// by passing an array with a regex in the following format
['/shop/product/{id}', 'id' => '\d+']

// Or, if you prefer, you can use the predefined patterns using RapidRoute\Pattern
['/shop/product/{id}', 'id' => Pattern::DIGITS]

// More complex routes patterns are supported
[
  '/shop/category/{category_id}/product/search/{filter_by}:{filter_value}',
  'category_id' => Pattern::DIGITS,
  'filter_by'   => Pattern::ALPHA_LOWER
]

// You can also inline the parameter regexps using the following syntax
// The following is equivalent to the previous route definition
'/shop/category/{category_id:\d+}/product/search/{filter_by:[a-z]+}:{filter_value}'

添加路由

要定义路由,路由API在路由器被编译时接收一个callable参数,该参数将使用RapidRoute\RouteCollection的实例调用。可以这样使用

function (RouteCollection $routes) {
    $routes->add('GET', '/', ['name' => 'home']);
    
    // There are also shortcuts for the standard HTTP methods
    // the following is equivalent to the previous call
    $routes->get('/', ['name' => 'home']);
    
    // Or if any HTTP method should be allowed:
    $routes->any('/contact', ['name' => 'contact']);
}

使用RouteCollection,您还可以全局定义路由参数正则表达式,以避免重复。

function (RouteCollection $routes) {
    $routes->param('product_id', Pattern::DIGITS);
    $routes->param('page_slug', Pattern::ALPHA_NUM_DASH);
    
    $routes->get('/shop/product/{product_id}', ['name' => 'shop.product.show']);
    $routes->get('/page/{page_slug}', ['name' => 'page.show']);
}

基本用法示例

当路由匹配时,相关的路由数据将可用。这是一个如何将此库作为独立路由包实现的非常基本的示例。路由数据包含关联的处理程序,因此当路由匹配时可以轻松分发。

use RapidRoute\Router;
use RapidRoute\RouteCollection;
use RapidRoute\Pattern;
use RapidRoute\MatchResult;

require __DIR__ . './vendor/autoload.php';

$compiledRouterPath = __DIR__ . '/path/to/compiled/router.php';

$router = new Router(
    $compiledRouterPath,
    function (RouteCollection $routes) {
        $routes->param('user_id', Pattern::DIGITS);

        $routes->get('/', ['handler' => ['HomeController', 'index']]);
        $routes->get('/user', ['handler' => ['UserController', 'index']]);
        $routes->get('/user/create', ['handler' => ['UserController', 'create']]);
        $routes->post('/user', ['handler' => ['UserController', 'store']]);
        $routes->get('/user/{user_id}', ['handler' => ['UserController', 'show']]);
        $routes->get('/user/{user_id}/edit', ['handler' => ['UserController', 'edit']]);
        $routes->add(['PUT', 'PATCH'], '/user/{user_id}', ['handler' => ['UserController', 'update']]);
        $routes->delete('/user/{user_id}', ['handler' => ['UserController', 'delete']]);
    }
);

$router->setDevelopmentMode($developmentMode);

$result = $router->match($_SERVER['REQUEST_METHOD'], parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH));

switch($result->getStatus()) {
    case MatchResult::NOT_FOUND:
        render((new ErrorController())->notFound());
        break;
    
    case MatchResult::HTTP_METHOD_NOT_ALLOWED:
        render((new ErrorController())->methodNotAllowed($result->getAllowedHttpMethods()));
        break;
    
    case MatchResult::FOUND:
        // Dispatcher matched route to associated handler
        list($controller, $method) = $result->getRouteData()['handler'];
        $parameters = $result->getParameters();
        
        render((new $controller())->{$method}($parameters));
        break;
}

以下是此设置应该如何处理传入请求的一些示例

请求 已分发处理程序
GET / HomeController::index([])
GET /user UserController::index([])
POST /user UserController::store([])
POST / ErrorController::methodNotAllowed(['GET'])
GET /abc ErrorController::notFound()
GET /user/123 UserController::show(['user_id' => '123'])
PUT /user/123 UserController::update(['user_id' => '123'])
PUT /user/abc ErrorController::notFound()

备注

  • 当匹配URI时,如果URI字符串不为空,则必须包含前导/
  • 定义了尾随斜线的路由将不会匹配不带斜线的URI
    • '/shop/product/'不会匹配'/shop/product',反之亦然
  • 允许GET方法的路由也将接受HTTP规范中的HEAD方法。

编译

鉴于此库将路由定义编译为纯PHP,有很多优化空间。当前的方法是使用树状结构匹配URI中的每个部分('/shop/product''shop''product'部分组成)。目前结构被编译成嵌套的switchif块,其中适用时使用优化比较。

编译路由的一个考虑因素是它必须能够直接调用,因此必须在编译的路由中处理任何预期的错误情况。

示例编译路由

路由定义

$router = CompiledRouter::generate(
    __DIR__ . '/compiled/rr.php',
    function (\RapidRoute\RouteCollection $routes) {
        $routes->param('post_slug', Pattern::APLHA_NUM_DASH);

        $routes->get('/', ['name' => 'home']);
        $routes->get('/blog', ['name' => 'blog.index']);
        $routes->get('/blog/post/{post_slug}', ['name' => 'blog.post.show']);
        $routes->post('/blog/post/{post_slug}/comment', ['name' => 'blog.post.comment']);
    }
)

目前上述编译的路由将类似于以下内容

use RapidRoute\RapidRouteException;

return function ($method, $uri) {
    if($uri === '') {
        return [0];
    } elseif ($uri[0] !== '/') {
        throw new RapidRouteException("Cannot match route: non-empty uri must be prefixed with '/', '{$uri}' given");
    }

    $segments = explode('/', substr($uri, 1));

    switch (count($segments)) {
        case 1:
            list($s0) = $segments;
            if ($s0 === '') {
                switch ($method) {
                    case 'GET':
                    case 'HEAD':
                        return [2, ['name' => 'home'], []];
                    default:
                        $allowedHttpMethods[] = 'GET';
                        $allowedHttpMethods[] = 'HEAD';
                        break;
                }
            }
            if ($s0 === 'blog') {
                switch ($method) {
                    case 'GET':
                    case 'HEAD':
                        return [2, ['name' => 'blog.index'], []];
                    default:
                        $allowedHttpMethods[] = 'GET';
                        $allowedHttpMethods[] = 'HEAD';
                        break;
                }
            }
            return isset($allowedHttpMethods) ? [1, $allowedHttpMethods] : [0];
            break;
        
        case 3:
            list($s0, $s1, $s2) = $segments;
            if ($s0 === 'blog' && $s1 === 'post' && ctype_alnum(str_replace('-', '', $s2))) {
                switch ($method) {
                    case 'GET':
                    case 'HEAD':
                        return [2, ['name' => 'blog.post.show'], ['post_slug' => $s2]];
                    default:
                        $allowedHttpMethods[] = 'GET';
                        $allowedHttpMethods[] = 'HEAD';
                        break;
                }
            }
            return isset($allowedHttpMethods) ? [1, $allowedHttpMethods] : [0];
            break;
        
        case 4:
            list($s0, $s1, $s2, $s3) = $segments;
            if ($s0 === 'blog' && $s1 === 'post' && $s3 === 'comment' && ctype_alnum(str_replace('-', '', $s2))) {
                switch ($method) {
                    case 'POST':
                        return [2, ['name' => 'blog.post.comment'], ['post_slug' => $s2]];
                    default:
                        $allowedHttpMethods[] = 'POST';
                        break;
                }
            }
            return isset($allowedHttpMethods) ? [1, $allowedHttpMethods] : [0];
            break;
        
        default:
            return [0];
    }
};

路由器的复杂性将与路由定义的数量和复杂性成比例增长。