akrz/phroute

为PHP提供快速、功能齐全的RESTful请求路由器(phroute/phroute的分支)

维护者

详细信息

github.com/akrz/phroute

源代码

安装: 309

依赖: 0

建议者: 0

安全: 0

星星: 1

关注者: 2

分支: 96

v2.2.16 2024-02-22 16:02 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'] 对。

这些方法还接受一个额外的参数,即中间件数组:目前支持 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());
$matchedRoute = $dispatcher->matchRoute($requestMethod, $requestUri);
$runner = $dispatcher->dispatch($matchedRoute);
    
// 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 SAPIs。请参阅: https://symfony.com.cn/doc/current/create_framework/http_foundation.html

$dispatcher = new Phroute\Phroute\Dispatcher($router->getData());
$matchedRoute = $dispatcher->matchRoute($requestMethod, $requestUri);
$response = $dispatcher->dispatch($matchedRoute);

matchRoute() 方法将调用匹配的路由,如果没有匹配,则抛出以下异常之一

# Route not found
Phroute\Phroute\Exception\HttpRouteNotFoundException;

# Route found, but method not allowed
Phroute\Phroute\Exception\HttpMethodNotAllowedException;

注意:HTTP规范要求405 Method Not Allowed响应包括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响应中删除实体主体,因此这种行为对大多数用户没有影响。

然而,使用Phroute在Web SAPI环境之外(例如,自定义服务器)的实现者必须不得发送对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)