phossa2/route

该包已被弃用且不再维护。没有建议的替代包。

适用于PHP的快速且功能齐全的路由库

2.0.0 2016-08-26 02:10 UTC

This package is not auto-updated.

Last update: 2020-01-24 16:10:12 UTC


README

请使用 phoole/route 库代替

Build Status Code Quality Code Climate PHP 7 ready HHVM Latest Stable Version License

phossa2/route 是一个 快速功能齐全特性丰富 的PHP应用级路由库。

它需要PHP 5.4,支持PHP 7.0+ 和 HHVM。它符合 PSR-1PSR-2PSR-3PSR-4,以及建议的 PSR-5

为什么还需要另一个路由库?

安装

通过 composer 工具安装。

composer require "phossa2/route"

或者将以下行添加到您的 composer.json

{
    "require": {
       "phossa2/route": "2.*"
    }
}

使用方法

将路由定义(模式、处理器、默认值等)注入分发器,然后调用 match()dispatch()

use Phossa2\Route\Route;
use Phossa2\Route\Dispatcher;

// dispatcher with default collector & resolver
$dispatcher = (new Dispatcher())
    ->addGet(
        '/blog/{action:xd}[/{year:d}[/{month:d}[/{date:d}]]]',
        function($result) {
            $params = $result->getParameters();
            echo "action is " . $params['action'];
        }
    )->addPost(
        '/blog/post',
        'handler2'
    )->addRoute(new Route(
        'GET,HEAD',
        '/blog/read[/{id:d}]',
        'handler3',
        ['id' => '1'] // default values
    ));

// diaptcher (match & execute controller action)
$dispatcher->dispatch('GET', '/blog/list/2016/05/01');

或者从数组中加载路由:

$routes = [
    '/user/{action:xd}/{id:d}' => [
        'GET,POST',               // methods,
        function ($result) {
            $params = $result->getParameters();
            echo "user id is " . $params['id'];
        },                        // handler,
        ['id' => 1]               // default values
    ],
    // ...
];
$dispatcher = (new Dispatcher())->loadRoutes($routes);
$dispatcher->dispatch('GET', '/user/view/123456');

路由语法

  • 命名参数

    路由模式语法中使用 {foo} 指定一个名为 foo 的命名参数或占位符,默认正则表达式模式为 [^/]++。为了匹配更具体的类型,您可能需要指定一个自定义的正则表达式模式,例如 {foo:[0-9]+}

    // with 'action' & 'id' two named params
    $dispatcher->addGet('/user/{action:[^0-9/][^/]*}/{id:[0-9]+}', 'handler1');

    可以使用以下方式使用预定义的快捷方式作为占位符:

    ':d}'   => ':[0-9]++}',             // digit only
    ':l}'   => ':[a-z]++}',             // lower case
    ':u}'   => ':[A-Z]++}',             // upper case
    ':a}'   => ':[0-9a-zA-Z]++}',       // alphanumeric
    ':c}'   => ':[0-9a-zA-Z+_\-\.]++}', // common chars
    ':nd}'  => ':[^0-9/]++}',           // not digits
    ':xd}'  => ':[^0-9/][^/]*+}',       // no leading digits

    前面的模式可以重写为:

    // with 'action' & 'id' two named params
    $dispatcher->addGet('/user/{action:xd}/{id:d}', 'handler1');
  • 可选段

    可以使用 [] 指定路由模式中的可选段,如下所示:

    // $action, $year/$month/$date are all optional
    $pattern = '/blog[/{action:xd}][/{year:d}[/{month:d}[/{date:d}]]]';

    其中可选段可以是 嵌套的。与其它库不同,可选段不仅限于模式的末尾,只要是一个有效的模式,就像示例中的 [/{action:xd}]

  • 语法限制

    • 参数名称 必须 以字母开头

      由于 {2} 在正则表达式中有特殊含义。参数名称 必须 以字母开头。此外,在占位符内部或外部使用 {} 可能会引起混淆,因此不推荐使用。

    • 占位符外部的 [] 表示 可选 的段

      [] 不能作为正则表达式模式的一部分使用,如果您确实需要 使用它们作为正则表达式模式的一部分,请将它们包含在 占位符内部

    • 不允许在占位符内部使用捕获组 ()

      捕获组 () 不能在占位符内部使用。例如,{user:(root|phossa)} 是无效的。相反,您可以使用 {user:root|phossa}{user:(?:root|phossa)}

  • 默认值

    可以在命名参数的末尾添加默认值,形式为 {action:xd=list}。默认值必须是字母数字字符。例如,

    // $action, $year/$month/$date are all optional
    $pattern = '/blog[/{action:xd=list}][/{year:d=2016}[/{month:d=01}[/{date:d=01}]]]';
    $dispatcher->addGet($pattern, function($result) {
        $params = $result->getParameters();
        echo $params['year'];
    })->dispatch('GET', '/blog');

