sn01615/bramus-router

一个轻量级且简单的面向对象的PHP路由器

v1.6.3 2019-06-08 00:20 UTC

This package is auto-updated.

Last update: 2024-09-08 12:32:47 UTC


README

Build Status Source Version Downloads License

一个轻量级且简单的面向对象的PHP路由器。由Bramus Van Damme (https://www.bram.us)贡献者 开发

特性

先决条件/要求

安装

可以使用Composer进行安装

composer require bramus/router ~1.6

示例

示例包含在 demo 子文件夹中。使用您喜欢的Web服务器或通过在Shell中执行 php -S localhost:8080 使用PHP 5.4+'s内置服务器来提供服务。包含用于Apache的 .htaccess 文件。

此外,还包含一个多语言路由器的示例。该示例位于 demo-multilang 子文件夹中,可以以相同的方式运行。

使用方法

创建一个 \Bramus\Router\Router 实例,将其上定义一些路由,并运行它。

// Require composer autoloader
require __DIR__ . '/vendor/autoload.php';

// Create Router instance
$router = new \Bramus\Router\Router();

// Define routes
// ...

// Run it!
$router->run();

路由

使用 $router->match(method(s), pattern, function)routes(一个或多个HTTP方法与模式的组合)钩子

$router->match('GET|POST', 'pattern', function() { … });

bramus/router 支持 GETPOSTPUTPATCHDELETEHEAD (见 注释)OPTIONS HTTP请求方法。传入单个请求方法,或使用 | 分隔的多个请求方法。

当路由与当前URL匹配时(例如,$_SERVER['REQUEST_URI']),将执行附加的 路由处理函数。路由处理函数必须是 可调用的。只有第一个匹配的路由将被处理。如果没有找到匹配的路由,将执行404处理程序。

路由简写

为单个请求方法提供简写

$router->get('pattern', function() { /* ... */ });
$router->post('pattern', function() { /* ... */ });
$router->put('pattern', function() { /* ... */ });
$router->delete('pattern', function() { /* ... */ });
$router->options('pattern', function() { /* ... */ });
$router->patch('pattern', function() { /* ... */ });

您可以使用此简写来访问可以使用任何方法的路由

$router->all('pattern', function() { … });

注意:必须在调用 $router->run(); 之前将路由钩子

注意:没有 match() 的简写,因为 bramus/router 将在内部将此类请求重定向到等效的 GET 请求,以符合RFC2616 (见 注释)

路由模式

路由模式可以是静态的或动态的

  • 静态路由模式 不包含动态部分,必须与当前URL的 path 部分完全匹配。
  • 动态路由模式 包含可以随请求而变化的动态部分。这些变化的部分被称为 子模式,可以使用Perl兼容正则表达式 (PCRE) 或使用 占位符 进行定义

静态路由模式

静态路由模式是一种表示URI的正则字符串。它将直接与当前URL的path部分进行比较。

示例

  • /about
  • /contact

用法示例

// This route handling function will only be executed when visiting http(s)://www.example.org/about
$router->get('/about', function($name) {
    echo 'About Page Contents';
});

基于PCRE的动态路由模式

这种路由模式包含动态部分,这些部分可以随请求而变化。变化的部分被命名为子模式,并使用正则表达式定义。

示例

  • /movies/(\d+)
  • /profile/(\w+)

在动态路由模式中常用到的基于PCRE的子模式包括

  • \d+ = 一个或多个数字(0-9)
  • \w+ = 一个或多个字母数字字符(a-z 0-9 _)
  • [a-z0-9_-]+ = 一个或多个字母数字字符(a-z 0-9 _)和破折号(-)
  • .* = 任何字符(包括/),零个或多个
  • [^/]+ = 除了/之外的任何字符,一个或多个

注意:PHP PCRE速查表可能会很有用。

在动态基于PCRE的路由模式中定义的子模式被转换为参数,并传递给路由处理函数。前提是这些子模式需要定义为括号子模式,这意味着它们应该被括号包围。

// Bad
$router->get('/hello/\w+', function($name) {
    echo 'Hello ' . htmlentities($name);
});

// Good
$router->get('/hello/(\w+)', function($name) {
    echo 'Hello ' . htmlentities($name);
});

注意:路由模式开头处的/不是必需的,但建议使用。

当定义多个子模式时,结果的路由处理参数将按照它们定义的顺序传递给路由处理函数。

$router->get('/movies/(\d+)/photos/(\d+)', function($movieId, $photoId) {
    echo 'Movie #' . $movieId . ', photo #' . $photoId);
});

