timetoogo / rapid-route
另一个快速路由库,用于PHP
Requires
- php: >=5.4.0
Requires (Dev)
- phpunit/phpunit: ~4.6.0
- satooshi/php-coveralls: 0.6.1
This package is auto-updated.
Last update: 2022-12-25 03:22:05 UTC
README
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'
部分组成)。目前结构被编译成嵌套的switch
和if
块,其中适用时使用优化比较。
编译路由的一个考虑因素是它必须能够直接调用,因此必须在编译的路由中处理任何预期的错误情况。
示例编译路由
路由定义
$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]; } };
路由器的复杂性将与路由定义的数量和复杂性成比例增长。