路由

  • 使用分发器定义路由

    您可以使用分发器定义路由。但实际上,这是通过分发器中的第一个路由收集器定义路由的。

    // a new route collector will be added automatically if not yet
    $dispatcher = (new Dispatcher())->addPost('/blog/post', 'handler2');

    addGet()addPost()addRoute(RouteInterface) 的包装器。

  • 多个路由收集器

    可以使用多个收集器将路由分组到不同的集合中。

    use Phossa2\Route\Collector\Collector;
    
    // '/user' related
    $collector_user = (new Collector())
        ->addGet('/user/list/{id:d}', 'handler1')
        ->addGet('/user/view/{id:d}', 'handler2')
        ->addPost('/user/new', 'handler3');
    
    // '/blog' related
    $collector_blog = (new Collector())
        ->addGet('/blog/list/{user_id:d}', 'handler4')
        ->addGet('/blog/read/{blog_id:d}', 'handler5');
    
    $dispatcher->addCollector($collector_user)
               ->addCollector($collector_blog);
  • 路径前缀匹配

    收集器可以使用 setPathPrefix() 设置路径前缀,以指示处理的确切URI路径前缀。找到任何不匹配的前缀将完全跳过收集器。

    // '/user/' prefix
    $collector_user = (new Collector())
        ->setPathPrefix('/user/')
        ->addGet('/user/list/{id:d}', 'handler1')
        ->addGet('/user/view/{id:d}', 'handler2')
        ->addPost('/user/new', 'handler3');
  • 相同的路由模式

    用户可以定义具有不同HTTP方法的相同路由模式。

    $dispatcher
        ->addGet('/user/{$id}', 'handler1')
        ->addPost('/user/{$id}', 'handler2');

分发

  • 使用分发器的 dispatch() 进行分发

    $dispatcher->dispatch('GET', '/user/view/123');
  • 匹配而不是分发

    如果使用 match() 方法,则在 dispatch() 中默认执行处理程序的情况下,用户可以获得更多控制。

    if ($dispatcher->match('GET', '/user/view/1234')) {
        $result = $dispatcher->getResult();
        switch($result->getStatus()) {
            case 200:
              // ...
              break;
            case 404:
              // ...
              break;
            default:
              // ...
              break;
        }
    } else {
        // no match found
        // ...
    }

处理程序

  • 路由处理程序

    仅定义了状态 200 OK 的处理程序的路由。

    use Phossa2\Route\Route;
    use Phossa2\Route\Status;
    
    $route = new Route(
        'GET',
        '/user/{action:xd}/{id:d}',
        function($result) { // handler for Status::OK
            // ...
        }
    );
  • 默认处理程序

    分发器和收集器可以具有与不同结果状态相对应的多个处理程序。

    如果结果没有设置处理程序(例如,没有找到匹配项),则将检索收集器的处理程序(相同状态码)。如果仍然没有成功,如果已定义,则将使用分发器的处理程序(相同状态码)。

    分发器级别的处理程序,

    use Phossa2\Route\Status;
    
    $dispatcher->addHandler(
        function($result) {
            echo "method " . $result->getMethod() . " not allowed";
        },
        Status::METHOD_NOT_ALLOWED
    );

    收集器级别的处理程序,

    $collector->addHandler(
        function($result) {
            // ...
        },
        Status::MOVED_PERMANENTLY
    );

    当使用状态设置为 0addHandler() 时,将导致此处理程序成为其他状态的其他默认处理程序。

    use Phossa2\Route\Status;
    
    $dispatcher->addHandler(
        function($result) {
            echo "no other handler found";
        },
        0 // <-- match all other status
    );
  • 处理程序解析

    大多数情况下,匹配的路由将返回一个类似 [ 'ControllerName', 'actionName' ] 的处理程序。处理程序解析器可以用于将此伪处理程序解析为真实可调用的。

    use Phossa2\Route\Collector\Collector;
    use Phossa2\Route\Resolver\ResolverSimple;
    
    // dispatcher with default resolver
    $dispatcher = new Route\Dispatcher(
        new Collector(),
        new ResolverSimple() // the default resolver anyway
    );

    用户可以通过实现 Phossa2\Route\Interfaces\ResolverInterface 来编写自己的处理程序解析器。