基于占位符的动态路由模式

这种类型的路由模式与基于PCRE的动态路由模式相同,但有一点不同:它们不使用正则表达式进行模式匹配,而是使用更简单的占位符。占位符是包围在花括号中的字符串,例如{name}。您不需要在占位符周围添加括号。

示例

  • /movies/{id}
  • /profile/{username}

占位符比PCRE更容易使用,但提供的控制较少,因为它们在内部被转换为匹配任何字符的正则表达式(.*)。

$router->get('/movies/{movieId}/photos/{photoId}', function($movieId, $photoId) {
    echo 'Movie #' . $movieId . ', photo #' . $photoId);
});

注意:占位符的名称不需要与传递给路由处理函数的参数名称匹配。

$router->get('/movies/{foo}/photos/{bar}', function($movieId, $photoId) {
    echo 'Movie #' . $movieId . ', photo #' . $photoId);
});

可选路由子模式

可以通过在子模式后添加一个?来使路由子模式可选。想想以/blog(/year)(/month)(/day)(/slug)形式呈现的博客URL。

$router->get(
    '/blog(/\d+)?(/\d+)?(/\d+)?(/[a-z0-9_-]+)?',
    function($year = null, $month = null, $day = null, $slug = null) {
        if (!$year) { echo 'Blog overview'; return; }
        if (!$month) { echo 'Blog year overview'; return; }
        if (!$day) { echo 'Blog month overview'; return; }
        if (!$slug) { echo 'Blog day overview'; return; }
        echo 'Blogpost ' . htmlentities($slug) . ' detail';
    }
);

上面的代码片段响应以下URL:/blog/blog/year/blog/year/month/blog/year/month/day/blog/year/month/day/slug

注意:使用可选参数时,确保将子模式的开头/放在子模式内部。别忘了为可选参数设置默认值。

不幸的是,上面的代码片段也响应了如/blog/foo之类的URL,并声明需要显示概览——这是不正确的。可以通过扩展括号子模式,使其包含其他可选子模式来使可选子模式连续:模式应该类似于/blog(/year(/month(/day(/slug))))而不是之前的/blog(/year)(/month)(/day)(/slug)

$router->get('/blog(/\d+(/\d+(/\d+(/[a-z0-9_-]+)?)?)?)?', function($year = null, $month = null, $day = null, $slug = null) {
    // ...
});

注意:强烈建议始终定义连续的可选参数。

使用量词来要求URL中正确数量的数字。

$router->get('/blog(/\d{4}(/\d{2}(/\d{2}(/[a-z0-9_-]+)?)?)?)?', function($year = null, $month = null, $day = null, $slug = null) {
    // ...
});

子路由/路由挂载

使用$router->mount($baseroute, $fn)将路由集合挂载到子路由模式上。子路由模式将作为前缀添加到作用域中定义的所有后续路由。例如,将回调$fn挂载到/movies上将在所有后续路由前添加/movies

