phroute/phroute

PHP 快速、功能丰富的 RESTful 请求路由器

v2.2.0 2022-05-04 16:22 UTC

README

Build Status Coverage Status Latest Stable Version License Total Downloads

此库提供了一个基于正则表达式的路由器的快速实现。

归功于 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'] 对。

方法还接受一个额外的参数,即中间件数组:目前支持 beforeafter 过滤器,以及使用 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)