扩展

扩展是可调用的,处理匹配结果或其他任务,在特定分发阶段的之前或之后。

扩展可以添加到 DispatcherCollector 或甚至 Route

  • 扩展的使用

    扩展 必须 返回一个布尔值,以指示是否继续进行分发过程。FALSE 表示停止并返回到顶级。

    扩展可以是 Phossa2\Event\EventableExtensionAbstract 类,通过 addExtension()addExt() 方法添加,或者是一个符合签名 callableName(Phossa2\Event\EventInterface $event): bool 的可调用对象,可以通过 addExt(callable, eventName, priority) 方法添加为扩展。

    use Phossa2\Route\Status;
    use Phossa2\Route\Dispatcher;
    use Phossa2\Route\Extensions\RedirectToHttps;
    
    // create dispatcher
    $dispatcher = new Dispatcher();
    
    // direct any HTTP request to HTTPS port before any routing
    $dispatcher
        ->addExtension(new RedirectToHttps())
        ->addHandler(function() {
              echo "redirect to https";
          }, Status::MOVED_PERMANENTLY)
        ->dispatch('GET', '/user/view/123');

    强制对以 '/user/' 前缀的任何 URL 进行身份验证,

    use Phossa2\Route\Status;
    use Phossa2\Route\Dispatcher;
    use Phossa2\Route\Extensions\UserAuth;
    
    $dispatcher = new Dispatcher();
    
    $dispatcher
      // add handler for unauthorized routing
      ->addHandler(
          function() {
              echo "need auth";
          }, Status::UNAUTHORIZED)
    
      // add a route
      ->addGet('/user/view/{id:d}', function() {
              echo "AUTHED!";
          })
    
      // add extension to force auth routes under '/user/'
      ->addExt(function($event) {
              $result = $event->getParam('result');
              $path = $result->getPath();
              if (!isset($_SESSION['authed']) && '/user/' === substr($path, 0, 6)) {
                  $result->setStatus(Status::UNAUTHORIZED);
                  return false;
              }
              return true;
          }, Dispatcher::EVENT_BEFORE_MATCH);
    
    // try a not authed route
    $dispatcher->dispatch('GET', '/user/view/123');
    
    // try a authed route
    $_SESSION['authed'] = 1;
    $dispatcher->dispatch('GET', '/user/view/123');
  • 扩展示例

    在路由上验证参数值,

    use Phossa2\Route\Status;
    use Phossa2\Route\Dispatcher;
    use Phossa2\Route\Extensions\IdValidation;
    
    $dispatcher = new Dispatcher();
    
    // add extension to a route
    $route = (new Route('GET', '/user/{id:d}', null))
      ->addExtension(new IdValidation());
    
    // will fail
    $dispatcher->addRoute($route)->dispatch('GET', '/user/1000');
  • 扩展事件

    三种类型的事件,分发器级别、收集器级别和路由级别。按执行顺序列出所有事件。

    • Dispatcher::EVENT_BEFORE_MATCH 在匹配开始之前

      • Collector::EVENT_BEFORE_MATCH 在收集器中的匹配之前

      • Collector::EVENT_AFTER_MATCH 在收集器中成功匹配之后

    • Dispatcher::EVENT_AFTER_MATCH 在分发器级别成功匹配之后

    • Dispatcher::EVENT_BEFORE_DISPATCH 在成功匹配后,在将任何处理器分发之前

      • Route::EVENT_BEFORE_HANDLER 在执行此路由的处理器(路由的或收集器的)之前

      • Route::EVENT_AFTER_HANDLER 处理器成功执行之后

    • Dispatcher::EVENT_AFTER_DISPATCH 返回到分发器级别,在处理器成功执行之后

    • Dispatcher::EVENT_BEFORE_HANDLER 匹配失败或未找到匹配路由的处理器之前,在执行分发器的默认处理器之前

    • Dispatcher::EVENT_AFTER_HANDLER 分发器的默认处理器执行之后

调试

有时,你需要知道发生了什么错误。

$dispatcher->enableDebug()->setDebugger($logger);

其中 $logger 是一个符合 PSR-3 的日志实现,实现了 Psr\Log\LoggerInterface 接口。分发器将分发过程的日志发送到日志记录器。

