phossa/phossa-route

一个快速且功能齐全的PHP路由库

1.0.2 2016-06-13 03:52 UTC

This package is not auto-updated.

Last update: 2024-09-14 19:58:39 UTC


README

Build Status Latest Stable Version License

查看新的库phoole/route 简介

phossa-route 是一个 快速功能齐全功能丰富 的PHP应用层路由库。它根据URL、HTTP头信息、会话信息等来分发请求。

它需要PHP 5.4,并支持PHP 7.0+、HHVM。它符合PSR-1PSR-2PSR-4规范。

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

入门

  • 安装

    使用composer工具安装。

    composer require "phossa/phossa-route=1.*"
    

    或者在你的composer.json中添加以下行

    {
        "require": {
          "phossa/phossa-route": "^1.0.0"
        }
    }
  • 简单用法

    use Phossa\Route\Dispatcher;
    
    // dispatcher with default collector & resolver
    $dispatcher = (new Dispatcher())
        ->addGet(
            '/blog/{action:xd}[/{year:d}[/{month:d}[/{date:d}]]]',
            function($result) {
                echo "action is " . $result->getParameter('action');
            })
        ->addPost('/blog/post', 'handler2')
        ->addRoute(new Route\Route(
            'GET,HEAD', // multiple methods
            '/blog/read[/{id:d}]',
            'handler3',
            ['id' => '1'])); // default $id value
    
    // route base on info provided by server
    $dispatcher->dispatch();
  • 从文件中加载路由

    use Phossa\Route\Dispatcher;
    
    /*
     * routes.php :
     * return [
     *     '/user/phossa'  => 'handler1',
     *     '/user/{action:xd}/{id:d}'   => [['controller', 'action'], 'GET,POST'],
     *     '/user/view[/{id:d}]' => ['handler2', 'GET', ['id' => 23]]
     * ];
     */
    $dispatcher = (new Dispatcher())->loadRoute('./routes.php');

路由语法