$router->mount('/movies', function() use ($router) {

    // will result in '/movies/'
    $router->get('/', function() {
        echo 'movies overview';
    });

    // will result in '/movies/id'
    $router->get('/(\d+)', function($id) {
        echo 'movie id ' . htmlentities($id);
    });

});

    $router->ns('Admin')->prefix('/admin')->group(function (\Bramus\Router\Router $router) {
        $router->get('login', 'LoginController@login');
    });
    
    $router->prefix('/novel')->get('/nothing', function () {
        echo 'yes';
    });
    
    $router->ns('Admin')->prefix('/admin')->get('/login', 'LoginController@login1');
    $router->get('/login', 'LoginController@login2');
    
    $router->ns('Admin')->prefix('/admin')->group(function (\Bramus\Router\Router $router) {
        $router->get('login', 'LoginController@login3');
        $router->ns('Novel')->prefix('/novel')->group(function (\Bramus\Router\Router $router) {
            $router->get('info', 'NovelController@getNovelInfo');
        });
    });

可以在已包含在先前的$router->mount()中的可调用函数中定义第二个$router->mount()来嵌套子路由。

Class@Method调用

我们可以这样路由到类动作

$router->get('/(\d+)', '\App\Controllers\User@showProfile');

当请求匹配指定的路由URI时,将执行《User》类的《showProfile》方法。定义的路由参数将传递到类方法中。

该方法可以是静态的或非静态的。在非静态方法的情况下,将创建该类的一个新实例。

如果大多数/所有处理类都在同一个命名空间中,您可以通过《setNamespace()`》设置在您的路由实例上使用的默认命名空间。

$router->setNamespace('\App\Controllers');
$router->get('/users/(\d+)', 'User@showProfile');
$router->get('/cars/(\d+)', 'Car@showProfile');

《Class::Method》调用

我们可以这样将路由到类的静态方法

$router->get('/(\d+)', '\App\Controllers\User::showProfile');

子域名

$router->domain('admin.router.com')->group(function (\Bramus\Router\Router $router) {
    $router->get('login', 'LoginController@login');
});

自定义404

默认的404处理程序设置404状态码并退出。您可以使用《$router->set404(callable);》覆盖此默认的404处理程序。

$router->set404(function() {
    header('HTTP/1.1 404 Not Found');
    // ... do something special here
});

也支持《Class@Method》调用

$router->set404('\App\Controllers\Error@notFound');

当没有路由模式与当前URL匹配时,将执行404处理程序。

路由中间件之前

《bramus/router》支持《路由前中间件》Before Route Middlewares,它们在处理路由之前执行。

与路由处理函数类似,您可以将处理函数连接到一种或多种HTTP请求方法和特定的路由模式。

$router->before('GET|POST', '/admin/.*', function() {
    if (!isset($_SESSION['user'])) {
        header('location: /auth/login');
        exit();
    }
});

与路由处理函数不同,当找到多个路由匹配时,将执行多个路由前中间件。

路由前中间件

路由前中间件是针对路由的。使用通用路由模式(即《所有URL》),它们可以成为《路由前中间件》(在其他项目中有时称为路由前应用中间件),无论请求的URL是什么,都会始终执行。

$router->before('GET', '/.*', function() {
    // ... this will always be executed
});

路由后中间件/运行回调

运行一个(1)中间件函数,在路由处理之后命名《路由后中间件》(在其他项目中有时称为应用后中间件)。只需将其传递给《$router->run()`》函数。运行回调是路由无关的。

$router->run(function() { … });

注意:如果路由处理函数已执行《exit()`》,则不会运行运行回调。

覆盖请求方法

使用《X-HTTP-Method-Override》覆盖HTTP请求方法。仅当原始请求方法为《POST》时才有效。允许的《X-HTTP-Method-Override》值为《PUT》、《DELETE》或《PATCH》。

子文件夹支持

默认情况下,《bramus/router》将在您放置它的任何(子)文件夹中运行……不需要对您的代码进行调整。您可以自由移动您的《入口脚本》《index.php》,路由器将自动根据当前文件夹的路径相对地适应,并将所有路由挂载到那个《basePath》。

假设您有一个服务器托管域名《www.example.org》,使用《public_html/》作为其文档根目录,并具有以下小的《入口脚本》《index.php》

$router->get('/', function() { echo 'Index'; });
$router->get('/hello', function() { echo 'Hello!'; });
  • 如果您将此文件(以及其伴随的《.htaccess》文件等)放置在文档根级别(例如《public_html/index.php》),《bramus/router》将挂载所有路由到域名根(例如《/》),因此响应《https://www.example.org/》和《https://www.example.org/hello》。

  • 如果您将此文件(以及其伴随的《.htaccess》文件等)移动到子文件夹中(例如《public_html/demo/index.php》),《bramus/router》将挂载所有路由到当前路径(例如《/demo》),因此响应《https://www.example.org/demo》和《https://www.example.org/demo/hello》。在这种情况下不需要《$router->mount(…)`。