路由策略

此库支持几种基于 URL 的路由策略。不同的策略收集器可以组合成一个分发器。

  • 参数对路由(PPR)

    使用参数和值对进行路由,

    http://servername/path/index.php/controller/action/id/1/name/nick
    

    参数顺序可以是任意的,但必须以对的形式出现。此方案的优势是快速且对网络爬虫友好。如果使用 URL 重写,则上述内容可以写成以下形式,

    http://servername/path/controller/action/id/1/name/nick
    

    除了 '?' 和 '&' 之外,可以使用任何有效的 URL 字符作为参数分隔符。

    http://servername/path/controller-action-id-1-name-nick
    

    此策略在 Phossa2\Route\Collector\CollectorPPR 类中实现。

  • 查询参数路由(QPR)

    路由信息直接嵌入在 URL 查询中。此方案的优势是快速且清晰。

    http://servername/path/?r=controller-action-id-1-name-nick
    

    此策略在 Phossa2\Route\Collector\CollectorQPR 类中实现。

  • 正则表达式路由(RER)

    正则表达式路由是此库的默认路由策略,并在 Phossa2\Route\Collector\Collector 类中实现。

    // created with default RER collector
    $dispatcher = (new Dispatcher())
        ->addCollector(new Collector())     // regex based routing first
        ->addCollector(new CollectorQPR()); // support for legacy QPR

正则表达式匹配算法

RER 收集器可以与不同的正则表达式匹配算法一起使用。

  • FastRoute 算法

    基于分组计数的算法Phossa2\Route\Parser\ParserGcb 类中实现,并在本文中详细解释 "使用正则表达式快速请求路由"

    phossa-route 默认使用此算法。

  • 标准算法

    此算法由 phossa2/route 开发,比 fastRoute GCB 算法慢一点。它在 Phossa2\Route\Parser\ParserStd 类中实现。

    使用此标准算法,

    use Phossa2\Route\Dispatcher;
    use Phossa2\Route\Parser\ParserStd;
    use Phossa2\Route\Collector\Collector;
    
    // use standard algorithm
    $dispatcher = new Dispatcher(new Collector(new ParserStd));
  • 关于路由算法的注释

    • 它并不像你想象的那么重要。

      如果您在应用程序中使用路由库,不同的算法在单个请求上的差异可能只有0.1 - 0.2毫秒,对于应用程序来说这似乎微不足道,除非您将其用作独立路由器。

    • 如果您确实在乎路由速度

      使用不同的路由策略,例如参数对路由(PPR),它比基于正则表达式的路由要快得多。通过精心设计您的路由,即使使用较慢的算法,您也可能获得更好的结果。

    • 如果您只是对速度狂热,尝试网络路由或服务器路由

变更日志

请参阅CHANGELOG获取更多信息。

测试

$ composer test

贡献

请参阅CONTRIBUTE获取更多信息。

依赖项

  • PHP >= 5.4.0

  • phossa2/event >= 2.1.5

  • phossa2/shared >= 2.0.27

许可证

MIT许可证

