phroute / phroute
PHP 快速、功能丰富的 RESTful 请求路由器
Requires
- php: >=5.4.0
Requires (Dev)
- php-coveralls/php-coveralls: ^2.5
- phpunit/phpunit: ^9.0
This package is not auto-updated.
Last update: 2024-09-20 05:11:07 UTC
README
此库提供了一个基于正则表达式的路由器的快速实现。
归功于 nikic/FastRoute。
尽管库的大部分内容和大量单元测试都是我自己的,但正则表达式匹配核心实现和基准测试的功劳归功于 nikic。请去阅读 nikic 的 博客文章,该文章解释了实现工作原理以及为什么它这么快。
对核心进行了许多修改以适应新的库包装器,并添加了诸如可选路由参数和反向路由等附加功能,但请前往查看 nikic 的库以了解核心的来源和工作原理。
安装
通过 composer 安装
composer require phroute/phroute
用法
示例
$router->get('/example', function(){ return 'This route responds to requests with the GET method at the path /example'; }); $router->post('/example/{id}', function($id){ return 'This route responds to requests with the POST method at the path /example/1234. It passes in the parameter as a function argument.'; }); $router->any('/example', function(){ return 'This route responds to any method (POST, GET, DELETE, OPTIONS, HEAD etc...) at the path /example'; });
定义路由
use Phroute\Phroute\RouteCollector; $router = new RouteCollector(); $router->get($route, $handler); # match only get requests $router->post($route, $handler); # match only post requests $router->delete($route, $handler); # match only delete requests $router->any($route, $handler); # match any request method etc...
这些辅助方法围绕
addRoute($method, $route, $handler)
包装。
此方法接受路由必须匹配的 HTTP 方法、路由模式和一个可调用的处理程序,处理程序可以是闭包、函数名或 ['ClassName', 'method']
对。
方法还接受一个额外的参数,即中间件数组:目前支持 before
和 after
过滤器,以及使用 prefix
进行路由前缀。有关更多信息和方法示例,请参阅过滤器和前缀部分。
默认情况下,使用路由模式语法,其中 {foo}
指定一个名称为 foo
的占位符,并匹配字符串 [^/]+
。要调整占位符匹配的模式,可以编写 {bar:[0-9]+}
指定自定义模式。然而,也可以通过在构造路由器时传递自定义路由解析器来调整模式语法。
$router->any('/example', function(){ return 'This route responds to any method (POST, GET, DELETE etc...) at the URI /example'; }); // or '/page/{id:i}' (see shortcuts) $router->post('/page/{id:\d+}', function($id){ // $id contains the url paramter return 'This route responds to the post method at the URI /page/{param} where param is at least one number'; }); $router->any('/', function(){ return 'This responds to the default route'; }); // Lazy load autoloaded route handling classes using strings for classnames // Calls the Controllers\User::displayUser($id) method with {id} parameter as an argument $router->any('/users/{id}', ['Controllers\User','displayUser']); // Optional Parameters // simply add a '?' after the route name to make the parameter optional // NB. be sure to add a default value for the function argument $router->get('/user/{id}?', function($id = null) { return 'second'; }); # NB. You can cache the return value from $router->getData() so you don't have to create the routes each request - massive speed gains $dispatcher = new Phroute\Phroute\Dispatcher($router->getData()); $response = $dispatcher->dispatch($_SERVER['REQUEST_METHOD'], parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH)); // Print out the value returned from the dispatched function echo $response;
正则表达式快捷方式
:i => :/d+ # numbers only
:a => :[a-zA-Z0-9]+ # alphanumeric
:c => :[a-zA-Z0-9+_\-\.]+ # alnumnumeric and + _ - . characters
:h => :[a-fA-F0-9]+ # hex
use in routes:
'/user/{name:i}'
'/user/{name:a}'
###反向路由的命名路由
将数组作为第一个参数传递,其中第一个项是您的路由,第二个项是稍后要引用的名称。
$router->get(['/user/{name}', 'username'], function($name){ return 'Hello ' . $name; }) ->get(['/page/{slug}/{id:\d+}', 'page'], function($id){ return 'You must be authenticated to see this page: ' . $id; }); // Use the routename and pass in any route parameters to reverse engineer an existing route path // If you change your route path above, you won't need to go through your code updating any links/references to that route $router->route('username', 'joe'); // string(9) '/user/joe' $router->route('page', ['intro', 456]); // string(15) '/page/intro/456'
###过滤器
$router->filter('statsStart', function(){ setPageStartTime(microtime(true)); }); $router->filter('statsComplete', function(){ var_dump('Page load time: ' . (microtime(true) - getPageStartTime())); }); $router->get('/user/{name}', function($name){ return 'Hello ' . $name; }, ['before' => 'statsStart', 'after' => 'statsComplete']);
###过滤器组
将多个路由包裹在路由组中,将过滤器应用于定义在其中的每个路由。如果需要,可以嵌套路由组。
// Any thing other than null returned from a filter will prevent the route handler from being dispatched $router->filter('auth', function(){ if(!isset($_SESSION['user'])) { header('Location: /login'); return false; } }); $router->group(['before' => 'auth'], function($router){ $router->get('/user/{name}', function($name){ return 'Hello ' . $name; }) ->get('/page/{id:\d+}', function($id){ return 'You must be authenticated to see this page: ' . $id; }); });
###前缀组
// You can combine a prefix with a filter, eg. `['prefix' => 'admin', 'before' => 'auth']` $router->group(['prefix' => 'admin'], function($router){ $router->get('pages', function(){ return 'page management'; }); $router->get('products', function(){ return 'product management'; }); $router->get('orders', function(){ return 'order management'; }); });
###控制器
namespace MyApp; class Test { public function anyIndex() { return 'This is the default page and will respond to /controller and /controller/index'; } /** * One required paramter and one optional parameter */ public function anyTest($param, $param2 = 'default') { return 'This will respond to /controller/test/{param}/{param2}? with any method'; } public function getTest() { return 'This will respond to /controller/test with only a GET method'; } public function postTest() { return 'This will respond to /controller/test with only a POST method'; } public function putTest() { return 'This will respond to /controller/test with only a PUT method'; } public function deleteTest() { return 'This will respond to /controller/test with only a DELETE method'; } } $router->controller('/controller', 'MyApp\\Test'); // Controller with associated filter $router->controller('/controller', 'MyApp\\Test', ['before' => 'auth']);
分发 URI
通过调用创建的分发器的 dispatch()
方法来分发 URI。此方法接受 HTTP 方法和 URI。获取这两部分信息(并适当地进行归一化)是您的工作 - 此库不绑定到 PHP Web SAPI。
$response = (new Phroute\Phroute\Dispatcher($router)) ->dispatch($_SERVER['REQUEST_METHOD'], $_SERVER['REQUEST_URI']);
dispatch()
方法将调用匹配的路由,如果没有匹配,将抛出以下异常之一
# Route not found
Phroute\Phroute\Exception\HttpRouteNotFoundException;
# Route found, but method not allowed
Phroute\Phroute\Exception\HttpMethodNotAllowedException;
注意:HTTP 规范要求
405 方法不允许
响应包括Allow:
标头,以详细说明请求资源的可用方法。此信息可以从抛出的异常消息内容中获取:它看起来像"Allow: HEAD, GET, POST"
等...这取决于您设置的方法。您应该捕获异常并使用此信息向客户端发送标头:header($e->getMessage());
###依赖注入
自定义依赖解析器简单且易于使用。路由器将通过依赖解析器尝试解析过滤器以及路由处理器。
以下示例展示了如何定义自己的解析器以与orno/di、pimple/pimple或其他集成。
use Orno\Di\Container; use Phroute\Phroute\HandlerResolverInterface; class RouterResolver implements HandlerResolverInterface { private $container; public function __construct(Container $container) { $this->container = $container; } public function resolve($handler) { /* * Only attempt resolve uninstantiated objects which will be in the form: * * $handler = ['App\Controllers\Home', 'method']; */ if(is_array($handler) and is_string($handler[0])) { $handler[0] = $this->container[$handler[0]]; } return $handler; } }
当您创建自己的分发器时
$appContainer = new Orno\Di; // Attach your controllers as normal // $appContainer->add('App\Controllers\Home') $resolver = new RouterResolver($appContainer); $response = (new Phroute\Phroute\Dispatcher($router, $resolver))->dispatch($requestMethod, $requestUri);
关于HEAD请求的注意事项
HTTP规范要求服务器[支持GET和HEAD方法][2616-511]。
GET和HEAD方法必须被所有通用服务器支持。
为了避免强制用户为每个资源手动注册HEAD路由,我们回退到匹配给定资源的可用GET路由。PHP Web SAPI透明地从HEAD响应中删除实体体,因此此行为对绝大多数用户没有影响。
然而,在Web SAPI环境之外(例如,自定义服务器)使用Phroute的实现者不得发送对HEAD请求生成的实体体。如果您是非SAPI用户,这是您的责任;Phroute无权阻止您在这些情况下破坏HTTP。
最后,请注意,应用程序可以始终为给定资源指定自己的HEAD方法路由,以完全绕过此行为。
性能
在以下机器上执行
- 处理器 2.3 GHz Intel Core i7
- 内存 8 GB 1600 MHz DDR3
####Phroute
此测试旨在部分展示轻量级路由核心的效率,但主要是与常规库相比,随着路由数量的增加,匹配速度没有下降。
有10条路由时,匹配第一条路由(最佳情况)
$ /usr/local/bin/ab -n 1000 -c 100 http://127.0.0.1:9943/
Finished 1000 requests
Time taken for tests: 3.062 seconds
Requests per second: 326.60 [#/sec] (mean)
Time per request: 306.181 [ms] (mean)
Time per request: 3.062 [ms] (mean, across all concurrent requests)
Transfer rate: 37.32 [Kbytes/sec] received
Percentage of the requests served within a certain time (ms)
50% 306
66% 307
75% 307
80% 308
90% 309
95% 309
98% 310
99% 310
100% 310 (longest request)
有10条路由时,匹配最后一条路由(最坏情况)
请注意,匹配速度与匹配第一条路由一样快
$ /usr/local/bin/ab -n 1000 -c 100 http://127.0.0.1:9943/thelastroute
Finished 1000 requests
Time taken for tests: 3.079 seconds
Requests per second: 324.80 [#/sec] (mean)
Time per request: 307.880 [ms] (mean)
Time per request: 3.079 [ms] (mean, across all concurrent requests)
Transfer rate: 37.11 [Kbytes/sec] received
Percentage of the requests served within a certain time (ms)
50% 307
66% 308
75% 309
80% 309
90% 310
95% 311
98% 312
99% 312
100% 313 (longest request)
有100条路由时,匹配最后一条路由(最坏情况)
$ /usr/local/bin/ab -n 1000 -c 100 http://127.0.0.1:9943/thelastroute
Finished 1000 requests
Time taken for tests: 3.195 seconds
Requests per second: 312.97 [#/sec] (mean)
Time per request: 319.515 [ms] (mean)
Time per request: 3.195 [ms] (mean, across all concurrent requests)
Transfer rate: 35.76 [Kbytes/sec] received
Percentage of the requests served within a certain time (ms)
50% 318
66% 319
75% 320
80% 320
90% 322
95% 323
98% 323
99% 324
100% 324 (longest request)
有1000条路由时,匹配最后一条路由(最坏情况)
$ /usr/local/bin/ab -n 1000 -c 100 http://127.0.0.1:9943/thelastroute
Finished 1000 requests
Time taken for tests: 4.497 seconds
Complete requests: 1000
Requests per second: 222.39 [#/sec] (mean)
Time per request: 449.668 [ms] (mean)
Time per request: 4.497 [ms] (mean, across all concurrent requests)
Transfer rate: 25.41 [Kbytes/sec] received
Percentage of the requests served within a certain time (ms)
50% 445
66% 447
75% 448
80% 449
90% 454
95% 456
98% 457
99% 458
100% 478 (longest request)
###与Laravel 4.0路由核心进行比较
请注意,这并非对Laravel的贬低——它基于一个路由循环,因此随着路由数量的增加,性能会下降。
有10条路由时,匹配第一条路由(最佳情况)
$ /usr/local/bin/ab -n 1000 -c 100 http://127.0.0.1:4968/
Finished 1000 requests
Time taken for tests: 13.366 seconds
Requests per second: 74.82 [#/sec] (mean)
Time per request: 1336.628 [ms] (mean)
Time per request: 13.366 [ms] (mean, across all concurrent requests)
Transfer rate: 8.55 [Kbytes/sec] received
Percentage of the requests served within a certain time (ms)
50% 1336
66% 1339
75% 1340
80% 1341
90% 1346
95% 1348
98% 1349
99% 1351
100% 1353 (longest request)
有10条路由时,匹配最后一条路由(最坏情况)
$ /usr/local/bin/ab -n 1000 -c 100 http://127.0.0.1:4968/thelastroute
Finished 1000 requests
Time taken for tests: 14.621 seconds
Requests per second: 68.39 [#/sec] (mean)
Time per request: 1462.117 [ms] (mean)
Time per request: 14.621 [ms] (mean, across all concurrent requests)
Transfer rate: 7.81 [Kbytes/sec] received
Percentage of the requests served within a certain time (ms)
50% 1461
66% 1465
75% 1469
80% 1472
90% 1476
95% 1479
98% 1480
99% 1482
100% 1484 (longest request)
有100条路由时,匹配最后一条路由(最坏情况)
$ /usr/local/bin/ab -n 1000 -c 100 http://127.0.0.1:4968/thelastroute
Finished 1000 requests
Time taken for tests: 31.254 seconds
Requests per second: 32.00 [#/sec] (mean)
Time per request: 3125.402 [ms] (mean)
Time per request: 31.254 [ms] (mean, across all concurrent requests)
Transfer rate: 3.66 [Kbytes/sec] received
Percentage of the requests served within a certain time (ms)
50% 3124
66% 3145
75% 3154
80% 3163
90% 3188
95% 3219
98% 3232
99% 3236
100% 3241 (longest request)
有1000条路由时,匹配最后一条路由(最坏情况)
$ /usr/local/bin/ab -n 1000 -c 100 http://127.0.0.1:5740/thelastroute
Finished 1000 requests
Time taken for tests: 197.366 seconds
Requests per second: 5.07 [#/sec] (mean)
Time per request: 19736.598 [ms] (mean)
Time per request: 197.366 [ms] (mean, across all concurrent requests)
Transfer rate: 0.58 [Kbytes/sec] received
Percentage of the requests served within a certain time (ms)
50% 19736
66% 19802
75% 19827
80% 19855
90% 19898
95% 19918
98% 19945
99% 19960
100% 19975 (longest request)