禁用子文件夹支持

如果您不希望《bramus/router》自动适应其放置的文件夹,可以通过调用《setBasePath()`》手动覆盖《basePath》。这在《(不常见)》情况下是必要的,其中您的《入口脚本》和您的《入口URL》不是紧密耦合的(例如,当入口脚本放置在一个不需要成为其响应URL的一部分的子文件夹中时)。

// Override auto base path detection
$router->setBasePath('/');

$router->get('/', function() { echo 'Index'; });
$router->get('/hello', function() { echo 'Hello!'; });

$router->run();

如果您将此文件放入子文件夹中(例如 public_html/some/sub/folder/index.php),它仍然会将路由挂载到域名根目录(例如 /),因此会响应 https://www.example.org/https://www.example.org/hello (假设您的 .htaccess 文件 – 放在文档根目录级别 – 重写请求到它)。

与其他库的集成

通过使用 use 关键字将依赖项传递到处理函数中,使用 bramus/router 集成其他库。

$tpl = new \Acme\Template\Template();

$router->get('/', function() use ($tpl) {
    $tpl->load('home.tpl');
    $tpl->setdata(array(
        'name' => 'Bramus!'
    ));
});

$router->run(function() use ($tpl) {
    $tpl->display();
});

根据这种结构,仍然可以在 After 路由中间件内部操作输出。

关于 PUT 操作的注意事项

PHP 中没有 $_PUT。必须模拟它。

$router->put('/movies/(\d+)', function($id) {

    // Fake $_PUT
    $_PUT  = array();
    parse_str(file_get_contents('php://input'), $_PUT);

    // ...

});

关于发起 HEAD 请求的注意事项

在发起 HEAD 请求时,所有输出都将被缓冲,以防止任何内容渗入响应体,如RFC2616(超文本传输协议 – HTTP/1.1)中定义。

HEAD 方法与 GET 方法相同,但服务器必须不在响应中返回消息体。对 HEAD 请求的响应中包含在 HTTP 头中的元信息应该与对 GET 请求的响应中发送的信息相同。此方法可以用于获取由请求暗示的实体的元信息,而不传输实体体。此方法通常用于测试超文本链接的有效性、可访问性和最近修改。

为了实现这一点,bramus/router 会将内部重定向 HEAD 请求到等效的 GET 请求,并自动抑制所有输出。

单元测试 & 代码覆盖率

bramus/router 随附使用 PHPUnit 的单元测试。

  • 如果 PHPUnit 已全局安装,运行 phpunit 以运行测试。

  • 如果 PHPUnit 未全局安装,通过运行 composer install --dev 在本地安装它。通过调用 vendor/bin/phpunit 来运行测试本身。

    包含的 composer.json 还将安装 php-code-coverage,允许您生成一个 代码覆盖率报告。运行 phpunit --coverage-html ./tests-report(需要 XDebug),报告将放置在 tests-report 子文件夹中。

致谢

bramus/routerKleinHamJREAM/route 的启发。虽然 Klein 提供了许多功能,但它不是面向对象的。虽然 Ham 是面向对象的,但它在 关注点分离 方面做得不好,因为它还在路由类内部提供了模板。虽然 JREAM/route 是一个很好的起点,但它所做的功能有限(例如,只有 GET 路由)。

许可证

bramus/router 在 MIT 公共许可证下发布。有关详细信息,请参阅附带的 LICENSE 文件。