注意:对其他路由库语法的支持在1.1.x版本中。用户可以使用phossa-route选择他/她的 favorite 路由语法 :)

  • {命名}参数

    路由模式语法使用,其中{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 = (new Dispatcher())->addPost('/blog/post', 'handler2');

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

  • 多个路由收集器

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

    // '/user' related
    $collector_user = (new Route\Collector\Collector())
        ->addGet('/user/list/{id:d}', 'handler1')
        ->addGet('/user/view/{id:d}', 'handler2')
        ->addPost('/user/new', 'handler3');
    
    // '/blog' related
    $collector_blog = (new Route\Collector\Collector())
        ->addGet('/blog/list/{user_id:d}', 'handler4')
        ->addGet('/blog/read/{blog_id:d}', 'handler5');
    
    $dispatcher->addCollector($collector_user)
               ->addCollector($collector_blog);
  • 相同的路由模式

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

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

    但不能定义具有不同 过滤器 的相同路由模式和方法。可能的解决方案是在 handler1 中处理逻辑或向路由添加 扩展

分发

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

    在脚本 index.php 中,dispatch() 通常是最后一行。

    // index.php
    // ...
    
    // dispatch base on server request info
    $dispatcher->dispatch();

    dispatch() 接收一个可选参数 Phossa\Route\Context\ResultInterface。如果没有提供,它将收集来自全局变量(如 $_SERVER$_REQUEST)的信息,并根据路由定义将分发到正确的例程或可调用对象。

  • 分发一个 URL

    用户可以分发一个 URL,

    $dispatcher->dispatchUrl('GET', '/error404');
  • 匹配而不是分发

    如果使用 match() 方法,则默认在 dispatch() 中执行处理器,用户可以拥有更多的控制权

    // use info from $_SERVER etc.
    if ($dispatcher->match()) {
        $result = $dispatcher->getResult();
        switch($result->getStatus()) {
            case 200:
              // ...
              break;
            case 404:
              // ...
              break;
            default:
              // ...
              break;
        }
    } else {
        // no match found
        // ...
    }

    matchUrl() 也提供了。

处理器

  • 多个处理器

    对于状态 200 OK,路由定义了一个处理器。支持其他结果状态的多处理器。

    use Phossa\Route\Route;
    use Phossa\Route\Status;
    
    $route = (new Route('GET', '/user/{action:xd}/{id:d}',
        function($result) { // handler for Status::OK
            $user_id = $result->getParameter('id');
            // ...
        })->addHandler(Status::METHOD_NOT_ALLOWED, 'handler1'); // extra handler

    如果路由匹配但方法无效,将执行处理器 handler1

  • 默认处理器

    分发器和收集器可以具有多个处理器,对应不同的状态。如果结果没有设置处理器,则将检索收集器的处理器(相同的状态码)。如果仍然没有运气,如果已定义,则将使用分发器的处理器(相同的状态码)。

    分发器级别的处理器,

    use Phossa\Route\Status;
    
    $dispatcher->addHandler(
        Status::SERVICE_UNAVAILABLE,
        function($result) {
            // ...
        }
    );

    收集器级别的处理器,

    $collector->addHandler(
        Status::MOVED_PERMANENTLY,
        function($result) {
            // ...
        }
    );
  • 处理器解析

    大多数时候,路由返回一个处理器,例如 [ 'className', 'method' ]。可以使用处理器解析器将此伪处理器解析为实际的可调用对象。

    use Phossa\Route;
    
    // dispatcher with default resolver
    $dispatcher = new Route\Dispatcher(
        new Route\Collector\Collector(),
        new Route\Handler\ResolverAbstract()
    );

    用户可以通过扩展 Phossa\Route\Handler\ResolverAbstract 类来编写自己的处理器解析器。

扩展

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

  • 扩展的使用

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

    use Phossa\Route\Dispatcher
    use Phossa\Route\Extensions\RedirectToHttpsExtension;
    
    // create dispatcher
    $dispatcher = new Dispatcher();
    
    // direct any HTTP request to HTTPS port before any routing
    dispatcher->addExtension(new RedirectToHttpsExtension());

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

    $dispatcher->addExtension(
        function($result) {
            $pattern = $result->getRequest()->getPattern();
            if ('/user' == substr($pattern, 0, 5) && !isset($_SESSION['auth'])) {
                $result->setStatus(Status::UNAUTHORIZED);
                return false; // return to dispatcher level
            }
            return true; // authed or not in /user
        },
        Dispatcher::BEFORE_MATCH // run this extension before matching
    );
    
    // set default auth handler at dispatcher level
    $dispatcher->addHandler(
        Status::UNAUTHORIZED,
        function($result) {
            // display auth page etc.
        }
    );
  • 扩展的示例

    参数值的有效性验证,

    $route->addExtension(
        function($result) {
            $id = (int) $result->getParameter('id');
            if ($id > 1000) { // not allowed
                $result->setStatus(Status::PRECONDITION_FAILED);
                return false;
            }
            return true;
        },
        Route::BEFORE_ROUTE // before execute route handler
    );

    路由集合的统计信息

    $collector->addExtension(
        function($result) {
            // collect statistics
        },
        Collector::BEFORE_COLL // before collector match
    )->addExtension(
        function($result) {
            // collect statistics
        },
        Collector::AFTER_COLL // after a successful match
    );
  • 扩展阶段

    三种类型阶段:分发器级别、收集器级别和路由级别。按执行顺序列出所有阶段。

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

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

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

    • Dispatcher::AFTER_MATCH 在派发器级别成功匹配后

    • Dispatcher::BEFORE_DISPATCH 在成功匹配后,但在派发到任何处理器之前

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

      • Route::AFTER_ROUTE 处理器成功执行后

    • Dispatcher::AFTER_DISPATCH 处理器成功执行后返回派发器级别

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

    • Dispatcher::AFTER_DEFAULT 派发器默认处理器执行后

过滤器

  • 过滤器使用

    有时,用户在决定如何派发之前可能需要查看其他信息。 扩展 是实现此目的的一种方法。但 addFilter() 方法在路由级别更为合适。

    // match against $_SERVER, $_REQUEST, $_SESSION, $_COOKIE etc.
    $route = (new Route('GET', '/user/list/{$id}', 'handler1'))
        ->addFilter('server.server_name', '(m|www).phossa.com')
        ->addFilter('cookie.vote_status', 'voted');

    甚至支持闭包

    // closure takes the value from $_SERVER['SERVER_NAME'] as input
    $route->addFilter('server.server_name', function($value) {
        switch($value) {
          case 'a1.phossa.com':
          case 'b2.phossa.com':
              return true;
          default:
              return false; // always return a bool
        }
    });
  • 与扩展的区别

    过滤器在匹配过程中使用,如果过滤失败,匹配过程将尝试下一个路由。

    而路由级别的扩展在成功匹配后、执行处理器之前执行。

调试

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

$dispatcher->setDebugMode(true)->setDebugger($logger);

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

路由策略

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

  • 查询参数路由 (QPR)

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

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

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

  • 参数对路由 (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
    

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

  • 正则表达式路由 (RER)

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

    // created with default RER collector
    $dispatcher = new Dispatcher();
    
    // add supprot for legacy query parameter routing
    $dispatcher->addCollector(new CollectorQPR());

正则表达式匹配算法

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

  • FastRoute 算法

    这个 基于分组计数算法Phossa\Route\Regex\ParserGcb 类中实现,并在本文 "使用正则表达式进行快速请求路由" 中详细解释。

    phossa-route 默认使用此算法。

  • 标准算法

    此算法由 phossa-route 开发,比 FastRoute GCB 算法稍慢。它在 Phossa\Route\Regex\ParserStd 类中实现。

    使用此标准算法:

    use Phossa\Route\Dispatcher;
    use Phossa\Route\Regex\ParserStd;
    use Phossa\Route\Collector\Collector;
    
    // use standard algorithm
    $dispatcher = new Dispatcher(new Collector(new ParserStd));
  • 对路由算法的评论

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

      如果你在你的应用程序中使用路由库,不同的算法可能只对单个请求有 0.1 - 0.2ms 的差异,这对于应用程序来说似乎没有意义,除非你将其用作独立的路由器。

    • 如果你 确实 关心路由速度

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

    • 如果您只是 极度追求速度,请尝试 网络路由或服务器路由

依赖项

  • PHP >= 5.4.0

  • phossa/phossa-shared >= 1.0.6

  • 如果您正在使用 调试,则需 phossa/phossa-logger >= 1.0.0

许可证

MIT许可证

附录

  • 性能

    • 最坏情况匹配

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

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

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

    • 首次路由匹配

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

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

      注意 FastRoutePhroute 都实现了静态路由表,因此它们在首次路由匹配(这是一个静态路由)方面非常快。

  • 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.conf 中的 Nginx 配置

      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或ngix配置进行简单路由。

      例如,如果您的服务器因维护而关闭,您可以按以下方式替换 .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 执行。所有请求都配置为首先由该脚本处理,然后路由到不同的程序。