附录

  • 性能

    • 最坏情况匹配

      此基准测试匹配最后一个路由和未知路由。它生成一个随机前缀和后缀的路由,试图阻止任何优化。1,000个路由,每个路由有8个参数。

      此基准测试由14个测试组成。每个测试执行1,000次,结果被修剪,然后取平均值。落在平均值的3个标准差之外的数据被丢弃。

      "参数对路由(PPR)"是最快的,用作基线。

      测试名称 结果 时间 + 间隔 变化
      Phossa PPR - 未知路由(1,000路由) 998 0.0000724551 +0.0000000000 基线
      Phossa PPR - 最后一个路由(1,000路由) 993 0.0000925307 +0.0000200755 慢28%
      Symfony2 Dumped - 未知路由(1,000路由) 998 0.0004353616 +0.0003629065 慢501%
      Phroute - 最后一个路由(1,000路由) 999 0.0006205601 +0.0005481050 慢756%
      Phossa - 未知路由(1,000路由) 998 0.0006903790 +0.0006179239 慢853%
      FastRoute - 未知路由(1,000路由) 1,000 0.0006911943 +0.0006187392 慢854%
      FastRoute - 最后一个路由(1,000路由) 999 0.0006962751 +0.0006238200 慢861%
      Phroute - 未知路由(1,000路由) 998 0.0007134676 +0.0006410125 慢885%
      Symfony2 Dumped - 最后一个路由(1,000路由) 993 0.0008066097 +0.0007341545 慢1013%
      Phossa - 最后一个路由(1,000路由) 998 0.0009104498 +0.0008379947 慢1157%
      Symfony2 - 未知路由(1,000路由) 989 0.0023998006 +0.0023273455 慢3212%
      Symfony2 - 最后一个路由(1,000路由) 999 0.0025880890 +0.0025156339 慢3472%
      Aura v2 - 最后一个路由(1,000路由) 981 0.0966411463 +0.0965686912 慢133281%
      Aura v2 - 未知路由(1,000路由) 992 0.1070026719 +0.1069302168 慢147581%
    • 第一次路由匹配

      此基准测试测试每个路由器匹配第一个路由的速度有多快。1,000个路由,每个路由有8个参数。

      此基准测试由7个测试组成。每个测试执行1,000次,结果被修剪,然后取平均值。落在平均值的3个标准差之外的数据被丢弃。

      注意 由于FastRoutePhroute都实现了静态路由表,因此在第一次路由匹配(这是一个静态路由)上它们都很快速。

      测试名称 结果 时间 + 间隔 变化
      FastRoute - 第一个路由 999 0.0000403543 +0.0000000000 基线
      Phroute - 第一个路由 998 0.0000405911 +0.0000002368 慢1%
      Symfony2导出 - 第一路由 999 0.0000590617 +0.0000187074 慢46%
      Phossa PPR - 第一路由 977 0.0000678727 +0.0000275184 慢68%
      Phossa - 第一路由 999 0.0000898475 +0.0000494932 慢123%
      Symfony2 - 第一路由 998 0.0003983802 +0.0003580259 慢887%
      Aura v2 - 第一路由 986 0.0004391784 +0.0003988241 慢988%
  • URL重写

    设置URL重写以使用index.php进行路由

    • Apache的.htaccess文件使用mod_rewrite模块已启用

      DirectorySlash Off
      Options -MultiViews
      DirectoryIndex index.php
      RewriteEngine On
      RewriteCond %{REQUEST_FILENAME} !-f
      RewriteCond %{REQUEST_FILENAME} !-d
      RewriteCond %{REQUEST_FILENAME} !-l
      RewriteRule ^ index.php [QSA,L]
      

      并在你的httpd.conf文件中启用使用.htaccess

      <VirtualHost *:80>
        ServerAdmin me@mysite.com
        DocumentRoot "/path/www.mysite.com/public"
        ServerName mysite.com
        ServerAlias www.mysite.com
      
        <Directory "/path/www.mysite.com/public">
          Options -Indexes +FollowSymLinks +Includes
          AllowOverride All
          Order allow,deny
          Allow from all
        </Directory>
      </VirtualHost>
      
    • Nginx配置在nginx.conf文件中

      server {
          listen       80;
          server_name  www.mysite.com mysite.com;
          root         /path/www.mysite.com/public;
      
          try_files $uri $uri/ /index.php$is_args$args;
      
          location /index.php {
              fastcgi_connect_timeout 3s;
              fastcgi_read_timeout 10s;
              include fastcgi.conf;
              fastcgi_pass 127.0.0.1:9000;
          }
      }
      
  • 路由问题

    基于请求信息,如请求设备、源IP、请求方法等,服务提供商可能将请求直接导向不同的主机、服务器、应用模块或处理器。

    • 网络级别路由

      常见情况,例如根据请求的源IP进行路由,将请求路由到最近的服务器,这在内容分发网络(CDN)中很常见,并在网络级别完成。

    • Web服务器路由

      出于性能考虑,一些简单的路由可以在Web服务器级别完成,例如使用Apache或Nginx配置进行简单路由。

      例如,如果您的服务器因维护而关闭,您可能需要替换以下.htaccess文件

      DirectorySlash Off
      Options -MultiViews
      DirectoryIndex maintenance.php
      RewriteEngine On
      RewriteCond %{REQUEST_FILENAME} !-f
      RewriteCond %{REQUEST_FILENAME} !-d
      RewriteCond %{REQUEST_FILENAME} !-l
      RewriteRule ^ maintenance.php [QSA,L]
      
    • 应用级别路由

      它解决了更多复杂的问题,并且更加灵活。

      通常,路由在单个点index.php完成。所有请求都被配置为首先由该脚本处理并路由到不同的程序。